collavre 0.22.0 → 0.24.0
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 +4 -4
- data/README.md +1 -0
- data/app/assets/images/collavre/landing/interface-en-dark.png +0 -0
- data/app/assets/images/collavre/landing/interface-en.png +0 -0
- data/app/assets/images/collavre/landing/interface-ko-dark.png +0 -0
- data/app/assets/images/collavre/landing/interface-ko.png +0 -0
- data/app/assets/stylesheets/collavre/actiontext.css +251 -90
- data/app/assets/stylesheets/collavre/code_highlight.css +7 -201
- data/app/assets/stylesheets/collavre/comments_popup.css +158 -61
- data/app/assets/stylesheets/collavre/creatives.css +11 -2
- data/app/assets/stylesheets/collavre/landing.css +92 -0
- data/app/assets/stylesheets/collavre/modal_dialog.css +32 -0
- data/app/assets/stylesheets/collavre/popup.css +4 -2
- data/app/assets/stylesheets/collavre/tables.css +91 -0
- data/app/channels/collavre/inbox_badge_channel.rb +30 -0
- data/app/controllers/collavre/admin/settings_controller.rb +52 -67
- data/app/controllers/collavre/api/v1/agents_controller.rb +17 -188
- data/app/controllers/collavre/api/v1/base_controller.rb +9 -27
- data/app/controllers/collavre/api/v1/mobile/agent_events_controller.rb +224 -0
- data/app/controllers/collavre/api/v1/mobile/base_controller.rb +95 -0
- data/app/controllers/collavre/api/v1/mobile/devices_controller.rb +31 -0
- data/app/controllers/collavre/api/v1/mobile/voice_commands_controller.rb +25 -0
- data/app/controllers/collavre/attachments_controller.rb +13 -3
- data/app/controllers/collavre/concerns/slide_viewable.rb +8 -32
- data/app/controllers/collavre/creatives_controller.rb +47 -103
- data/app/controllers/collavre/inbox_items_controller.rb +10 -4
- data/app/controllers/collavre/landing_controller.rb +5 -2
- data/app/controllers/collavre/tasks_controller.rb +13 -4
- data/app/controllers/collavre/topics_controller.rb +54 -1
- data/app/controllers/collavre/typo_corrections_controller.rb +39 -0
- data/app/controllers/concerns/collavre/api/authenticatable.rb +40 -0
- data/app/controllers/concerns/collavre/users_controller/profile_and_settings.rb +16 -1
- data/app/controllers/concerns/collavre/users_controller/registration.rb +41 -1
- data/app/errors/collavre/api_error.rb +20 -0
- data/app/helpers/collavre/application_helper.rb +1 -0
- data/app/helpers/collavre/creatives_helper.rb +30 -8
- data/app/javascript/collavre.js +2 -0
- data/app/javascript/components/ImageResizer.jsx +9 -3
- data/app/javascript/components/InlineLexicalEditor.jsx +157 -72
- data/app/javascript/components/__tests__/creative_tree_row_parent_id.test.js +87 -0
- data/app/javascript/components/creative_tree_row.js +28 -5
- data/app/javascript/components/plugins/list_tab_indent_plugin.jsx +16 -0
- data/app/javascript/components/plugins/markdown_shortcuts_plugin.jsx +15 -4
- data/app/javascript/components/plugins/table_hover_actions_plugin.jsx +405 -0
- data/app/javascript/controllers/__tests__/inbox_badge_controller.test.js +73 -0
- data/app/javascript/controllers/__tests__/search_popup_controller.test.js +104 -0
- data/app/javascript/controllers/__tests__/topic_list_controller.test.js +82 -0
- data/app/javascript/controllers/comment_controller.js +5 -4
- data/app/javascript/controllers/comment_version_controller.js +2 -1
- data/app/javascript/controllers/comments/__tests__/form_controller_double_submit.test.js +159 -0
- data/app/javascript/controllers/comments/__tests__/presence_controller.test.js +3 -2
- data/app/javascript/controllers/comments/__tests__/topics_controller_delete.test.js +94 -0
- data/app/javascript/controllers/comments/__tests__/topics_controller_topic_list.test.js +61 -0
- data/app/javascript/controllers/comments/form_controller.js +21 -5
- data/app/javascript/controllers/comments/list_controller.js +18 -17
- data/app/javascript/controllers/comments/presence_controller.js +2 -1
- data/app/javascript/controllers/comments/topics_controller.js +61 -8
- data/app/javascript/controllers/common_popup_controller.js +2 -2
- data/app/javascript/controllers/creatives/__tests__/tree_controller.test.js +150 -0
- data/app/javascript/controllers/creatives/import_controller.js +2 -1
- data/app/javascript/controllers/creatives/select_mode_controller.js +2 -1
- data/app/javascript/controllers/creatives/tree_controller.js +142 -1
- data/app/javascript/controllers/image_lightbox_controller.js +2 -1
- data/app/javascript/controllers/inbox_badge_controller.js +33 -0
- data/app/javascript/controllers/index.js +7 -1
- data/app/javascript/controllers/search_popup_controller.js +17 -1
- data/app/javascript/controllers/share_modal_controller.js +4 -3
- data/app/javascript/controllers/topic_list_controller.js +78 -0
- data/app/javascript/controllers/topic_search_controller.js +2 -1
- data/app/javascript/creatives/drag_drop/event_handlers.js +14 -5
- data/app/javascript/creatives/topic_move_members_popup.js +156 -0
- data/app/javascript/creatives/tree_renderer.js +11 -0
- data/app/javascript/lib/__tests__/common_popup_bounds.test.js +58 -0
- data/app/javascript/lib/__tests__/turbo_confirm.test.js +81 -0
- data/app/javascript/lib/__tests__/typo_correction.test.js +192 -0
- data/app/javascript/lib/api/__tests__/api_error.test.js +96 -0
- data/app/javascript/lib/api/__tests__/queue_manager.test.js +88 -1
- data/app/javascript/lib/api/api_error.js +108 -0
- data/app/javascript/lib/api/queue_manager.js +38 -4
- data/app/javascript/lib/common_popup.js +42 -10
- data/app/javascript/lib/editor/__tests__/code_edit_view_token_parity.test.js +121 -0
- data/app/javascript/lib/editor/__tests__/code_language_roundtrip.test.js +152 -0
- data/app/javascript/lib/editor/__tests__/code_languages.test.js +93 -0
- data/app/javascript/lib/editor/code_languages.js +173 -0
- data/app/javascript/lib/editor/code_token_theme.js +41 -0
- data/app/javascript/lib/lexical/__tests__/code_block_toggle.test.js +326 -0
- data/app/javascript/lib/lexical/__tests__/code_fence_shortcut.test.js +179 -0
- data/app/javascript/lib/lexical/__tests__/image_focus.test.js +139 -0
- data/app/javascript/lib/lexical/__tests__/list_tab_indent.test.js +633 -0
- data/app/javascript/lib/lexical/__tests__/markdown_serialize.test.js +627 -0
- data/app/javascript/lib/lexical/__tests__/minimize_html.test.js +20 -1
- data/app/javascript/lib/lexical/__tests__/selection_boundary.test.js +88 -0
- data/app/javascript/lib/lexical/__tests__/table_transformer.test.js +163 -0
- data/app/javascript/lib/lexical/__tests__/trailing_paragraph.test.js +104 -0
- data/app/javascript/lib/lexical/code_block_toggle.js +130 -0
- data/app/javascript/lib/lexical/code_fence_shortcut.js +60 -0
- data/app/javascript/lib/lexical/list_tab_indent.js +210 -0
- data/app/javascript/lib/lexical/markdown_serialize.js +320 -0
- data/app/javascript/lib/lexical/selection_boundary.js +58 -0
- data/app/javascript/lib/lexical/table_transformer.js +182 -0
- data/app/javascript/lib/lexical/trailing_paragraph.js +29 -0
- data/app/javascript/lib/turbo_confirm.js +46 -0
- data/app/javascript/lib/typo_correction.js +146 -0
- data/app/javascript/lib/utils/__tests__/confirm_dialog.test.js +88 -0
- data/app/javascript/lib/utils/__tests__/dialog.test.js +92 -0
- data/app/javascript/lib/utils/__tests__/markdown.test.js +153 -0
- data/app/javascript/lib/utils/__tests__/sanitize_description.test.js +68 -0
- data/app/javascript/lib/utils/__tests__/table_download.test.js +93 -0
- data/app/javascript/lib/utils/confirm_dialog.js +10 -0
- data/app/javascript/lib/utils/dialog.js +300 -0
- data/app/javascript/lib/utils/markdown.js +154 -67
- data/app/javascript/lib/utils/sanitize_description.js +31 -0
- data/app/javascript/lib/utils/table_download.js +15 -0
- data/app/javascript/modules/__tests__/creative_inline_payload.test.js +231 -0
- data/app/javascript/modules/__tests__/creative_save_queue.test.js +228 -0
- data/app/javascript/modules/__tests__/creative_tree_dom.test.js +425 -0
- data/app/javascript/modules/__tests__/typo_corrector.test.js +365 -0
- data/app/javascript/modules/creative_inline_payload.js +86 -0
- data/app/javascript/modules/creative_row_editor.js +231 -471
- data/app/javascript/modules/creative_row_editor_helpers.js +76 -0
- data/app/javascript/modules/creative_save_queue.js +95 -0
- data/app/javascript/modules/creative_tree_dom.js +260 -0
- data/app/javascript/modules/export_to_markdown.js +2 -1
- data/app/javascript/modules/lexical_inline_editor.jsx +2 -1
- data/app/javascript/modules/slide_view.js +11 -2
- data/app/javascript/modules/typo_corrector.js +534 -0
- data/app/jobs/collavre/ai_agent_job.rb +7 -4
- data/app/jobs/collavre/compress_job.rb +6 -2
- data/app/jobs/collavre/permission_cache_job.rb +20 -2
- data/app/jobs/collavre/trigger_loop_check_job.rb +20 -3
- data/app/jobs/collavre/update_mcp_tools_job.rb +17 -0
- data/app/models/collavre/channel.rb +16 -0
- data/app/models/collavre/comment/approvable.rb +16 -0
- data/app/models/collavre/comment/broadcastable.rb +46 -7
- data/app/models/collavre/comment/notifiable.rb +15 -5
- data/app/models/collavre/comment.rb +87 -32
- data/app/models/collavre/comment_version.rb +1 -1
- data/app/models/collavre/creative/describable.rb +104 -19
- data/app/models/collavre/creative/linkable.rb +9 -0
- data/app/models/collavre/creative/permissible.rb +97 -49
- data/app/models/collavre/creative.rb +73 -5
- data/app/models/collavre/creative_share.rb +78 -26
- data/app/models/collavre/inbox_item.rb +2 -2
- data/app/models/collavre/task.rb +27 -0
- data/app/models/collavre/user.rb +90 -10
- data/app/models/concerns/collavre/indexed_json_columns.rb +58 -0
- data/app/services/collavre/agent_type_classifier.rb +23 -0
- data/app/services/collavre/ai_agent/a2a_dispatcher.rb +4 -0
- data/app/services/collavre/ai_agent/message_builder.rb +1 -1
- data/app/services/collavre/ai_agent/session_provisioner.rb +126 -0
- data/app/services/collavre/ai_agent/task_claim_service.rb +96 -0
- data/app/services/collavre/ai_client.rb +81 -12
- data/app/services/collavre/auto_theme_generator.rb +15 -11
- data/app/services/collavre/creatives/children_index.rb +86 -0
- data/app/services/collavre/creatives/comment_badge_index.rb +73 -0
- data/app/services/collavre/creatives/creative_tree_serializer.rb +122 -0
- data/app/services/collavre/creatives/effective_creative_resolution.rb +37 -0
- data/app/services/collavre/creatives/index_query.rb +85 -16
- data/app/services/collavre/creatives/permission_checker.rb +1 -1
- data/app/services/collavre/creatives/permission_filter.rb +144 -17
- data/app/services/collavre/creatives/tree_builder.rb +107 -78
- data/app/services/collavre/gemini_parent_recommender.rb +1 -1
- data/app/services/collavre/http_client.rb +119 -0
- data/app/services/collavre/inbox_reply_service.rb +5 -0
- data/app/services/collavre/link_preview_fetcher.rb +129 -38
- data/app/services/collavre/markdown_converter.rb +13 -3
- data/app/services/collavre/markdown_importer.rb +30 -6
- data/app/services/collavre/mobile/event_summarizer.rb +40 -0
- data/app/services/collavre/orchestration/agent_context_builder.rb +1 -15
- data/app/services/collavre/orchestration/agent_orchestrator.rb +34 -8
- data/app/services/collavre/orchestration/arbiter.rb +16 -0
- data/app/services/collavre/orchestration/matcher.rb +79 -4
- data/app/services/collavre/orchestration/policy_resolver.rb +4 -3
- data/app/services/collavre/orchestration/stuck_detector.rb +141 -34
- data/app/services/collavre/system_events/context_builder.rb +1 -14
- data/app/services/collavre/tools/creative_batch_service.rb +3 -2
- data/app/services/collavre/tools/creative_create_service.rb +8 -8
- data/app/services/collavre/tools/creative_update_service.rb +23 -8
- data/app/services/collavre/tools/preview_attach_service.rb +5 -7
- data/app/services/collavre/typo_corrector.rb +188 -0
- data/app/views/collavre/comments/_comment.html.erb +9 -4
- data/app/views/collavre/comments/_comments_popup.html.erb +25 -4
- data/app/views/collavre/creatives/_inline_edit_form.html.erb +10 -0
- data/app/views/collavre/creatives/_topic_move_members_modal.html.erb +42 -0
- data/app/views/collavre/creatives/index.html.erb +14 -1
- data/app/views/collavre/creatives/slide_view.html.erb +1 -1
- data/app/views/collavre/landing/show.html.erb +52 -10
- data/app/views/collavre/users/show.html.erb +3 -0
- data/app/views/collavre/users/typo_correction.html.erb +50 -0
- data/app/views/layouts/collavre/slide.html.erb +1 -0
- data/config/locales/comments.en.yml +16 -0
- data/config/locales/comments.ko.yml +16 -0
- data/config/locales/creatives.en.yml +6 -0
- data/config/locales/creatives.ko.yml +5 -0
- data/config/locales/integrations.en.yml +1 -1
- data/config/locales/integrations.ko.yml +1 -1
- data/config/locales/landing.en.yml +22 -10
- data/config/locales/landing.ko.yml +22 -10
- data/config/locales/mobile.en.yml +16 -0
- data/config/locales/mobile.ko.yml +16 -0
- data/config/locales/orchestration.en.yml +1 -0
- data/config/locales/orchestration.ko.yml +1 -0
- data/config/locales/users.en.yml +15 -0
- data/config/locales/users.ko.yml +15 -0
- data/config/routes.rb +13 -0
- data/db/migrate/20260120045354_encrypt_oauth_tokens.rb +13 -4
- data/db/migrate/20260612000000_add_topic_concurrency_defer_to_comments.rb +38 -0
- data/db/migrate/20260617090000_add_typo_correction_settings_to_users.rb +18 -0
- data/db/migrate/20260702000001_enforce_system_settings_key_not_null.rb +9 -0
- data/db/migrate/20260702000002_add_creative_index_and_fk_to_tags.rb +11 -0
- data/db/migrate/20260702000003_add_creative_type_index_to_labels.rb +10 -0
- data/db/migrate/20260702000004_add_creative_created_at_index_to_comments.rb +10 -0
- data/db/migrate/20260702000005_promote_channel_config_index_columns.rb +76 -0
- data/db/migrate/20260713000001_add_comments_count_to_creatives.rb +20 -0
- data/db/migrate/20260713000002_add_comment_versions_count_to_comments.rb +20 -0
- data/db/migrate/20260715000000_change_creatives_description_to_plain_text.rb +16 -0
- data/db/seeds.rb +51 -0
- data/lib/collavre/version.rb +1 -1
- data/lib/generators/collavre/install/install_generator.rb +1 -0
- metadata +96 -2
- data/app/services/collavre/tools/description_normalizable.rb +0 -16
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Regression: parentId single source of truth.
|
|
3
|
+
*
|
|
4
|
+
* The tree row component used to render `data-parent-id=${this.parentId ?? nothing}`,
|
|
5
|
+
* which OMITTED the attribute whenever parentId was null. `addNew` derives a new
|
|
6
|
+
* sibling's parent from `prev.dataset.parentId`, so an absent attribute produced an
|
|
7
|
+
* `undefined` parent downstream and lost the real parent for newly-added siblings.
|
|
8
|
+
*
|
|
9
|
+
* The single convention: `data-parent-id` is ALWAYS emitted on `.creative-tree`.
|
|
10
|
+
* A non-empty value is the parent id; "" means "root / no parent". This mirrors the
|
|
11
|
+
* dynamic-creation path (`dataset.parentId = parentId || ''`).
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
// The component calls `customElements.define(...)` at module-eval time and reads
|
|
15
|
+
// `customElements` as a bare global. The jest environment only exposes it on
|
|
16
|
+
// `window`, so bridge it before importing the component.
|
|
17
|
+
beforeAll(() => {
|
|
18
|
+
if (typeof globalThis.customElements === "undefined") {
|
|
19
|
+
globalThis.customElements = window.customElements;
|
|
20
|
+
}
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
async function mountRow(props) {
|
|
24
|
+
await import("../creative_tree_row.js");
|
|
25
|
+
const el = document.createElement("creative-tree-row");
|
|
26
|
+
Object.assign(el, props);
|
|
27
|
+
document.body.appendChild(el);
|
|
28
|
+
await el.updateComplete;
|
|
29
|
+
return el;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// The `.creative-tree` element is what the editor reads via `tree.dataset.parentId`.
|
|
33
|
+
function treeOf(el) {
|
|
34
|
+
return el.querySelector(".creative-tree");
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
afterEach(() => {
|
|
38
|
+
document.body.innerHTML = "";
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
describe("creative-tree-row parentId convention", () => {
|
|
42
|
+
test("non-title row under a page-title parent keeps the parent id", async () => {
|
|
43
|
+
const el = await mountRow({ creativeId: "8", parentId: "5", level: 2 });
|
|
44
|
+
const tree = treeOf(el);
|
|
45
|
+
|
|
46
|
+
// The attribute is always present and carries the real parent id — this is
|
|
47
|
+
// exactly what addNew reads (`prev.dataset.parentId`) when deriving a sibling.
|
|
48
|
+
expect(tree.hasAttribute("data-parent-id")).toBe(true);
|
|
49
|
+
expect(tree.dataset.parentId).toBe("5");
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test("root row emits an explicit empty marker instead of omitting the attribute", async () => {
|
|
53
|
+
const el = await mountRow({ creativeId: "5", parentId: null, level: 1 });
|
|
54
|
+
const tree = treeOf(el);
|
|
55
|
+
|
|
56
|
+
// Before the fix this attribute was omitted (ambiguous root vs unknown).
|
|
57
|
+
expect(tree.hasAttribute("data-parent-id")).toBe(true);
|
|
58
|
+
expect(tree.dataset.parentId).toBe("");
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test("title row preserves its own parent (nested page) instead of dropping it", async () => {
|
|
62
|
+
const el = await mountRow({ creativeId: "5", parentId: "2", isTitle: true });
|
|
63
|
+
const tree = treeOf(el);
|
|
64
|
+
|
|
65
|
+
expect(tree.classList.contains("creative-tree-title")).toBe(true);
|
|
66
|
+
expect(tree.hasAttribute("data-parent-id")).toBe(true);
|
|
67
|
+
expect(tree.dataset.parentId).toBe("2");
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test("root title row emits explicit empty marker", async () => {
|
|
71
|
+
const el = await mountRow({ creativeId: "5", parentId: null, isTitle: true });
|
|
72
|
+
const tree = treeOf(el);
|
|
73
|
+
|
|
74
|
+
expect(tree.hasAttribute("data-parent-id")).toBe(true);
|
|
75
|
+
expect(tree.dataset.parentId).toBe("");
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test("addNew sibling derivation reads the correct parent from a page-title child", async () => {
|
|
79
|
+
// Simulate the editor's else-branch derivation: parentId = prev.dataset.parentId
|
|
80
|
+
// for a child row that lives under a page-title parent (id 5).
|
|
81
|
+
const child = await mountRow({ creativeId: "8", parentId: "5", level: 2 });
|
|
82
|
+
const prev = treeOf(child);
|
|
83
|
+
|
|
84
|
+
const derivedParentId = prev.dataset.parentId || "";
|
|
85
|
+
expect(derivedParentId).toBe("5"); // sibling inherits the page-title parent
|
|
86
|
+
});
|
|
87
|
+
});
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { LitElement, html, svg, nothing } from "lit";
|
|
2
|
-
import DOMPurify from "dompurify";
|
|
3
2
|
import { unsafeHTML } from "lit/directives/unsafe-html.js";
|
|
4
3
|
import { parseEmojis } from "../utils/emoji_parser";
|
|
4
|
+
import { highlightCodeBlocks } from "../lib/utils/markdown";
|
|
5
|
+
import { addCreativeTableDownloadButtons } from "../lib/utils/table_download";
|
|
6
|
+
import { sanitizeDescriptionHtml } from "../lib/utils/sanitize_description";
|
|
5
7
|
import csrfFetch from "../lib/api/csrf_fetch";
|
|
6
8
|
|
|
7
9
|
const BULLET_STARTING_LEVEL = 3;
|
|
@@ -78,6 +80,19 @@ class CreativeTreeRow extends LitElement {
|
|
|
78
80
|
updated(changedProperties) {
|
|
79
81
|
this._attachHandlers();
|
|
80
82
|
|
|
83
|
+
// Re-tokenize the server-rendered description code blocks with hljs so they
|
|
84
|
+
// match the editor's palette and follow light/dark theme. Idempotent: only
|
|
85
|
+
// unmarked blocks are processed, and lit re-renders description DOM (dropping
|
|
86
|
+
// the marker) only when descriptionHtml actually changes.
|
|
87
|
+
highlightCodeBlocks(this);
|
|
88
|
+
|
|
89
|
+
// Attach CSV/Excel download toolbars to markdown tables in the description
|
|
90
|
+
// display areas so creative tables match the chat-message table UI.
|
|
91
|
+
// Scoped to .creative-content/.creative-title-content (not the whole row)
|
|
92
|
+
// so the inline editor's own tables are never wrapped. Idempotent, safe on
|
|
93
|
+
// every Lit re-render.
|
|
94
|
+
addCreativeTableDownloadButtons(this);
|
|
95
|
+
|
|
81
96
|
if (changedProperties.has('loadingChildren')) {
|
|
82
97
|
if (this.loadingChildren) {
|
|
83
98
|
this._startAnimation();
|
|
@@ -99,8 +114,10 @@ class CreativeTreeRow extends LitElement {
|
|
|
99
114
|
|
|
100
115
|
set descriptionHtml(value) {
|
|
101
116
|
const oldValue = this._descriptionHtml;
|
|
102
|
-
// Always sanitize when setting new HTML
|
|
103
|
-
|
|
117
|
+
// Always sanitize when setting new HTML. Uses the shared sanitizer so the
|
|
118
|
+
// server-generated YouTube preview iframe survives (default DOMPurify config
|
|
119
|
+
// strips all iframes, which is what broke the YouTube link preview).
|
|
120
|
+
const sanitized = sanitizeDescriptionHtml(value);
|
|
104
121
|
this._descriptionHtml = sanitized;
|
|
105
122
|
this.dataset.descriptionHtml = sanitized;
|
|
106
123
|
this.requestUpdate("descriptionHtml", oldValue);
|
|
@@ -191,6 +208,12 @@ class CreativeTreeRow extends LitElement {
|
|
|
191
208
|
if (this._progressToggle) this._progressToggle.addEventListener("click", this._handleProgressToggle, { passive: false });
|
|
192
209
|
}
|
|
193
210
|
|
|
211
|
+
// parentId convention (single source of truth): `data-parent-id` is ALWAYS
|
|
212
|
+
// emitted on the `.creative-tree` element. A non-empty value is the parent
|
|
213
|
+
// creative id; an empty string ("") means "root / no parent". This mirrors
|
|
214
|
+
// the dynamic-creation path (`dataset.parentId = parentId || ''`) so readers
|
|
215
|
+
// never have to disambiguate an *absent* attribute (unknown) from a real
|
|
216
|
+
// root, which previously dropped the parent for newly-added siblings.
|
|
194
217
|
render() {
|
|
195
218
|
if (this.isTitle) {
|
|
196
219
|
return this._renderTitle();
|
|
@@ -207,7 +230,7 @@ class CreativeTreeRow extends LitElement {
|
|
|
207
230
|
class="creative-tree"
|
|
208
231
|
id=${this.domId ?? nothing}
|
|
209
232
|
data-id=${this.creativeId ?? nothing}
|
|
210
|
-
data-parent-id=${this.parentId ??
|
|
233
|
+
data-parent-id=${this.parentId ?? ""}
|
|
211
234
|
data-level=${this.level ?? nothing}
|
|
212
235
|
draggable=${draggableAttr}
|
|
213
236
|
data-action=${dragActions}
|
|
@@ -233,7 +256,7 @@ class CreativeTreeRow extends LitElement {
|
|
|
233
256
|
class="creative-tree creative-tree-title"
|
|
234
257
|
id=${this.domId ?? nothing}
|
|
235
258
|
data-id=${this.creativeId ?? nothing}
|
|
236
|
-
data-parent-id=${this.parentId ??
|
|
259
|
+
data-parent-id=${this.parentId ?? ""}
|
|
237
260
|
>
|
|
238
261
|
<div class="creative-row" style="background-color: transparent;" data-creatives--select-mode-target="row">
|
|
239
262
|
<div class="creative-row-start" style="align-items: center;">
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { useEffect } from "react"
|
|
2
|
+
import { useLexicalComposerContext } from "@lexical/react/LexicalComposerContext"
|
|
3
|
+
import { registerListTabIndentation } from "../../lib/lexical/list_tab_indent"
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Enables Tab / Shift+Tab nesting for list items in the inline editor.
|
|
7
|
+
* The actual command handling lives in registerListTabIndentation so it can be
|
|
8
|
+
* unit-tested against a headless editor.
|
|
9
|
+
*/
|
|
10
|
+
export default function ListTabIndentPlugin() {
|
|
11
|
+
const [editor] = useLexicalComposerContext()
|
|
12
|
+
|
|
13
|
+
useEffect(() => registerListTabIndentation(editor), [editor])
|
|
14
|
+
|
|
15
|
+
return null
|
|
16
|
+
}
|
|
@@ -6,16 +6,19 @@ import {
|
|
|
6
6
|
ORDERED_LIST,
|
|
7
7
|
CODE
|
|
8
8
|
} from "@lexical/markdown"
|
|
9
|
+
import { registerCodeFenceShortcut } from "../../lib/lexical/code_fence_shortcut"
|
|
9
10
|
|
|
10
11
|
/**
|
|
11
12
|
* Markdown-style shortcuts for the creative inline editor:
|
|
12
13
|
*
|
|
13
14
|
* - "* " / "- " / "+ " at line start → unordered list
|
|
14
15
|
* - "1. " (any number) at line start → ordered list
|
|
15
|
-
* - "```"
|
|
16
|
+
* - "```" (optionally "```lang") on its own line → code block
|
|
16
17
|
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
18
|
+
* The list transformers fire on text change. The built-in CODE transformer
|
|
19
|
+
* needs "``` " (trailing space); registerCodeFenceShortcut adds the Enter path,
|
|
20
|
+
* converting a "```" (or "```lang") line to a code block when Enter is pressed —
|
|
21
|
+
* so the language can be typed before the fence commits.
|
|
19
22
|
*/
|
|
20
23
|
const CREATIVE_MARKDOWN_TRANSFORMERS = [
|
|
21
24
|
UNORDERED_LIST,
|
|
@@ -27,7 +30,15 @@ export default function MarkdownShortcutsPlugin() {
|
|
|
27
30
|
const [editor] = useLexicalComposerContext()
|
|
28
31
|
|
|
29
32
|
useEffect(() => {
|
|
30
|
-
|
|
33
|
+
const unregisterShortcuts = registerMarkdownShortcuts(
|
|
34
|
+
editor,
|
|
35
|
+
CREATIVE_MARKDOWN_TRANSFORMERS
|
|
36
|
+
)
|
|
37
|
+
const unregisterFence = registerCodeFenceShortcut(editor)
|
|
38
|
+
return () => {
|
|
39
|
+
unregisterShortcuts()
|
|
40
|
+
unregisterFence()
|
|
41
|
+
}
|
|
31
42
|
}, [editor])
|
|
32
43
|
|
|
33
44
|
return null
|
|
@@ -0,0 +1,405 @@
|
|
|
1
|
+
import { useEffect, useMemo, useRef, useState } from "react"
|
|
2
|
+
import { createPortal } from "react-dom"
|
|
3
|
+
import { useLexicalComposerContext } from "@lexical/react/LexicalComposerContext"
|
|
4
|
+
import { useLexicalEditable } from "@lexical/react/useLexicalEditable"
|
|
5
|
+
import {
|
|
6
|
+
$deleteTableColumnAtSelection,
|
|
7
|
+
$deleteTableRowAtSelection,
|
|
8
|
+
$getTableAndElementByKey,
|
|
9
|
+
$getTableColumnIndexFromTableCellNode,
|
|
10
|
+
$getTableRowIndexFromTableCellNode,
|
|
11
|
+
$insertTableColumnAtSelection,
|
|
12
|
+
$insertTableRowAtSelection,
|
|
13
|
+
$isTableCellNode,
|
|
14
|
+
$isTableNode,
|
|
15
|
+
getTableElement,
|
|
16
|
+
TableNode
|
|
17
|
+
} from "@lexical/table"
|
|
18
|
+
import { $findMatchingParent, mergeRegister } from "@lexical/utils"
|
|
19
|
+
import { $getNearestNodeFromDOMNode, isHTMLElement } from "lexical"
|
|
20
|
+
|
|
21
|
+
// Cell-boundary action buttons, all driven purely by `mousemove` (never by focus
|
|
22
|
+
// or selection): hover near a table's bottom edge to append a row, near its right
|
|
23
|
+
// edge to append a column, and hover any cell to reveal "×" buttons in the row's
|
|
24
|
+
// left gutter / the column's top gutter that delete that row / column.
|
|
25
|
+
//
|
|
26
|
+
// Delete used to live in a chevron dropdown (TableActionMenuPlugin) that tracked
|
|
27
|
+
// the editor selection and portaled a floating menu. On desktop, clicking the
|
|
28
|
+
// chevron stole focus from the contenteditable, which collapsed the selection out
|
|
29
|
+
// of the editor and made the menu flash-then-close while the chevron bounced — a
|
|
30
|
+
// focus/selection/render-timing race that could not be reproduced headless and
|
|
31
|
+
// survived four positioning fixes. This mousemove-driven approach has none of that
|
|
32
|
+
// machinery (no selection tracking, no $moveMenu, no click-outside, no focus
|
|
33
|
+
// dependency), which is exactly why the add "+" buttons always worked on PC and
|
|
34
|
+
// mobile alike. Ported/extended from the Lexical playground's TableHoverActions.
|
|
35
|
+
|
|
36
|
+
const BUTTON_WIDTH_PX = 20
|
|
37
|
+
const DELETE_BUTTON_PX = 18
|
|
38
|
+
|
|
39
|
+
// Build a CSS selector from a theme class name (mirrors the playground helper).
|
|
40
|
+
function getThemeSelector(getTheme, name) {
|
|
41
|
+
const className = getTheme()?.[name]
|
|
42
|
+
if (typeof className !== "string") {
|
|
43
|
+
throw new Error(`getThemeSelector: required theme property ${name} not defined`)
|
|
44
|
+
}
|
|
45
|
+
return className
|
|
46
|
+
.split(/\s+/g)
|
|
47
|
+
.map((cls) => `.${cls}`)
|
|
48
|
+
.join()
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Trailing-edge debounce with a maxWait ceiling and a cancel() method.
|
|
52
|
+
function useDebounce(fn, ms, maxWait) {
|
|
53
|
+
const fnRef = useRef(fn)
|
|
54
|
+
fnRef.current = fn
|
|
55
|
+
return useMemo(() => {
|
|
56
|
+
let timer = null
|
|
57
|
+
let firstCall = 0
|
|
58
|
+
const invoke = (args) => {
|
|
59
|
+
timer = null
|
|
60
|
+
firstCall = 0
|
|
61
|
+
fnRef.current(...args)
|
|
62
|
+
}
|
|
63
|
+
const debounced = (...args) => {
|
|
64
|
+
const now = Date.now()
|
|
65
|
+
if (!timer) firstCall = now
|
|
66
|
+
if (timer) clearTimeout(timer)
|
|
67
|
+
const waitedFor = now - firstCall
|
|
68
|
+
const remainingMax = maxWait != null ? Math.max(0, maxWait - waitedFor) : Infinity
|
|
69
|
+
const delay = Math.min(ms, remainingMax)
|
|
70
|
+
timer = setTimeout(() => invoke(args), delay)
|
|
71
|
+
}
|
|
72
|
+
debounced.cancel = () => {
|
|
73
|
+
if (timer) clearTimeout(timer)
|
|
74
|
+
timer = null
|
|
75
|
+
firstCall = 0
|
|
76
|
+
}
|
|
77
|
+
return debounced
|
|
78
|
+
}, [ms, maxWait])
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function getMouseInfo(event, getTheme) {
|
|
82
|
+
const target = event.target
|
|
83
|
+
const tableCellClass = getThemeSelector(getTheme, "tableCell")
|
|
84
|
+
|
|
85
|
+
if (isHTMLElement(target)) {
|
|
86
|
+
const tableDOMNode = target.closest(`td${tableCellClass}, th${tableCellClass}`)
|
|
87
|
+
|
|
88
|
+
const isOutside = !(
|
|
89
|
+
tableDOMNode ||
|
|
90
|
+
target.closest(`button${getThemeSelector(getTheme, "tableAddRows")}`) ||
|
|
91
|
+
target.closest(`button${getThemeSelector(getTheme, "tableAddColumns")}`) ||
|
|
92
|
+
target.closest(`button${getThemeSelector(getTheme, "tableDeleteRows")}`) ||
|
|
93
|
+
target.closest(`button${getThemeSelector(getTheme, "tableDeleteColumns")}`) ||
|
|
94
|
+
target.closest("div.TableCellResizer__resizer")
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
return { isOutside, tableDOMNode }
|
|
98
|
+
}
|
|
99
|
+
return { isOutside: true, tableDOMNode: null }
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function TableHoverActionsContainer({ anchorElem }) {
|
|
103
|
+
const [editor, { getTheme }] = useLexicalComposerContext()
|
|
104
|
+
const isEditable = useLexicalEditable()
|
|
105
|
+
const [isShownRow, setShownRow] = useState(false)
|
|
106
|
+
const [isShownColumn, setShownColumn] = useState(false)
|
|
107
|
+
const [isShownDeleteRow, setShownDeleteRow] = useState(false)
|
|
108
|
+
const [isShownDeleteColumn, setShownDeleteColumn] = useState(false)
|
|
109
|
+
const [shouldListenMouseMove, setShouldListenMouseMove] = useState(false)
|
|
110
|
+
const [position, setPosition] = useState({})
|
|
111
|
+
const [deleteRowPosition, setDeleteRowPosition] = useState({})
|
|
112
|
+
const [deleteColumnPosition, setDeleteColumnPosition] = useState({})
|
|
113
|
+
const tableSetRef = useRef(new Set())
|
|
114
|
+
const tableCellDOMNodeRef = useRef(null)
|
|
115
|
+
|
|
116
|
+
const hideAll = () => {
|
|
117
|
+
setShownRow(false)
|
|
118
|
+
setShownColumn(false)
|
|
119
|
+
setShownDeleteRow(false)
|
|
120
|
+
setShownDeleteColumn(false)
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const debouncedOnMouseMove = useDebounce(
|
|
124
|
+
(event) => {
|
|
125
|
+
const { isOutside, tableDOMNode } = getMouseInfo(event, getTheme)
|
|
126
|
+
|
|
127
|
+
if (isOutside) {
|
|
128
|
+
hideAll()
|
|
129
|
+
return
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (!tableDOMNode) return
|
|
133
|
+
|
|
134
|
+
tableCellDOMNodeRef.current = tableDOMNode
|
|
135
|
+
|
|
136
|
+
let hoveredRowNode = null
|
|
137
|
+
let hoveredColumnNode = null
|
|
138
|
+
let tableDOMElement = null
|
|
139
|
+
let canDeleteRow = false
|
|
140
|
+
let canDeleteColumn = false
|
|
141
|
+
|
|
142
|
+
editor.getEditorState().read(
|
|
143
|
+
() => {
|
|
144
|
+
const maybeTableCell = $getNearestNodeFromDOMNode(tableDOMNode)
|
|
145
|
+
|
|
146
|
+
if ($isTableCellNode(maybeTableCell)) {
|
|
147
|
+
const table = $findMatchingParent(maybeTableCell, (node) => $isTableNode(node))
|
|
148
|
+
if (!$isTableNode(table)) return
|
|
149
|
+
|
|
150
|
+
tableDOMElement = getTableElement(table, editor.getElementByKey(table.getKey()))
|
|
151
|
+
|
|
152
|
+
if (tableDOMElement) {
|
|
153
|
+
const rowCount = table.getChildrenSize()
|
|
154
|
+
const colCount = table.getChildAtIndex(0)?.getChildrenSize()
|
|
155
|
+
|
|
156
|
+
const rowIndex = $getTableRowIndexFromTableCellNode(maybeTableCell)
|
|
157
|
+
const colIndex = $getTableColumnIndexFromTableCellNode(maybeTableCell)
|
|
158
|
+
|
|
159
|
+
if (rowIndex === rowCount - 1) {
|
|
160
|
+
hoveredRowNode = maybeTableCell
|
|
161
|
+
} else if (colIndex === colCount - 1) {
|
|
162
|
+
hoveredColumnNode = maybeTableCell
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Deleting the only row/column would leave a degenerate table, so
|
|
166
|
+
// the gutter "×" only appears when more than one remains.
|
|
167
|
+
canDeleteRow = rowCount > 1
|
|
168
|
+
canDeleteColumn = (colCount ?? 0) > 1
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
},
|
|
172
|
+
{ editor }
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
if (!tableDOMElement) return
|
|
176
|
+
|
|
177
|
+
const {
|
|
178
|
+
width: tableElemWidth,
|
|
179
|
+
y: tableElemY,
|
|
180
|
+
right: tableElemRight,
|
|
181
|
+
left: tableElemLeft,
|
|
182
|
+
bottom: tableElemBottom,
|
|
183
|
+
height: tableElemHeight
|
|
184
|
+
} = tableDOMElement.getBoundingClientRect()
|
|
185
|
+
|
|
186
|
+
const parentElement = tableDOMElement.parentElement
|
|
187
|
+
let tableHasScroll = false
|
|
188
|
+
if (parentElement && parentElement.classList.contains("lexical-table-wrapper")) {
|
|
189
|
+
tableHasScroll = parentElement.scrollWidth > parentElement.clientWidth
|
|
190
|
+
}
|
|
191
|
+
const { y: editorElemY, left: editorElemLeft } = anchorElem.getBoundingClientRect()
|
|
192
|
+
|
|
193
|
+
// ----- Append "+" bars (last row / last column), unchanged behaviour. -----
|
|
194
|
+
if (hoveredRowNode) {
|
|
195
|
+
const isMac = /^mac/i.test(navigator.platform)
|
|
196
|
+
|
|
197
|
+
setShownColumn(false)
|
|
198
|
+
setShownRow(true)
|
|
199
|
+
setPosition({
|
|
200
|
+
height: BUTTON_WIDTH_PX,
|
|
201
|
+
left:
|
|
202
|
+
tableHasScroll && parentElement
|
|
203
|
+
? parentElement.offsetLeft
|
|
204
|
+
: tableElemLeft - editorElemLeft,
|
|
205
|
+
top: tableElemBottom - editorElemY + (tableHasScroll && !isMac ? 16 : 5),
|
|
206
|
+
width:
|
|
207
|
+
tableHasScroll && parentElement ? parentElement.offsetWidth : tableElemWidth
|
|
208
|
+
})
|
|
209
|
+
} else if (hoveredColumnNode) {
|
|
210
|
+
setShownColumn(true)
|
|
211
|
+
setShownRow(false)
|
|
212
|
+
setPosition({
|
|
213
|
+
height: tableElemHeight,
|
|
214
|
+
left: tableElemRight - editorElemLeft + 5,
|
|
215
|
+
top: tableElemY - editorElemY,
|
|
216
|
+
width: BUTTON_WIDTH_PX
|
|
217
|
+
})
|
|
218
|
+
} else {
|
|
219
|
+
setShownRow(false)
|
|
220
|
+
setShownColumn(false)
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// ----- Delete "×" buttons for the hovered cell's own row and column. -----
|
|
224
|
+
const cellRect = tableDOMNode.getBoundingClientRect()
|
|
225
|
+
|
|
226
|
+
if (canDeleteRow) {
|
|
227
|
+
// Vertical strip in the row's left gutter, just outside the table edge.
|
|
228
|
+
setShownDeleteRow(true)
|
|
229
|
+
setDeleteRowPosition({
|
|
230
|
+
top: cellRect.top - editorElemY,
|
|
231
|
+
height: cellRect.height,
|
|
232
|
+
left: Math.max(0, tableElemLeft - editorElemLeft - DELETE_BUTTON_PX - 4),
|
|
233
|
+
width: DELETE_BUTTON_PX
|
|
234
|
+
})
|
|
235
|
+
} else {
|
|
236
|
+
setShownDeleteRow(false)
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
if (canDeleteColumn) {
|
|
240
|
+
// Horizontal strip in the column's top gutter, just above the table edge.
|
|
241
|
+
setShownDeleteColumn(true)
|
|
242
|
+
setDeleteColumnPosition({
|
|
243
|
+
left: cellRect.left - editorElemLeft,
|
|
244
|
+
width: cellRect.width,
|
|
245
|
+
top: Math.max(0, tableElemY - editorElemY - DELETE_BUTTON_PX - 4),
|
|
246
|
+
height: DELETE_BUTTON_PX
|
|
247
|
+
})
|
|
248
|
+
} else {
|
|
249
|
+
setShownDeleteColumn(false)
|
|
250
|
+
}
|
|
251
|
+
},
|
|
252
|
+
50,
|
|
253
|
+
250
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
// Hide the buttons whenever a table resizes so a button can't end up overlapping
|
|
257
|
+
// a row/column after text entry grows a cell.
|
|
258
|
+
const tableResizeObserver = useMemo(() => {
|
|
259
|
+
return new ResizeObserver(() => {
|
|
260
|
+
hideAll()
|
|
261
|
+
})
|
|
262
|
+
}, [])
|
|
263
|
+
|
|
264
|
+
useEffect(() => {
|
|
265
|
+
if (!shouldListenMouseMove) return
|
|
266
|
+
|
|
267
|
+
document.addEventListener("mousemove", debouncedOnMouseMove)
|
|
268
|
+
|
|
269
|
+
return () => {
|
|
270
|
+
hideAll()
|
|
271
|
+
debouncedOnMouseMove.cancel()
|
|
272
|
+
document.removeEventListener("mousemove", debouncedOnMouseMove)
|
|
273
|
+
}
|
|
274
|
+
}, [shouldListenMouseMove, debouncedOnMouseMove])
|
|
275
|
+
|
|
276
|
+
useEffect(() => {
|
|
277
|
+
const unregister = mergeRegister(
|
|
278
|
+
editor.registerMutationListener(
|
|
279
|
+
TableNode,
|
|
280
|
+
(mutations) => {
|
|
281
|
+
editor.getEditorState().read(
|
|
282
|
+
() => {
|
|
283
|
+
let resetObserver = false
|
|
284
|
+
for (const [key, type] of mutations) {
|
|
285
|
+
if (type === "created") {
|
|
286
|
+
tableSetRef.current.add(key)
|
|
287
|
+
resetObserver = true
|
|
288
|
+
} else if (type === "destroyed") {
|
|
289
|
+
tableSetRef.current.delete(key)
|
|
290
|
+
resetObserver = true
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
if (resetObserver) {
|
|
294
|
+
tableResizeObserver.disconnect()
|
|
295
|
+
for (const tableKey of tableSetRef.current) {
|
|
296
|
+
const { tableElement } = $getTableAndElementByKey(tableKey)
|
|
297
|
+
// Guard: the key may be destroyed between the mutation batch
|
|
298
|
+
// and this read, leaving no element to observe.
|
|
299
|
+
if (tableElement) tableResizeObserver.observe(tableElement)
|
|
300
|
+
}
|
|
301
|
+
setShouldListenMouseMove(tableSetRef.current.size > 0)
|
|
302
|
+
}
|
|
303
|
+
},
|
|
304
|
+
{ editor }
|
|
305
|
+
)
|
|
306
|
+
},
|
|
307
|
+
{ skipInitialization: false }
|
|
308
|
+
)
|
|
309
|
+
)
|
|
310
|
+
// Disconnect the observer on unmount; mergeRegister only unregisters the
|
|
311
|
+
// mutation listener, so without this the observer leaks table DOM refs
|
|
312
|
+
// across editor open/close cycles.
|
|
313
|
+
return () => {
|
|
314
|
+
unregister()
|
|
315
|
+
tableResizeObserver.disconnect()
|
|
316
|
+
}
|
|
317
|
+
}, [editor, tableResizeObserver])
|
|
318
|
+
|
|
319
|
+
const insertAction = (insertRow) => {
|
|
320
|
+
editor.update(() => {
|
|
321
|
+
if (tableCellDOMNodeRef.current) {
|
|
322
|
+
const maybeTableNode = $getNearestNodeFromDOMNode(tableCellDOMNodeRef.current)
|
|
323
|
+
maybeTableNode?.selectEnd()
|
|
324
|
+
if (insertRow) {
|
|
325
|
+
$insertTableRowAtSelection()
|
|
326
|
+
setShownRow(false)
|
|
327
|
+
} else {
|
|
328
|
+
$insertTableColumnAtSelection()
|
|
329
|
+
setShownColumn(false)
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
})
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
const deleteAction = (deleteRow) => {
|
|
336
|
+
editor.update(() => {
|
|
337
|
+
if (tableCellDOMNodeRef.current) {
|
|
338
|
+
const maybeTableCell = $getNearestNodeFromDOMNode(tableCellDOMNodeRef.current)
|
|
339
|
+
if ($isTableCellNode(maybeTableCell)) {
|
|
340
|
+
// Anchor the selection in the hovered cell so the delete targets its
|
|
341
|
+
// row/column (mousemove never moved the editor selection there).
|
|
342
|
+
maybeTableCell.selectEnd()
|
|
343
|
+
if (deleteRow) {
|
|
344
|
+
$deleteTableRowAtSelection()
|
|
345
|
+
setShownDeleteRow(false)
|
|
346
|
+
} else {
|
|
347
|
+
$deleteTableColumnAtSelection()
|
|
348
|
+
setShownDeleteColumn(false)
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
})
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
if (!isEditable) return null
|
|
356
|
+
|
|
357
|
+
return (
|
|
358
|
+
<>
|
|
359
|
+
{isShownRow && (
|
|
360
|
+
<button
|
|
361
|
+
type="button"
|
|
362
|
+
aria-label="Add row"
|
|
363
|
+
className={`${getTheme()?.tableAddRows}`}
|
|
364
|
+
style={{ ...position }}
|
|
365
|
+
onClick={() => insertAction(true)}
|
|
366
|
+
/>
|
|
367
|
+
)}
|
|
368
|
+
{isShownColumn && (
|
|
369
|
+
<button
|
|
370
|
+
type="button"
|
|
371
|
+
aria-label="Add column"
|
|
372
|
+
className={`${getTheme()?.tableAddColumns}`}
|
|
373
|
+
style={{ ...position }}
|
|
374
|
+
onClick={() => insertAction(false)}
|
|
375
|
+
/>
|
|
376
|
+
)}
|
|
377
|
+
{isShownDeleteRow && (
|
|
378
|
+
<button
|
|
379
|
+
type="button"
|
|
380
|
+
aria-label="Delete row"
|
|
381
|
+
className={`${getTheme()?.tableDeleteRows}`}
|
|
382
|
+
style={{ ...deleteRowPosition }}
|
|
383
|
+
onClick={() => deleteAction(true)}
|
|
384
|
+
/>
|
|
385
|
+
)}
|
|
386
|
+
{isShownDeleteColumn && (
|
|
387
|
+
<button
|
|
388
|
+
type="button"
|
|
389
|
+
aria-label="Delete column"
|
|
390
|
+
className={`${getTheme()?.tableDeleteColumns}`}
|
|
391
|
+
style={{ ...deleteColumnPosition }}
|
|
392
|
+
onClick={() => deleteAction(false)}
|
|
393
|
+
/>
|
|
394
|
+
)}
|
|
395
|
+
</>
|
|
396
|
+
)
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
export default function TableHoverActionsPlugin({ anchorElem = document.body } = {}) {
|
|
400
|
+
const isEditable = useLexicalEditable()
|
|
401
|
+
|
|
402
|
+
return isEditable
|
|
403
|
+
? createPortal(<TableHoverActionsContainer anchorElem={anchorElem} />, anchorElem)
|
|
404
|
+
: null
|
|
405
|
+
}
|