inkpen 0.7.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.
- checksums.yaml +7 -0
- data/.DS_Store +0 -0
- data/.rubocop.yml +8 -0
- data/.yardopts +11 -0
- data/CLAUDE.md +141 -0
- data/README.md +409 -0
- data/Rakefile +19 -0
- data/app/assets/javascripts/inkpen/controllers/editor_controller.js +2050 -0
- data/app/assets/javascripts/inkpen/controllers/sticky_toolbar_controller.js +667 -0
- data/app/assets/javascripts/inkpen/controllers/toolbar_controller.js +693 -0
- data/app/assets/javascripts/inkpen/export/html.js +637 -0
- data/app/assets/javascripts/inkpen/export/index.js +30 -0
- data/app/assets/javascripts/inkpen/export/markdown.js +697 -0
- data/app/assets/javascripts/inkpen/export/pdf.js +372 -0
- data/app/assets/javascripts/inkpen/extensions/advanced_table.js +640 -0
- data/app/assets/javascripts/inkpen/extensions/block_commands.js +300 -0
- data/app/assets/javascripts/inkpen/extensions/block_gutter.js +338 -0
- data/app/assets/javascripts/inkpen/extensions/callout.js +303 -0
- data/app/assets/javascripts/inkpen/extensions/columns.js +403 -0
- data/app/assets/javascripts/inkpen/extensions/database.js +990 -0
- data/app/assets/javascripts/inkpen/extensions/document_section.js +352 -0
- data/app/assets/javascripts/inkpen/extensions/drag_handle.js +407 -0
- data/app/assets/javascripts/inkpen/extensions/embed.js +629 -0
- data/app/assets/javascripts/inkpen/extensions/enhanced_image.js +566 -0
- data/app/assets/javascripts/inkpen/extensions/export_commands.js +271 -0
- data/app/assets/javascripts/inkpen/extensions/file_attachment.js +593 -0
- data/app/assets/javascripts/inkpen/extensions/inkpen_table/index.js +58 -0
- data/app/assets/javascripts/inkpen/extensions/inkpen_table/inkpen_table.js +638 -0
- data/app/assets/javascripts/inkpen/extensions/inkpen_table/inkpen_table_cell.js +100 -0
- data/app/assets/javascripts/inkpen/extensions/inkpen_table/inkpen_table_header.js +100 -0
- data/app/assets/javascripts/inkpen/extensions/inkpen_table/table_constants.js +152 -0
- data/app/assets/javascripts/inkpen/extensions/inkpen_table/table_helpers.js +254 -0
- data/app/assets/javascripts/inkpen/extensions/inkpen_table/table_menu.js +282 -0
- data/app/assets/javascripts/inkpen/extensions/preformatted.js +239 -0
- data/app/assets/javascripts/inkpen/extensions/section.js +281 -0
- data/app/assets/javascripts/inkpen/extensions/section_title.js +126 -0
- data/app/assets/javascripts/inkpen/extensions/slash_commands.js +439 -0
- data/app/assets/javascripts/inkpen/extensions/table_of_contents.js +474 -0
- data/app/assets/javascripts/inkpen/extensions/toggle_block.js +332 -0
- data/app/assets/javascripts/inkpen/index.js +87 -0
- data/app/assets/stylesheets/inkpen/advanced_table.css +514 -0
- data/app/assets/stylesheets/inkpen/animations.css +626 -0
- data/app/assets/stylesheets/inkpen/block_gutter.css +265 -0
- data/app/assets/stylesheets/inkpen/callout.css +359 -0
- data/app/assets/stylesheets/inkpen/columns.css +314 -0
- data/app/assets/stylesheets/inkpen/database.css +658 -0
- data/app/assets/stylesheets/inkpen/document_section.css +305 -0
- data/app/assets/stylesheets/inkpen/drag_drop.css +220 -0
- data/app/assets/stylesheets/inkpen/editor.css +652 -0
- data/app/assets/stylesheets/inkpen/embed.css +468 -0
- data/app/assets/stylesheets/inkpen/enhanced_image.css +453 -0
- data/app/assets/stylesheets/inkpen/export.css +499 -0
- data/app/assets/stylesheets/inkpen/file_attachment.css +347 -0
- data/app/assets/stylesheets/inkpen/footnotes.css +136 -0
- data/app/assets/stylesheets/inkpen/inkpen_table.css +608 -0
- data/app/assets/stylesheets/inkpen/preformatted.css +215 -0
- data/app/assets/stylesheets/inkpen/search_replace.css +58 -0
- data/app/assets/stylesheets/inkpen/section.css +236 -0
- data/app/assets/stylesheets/inkpen/slash_menu.css +252 -0
- data/app/assets/stylesheets/inkpen/sticky_toolbar.css +314 -0
- data/app/assets/stylesheets/inkpen/toc.css +386 -0
- data/app/assets/stylesheets/inkpen/toggle.css +260 -0
- data/app/helpers/inkpen/editor_helper.rb +114 -0
- data/app/views/inkpen/_editor.html.erb +139 -0
- data/config/importmap.rb +170 -0
- data/docs/.DS_Store +0 -0
- data/docs/CHANGELOG.md +571 -0
- data/docs/FEATURES.md +436 -0
- data/docs/ROADMAP.md +3029 -0
- data/docs/VISION.md +235 -0
- data/docs/extensions/INKPEN_TABLE.md +482 -0
- data/docs/thinking/CORRECTED_NO_VUE.md +756 -0
- data/docs/thinking/EXECUTIVE_SUMMARY.md +403 -0
- data/docs/thinking/INKPEN_CODE_SAMPLES.md +1479 -0
- data/docs/thinking/INKPEN_MASTER_GUIDE.md +891 -0
- data/docs/thinking/README_START_HERE.md +341 -0
- data/lib/inkpen/configuration.rb +175 -0
- data/lib/inkpen/editor.rb +204 -0
- data/lib/inkpen/engine.rb +32 -0
- data/lib/inkpen/extensions/base.rb +109 -0
- data/lib/inkpen/extensions/code_block_syntax.rb +177 -0
- data/lib/inkpen/extensions/document_section.rb +111 -0
- data/lib/inkpen/extensions/forced_document.rb +183 -0
- data/lib/inkpen/extensions/mention.rb +155 -0
- data/lib/inkpen/extensions/preformatted.rb +111 -0
- data/lib/inkpen/extensions/section.rb +139 -0
- data/lib/inkpen/extensions/slash_commands.rb +100 -0
- data/lib/inkpen/extensions/table.rb +182 -0
- data/lib/inkpen/extensions/task_list.rb +145 -0
- data/lib/inkpen/sticky_toolbar.rb +157 -0
- data/lib/inkpen/toolbar.rb +145 -0
- data/lib/inkpen/version.rb +5 -0
- data/lib/inkpen.rb +101 -0
- data/sig/inkpen.rbs +4 -0
- metadata +165 -0
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* InkpenTableCell Extension
|
|
3
|
+
*
|
|
4
|
+
* Enhanced table cell with alignment, background color, and text color support.
|
|
5
|
+
*
|
|
6
|
+
* @since 0.8.0
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import TableCell from "@tiptap/extension-table-cell"
|
|
10
|
+
|
|
11
|
+
export const InkpenTableCell = TableCell.extend({
|
|
12
|
+
name: "tableCell",
|
|
13
|
+
|
|
14
|
+
addAttributes() {
|
|
15
|
+
return {
|
|
16
|
+
...this.parent?.(),
|
|
17
|
+
|
|
18
|
+
// Text alignment
|
|
19
|
+
align: {
|
|
20
|
+
default: null,
|
|
21
|
+
parseHTML: element => {
|
|
22
|
+
return element.getAttribute("data-align") ||
|
|
23
|
+
element.style.textAlign ||
|
|
24
|
+
null
|
|
25
|
+
},
|
|
26
|
+
renderHTML: attributes => {
|
|
27
|
+
if (!attributes.align) return {}
|
|
28
|
+
return {
|
|
29
|
+
"data-align": attributes.align,
|
|
30
|
+
style: `text-align: ${attributes.align}`
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
|
|
35
|
+
// Background color
|
|
36
|
+
background: {
|
|
37
|
+
default: null,
|
|
38
|
+
parseHTML: element => element.getAttribute("data-background"),
|
|
39
|
+
renderHTML: attributes => {
|
|
40
|
+
if (!attributes.background) return {}
|
|
41
|
+
return { "data-background": attributes.background }
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
|
|
45
|
+
// Text color (NEW)
|
|
46
|
+
textColor: {
|
|
47
|
+
default: null,
|
|
48
|
+
parseHTML: element => element.getAttribute("data-text-color"),
|
|
49
|
+
renderHTML: attributes => {
|
|
50
|
+
if (!attributes.textColor) return {}
|
|
51
|
+
return { "data-text-color": attributes.textColor }
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
|
|
55
|
+
// Colspan
|
|
56
|
+
colspan: {
|
|
57
|
+
default: 1,
|
|
58
|
+
parseHTML: element => {
|
|
59
|
+
const colspan = element.getAttribute("colspan")
|
|
60
|
+
return colspan ? parseInt(colspan, 10) : 1
|
|
61
|
+
},
|
|
62
|
+
renderHTML: attributes => {
|
|
63
|
+
if (attributes.colspan === 1) return {}
|
|
64
|
+
return { colspan: attributes.colspan }
|
|
65
|
+
}
|
|
66
|
+
},
|
|
67
|
+
|
|
68
|
+
// Rowspan
|
|
69
|
+
rowspan: {
|
|
70
|
+
default: 1,
|
|
71
|
+
parseHTML: element => {
|
|
72
|
+
const rowspan = element.getAttribute("rowspan")
|
|
73
|
+
return rowspan ? parseInt(rowspan, 10) : 1
|
|
74
|
+
},
|
|
75
|
+
renderHTML: attributes => {
|
|
76
|
+
if (attributes.rowspan === 1) return {}
|
|
77
|
+
return { rowspan: attributes.rowspan }
|
|
78
|
+
}
|
|
79
|
+
},
|
|
80
|
+
|
|
81
|
+
// Column width for resizing
|
|
82
|
+
colwidth: {
|
|
83
|
+
default: null,
|
|
84
|
+
parseHTML: element => {
|
|
85
|
+
const colwidth = element.getAttribute("colwidth")
|
|
86
|
+
return colwidth ? colwidth.split(",").map(Number) : null
|
|
87
|
+
},
|
|
88
|
+
renderHTML: attributes => {
|
|
89
|
+
if (!attributes.colwidth) return {}
|
|
90
|
+
return {
|
|
91
|
+
colwidth: attributes.colwidth.join(","),
|
|
92
|
+
style: `width: ${attributes.colwidth[0]}px`
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
export default InkpenTableCell
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* InkpenTableHeader Extension
|
|
3
|
+
*
|
|
4
|
+
* Enhanced table header cell with alignment, background color, and text color support.
|
|
5
|
+
*
|
|
6
|
+
* @since 0.8.0
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import TableHeader from "@tiptap/extension-table-header"
|
|
10
|
+
|
|
11
|
+
export const InkpenTableHeader = TableHeader.extend({
|
|
12
|
+
name: "tableHeader",
|
|
13
|
+
|
|
14
|
+
addAttributes() {
|
|
15
|
+
return {
|
|
16
|
+
...this.parent?.(),
|
|
17
|
+
|
|
18
|
+
// Text alignment
|
|
19
|
+
align: {
|
|
20
|
+
default: null,
|
|
21
|
+
parseHTML: element => {
|
|
22
|
+
return element.getAttribute("data-align") ||
|
|
23
|
+
element.style.textAlign ||
|
|
24
|
+
null
|
|
25
|
+
},
|
|
26
|
+
renderHTML: attributes => {
|
|
27
|
+
if (!attributes.align) return {}
|
|
28
|
+
return {
|
|
29
|
+
"data-align": attributes.align,
|
|
30
|
+
style: `text-align: ${attributes.align}`
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
|
|
35
|
+
// Background color
|
|
36
|
+
background: {
|
|
37
|
+
default: null,
|
|
38
|
+
parseHTML: element => element.getAttribute("data-background"),
|
|
39
|
+
renderHTML: attributes => {
|
|
40
|
+
if (!attributes.background) return {}
|
|
41
|
+
return { "data-background": attributes.background }
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
|
|
45
|
+
// Text color (NEW)
|
|
46
|
+
textColor: {
|
|
47
|
+
default: null,
|
|
48
|
+
parseHTML: element => element.getAttribute("data-text-color"),
|
|
49
|
+
renderHTML: attributes => {
|
|
50
|
+
if (!attributes.textColor) return {}
|
|
51
|
+
return { "data-text-color": attributes.textColor }
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
|
|
55
|
+
// Colspan
|
|
56
|
+
colspan: {
|
|
57
|
+
default: 1,
|
|
58
|
+
parseHTML: element => {
|
|
59
|
+
const colspan = element.getAttribute("colspan")
|
|
60
|
+
return colspan ? parseInt(colspan, 10) : 1
|
|
61
|
+
},
|
|
62
|
+
renderHTML: attributes => {
|
|
63
|
+
if (attributes.colspan === 1) return {}
|
|
64
|
+
return { colspan: attributes.colspan }
|
|
65
|
+
}
|
|
66
|
+
},
|
|
67
|
+
|
|
68
|
+
// Rowspan
|
|
69
|
+
rowspan: {
|
|
70
|
+
default: 1,
|
|
71
|
+
parseHTML: element => {
|
|
72
|
+
const rowspan = element.getAttribute("rowspan")
|
|
73
|
+
return rowspan ? parseInt(rowspan, 10) : 1
|
|
74
|
+
},
|
|
75
|
+
renderHTML: attributes => {
|
|
76
|
+
if (attributes.rowspan === 1) return {}
|
|
77
|
+
return { rowspan: attributes.rowspan }
|
|
78
|
+
}
|
|
79
|
+
},
|
|
80
|
+
|
|
81
|
+
// Column width for resizing
|
|
82
|
+
colwidth: {
|
|
83
|
+
default: null,
|
|
84
|
+
parseHTML: element => {
|
|
85
|
+
const colwidth = element.getAttribute("colwidth")
|
|
86
|
+
return colwidth ? colwidth.split(",").map(Number) : null
|
|
87
|
+
},
|
|
88
|
+
renderHTML: attributes => {
|
|
89
|
+
if (!attributes.colwidth) return {}
|
|
90
|
+
return {
|
|
91
|
+
colwidth: attributes.colwidth.join(","),
|
|
92
|
+
style: `width: ${attributes.colwidth[0]}px`
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
export default InkpenTableHeader
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* InkpenTable Constants
|
|
3
|
+
*
|
|
4
|
+
* Colors, menu items, and configuration for the enhanced table extension.
|
|
5
|
+
*
|
|
6
|
+
* @since 0.8.0
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
// =============================================================================
|
|
10
|
+
// Text Colors
|
|
11
|
+
// =============================================================================
|
|
12
|
+
|
|
13
|
+
export const TEXT_COLORS = [
|
|
14
|
+
{ name: "default", label: "Default", value: null },
|
|
15
|
+
{ name: "gray", label: "Gray", value: "#787774" },
|
|
16
|
+
{ name: "red", label: "Red", value: "#d44c47" },
|
|
17
|
+
{ name: "orange", label: "Orange", value: "#d9730d" },
|
|
18
|
+
{ name: "yellow", label: "Yellow", value: "#cb912f" },
|
|
19
|
+
{ name: "green", label: "Green", value: "#448361" },
|
|
20
|
+
{ name: "blue", label: "Blue", value: "#337ea9" },
|
|
21
|
+
{ name: "purple", label: "Purple", value: "#9065b0" },
|
|
22
|
+
{ name: "pink", label: "Pink", value: "#c14c8a" }
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
// =============================================================================
|
|
26
|
+
// Background Colors
|
|
27
|
+
// =============================================================================
|
|
28
|
+
|
|
29
|
+
export const BACKGROUND_COLORS = [
|
|
30
|
+
{ name: "default", label: "Default", value: null },
|
|
31
|
+
{ name: "gray", label: "Gray", value: "rgba(120, 120, 120, 0.12)" },
|
|
32
|
+
{ name: "red", label: "Red", value: "rgba(239, 68, 68, 0.15)" },
|
|
33
|
+
{ name: "orange", label: "Orange", value: "rgba(249, 115, 22, 0.15)" },
|
|
34
|
+
{ name: "yellow", label: "Yellow", value: "rgba(234, 179, 8, 0.15)" },
|
|
35
|
+
{ name: "green", label: "Green", value: "rgba(34, 197, 94, 0.15)" },
|
|
36
|
+
{ name: "blue", label: "Blue", value: "rgba(59, 130, 246, 0.15)" },
|
|
37
|
+
{ name: "purple", label: "Purple", value: "rgba(168, 85, 247, 0.15)" },
|
|
38
|
+
{ name: "pink", label: "Pink", value: "rgba(236, 72, 153, 0.15)" }
|
|
39
|
+
]
|
|
40
|
+
|
|
41
|
+
// =============================================================================
|
|
42
|
+
// Table Variants
|
|
43
|
+
// =============================================================================
|
|
44
|
+
|
|
45
|
+
export const TABLE_VARIANTS = [
|
|
46
|
+
{ name: "default", label: "Default" },
|
|
47
|
+
{ name: "striped", label: "Striped" },
|
|
48
|
+
{ name: "borderless", label: "Borderless" },
|
|
49
|
+
{ name: "minimal", label: "Minimal" }
|
|
50
|
+
]
|
|
51
|
+
|
|
52
|
+
// =============================================================================
|
|
53
|
+
// Row Menu Items
|
|
54
|
+
// =============================================================================
|
|
55
|
+
|
|
56
|
+
export const ROW_MENU_ITEMS = [
|
|
57
|
+
{ type: "action", id: "addRowAbove", label: "Add row above", icon: "↑" },
|
|
58
|
+
{ type: "action", id: "addRowBelow", label: "Add row below", icon: "↓" },
|
|
59
|
+
{ type: "separator" },
|
|
60
|
+
{ type: "action", id: "duplicateRow", label: "Duplicate row", icon: "⧉" },
|
|
61
|
+
{ type: "action", id: "deleteRow", label: "Delete row", icon: "✕", danger: true },
|
|
62
|
+
{ type: "separator" },
|
|
63
|
+
{ type: "action", id: "moveRowUp", label: "Move up", icon: "↑" },
|
|
64
|
+
{ type: "action", id: "moveRowDown", label: "Move down", icon: "↓" },
|
|
65
|
+
{ type: "separator" },
|
|
66
|
+
{ type: "action", id: "toggleHeaderRow", label: "Toggle header", icon: "H" },
|
|
67
|
+
{ type: "separator" },
|
|
68
|
+
{ type: "submenu", id: "textColor", label: "Text color", icon: "A" },
|
|
69
|
+
{ type: "submenu", id: "backgroundColor", label: "Background", icon: "◼" }
|
|
70
|
+
]
|
|
71
|
+
|
|
72
|
+
// =============================================================================
|
|
73
|
+
// Column Menu Items
|
|
74
|
+
// =============================================================================
|
|
75
|
+
|
|
76
|
+
export const COLUMN_MENU_ITEMS = [
|
|
77
|
+
{ type: "action", id: "addColumnLeft", label: "Add column left", icon: "←" },
|
|
78
|
+
{ type: "action", id: "addColumnRight", label: "Add column right", icon: "→" },
|
|
79
|
+
{ type: "separator" },
|
|
80
|
+
{ type: "action", id: "duplicateColumn", label: "Duplicate column", icon: "⧉" },
|
|
81
|
+
{ type: "action", id: "deleteColumn", label: "Delete column", icon: "✕", danger: true },
|
|
82
|
+
{ type: "separator" },
|
|
83
|
+
{ type: "action", id: "moveColumnLeft", label: "Move left", icon: "←" },
|
|
84
|
+
{ type: "action", id: "moveColumnRight", label: "Move right", icon: "→" },
|
|
85
|
+
{ type: "separator" },
|
|
86
|
+
{ type: "submenu", id: "alignment", label: "Alignment", icon: "≡" },
|
|
87
|
+
{ type: "separator" },
|
|
88
|
+
{ type: "submenu", id: "textColor", label: "Text color", icon: "A" },
|
|
89
|
+
{ type: "submenu", id: "backgroundColor", label: "Background", icon: "◼" }
|
|
90
|
+
]
|
|
91
|
+
|
|
92
|
+
// =============================================================================
|
|
93
|
+
// Alignment Options
|
|
94
|
+
// =============================================================================
|
|
95
|
+
|
|
96
|
+
export const ALIGNMENT_OPTIONS = [
|
|
97
|
+
{ name: "left", label: "Left", icon: "←" },
|
|
98
|
+
{ name: "center", label: "Center", icon: "↔" },
|
|
99
|
+
{ name: "right", label: "Right", icon: "→" }
|
|
100
|
+
]
|
|
101
|
+
|
|
102
|
+
// =============================================================================
|
|
103
|
+
// Default Configuration
|
|
104
|
+
// =============================================================================
|
|
105
|
+
|
|
106
|
+
export const DEFAULT_CONFIG = {
|
|
107
|
+
resizable: true,
|
|
108
|
+
defaultVariant: "default",
|
|
109
|
+
showHandles: true,
|
|
110
|
+
showAddButtons: true,
|
|
111
|
+
showCaption: true,
|
|
112
|
+
stickyHeader: false,
|
|
113
|
+
cellBackgrounds: BACKGROUND_COLORS,
|
|
114
|
+
textColors: TEXT_COLORS
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// =============================================================================
|
|
118
|
+
// CSS Classes
|
|
119
|
+
// =============================================================================
|
|
120
|
+
|
|
121
|
+
export const CSS_CLASSES = {
|
|
122
|
+
wrapper: "inkpen-table-wrapper",
|
|
123
|
+
table: "inkpen-table",
|
|
124
|
+
colHandles: "inkpen-table__col-handles",
|
|
125
|
+
rowHandles: "inkpen-table__row-handles",
|
|
126
|
+
handle: "inkpen-table__handle",
|
|
127
|
+
handleActive: "inkpen-table__handle--active",
|
|
128
|
+
addRow: "inkpen-table__add-row",
|
|
129
|
+
addCol: "inkpen-table__add-col",
|
|
130
|
+
body: "inkpen-table__body",
|
|
131
|
+
content: "inkpen-table__content",
|
|
132
|
+
caption: "inkpen-table__caption",
|
|
133
|
+
menu: "inkpen-table-menu",
|
|
134
|
+
menuItem: "inkpen-table-menu__item",
|
|
135
|
+
menuSeparator: "inkpen-table-menu__separator",
|
|
136
|
+
menuSubmenu: "inkpen-table-menu__submenu",
|
|
137
|
+
colorSwatch: "inkpen-table-menu__color-swatch",
|
|
138
|
+
selected: "inkpen-table--selected",
|
|
139
|
+
rowSelected: "inkpen-table__row--selected",
|
|
140
|
+
colSelected: "inkpen-table__col--selected"
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// =============================================================================
|
|
144
|
+
// Keyboard Shortcuts
|
|
145
|
+
// =============================================================================
|
|
146
|
+
|
|
147
|
+
export const KEYBOARD_SHORTCUTS = {
|
|
148
|
+
moveRowUp: { key: "ArrowUp", modifiers: ["Mod", "Shift"] },
|
|
149
|
+
moveRowDown: { key: "ArrowDown", modifiers: ["Mod", "Shift"] },
|
|
150
|
+
moveColumnLeft: { key: "ArrowLeft", modifiers: ["Mod", "Shift"] },
|
|
151
|
+
moveColumnRight: { key: "ArrowRight", modifiers: ["Mod", "Shift"] }
|
|
152
|
+
}
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* InkpenTable Helpers
|
|
3
|
+
*
|
|
4
|
+
* DOM utilities and positioning functions for the enhanced table extension.
|
|
5
|
+
* Follows Fizzy patterns: named function exports, no object exports.
|
|
6
|
+
*
|
|
7
|
+
* @since 0.8.0
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
// =============================================================================
|
|
11
|
+
// DOM Utilities
|
|
12
|
+
// =============================================================================
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Create an element with optional attributes and children
|
|
16
|
+
*/
|
|
17
|
+
export function createElement(tag, attrs = {}, children = []) {
|
|
18
|
+
const el = document.createElement(tag)
|
|
19
|
+
|
|
20
|
+
for (const [key, value] of Object.entries(attrs)) {
|
|
21
|
+
if (key === "className") {
|
|
22
|
+
el.className = value
|
|
23
|
+
} else if (key === "dataset") {
|
|
24
|
+
Object.assign(el.dataset, value)
|
|
25
|
+
} else if (key.startsWith("on") && typeof value === "function") {
|
|
26
|
+
el.addEventListener(key.slice(2).toLowerCase(), value)
|
|
27
|
+
} else {
|
|
28
|
+
el.setAttribute(key, value)
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
for (const child of children) {
|
|
33
|
+
if (typeof child === "string") {
|
|
34
|
+
el.appendChild(document.createTextNode(child))
|
|
35
|
+
} else if (child) {
|
|
36
|
+
el.appendChild(child)
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return el
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Wait for next animation frame
|
|
45
|
+
*/
|
|
46
|
+
export function nextFrame() {
|
|
47
|
+
return new Promise(resolve => requestAnimationFrame(resolve))
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Wait for multiple animation frames
|
|
52
|
+
*/
|
|
53
|
+
export function waitFrames(count = 2) {
|
|
54
|
+
return new Promise(resolve => {
|
|
55
|
+
let remaining = count
|
|
56
|
+
const tick = () => {
|
|
57
|
+
remaining--
|
|
58
|
+
if (remaining <= 0) {
|
|
59
|
+
resolve()
|
|
60
|
+
} else {
|
|
61
|
+
requestAnimationFrame(tick)
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
requestAnimationFrame(tick)
|
|
65
|
+
})
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// =============================================================================
|
|
69
|
+
// Positioning
|
|
70
|
+
// =============================================================================
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Position an element below a reference element
|
|
74
|
+
*/
|
|
75
|
+
export function positionBelow(element, reference, options = {}) {
|
|
76
|
+
const { offsetX = 0, offsetY = 4 } = options
|
|
77
|
+
const refRect = reference.getBoundingClientRect()
|
|
78
|
+
const elRect = element.getBoundingClientRect()
|
|
79
|
+
|
|
80
|
+
let left = refRect.left + offsetX
|
|
81
|
+
let top = refRect.bottom + offsetY
|
|
82
|
+
|
|
83
|
+
// Keep within viewport horizontally
|
|
84
|
+
const viewportWidth = window.innerWidth
|
|
85
|
+
if (left + elRect.width > viewportWidth - 8) {
|
|
86
|
+
left = viewportWidth - elRect.width - 8
|
|
87
|
+
}
|
|
88
|
+
if (left < 8) {
|
|
89
|
+
left = 8
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Flip above if not enough space below
|
|
93
|
+
const viewportHeight = window.innerHeight
|
|
94
|
+
if (top + elRect.height > viewportHeight - 8) {
|
|
95
|
+
top = refRect.top - elRect.height - offsetY
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
element.style.position = "fixed"
|
|
99
|
+
element.style.left = `${left}px`
|
|
100
|
+
element.style.top = `${top}px`
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Position an element to the right of a reference element (for submenus)
|
|
105
|
+
*/
|
|
106
|
+
export function positionRight(element, reference, options = {}) {
|
|
107
|
+
const { offsetX = 0, offsetY = 0 } = options
|
|
108
|
+
const refRect = reference.getBoundingClientRect()
|
|
109
|
+
const elRect = element.getBoundingClientRect()
|
|
110
|
+
|
|
111
|
+
let left = refRect.right + offsetX
|
|
112
|
+
let top = refRect.top + offsetY
|
|
113
|
+
|
|
114
|
+
// Flip to left if not enough space on right
|
|
115
|
+
const viewportWidth = window.innerWidth
|
|
116
|
+
if (left + elRect.width > viewportWidth - 8) {
|
|
117
|
+
left = refRect.left - elRect.width - offsetX
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Keep within viewport vertically
|
|
121
|
+
const viewportHeight = window.innerHeight
|
|
122
|
+
if (top + elRect.height > viewportHeight - 8) {
|
|
123
|
+
top = viewportHeight - elRect.height - 8
|
|
124
|
+
}
|
|
125
|
+
if (top < 8) {
|
|
126
|
+
top = 8
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
element.style.position = "fixed"
|
|
130
|
+
element.style.left = `${left}px`
|
|
131
|
+
element.style.top = `${top}px`
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// =============================================================================
|
|
135
|
+
// Table Utilities
|
|
136
|
+
// =============================================================================
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Get table dimensions from a table element
|
|
140
|
+
*/
|
|
141
|
+
export function getTableDimensions(tableElement) {
|
|
142
|
+
const rows = tableElement.querySelectorAll("tr")
|
|
143
|
+
const rowCount = rows.length
|
|
144
|
+
const colCount = rows[0]?.querySelectorAll("td, th").length || 0
|
|
145
|
+
|
|
146
|
+
return { rowCount, colCount }
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Get row index from a cell element
|
|
151
|
+
*/
|
|
152
|
+
export function getRowIndex(cellElement) {
|
|
153
|
+
const row = cellElement.closest("tr")
|
|
154
|
+
if (!row) return -1
|
|
155
|
+
|
|
156
|
+
const tbody = row.parentElement
|
|
157
|
+
return Array.from(tbody.children).indexOf(row)
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Get column index from a cell element
|
|
162
|
+
*/
|
|
163
|
+
export function getColumnIndex(cellElement) {
|
|
164
|
+
const row = cellElement.closest("tr")
|
|
165
|
+
if (!row) return -1
|
|
166
|
+
|
|
167
|
+
return Array.from(row.children).indexOf(cellElement)
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Find the table wrapper from any child element
|
|
172
|
+
*/
|
|
173
|
+
export function findTableWrapper(element) {
|
|
174
|
+
return element.closest(".inkpen-table-wrapper")
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Find the actual table element from a wrapper
|
|
179
|
+
*/
|
|
180
|
+
export function findTableElement(wrapper) {
|
|
181
|
+
return wrapper?.querySelector("table")
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// =============================================================================
|
|
185
|
+
// Selection Utilities
|
|
186
|
+
// =============================================================================
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Check if a ProseMirror selection is a cell selection
|
|
190
|
+
*/
|
|
191
|
+
export function isCellSelection(selection) {
|
|
192
|
+
return selection && selection.$anchorCell !== undefined
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Get selected cell positions from a CellSelection
|
|
197
|
+
*/
|
|
198
|
+
export function getSelectedCells(selection) {
|
|
199
|
+
if (!isCellSelection(selection)) return []
|
|
200
|
+
|
|
201
|
+
const cells = []
|
|
202
|
+
selection.forEachCell((node, pos) => {
|
|
203
|
+
cells.push({ node, pos })
|
|
204
|
+
})
|
|
205
|
+
return cells
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// =============================================================================
|
|
209
|
+
// Event Utilities
|
|
210
|
+
// =============================================================================
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Stop event propagation and prevent default
|
|
214
|
+
*/
|
|
215
|
+
export function stopEvent(event) {
|
|
216
|
+
event.preventDefault()
|
|
217
|
+
event.stopPropagation()
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Create a click-outside handler
|
|
222
|
+
*/
|
|
223
|
+
export function onClickOutside(element, callback) {
|
|
224
|
+
const handler = (event) => {
|
|
225
|
+
if (!element.contains(event.target)) {
|
|
226
|
+
callback(event)
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
document.addEventListener("mousedown", handler)
|
|
231
|
+
document.addEventListener("touchstart", handler)
|
|
232
|
+
|
|
233
|
+
return () => {
|
|
234
|
+
document.removeEventListener("mousedown", handler)
|
|
235
|
+
document.removeEventListener("touchstart", handler)
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Create an escape key handler
|
|
241
|
+
*/
|
|
242
|
+
export function onEscapeKey(callback) {
|
|
243
|
+
const handler = (event) => {
|
|
244
|
+
if (event.key === "Escape") {
|
|
245
|
+
callback(event)
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
document.addEventListener("keydown", handler)
|
|
250
|
+
|
|
251
|
+
return () => {
|
|
252
|
+
document.removeEventListener("keydown", handler)
|
|
253
|
+
}
|
|
254
|
+
}
|