astro-tractstack 2.0.0-rc.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.
- package/LICENSE +110 -0
- package/README.md +56 -0
- package/astro.d.ts +64 -0
- package/bin/create-tractstack.js +483 -0
- package/dist/config.js +80 -0
- package/dist/index.js +2129 -0
- package/package.json +89 -0
- package/templates/artpacks/kCz/captainBreakfast_1080px.webp +0 -0
- package/templates/artpacks/kCz/captainBreakfast_1920px.webp +0 -0
- package/templates/artpacks/kCz/captainBreakfast_600px.webp +0 -0
- package/templates/artpacks/kCz/cleanDrips_1080px.webp +0 -0
- package/templates/artpacks/kCz/cleanDrips_1920px.webp +0 -0
- package/templates/artpacks/kCz/cleanDrips_600px.webp +0 -0
- package/templates/artpacks/kCz/crispwaves_1080px.webp +0 -0
- package/templates/artpacks/kCz/crispwaves_1920px.webp +0 -0
- package/templates/artpacks/kCz/crispwaves_600px.webp +0 -0
- package/templates/artpacks/kCz/dragonSkin_1080px.webp +0 -0
- package/templates/artpacks/kCz/dragonSkin_1920px.webp +0 -0
- package/templates/artpacks/kCz/dragonSkin_600px.webp +0 -0
- package/templates/artpacks/kCz/dragon_1080px.webp +0 -0
- package/templates/artpacks/kCz/dragon_1920px.webp +0 -0
- package/templates/artpacks/kCz/dragon_600px.webp +0 -0
- package/templates/artpacks/kCz/nightcity_1080px.webp +0 -0
- package/templates/artpacks/kCz/nightcity_1920px.webp +0 -0
- package/templates/artpacks/kCz/nightcity_600px.webp +0 -0
- package/templates/artpacks/kCz/pattern1_1080px.webp +0 -0
- package/templates/artpacks/kCz/pattern1_1920px.webp +0 -0
- package/templates/artpacks/kCz/pattern1_600px.webp +0 -0
- package/templates/artpacks/kCz/pattern2_1080px.webp +0 -0
- package/templates/artpacks/kCz/pattern2_1920px.webp +0 -0
- package/templates/artpacks/kCz/pattern2_600px.webp +0 -0
- package/templates/artpacks/kCz/skindrips_1080px.webp +0 -0
- package/templates/artpacks/kCz/skindrips_1920px.webp +0 -0
- package/templates/artpacks/kCz/skindrips_600px.webp +0 -0
- package/templates/artpacks/kCz/slimetime_1080px.webp +0 -0
- package/templates/artpacks/kCz/slimetime_1920px.webp +0 -0
- package/templates/artpacks/kCz/slimetime_600px.webp +0 -0
- package/templates/artpacks/kCz/snake_1080px.webp +0 -0
- package/templates/artpacks/kCz/snake_1920px.webp +0 -0
- package/templates/artpacks/kCz/snake_600px.webp +0 -0
- package/templates/artpacks/kCz/toxicshock_1080px.webp +0 -0
- package/templates/artpacks/kCz/toxicshock_1920px.webp +0 -0
- package/templates/artpacks/kCz/toxicshock_600px.webp +0 -0
- package/templates/artpacks/kCz/tractstack_1080px.webp +0 -0
- package/templates/artpacks/kCz/tractstack_1920px.webp +0 -0
- package/templates/artpacks/kCz/tractstack_600px.webp +0 -0
- package/templates/artpacks/kCz/tripdrips_1080px.webp +0 -0
- package/templates/artpacks/kCz/tripdrips_1920px.webp +0 -0
- package/templates/artpacks/kCz/tripdrips_600px.webp +0 -0
- package/templates/artpacks/kCz/wavedrips_1080px.webp +0 -0
- package/templates/artpacks/kCz/wavedrips_1920px.webp +0 -0
- package/templates/artpacks/kCz/wavedrips_600px.webp +0 -0
- package/templates/artpacks/t8k/beach_1080px.webp +0 -0
- package/templates/artpacks/t8k/beach_1920px.webp +0 -0
- package/templates/artpacks/t8k/beach_600px.webp +0 -0
- package/templates/artpacks/t8k/blast_1080px.webp +0 -0
- package/templates/artpacks/t8k/blast_1920px.webp +0 -0
- package/templates/artpacks/t8k/blast_600px.webp +0 -0
- package/templates/artpacks/t8k/bokeh_1080px.webp +0 -0
- package/templates/artpacks/t8k/bokeh_1920px.webp +0 -0
- package/templates/artpacks/t8k/bokeh_600px.webp +0 -0
- package/templates/artpacks/t8k/cartoon_1080px.webp +0 -0
- package/templates/artpacks/t8k/cartoon_1920px.webp +0 -0
- package/templates/artpacks/t8k/cartoon_600px.webp +0 -0
- package/templates/artpacks/t8k/darkeggshell_1080px.webp +0 -0
- package/templates/artpacks/t8k/darkeggshell_1920px.webp +0 -0
- package/templates/artpacks/t8k/darkeggshell_600px.webp +0 -0
- package/templates/artpacks/t8k/explosion_1080px.webp +0 -0
- package/templates/artpacks/t8k/explosion_1920px.webp +0 -0
- package/templates/artpacks/t8k/explosion_600px.webp +0 -0
- package/templates/artpacks/t8k/floral_1080px.webp +0 -0
- package/templates/artpacks/t8k/floral_1920px.webp +0 -0
- package/templates/artpacks/t8k/floral_600px.webp +0 -0
- package/templates/artpacks/t8k/flower_1080px.webp +0 -0
- package/templates/artpacks/t8k/flower_1920px.webp +0 -0
- package/templates/artpacks/t8k/flower_600px.webp +0 -0
- package/templates/artpacks/t8k/foliage_1080px.webp +0 -0
- package/templates/artpacks/t8k/foliage_1920px.webp +0 -0
- package/templates/artpacks/t8k/foliage_600px.webp +0 -0
- package/templates/artpacks/t8k/mist_1080px.webp +0 -0
- package/templates/artpacks/t8k/mist_1920px.webp +0 -0
- package/templates/artpacks/t8k/mist_600px.webp +0 -0
- package/templates/artpacks/t8k/portal_1080px.webp +0 -0
- package/templates/artpacks/t8k/portal_1920px.webp +0 -0
- package/templates/artpacks/t8k/portal_600px.webp +0 -0
- package/templates/artpacks/t8k/storytime_1080px.webp +0 -0
- package/templates/artpacks/t8k/storytime_1920px.webp +0 -0
- package/templates/artpacks/t8k/storytime_600px.webp +0 -0
- package/templates/artpacks/t8k/tacky_1080px.webp +0 -0
- package/templates/artpacks/t8k/tacky_1920px.webp +0 -0
- package/templates/artpacks/t8k/tacky_600px.webp +0 -0
- package/templates/artpacks/t8k/wallpaper_1080px.webp +0 -0
- package/templates/artpacks/t8k/wallpaper_1920px.webp +0 -0
- package/templates/artpacks/t8k/wallpaper_600px.webp +0 -0
- package/templates/brand/favicon.ico +0 -0
- package/templates/brand/logo.svg +19 -0
- package/templates/brand/static.jpg +0 -0
- package/templates/brand/wordmark.svg +4 -0
- package/templates/css/custom.css +51 -0
- package/templates/css/frontend.css +3519 -0
- package/templates/css/storykeep.css +92872 -0
- package/templates/custom/minimal/CodeHook.astro +53 -0
- package/templates/custom/minimal/CustomRoutes.astro +46 -0
- package/templates/custom/with-examples/CodeHook.astro +49 -0
- package/templates/custom/with-examples/CustomHero.astro +13 -0
- package/templates/custom/with-examples/CustomRoutes.astro +39 -0
- package/templates/custom/with-examples/pages/Collections.astro +110 -0
- package/templates/env.example +8 -0
- package/templates/fonts/Inter-Black.woff2 +0 -0
- package/templates/fonts/Inter-Bold.woff2 +0 -0
- package/templates/fonts/Inter-Regular.woff2 +0 -0
- package/templates/icons/h2.svg +1 -0
- package/templates/icons/h3.svg +1 -0
- package/templates/icons/h4.svg +1 -0
- package/templates/icons/h5.svg +1 -0
- package/templates/icons/image.svg +7 -0
- package/templates/icons/text.svg +6 -0
- package/templates/socials/codepen.svg +1 -0
- package/templates/socials/discord.svg +1 -0
- package/templates/socials/facebook.svg +1 -0
- package/templates/socials/github.svg +1 -0
- package/templates/socials/instagram.svg +1 -0
- package/templates/socials/linkedin.svg +1 -0
- package/templates/socials/mail.svg +1 -0
- package/templates/socials/rumble.svg +1 -0
- package/templates/socials/tiktok.svg +1 -0
- package/templates/socials/twitch.svg +1 -0
- package/templates/socials/twitter.svg +1 -0
- package/templates/socials/x.svg +1 -0
- package/templates/socials/youtube.svg +1 -0
- package/templates/src/client/analytics-events.ts +213 -0
- package/templates/src/client/belief-events.ts +205 -0
- package/templates/src/client/sse.ts +667 -0
- package/templates/src/components/Footer.astro +246 -0
- package/templates/src/components/Fragment.astro +70 -0
- package/templates/src/components/Header.astro +458 -0
- package/templates/src/components/Menu.tsx +196 -0
- package/templates/src/components/codehooks/BunnyVideoSetup.tsx +692 -0
- package/templates/src/components/codehooks/BunnyVideoWrapper.astro +78 -0
- package/templates/src/components/codehooks/EpinetDurationSelector.tsx +1020 -0
- package/templates/src/components/codehooks/EpinetTableView.tsx +594 -0
- package/templates/src/components/codehooks/EpinetWrapper.tsx +424 -0
- package/templates/src/components/codehooks/FeaturedContent.astro +273 -0
- package/templates/src/components/codehooks/FeaturedContentSetup.tsx +738 -0
- package/templates/src/components/codehooks/ListContent.astro +460 -0
- package/templates/src/components/codehooks/ListContentSetup.tsx +649 -0
- package/templates/src/components/codehooks/SankeyDiagram.tsx +359 -0
- package/templates/src/components/compositor/Compositor.tsx +144 -0
- package/templates/src/components/compositor/Node.tsx +415 -0
- package/templates/src/components/compositor/NodeWithGuid.tsx +25 -0
- package/templates/src/components/compositor/PanelVisibilityWrapper.tsx +87 -0
- package/templates/src/components/compositor/elements/Belief.tsx +148 -0
- package/templates/src/components/compositor/elements/BgImage.tsx +118 -0
- package/templates/src/components/compositor/elements/BgVisualBreak.tsx +102 -0
- package/templates/src/components/compositor/elements/BunnyVideo.tsx +63 -0
- package/templates/src/components/compositor/elements/IdentifyAs.tsx +66 -0
- package/templates/src/components/compositor/elements/PlayButton.tsx +19 -0
- package/templates/src/components/compositor/elements/SignUp.tsx +179 -0
- package/templates/src/components/compositor/elements/Svg.tsx +33 -0
- package/templates/src/components/compositor/elements/ToggleBelief.tsx +36 -0
- package/templates/src/components/compositor/elements/YouTubeWrapper.tsx +33 -0
- package/templates/src/components/compositor/nodes/BgPaneWrapper.tsx +35 -0
- package/templates/src/components/compositor/nodes/GhostInsertBlock.tsx +189 -0
- package/templates/src/components/compositor/nodes/Markdown.tsx +179 -0
- package/templates/src/components/compositor/nodes/Pane.tsx +277 -0
- package/templates/src/components/compositor/nodes/Pane_eraser.tsx +69 -0
- package/templates/src/components/compositor/nodes/Pane_layout.tsx +77 -0
- package/templates/src/components/compositor/nodes/RenderChildren.tsx +19 -0
- package/templates/src/components/compositor/nodes/StoryFragment.tsx +35 -0
- package/templates/src/components/compositor/nodes/TagElement.tsx +14 -0
- package/templates/src/components/compositor/nodes/Widget.tsx +115 -0
- package/templates/src/components/compositor/nodes/tagElements/NodeA.tsx +4 -0
- package/templates/src/components/compositor/nodes/tagElements/NodeA_eraser.tsx +26 -0
- package/templates/src/components/compositor/nodes/tagElements/NodeAnchorComponent.tsx +248 -0
- package/templates/src/components/compositor/nodes/tagElements/NodeBasicTag.tsx +684 -0
- package/templates/src/components/compositor/nodes/tagElements/NodeBasicTag_eraser.tsx +62 -0
- package/templates/src/components/compositor/nodes/tagElements/NodeBasicTag_insert.tsx +120 -0
- package/templates/src/components/compositor/nodes/tagElements/NodeBasicTag_settings.tsx +62 -0
- package/templates/src/components/compositor/nodes/tagElements/NodeButton.tsx +5 -0
- package/templates/src/components/compositor/nodes/tagElements/NodeButton_eraser.tsx +26 -0
- package/templates/src/components/compositor/nodes/tagElements/NodeImg.tsx +28 -0
- package/templates/src/components/compositor/nodes/tagElements/NodeText.tsx +18 -0
- package/templates/src/components/compositor/nodes/tagElements/TabIndicator.tsx +51 -0
- package/templates/src/components/compositor/preview/FeaturedContentPreview.tsx +128 -0
- package/templates/src/components/compositor/preview/ListContentPreview.tsx +213 -0
- package/templates/src/components/compositor/preview/OgImagePreview.tsx +223 -0
- package/templates/src/components/compositor/preview/PaneSnapshotGenerator.tsx +199 -0
- package/templates/src/components/compositor/preview/PanesPreviewGenerator.tsx +123 -0
- package/templates/src/components/compositor/preview/VisualBreakPreview.tsx +154 -0
- package/templates/src/components/edit/Header.tsx +181 -0
- package/templates/src/components/edit/PanelSwitch.tsx +446 -0
- package/templates/src/components/edit/SettingsPanel.tsx +70 -0
- package/templates/src/components/edit/ToolBar.tsx +101 -0
- package/templates/src/components/edit/ToolMode.tsx +121 -0
- package/templates/src/components/edit/context/ContextPaneConfig.tsx +91 -0
- package/templates/src/components/edit/context/ContextPaneConfig_slug.tsx +174 -0
- package/templates/src/components/edit/context/ContextPaneConfig_title.tsx +186 -0
- package/templates/src/components/edit/pane/AddPanePanel.tsx +136 -0
- package/templates/src/components/edit/pane/AddPanePanel_break.tsx +470 -0
- package/templates/src/components/edit/pane/AddPanePanel_codehook.tsx +264 -0
- package/templates/src/components/edit/pane/AddPanePanel_new.tsx +623 -0
- package/templates/src/components/edit/pane/AddPanePanel_newAICopy.tsx +107 -0
- package/templates/src/components/edit/pane/AddPanePanel_newAICopy_modal.tsx +217 -0
- package/templates/src/components/edit/pane/AddPanePanel_newCopyMode.tsx +109 -0
- package/templates/src/components/edit/pane/AddPanePanel_newCustomCopy.tsx +39 -0
- package/templates/src/components/edit/pane/AddPanePanel_reuse.tsx +445 -0
- package/templates/src/components/edit/pane/ConfigPanePanel.tsx +245 -0
- package/templates/src/components/edit/pane/PageGen.tsx +485 -0
- package/templates/src/components/edit/pane/PageGenSelector.tsx +238 -0
- package/templates/src/components/edit/pane/PageGenSpecial.tsx +362 -0
- package/templates/src/components/edit/pane/PageGen_preview.tsx +495 -0
- package/templates/src/components/edit/pane/PanePanel_impression.tsx +258 -0
- package/templates/src/components/edit/pane/PanePanel_path.tsx +268 -0
- package/templates/src/components/edit/pane/PanePanel_slug.tsx +219 -0
- package/templates/src/components/edit/pane/PanePanel_title.tsx +142 -0
- package/templates/src/components/edit/panels/StyleBreakPanel.tsx +182 -0
- package/templates/src/components/edit/panels/StyleCodeHookPanel.tsx +439 -0
- package/templates/src/components/edit/panels/StyleElementPanel.tsx +177 -0
- package/templates/src/components/edit/panels/StyleElementPanel_add.tsx +349 -0
- package/templates/src/components/edit/panels/StyleElementPanel_remove.tsx +159 -0
- package/templates/src/components/edit/panels/StyleElementPanel_update.tsx +320 -0
- package/templates/src/components/edit/panels/StyleImagePanel.tsx +460 -0
- package/templates/src/components/edit/panels/StyleImagePanel_add.tsx +296 -0
- package/templates/src/components/edit/panels/StyleImagePanel_remove.tsx +153 -0
- package/templates/src/components/edit/panels/StyleImagePanel_update.tsx +312 -0
- package/templates/src/components/edit/panels/StyleLiElementPanel.tsx +273 -0
- package/templates/src/components/edit/panels/StyleLiElementPanel_add.tsx +301 -0
- package/templates/src/components/edit/panels/StyleLiElementPanel_remove.tsx +132 -0
- package/templates/src/components/edit/panels/StyleLiElementPanel_update.tsx +313 -0
- package/templates/src/components/edit/panels/StyleLinkPanel.tsx +346 -0
- package/templates/src/components/edit/panels/StyleLinkPanel_add.tsx +265 -0
- package/templates/src/components/edit/panels/StyleLinkPanel_config.tsx +240 -0
- package/templates/src/components/edit/panels/StyleLinkPanel_remove.tsx +94 -0
- package/templates/src/components/edit/panels/StyleLinkPanel_update.tsx +110 -0
- package/templates/src/components/edit/panels/StyleParentPanel.tsx +263 -0
- package/templates/src/components/edit/panels/StyleParentPanel_add.tsx +275 -0
- package/templates/src/components/edit/panels/StyleParentPanel_deleteLayer.tsx +112 -0
- package/templates/src/components/edit/panels/StyleParentPanel_remove.tsx +87 -0
- package/templates/src/components/edit/panels/StyleParentPanel_update.tsx +141 -0
- package/templates/src/components/edit/panels/StyleWidgetPanel.tsx +428 -0
- package/templates/src/components/edit/panels/StyleWidgetPanel_add.tsx +292 -0
- package/templates/src/components/edit/panels/StyleWidgetPanel_config.tsx +190 -0
- package/templates/src/components/edit/panels/StyleWidgetPanel_remove.tsx +152 -0
- package/templates/src/components/edit/panels/StyleWidgetPanel_update.tsx +308 -0
- package/templates/src/components/edit/state/SaveModal.tsx +811 -0
- package/templates/src/components/edit/state/StylesMemory.tsx +310 -0
- package/templates/src/components/edit/storyfragment/StoryFragmentConfigPanel.tsx +289 -0
- package/templates/src/components/edit/storyfragment/StoryFragmentPanel_menu.tsx +320 -0
- package/templates/src/components/edit/storyfragment/StoryFragmentPanel_og.tsx +888 -0
- package/templates/src/components/edit/storyfragment/StoryFragmentPanel_slug.tsx +269 -0
- package/templates/src/components/edit/storyfragment/StoryFragmentPanel_title.tsx +190 -0
- package/templates/src/components/edit/widgets/BeliefWidget.tsx +183 -0
- package/templates/src/components/edit/widgets/BunnyWidget.tsx +134 -0
- package/templates/src/components/edit/widgets/IdentifyAsWidget.tsx +193 -0
- package/templates/src/components/edit/widgets/SignupWidget.tsx +177 -0
- package/templates/src/components/edit/widgets/ToggleWidget.tsx +152 -0
- package/templates/src/components/edit/widgets/YouTubeWidget.tsx +65 -0
- package/templates/src/components/fields/ActionBuilderTimeSelector.tsx +353 -0
- package/templates/src/components/fields/ArtpackImage.tsx +480 -0
- package/templates/src/components/fields/BackgroundImage.tsx +530 -0
- package/templates/src/components/fields/BackgroundImageWrapper.tsx +192 -0
- package/templates/src/components/fields/BooleanParam.tsx +67 -0
- package/templates/src/components/fields/BunnyMomentSelector.tsx +56 -0
- package/templates/src/components/fields/ColorPickerCombo.tsx +284 -0
- package/templates/src/components/fields/ImageUpload.tsx +405 -0
- package/templates/src/components/fields/MultiParam.tsx +75 -0
- package/templates/src/components/fields/PaneBreakCollectionSelector.tsx +97 -0
- package/templates/src/components/fields/PaneBreakShapeSelector.tsx +134 -0
- package/templates/src/components/fields/SelectedTailwindClass.tsx +44 -0
- package/templates/src/components/fields/SingleParam.tsx +73 -0
- package/templates/src/components/fields/ViewportComboBox.tsx +252 -0
- package/templates/src/components/form/ActionBuilderField.tsx +282 -0
- package/templates/src/components/form/ActionBuilderSlugSelector.tsx +182 -0
- package/templates/src/components/form/BooleanToggle.tsx +94 -0
- package/templates/src/components/form/ColorPicker.tsx +153 -0
- package/templates/src/components/form/DateTimeInput.tsx +638 -0
- package/templates/src/components/form/EnumSelect.tsx +88 -0
- package/templates/src/components/form/FileUpload.tsx +465 -0
- package/templates/src/components/form/MagicPathBuilder.tsx +546 -0
- package/templates/src/components/form/NumberInput.tsx +101 -0
- package/templates/src/components/form/ParagraphArrayInput.tsx +207 -0
- package/templates/src/components/form/StringArrayInput.tsx +163 -0
- package/templates/src/components/form/StringInput.tsx +88 -0
- package/templates/src/components/form/UnsavedChangesBar.tsx +295 -0
- package/templates/src/components/form/advanced/APIConfigSection.tsx +69 -0
- package/templates/src/components/form/advanced/AuthConfigSection.tsx +97 -0
- package/templates/src/components/form/brand/BrandAssetsSection.tsx +93 -0
- package/templates/src/components/form/brand/BrandColorsSection.tsx +201 -0
- package/templates/src/components/form/brand/SEOSection.tsx +101 -0
- package/templates/src/components/form/brand/SiteConfigSection.tsx +61 -0
- package/templates/src/components/form/brand/SocialLinksSection.tsx +393 -0
- package/templates/src/components/profile/ProfileConsent.tsx +65 -0
- package/templates/src/components/profile/ProfileCreate.tsx +462 -0
- package/templates/src/components/profile/ProfileEdit.tsx +409 -0
- package/templates/src/components/profile/ProfileSwitch.tsx +255 -0
- package/templates/src/components/profile/ProfileUnlock.tsx +221 -0
- package/templates/src/components/storykeep/Dashboard.tsx +160 -0
- package/templates/src/components/storykeep/Dashboard_Activity.tsx +56 -0
- package/templates/src/components/storykeep/Dashboard_Advanced.tsx +165 -0
- package/templates/src/components/storykeep/Dashboard_Analytics.tsx +451 -0
- package/templates/src/components/storykeep/Dashboard_Branding.tsx +95 -0
- package/templates/src/components/storykeep/Dashboard_Content.tsx +191 -0
- package/templates/src/components/storykeep/controls/UsageCell.tsx +71 -0
- package/templates/src/components/storykeep/controls/content/BeliefForm.tsx +378 -0
- package/templates/src/components/storykeep/controls/content/BeliefTable.tsx +329 -0
- package/templates/src/components/storykeep/controls/content/ContentBrowser.tsx +385 -0
- package/templates/src/components/storykeep/controls/content/ContentSummary.tsx +149 -0
- package/templates/src/components/storykeep/controls/content/KnownResourceForm.tsx +397 -0
- package/templates/src/components/storykeep/controls/content/KnownResourceTable.tsx +260 -0
- package/templates/src/components/storykeep/controls/content/ManageContent.tsx +439 -0
- package/templates/src/components/storykeep/controls/content/MenuForm.tsx +239 -0
- package/templates/src/components/storykeep/controls/content/MenuTable.tsx +332 -0
- package/templates/src/components/storykeep/controls/content/ResourceBulkIngest.tsx +724 -0
- package/templates/src/components/storykeep/controls/content/ResourceForm.tsx +355 -0
- package/templates/src/components/storykeep/controls/content/ResourceTable.tsx +222 -0
- package/templates/src/components/storykeep/controls/content/StoryFragmentTable.tsx +482 -0
- package/templates/src/components/storykeep/state/BrandingWrapper.tsx +42 -0
- package/templates/src/components/storykeep/state/FetchAnalytics.tsx +350 -0
- package/templates/src/components/storykeep/widgets/ResponsiveLine.tsx +319 -0
- package/templates/src/components/storykeep/widgets/Wizard.tsx +278 -0
- package/templates/src/components/tenant/RegistrationForm.tsx +447 -0
- package/templates/src/components/widgets/BunnyVideoHero.astro +775 -0
- package/templates/src/components/widgets/Impression.tsx +102 -0
- package/templates/src/components/widgets/ImpressionWrapper.tsx +214 -0
- package/templates/src/constants/beliefs.ts +61 -0
- package/templates/src/constants/brandThemes.ts +133 -0
- package/templates/src/constants/prompts.json +55 -0
- package/templates/src/constants/shapes.ts +556 -0
- package/templates/src/constants/stopWords.ts +116 -0
- package/templates/src/constants/tailwindColors.json +344 -0
- package/templates/src/constants.ts +274 -0
- package/templates/src/hooks/useFormState.ts +203 -0
- package/templates/src/layouts/Layout.astro +290 -0
- package/templates/src/lib/session.ts +126 -0
- package/templates/src/lib/storyData.ts +56 -0
- package/templates/src/middleware.ts +52 -0
- package/templates/src/pages/404.astro +54 -0
- package/templates/src/pages/[...slug]/edit.astro +216 -0
- package/templates/src/pages/[...slug].astro +148 -0
- package/templates/src/pages/api/auth/decode.ts +101 -0
- package/templates/src/pages/api/auth/login.ts +122 -0
- package/templates/src/pages/api/auth/logout.ts +37 -0
- package/templates/src/pages/api/auth/profile.ts +76 -0
- package/templates/src/pages/api/orphan-analysis.ts +106 -0
- package/templates/src/pages/api/tailwind.ts +116 -0
- package/templates/src/pages/collections/[param1].astro +65 -0
- package/templates/src/pages/context/[...contextSlug]/edit.astro +207 -0
- package/templates/src/pages/context/[...contextSlug].astro +161 -0
- package/templates/src/pages/llms.txt.ts +122 -0
- package/templates/src/pages/maint.astro +183 -0
- package/templates/src/pages/media/[...slug].astro +67 -0
- package/templates/src/pages/robots.txt.ts +36 -0
- package/templates/src/pages/sandbox/activate.astro +258 -0
- package/templates/src/pages/sandbox/register.astro +44 -0
- package/templates/src/pages/sandbox/success.astro +179 -0
- package/templates/src/pages/sitemap.xml.ts +119 -0
- package/templates/src/pages/storykeep/advanced.astro +69 -0
- package/templates/src/pages/storykeep/branding.astro +57 -0
- package/templates/src/pages/storykeep/content.astro +71 -0
- package/templates/src/pages/storykeep/init.astro +36 -0
- package/templates/src/pages/storykeep/login.astro +266 -0
- package/templates/src/pages/storykeep/logout.astro +84 -0
- package/templates/src/pages/storykeep/profile.astro +98 -0
- package/templates/src/pages/storykeep.astro +81 -0
- package/templates/src/stores/analytics.ts +171 -0
- package/templates/src/stores/backend.ts +16 -0
- package/templates/src/stores/navigation.ts +149 -0
- package/templates/src/stores/nodes.ts +2390 -0
- package/templates/src/stores/nodesHistory.ts +85 -0
- package/templates/src/stores/notificationSystem.ts +41 -0
- package/templates/src/stores/orphanAnalysis.ts +409 -0
- package/templates/src/stores/storykeep.ts +247 -0
- package/templates/src/types/astro.ts +86 -0
- package/templates/src/types/compositorTypes.ts +456 -0
- package/templates/src/types/formTypes.ts +281 -0
- package/templates/src/types/multiTenant.ts +77 -0
- package/templates/src/types/nodeProps.ts +66 -0
- package/templates/src/types/tractstack.ts +445 -0
- package/templates/src/utils/aai/getTitleSlug.ts +72 -0
- package/templates/src/utils/actions/actionButton.ts +101 -0
- package/templates/src/utils/actions/lispLexer.ts +57 -0
- package/templates/src/utils/actions/preParse_Action.ts +85 -0
- package/templates/src/utils/actions/preParse_Bunny.ts +50 -0
- package/templates/src/utils/actions/preParse_Clicked.ts +87 -0
- package/templates/src/utils/actions/preParse_Impression.ts +71 -0
- package/templates/src/utils/api/advancedConfig.ts +66 -0
- package/templates/src/utils/api/advancedHelpers.ts +134 -0
- package/templates/src/utils/api/beliefConfig.ts +87 -0
- package/templates/src/utils/api/beliefHelpers.ts +196 -0
- package/templates/src/utils/api/brandConfig.ts +126 -0
- package/templates/src/utils/api/brandHelpers.ts +155 -0
- package/templates/src/utils/api/fileHelpers.ts +306 -0
- package/templates/src/utils/api/menuConfig.ts +57 -0
- package/templates/src/utils/api/menuHelpers.ts +156 -0
- package/templates/src/utils/api/resourceConfig.ts +158 -0
- package/templates/src/utils/api/resourceHelpers.ts +72 -0
- package/templates/src/utils/api/tenantConfig.ts +97 -0
- package/templates/src/utils/api/tenantHelpers.ts +172 -0
- package/templates/src/utils/api.ts +183 -0
- package/templates/src/utils/auth.ts +150 -0
- package/templates/src/utils/backend.ts +243 -0
- package/templates/src/utils/compositor/TemplateMarkdowns.ts +118 -0
- package/templates/src/utils/compositor/TemplateNodes.ts +138 -0
- package/templates/src/utils/compositor/TemplatePanes.ts +100 -0
- package/templates/src/utils/compositor/allowInsert.ts +100 -0
- package/templates/src/utils/compositor/domHelpers.ts +37 -0
- package/templates/src/utils/compositor/handleClickEvent.ts +131 -0
- package/templates/src/utils/compositor/nodesHelper.ts +491 -0
- package/templates/src/utils/compositor/nodesMarkdownGenerator.ts +292 -0
- package/templates/src/utils/compositor/processMarkdown.ts +431 -0
- package/templates/src/utils/compositor/reduceNodesClassNames.ts +192 -0
- package/templates/src/utils/compositor/tailwindClasses.ts +1795 -0
- package/templates/src/utils/compositor/tailwindColors.ts +227 -0
- package/templates/src/utils/compositor/templateMarkdownStyles.ts +1265 -0
- package/templates/src/utils/compositor/typeGuards.ts +193 -0
- package/templates/src/utils/etl/extractor.ts +119 -0
- package/templates/src/utils/etl/index.ts +88 -0
- package/templates/src/utils/etl/loader.ts +36 -0
- package/templates/src/utils/etl/transformer.ts +286 -0
- package/templates/src/utils/helpers.ts +435 -0
- package/templates/src/utils/layout.ts +209 -0
- package/templates/src/utils/profileStorage.ts +306 -0
- package/templates/src/utils/useInterval.ts +27 -0
- package/templates/tailwind.config.cjs +169 -0
- package/utils/create-resolver.ts +10 -0
- package/utils/inject-files.ts +2140 -0
- package/utils/validate-config.ts +43 -0
|
@@ -0,0 +1,2390 @@
|
|
|
1
|
+
import { atom, map } from 'nanostores';
|
|
2
|
+
import {
|
|
3
|
+
hasButtonPayload,
|
|
4
|
+
hasTagName,
|
|
5
|
+
isDefined,
|
|
6
|
+
isValidTag,
|
|
7
|
+
toTag,
|
|
8
|
+
} from '@/utils/compositor/typeGuards';
|
|
9
|
+
import { startLoadingAnimation } from '@/utils/helpers';
|
|
10
|
+
import { settingsPanelStore } from '@/stores/storykeep';
|
|
11
|
+
import {
|
|
12
|
+
PaneAddMode,
|
|
13
|
+
StoryFragmentMode,
|
|
14
|
+
ContextPaneMode,
|
|
15
|
+
} from '@/types/compositorTypes';
|
|
16
|
+
import type {
|
|
17
|
+
PanelState,
|
|
18
|
+
BaseNode,
|
|
19
|
+
FlatNode,
|
|
20
|
+
ImpressionNode,
|
|
21
|
+
MarkdownPaneFragmentNode,
|
|
22
|
+
MenuNode,
|
|
23
|
+
NodeType,
|
|
24
|
+
PaneFragmentNode,
|
|
25
|
+
PaneNode,
|
|
26
|
+
StoryFragmentNode,
|
|
27
|
+
Tag,
|
|
28
|
+
TemplateMarkdown,
|
|
29
|
+
TemplateNode,
|
|
30
|
+
TemplatePane,
|
|
31
|
+
ToolModeVal,
|
|
32
|
+
ToolAddMode,
|
|
33
|
+
TractStackNode,
|
|
34
|
+
ViewportKey,
|
|
35
|
+
OgImageParams,
|
|
36
|
+
VisualBreakNode,
|
|
37
|
+
BeliefDatum,
|
|
38
|
+
LoadData,
|
|
39
|
+
ArtpackImageNode,
|
|
40
|
+
} from '@/types/compositorTypes';
|
|
41
|
+
import type { NodeProps, WidgetProps } from '@/types/nodeProps';
|
|
42
|
+
import type { CSSProperties } from 'react';
|
|
43
|
+
import { processClassesForViewports } from '@/utils/compositor/reduceNodesClassNames';
|
|
44
|
+
import { ulid } from 'ulid';
|
|
45
|
+
import { NotificationSystem } from '@/stores/notificationSystem';
|
|
46
|
+
import { cloneDeep, isDeepEqual } from '@/utils/helpers';
|
|
47
|
+
import { extractClassesFromNodes } from '@/utils/compositor/nodesHelper';
|
|
48
|
+
import { handleClickEventDefault } from '@/utils/compositor/handleClickEvent';
|
|
49
|
+
import allowInsert from '@/utils/compositor/allowInsert';
|
|
50
|
+
import { reservedSlugs } from '@/constants';
|
|
51
|
+
import { NodesHistory, PatchOp } from '@/stores/nodesHistory';
|
|
52
|
+
import { moveNodeAtLocationInContext } from '@/utils/compositor/nodesHelper';
|
|
53
|
+
import { MarkdownGenerator } from '@/utils/compositor/nodesMarkdownGenerator';
|
|
54
|
+
import type { CompositorProps } from '@/components/compositor/Compositor';
|
|
55
|
+
|
|
56
|
+
const blockedClickNodes = new Set<string>(['em', 'strong']);
|
|
57
|
+
export const ROOT_NODE_NAME = 'root';
|
|
58
|
+
export const UNDO_REDO_HISTORY_CAPACITY = 500;
|
|
59
|
+
|
|
60
|
+
function strippedStyles(obj: Record<string, string[]>) {
|
|
61
|
+
return Object.fromEntries(
|
|
62
|
+
Object.entries(obj).map(([key, value]) => [key, value[0]])
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
function addHoverPrefix(str: string): string {
|
|
66
|
+
return str
|
|
67
|
+
.split(' ')
|
|
68
|
+
.map((word) => `hover:${word}`)
|
|
69
|
+
.join(' ');
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export class NodesContext {
|
|
73
|
+
constructor() {}
|
|
74
|
+
|
|
75
|
+
notifications = new NotificationSystem<BaseNode>();
|
|
76
|
+
allNodes = atom<Map<string, BaseNode>>(new Map<string, BaseNode>());
|
|
77
|
+
impressionNodes = atom<Set<ImpressionNode>>(new Set<ImpressionNode>());
|
|
78
|
+
parentNodes = atom<Map<string, string[]>>(new Map<string, string[]>());
|
|
79
|
+
hasTitle = atom<boolean>(false);
|
|
80
|
+
hasPanes = atom<boolean>(false);
|
|
81
|
+
isTemplate = atom<boolean>(false);
|
|
82
|
+
rootNodeId = atom<string>('');
|
|
83
|
+
clickedNodeId = atom<string>('');
|
|
84
|
+
ghostTextActiveId = atom<string>('');
|
|
85
|
+
clickedParentLayer = atom<number | null>(null);
|
|
86
|
+
activePaneMode = atom<PanelState>({
|
|
87
|
+
paneId: '',
|
|
88
|
+
mode: '',
|
|
89
|
+
panel: '',
|
|
90
|
+
});
|
|
91
|
+
editingNodeId = atom<string | null>(null);
|
|
92
|
+
history = new NodesHistory(this, UNDO_REDO_HISTORY_CAPACITY);
|
|
93
|
+
|
|
94
|
+
toolModeValStore = map<{ value: ToolModeVal }>({
|
|
95
|
+
value: 'text',
|
|
96
|
+
});
|
|
97
|
+
toolAddModeStore = map<{ value: ToolAddMode }>({
|
|
98
|
+
value: 'p',
|
|
99
|
+
});
|
|
100
|
+
showGuids = atom<boolean>(false);
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Sets an edit lock on a specific node to prevent re-renders during editing
|
|
104
|
+
* @param nodeId - The node ID to lock, or null to clear the lock
|
|
105
|
+
*/
|
|
106
|
+
setEditLock(nodeId: string | null): void {
|
|
107
|
+
this.editingNodeId.set(nodeId);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Checks if a specific node is currently edit-locked
|
|
112
|
+
* @param nodeId - The node ID to check
|
|
113
|
+
* @returns true if the node is locked for editing
|
|
114
|
+
*/
|
|
115
|
+
isEditLocked(nodeId: string): boolean {
|
|
116
|
+
return this.editingNodeId.get() === nodeId;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Clears the current edit lock
|
|
121
|
+
*/
|
|
122
|
+
clearEditLock(): void {
|
|
123
|
+
this.editingNodeId.set(null);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Cleanup method to handle orphaned edit locks
|
|
128
|
+
*/
|
|
129
|
+
cleanupEditState(): void {
|
|
130
|
+
const editingId = this.editingNodeId.get();
|
|
131
|
+
if (editingId) {
|
|
132
|
+
// Check if the node still exists
|
|
133
|
+
const node = this.allNodes.get().get(editingId);
|
|
134
|
+
if (!node) {
|
|
135
|
+
this.clearEditLock();
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
notifyNode(nodeId: string, payload?: BaseNode) {
|
|
141
|
+
// Skip notification if this node is edit-locked
|
|
142
|
+
if (this.isEditLocked(nodeId)) {
|
|
143
|
+
// Still notify parent nodes as they may need updates
|
|
144
|
+
const node = this.allNodes.get().get(nodeId);
|
|
145
|
+
if (node && node.parentId) {
|
|
146
|
+
const parentNodeToNotify = this.nodeToNotify(nodeId, node.nodeType);
|
|
147
|
+
if (parentNodeToNotify && parentNodeToNotify !== nodeId) {
|
|
148
|
+
this.notifyNode(parentNodeToNotify, payload);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
// Original notifyNode implementation
|
|
154
|
+
let notifyNodeId = nodeId;
|
|
155
|
+
if (notifyNodeId === this.rootNodeId.get()) {
|
|
156
|
+
notifyNodeId = ROOT_NODE_NAME;
|
|
157
|
+
}
|
|
158
|
+
if (nodeId === `root`) startLoadingAnimation();
|
|
159
|
+
this.updateHasPanesStatus();
|
|
160
|
+
this.notifications.notify(notifyNodeId, payload);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
getPanelMode(nodeId: string, panel: string): string {
|
|
164
|
+
const activeMode = this.activePaneMode.get();
|
|
165
|
+
if (activeMode.panel === panel && activeMode.paneId === nodeId) {
|
|
166
|
+
return activeMode.mode;
|
|
167
|
+
}
|
|
168
|
+
return '';
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
setPanelMode(nodeId: string, panel: string, mode: string) {
|
|
172
|
+
this.closeAllPanelsExcept(nodeId, panel);
|
|
173
|
+
this.activePaneMode.set({
|
|
174
|
+
paneId: nodeId,
|
|
175
|
+
panel: panel,
|
|
176
|
+
mode: mode,
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
getPaneAddMode(nodeId: string): PaneAddMode {
|
|
181
|
+
const mode = this.getPanelMode(nodeId, 'add');
|
|
182
|
+
return mode ? (mode as PaneAddMode) : PaneAddMode.DEFAULT;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
setPaneAddMode(nodeId: string, mode: PaneAddMode) {
|
|
186
|
+
this.setPanelMode(nodeId, 'add', mode);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
getContextPaneMode(nodeId: string): ContextPaneMode {
|
|
190
|
+
const mode = this.getPanelMode(nodeId, 'context');
|
|
191
|
+
return mode ? (mode as ContextPaneMode) : ContextPaneMode.DEFAULT;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
setContextPaneMode(nodeId: string, mode: ContextPaneMode) {
|
|
195
|
+
this.setPanelMode(nodeId, 'context', mode);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
getStoryFragmentMode(nodeId: string): StoryFragmentMode {
|
|
199
|
+
const mode = this.getPanelMode(nodeId, 'storyfragment');
|
|
200
|
+
return mode ? (mode as StoryFragmentMode) : StoryFragmentMode.DEFAULT;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
setStoryFragmentMode(nodeId: string, mode: StoryFragmentMode) {
|
|
204
|
+
this.setPanelMode(nodeId, 'storyfragment', mode);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
closeAllPanels() {
|
|
208
|
+
this.activePaneMode.set({
|
|
209
|
+
paneId: '',
|
|
210
|
+
panel: '',
|
|
211
|
+
mode: '',
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
closeAllPanelsExcept(nodeId: string, panel: string) {
|
|
216
|
+
if (panel === 'styles-memory') {
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
const currentPanel = this.activePaneMode.get();
|
|
220
|
+
if (currentPanel.paneId !== nodeId || currentPanel.panel !== panel) {
|
|
221
|
+
settingsPanelStore.set(null);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
ogImageParamsStore = map<Record<string, OgImageParams>>({});
|
|
226
|
+
|
|
227
|
+
getOgImageParams(nodeId: string): OgImageParams {
|
|
228
|
+
const params = this.ogImageParamsStore.get()[nodeId];
|
|
229
|
+
return (
|
|
230
|
+
params || {
|
|
231
|
+
textColor: '#fcfcfc',
|
|
232
|
+
bgColor: '#10120d',
|
|
233
|
+
fontSize: undefined,
|
|
234
|
+
}
|
|
235
|
+
);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
setOgImageParams(nodeId: string, params: Partial<OgImageParams>): void {
|
|
239
|
+
const currentParams = this.getOgImageParams(nodeId);
|
|
240
|
+
this.ogImageParamsStore.setKey(nodeId, {
|
|
241
|
+
...currentParams,
|
|
242
|
+
...params,
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
//setActiveGhost(nodeId: string): void {
|
|
247
|
+
// const currentActiveId = this.ghostTextActiveId.get();
|
|
248
|
+
// // If this is already the active ghost, do nothing
|
|
249
|
+
// if (currentActiveId === nodeId) return;
|
|
250
|
+
// // If another ghost is active, clear it first
|
|
251
|
+
// if (currentActiveId && currentActiveId !== nodeId) {
|
|
252
|
+
// // Set to empty string to close any existing ghost
|
|
253
|
+
// this.ghostTextActiveId.set("");
|
|
254
|
+
// // After a short delay to allow the previous ghost to close,
|
|
255
|
+
// // set the new active ghost
|
|
256
|
+
// setTimeout(() => {
|
|
257
|
+
// this.ghostTextActiveId.set(nodeId);
|
|
258
|
+
// }, 100);
|
|
259
|
+
// } else {
|
|
260
|
+
// this.ghostTextActiveId.set(nodeId);
|
|
261
|
+
// }
|
|
262
|
+
//}
|
|
263
|
+
|
|
264
|
+
updateHasPanesStatus() {
|
|
265
|
+
const allNodes = this.allNodes.get();
|
|
266
|
+
const storyFragments = Array.from(allNodes.values()).filter(
|
|
267
|
+
(node) => node.nodeType === 'StoryFragment'
|
|
268
|
+
);
|
|
269
|
+
const hasPanes = storyFragments.some(
|
|
270
|
+
(node) => 'paneIds' in node && (node.paneIds as string[]).length > 0
|
|
271
|
+
);
|
|
272
|
+
this.hasPanes.set(hasPanes);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
cleanNode(nodeId: string) {
|
|
276
|
+
const node = this.allNodes.get().get(nodeId);
|
|
277
|
+
if (!node) return;
|
|
278
|
+
const newNodes = new Map(this.allNodes.get());
|
|
279
|
+
const cleanedNode = cloneDeep(node);
|
|
280
|
+
if (cleanedNode.isChanged) delete cleanedNode.isChanged;
|
|
281
|
+
newNodes.set(nodeId, cleanedNode);
|
|
282
|
+
this.allNodes.set(newNodes);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
getDirtyNodes(): BaseNode[] {
|
|
286
|
+
const allNodes = Array.from(this.allNodes.get().values());
|
|
287
|
+
return allNodes.filter(
|
|
288
|
+
(node): node is BaseNode => 'isChanged' in node && node.isChanged === true
|
|
289
|
+
);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
clearUndoHistory() {
|
|
293
|
+
this.history.clearHistory();
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
getChildNodeIDs(parentNodeId: string): string[] {
|
|
297
|
+
const returnVal = this.parentNodes.get()?.get(parentNodeId) || [];
|
|
298
|
+
return returnVal;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
setClickedParentLayer(layer: number | null) {
|
|
302
|
+
this.clickedParentLayer.set(layer);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
handleEraseEvent(nodeId: string) {
|
|
306
|
+
const node = this.allNodes.get().get(nodeId) as FlatNode;
|
|
307
|
+
if (!node) return;
|
|
308
|
+
switch (node.nodeType) {
|
|
309
|
+
case `Pane`: {
|
|
310
|
+
const storyfragmentNodeId = this.getClosestNodeTypeFromId(
|
|
311
|
+
nodeId,
|
|
312
|
+
'StoryFragment'
|
|
313
|
+
);
|
|
314
|
+
const storyfragmentNode = cloneDeep(
|
|
315
|
+
this.allNodes.get().get(storyfragmentNodeId)
|
|
316
|
+
) as StoryFragmentNode;
|
|
317
|
+
this.modifyNodes([{ ...storyfragmentNode, isChanged: true }]);
|
|
318
|
+
break;
|
|
319
|
+
}
|
|
320
|
+
case `TagElement`: {
|
|
321
|
+
const paneNodeId = this.getClosestNodeTypeFromId(nodeId, 'Pane');
|
|
322
|
+
const paneNode = cloneDeep(
|
|
323
|
+
this.allNodes.get().get(paneNodeId)
|
|
324
|
+
) as PaneNode;
|
|
325
|
+
this.modifyNodes([{ ...paneNode, isChanged: true }]);
|
|
326
|
+
break;
|
|
327
|
+
}
|
|
328
|
+
default:
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
handleClickEvent(dblClick: boolean = false) {
|
|
333
|
+
const toolModeVal = this.toolModeValStore.get().value;
|
|
334
|
+
const node = this.allNodes.get().get(this.clickedNodeId.get()) as FlatNode;
|
|
335
|
+
if (!node) return;
|
|
336
|
+
|
|
337
|
+
// click handler based on toolModeVal
|
|
338
|
+
switch (toolModeVal) {
|
|
339
|
+
case `styles`:
|
|
340
|
+
handleClickEventDefault(node, dblClick, this.clickedParentLayer.get());
|
|
341
|
+
break;
|
|
342
|
+
case `text`:
|
|
343
|
+
if (dblClick && ![`Markdown`].includes(node.nodeType)) {
|
|
344
|
+
this.toolModeValStore.set({ value: 'styles' });
|
|
345
|
+
handleClickEventDefault(
|
|
346
|
+
node,
|
|
347
|
+
dblClick,
|
|
348
|
+
this.clickedParentLayer.get()
|
|
349
|
+
);
|
|
350
|
+
} else {
|
|
351
|
+
handleClickEventDefault(
|
|
352
|
+
node,
|
|
353
|
+
dblClick,
|
|
354
|
+
this.clickedParentLayer.get()
|
|
355
|
+
);
|
|
356
|
+
}
|
|
357
|
+
break;
|
|
358
|
+
case `eraser`:
|
|
359
|
+
this.handleEraseEvent(node.id);
|
|
360
|
+
this.deleteNode(node.id);
|
|
361
|
+
break;
|
|
362
|
+
default:
|
|
363
|
+
}
|
|
364
|
+
// reset on parentLayer
|
|
365
|
+
this.setClickedParentLayer(null);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
private clickTimer: number | null = null;
|
|
369
|
+
private DOUBLE_CLICK_DELAY = 300;
|
|
370
|
+
private isProcessingDoubleClick = false;
|
|
371
|
+
private lastProcessedTime = 0;
|
|
372
|
+
|
|
373
|
+
setClickedNodeId(nodeId: string, dblClick: boolean = false) {
|
|
374
|
+
//settingsPanelStore.set(null);
|
|
375
|
+
const now = Date.now();
|
|
376
|
+
// Prevent processing if we're too close to the last event
|
|
377
|
+
if (now - this.lastProcessedTime < 50 || this.isProcessingDoubleClick)
|
|
378
|
+
return;
|
|
379
|
+
let node = this.allNodes.get().get(nodeId) as FlatNode;
|
|
380
|
+
if (node && 'tagName' in node) {
|
|
381
|
+
while (node.parentId !== null && blockedClickNodes.has(node.tagName)) {
|
|
382
|
+
node = this.allNodes.get().get(node.parentId) as FlatNode;
|
|
383
|
+
}
|
|
384
|
+
if (!node) return;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// Handle double click
|
|
388
|
+
if (dblClick) {
|
|
389
|
+
if (this.clickTimer) {
|
|
390
|
+
window.clearTimeout(this.clickTimer);
|
|
391
|
+
this.clickTimer = null;
|
|
392
|
+
}
|
|
393
|
+
this.isProcessingDoubleClick = true;
|
|
394
|
+
this.clickedNodeId.set(node.id);
|
|
395
|
+
this.lastProcessedTime = now;
|
|
396
|
+
window.setTimeout(() => {
|
|
397
|
+
this.isProcessingDoubleClick = false;
|
|
398
|
+
}, 100);
|
|
399
|
+
this.handleClickEvent(true);
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// Handle single click with delay for potential double click
|
|
404
|
+
if (this.clickTimer) {
|
|
405
|
+
window.clearTimeout(this.clickTimer);
|
|
406
|
+
}
|
|
407
|
+
this.clickTimer = window.setTimeout(() => {
|
|
408
|
+
if (!this.isProcessingDoubleClick) {
|
|
409
|
+
this.clickTimer = null;
|
|
410
|
+
this.clickedNodeId.set(node.id);
|
|
411
|
+
this.lastProcessedTime = Date.now();
|
|
412
|
+
this.handleClickEvent(false);
|
|
413
|
+
}
|
|
414
|
+
}, this.DOUBLE_CLICK_DELAY);
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
clearAll() {
|
|
418
|
+
this.allNodes.get().clear();
|
|
419
|
+
this.parentNodes.get().clear();
|
|
420
|
+
this.impressionNodes.get().clear();
|
|
421
|
+
this.rootNodeId.set('');
|
|
422
|
+
this.notifications.clear();
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
buildNodesTreeFromRowDataMadeNodes(nodes: LoadData | null) {
|
|
426
|
+
if (nodes !== null) {
|
|
427
|
+
this.clearAll();
|
|
428
|
+
//if (nodes?.fileNodes) this.addNodes(nodes.fileNodes);
|
|
429
|
+
if (nodes?.menuNodes) this.addNodes(nodes.menuNodes);
|
|
430
|
+
//if (nodes?.resourceNodes) this.addNodes(nodes.resourceNodes);
|
|
431
|
+
if (nodes?.tractstackNodes) this.addNodes(nodes.tractstackNodes);
|
|
432
|
+
// IMPORTANT!
|
|
433
|
+
// pane nodes have to be added BEFORE StoryFragment nodes so they can register in this.allNodes
|
|
434
|
+
if (nodes?.paneNodes) this.addNodes(nodes.paneNodes);
|
|
435
|
+
// add childNodes after panes
|
|
436
|
+
if (nodes?.childNodes) this.addNodes(nodes.childNodes);
|
|
437
|
+
// then storyfragment nodes will link pane nodes from above
|
|
438
|
+
// then add storyfragmentNodes
|
|
439
|
+
if (nodes?.storyfragmentNodes) this.addNodes(nodes.storyfragmentNodes);
|
|
440
|
+
|
|
441
|
+
this.updateHasPanesStatus();
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
linkChildToParent(
|
|
446
|
+
nodeId: string,
|
|
447
|
+
parentId: string,
|
|
448
|
+
specificIndex: number = -1
|
|
449
|
+
) {
|
|
450
|
+
const parentNode = this.parentNodes.get();
|
|
451
|
+
if (parentNode.has(parentId)) {
|
|
452
|
+
if (specificIndex === -1) {
|
|
453
|
+
parentNode.get(parentId)?.push(nodeId);
|
|
454
|
+
} else {
|
|
455
|
+
parentNode.get(parentId)?.splice(Math.max(0, specificIndex), 0, nodeId);
|
|
456
|
+
}
|
|
457
|
+
this.parentNodes.set(new Map<string, string[]>(parentNode));
|
|
458
|
+
} else {
|
|
459
|
+
parentNode.set(parentId, [nodeId]);
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
addNode(data: BaseNode) {
|
|
464
|
+
this.allNodes.get().set(data.id, data);
|
|
465
|
+
|
|
466
|
+
// root node
|
|
467
|
+
if (data.parentId === null && this.rootNodeId.get().length === 0) {
|
|
468
|
+
this.rootNodeId.set(data.id);
|
|
469
|
+
return;
|
|
470
|
+
}
|
|
471
|
+
const parentNode = this.parentNodes.get();
|
|
472
|
+
if (!parentNode) return;
|
|
473
|
+
|
|
474
|
+
if (data.parentId !== null) {
|
|
475
|
+
// if storyfragment then iterate over its paneIDs
|
|
476
|
+
if (data.nodeType === 'StoryFragment') {
|
|
477
|
+
const storyFragment = data as StoryFragmentNode;
|
|
478
|
+
this.linkChildToParent(data.id, data.parentId);
|
|
479
|
+
|
|
480
|
+
storyFragment.paneIds.forEach((paneId: string) => {
|
|
481
|
+
// pane should already exist by now, tell it where it belongs to
|
|
482
|
+
const pane = this.allNodes.get().get(paneId);
|
|
483
|
+
if (pane) {
|
|
484
|
+
pane.parentId = data.id;
|
|
485
|
+
}
|
|
486
|
+
this.linkChildToParent(paneId, data.id);
|
|
487
|
+
});
|
|
488
|
+
// skip panes, they get linked along with story fragment
|
|
489
|
+
} else if (data.nodeType !== 'Pane') {
|
|
490
|
+
this.linkChildToParent(data.id, data.parentId);
|
|
491
|
+
|
|
492
|
+
if (data.nodeType === 'Impression') {
|
|
493
|
+
this.impressionNodes.get().add(data as ImpressionNode);
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
this.updateHasPanesStatus();
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
addNodes(nodes: BaseNode[]) {
|
|
501
|
+
for (const node of nodes) {
|
|
502
|
+
this.addNode(node);
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
allowInsert(
|
|
507
|
+
nodeId: string,
|
|
508
|
+
tagNameStr: string
|
|
509
|
+
): {
|
|
510
|
+
allowInsertBefore: boolean;
|
|
511
|
+
allowInsertAfter: boolean;
|
|
512
|
+
} {
|
|
513
|
+
const node = this.allNodes.get().get(nodeId);
|
|
514
|
+
if (!isDefined(node) || !hasTagName(node)) {
|
|
515
|
+
return { allowInsertBefore: false, allowInsertAfter: false };
|
|
516
|
+
}
|
|
517
|
+
const markdownId = this.getClosestNodeTypeFromId(nodeId, 'Markdown');
|
|
518
|
+
const tagNameIds = this.getChildNodeIDs(markdownId);
|
|
519
|
+
const tagNames = tagNameIds
|
|
520
|
+
.map((id) => {
|
|
521
|
+
const name = this.getNodeTagName(id);
|
|
522
|
+
return toTag(name);
|
|
523
|
+
})
|
|
524
|
+
.filter((name): name is Tag => name !== null);
|
|
525
|
+
|
|
526
|
+
const offset = tagNameIds.indexOf(nodeId);
|
|
527
|
+
const tagName = toTag(tagNameStr);
|
|
528
|
+
|
|
529
|
+
if (!tagName || !isValidTag(node.tagName)) {
|
|
530
|
+
return { allowInsertBefore: false, allowInsertAfter: false };
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
const allowInsertBefore =
|
|
534
|
+
offset > -1
|
|
535
|
+
? allowInsert(
|
|
536
|
+
node,
|
|
537
|
+
node.tagName as Tag,
|
|
538
|
+
tagName,
|
|
539
|
+
offset ? tagNames[offset - 1] : undefined
|
|
540
|
+
)
|
|
541
|
+
: allowInsert(node, node.tagName as Tag, tagName);
|
|
542
|
+
|
|
543
|
+
const allowInsertAfter =
|
|
544
|
+
tagNames.length > offset
|
|
545
|
+
? allowInsert(node, node.tagName as Tag, tagName, tagNames[offset + 1])
|
|
546
|
+
: allowInsert(node, node.tagName as Tag, tagName);
|
|
547
|
+
|
|
548
|
+
return { allowInsertBefore, allowInsertAfter };
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
allowInsertLi(
|
|
552
|
+
nodeId: string,
|
|
553
|
+
tagNameStr: string
|
|
554
|
+
): {
|
|
555
|
+
allowInsertBefore: boolean;
|
|
556
|
+
allowInsertAfter: boolean;
|
|
557
|
+
} {
|
|
558
|
+
const node = this.allNodes.get().get(nodeId);
|
|
559
|
+
if (!isDefined(node) || !hasTagName(node) || !node.parentId) {
|
|
560
|
+
return { allowInsertBefore: false, allowInsertAfter: false };
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
const tagNameIds = this.getChildNodeIDs(node.parentId);
|
|
564
|
+
const tagNames = tagNameIds
|
|
565
|
+
.map((id) => {
|
|
566
|
+
const name = this.getNodeTagName(id);
|
|
567
|
+
return toTag(name);
|
|
568
|
+
})
|
|
569
|
+
.filter((name): name is Tag => name !== null);
|
|
570
|
+
|
|
571
|
+
const offset = tagNameIds.indexOf(nodeId);
|
|
572
|
+
const tagName = toTag(tagNameStr);
|
|
573
|
+
|
|
574
|
+
if (!tagName || !isValidTag(node.tagName)) {
|
|
575
|
+
return { allowInsertBefore: false, allowInsertAfter: false };
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
const allowInsertBefore =
|
|
579
|
+
offset > 0
|
|
580
|
+
? allowInsert(node, node.tagName as Tag, tagName, tagNames[offset - 1])
|
|
581
|
+
: allowInsert(node, node.tagName as Tag, tagName);
|
|
582
|
+
|
|
583
|
+
const allowInsertAfter =
|
|
584
|
+
tagNames.length < offset
|
|
585
|
+
? allowInsert(node, node.tagName as Tag, tagName, tagNames[offset + 1])
|
|
586
|
+
: allowInsert(node, node.tagName as Tag, tagName);
|
|
587
|
+
|
|
588
|
+
return { allowInsertBefore, allowInsertAfter };
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
getClosestNodeTypeFromId(startNodeId: string, nodeType: NodeType): string {
|
|
592
|
+
const node = this.allNodes.get().get(startNodeId);
|
|
593
|
+
if (!node || node.nodeType === 'Root') return '';
|
|
594
|
+
|
|
595
|
+
const parentId = node.parentId || '';
|
|
596
|
+
const parentNode = this.allNodes.get().get(parentId);
|
|
597
|
+
if (parentNode && parentNode.nodeType === nodeType) {
|
|
598
|
+
return parentId;
|
|
599
|
+
} else {
|
|
600
|
+
return this.getClosestNodeTypeFromId(parentId, nodeType);
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
getChildNodeByTagNames(startNodeId: string, tagNames: string[]): string {
|
|
605
|
+
const node = this.allNodes.get().get(startNodeId);
|
|
606
|
+
if (!node || node.nodeType === 'Root') return '';
|
|
607
|
+
|
|
608
|
+
let firstChildId = '';
|
|
609
|
+
if ('tagName' in node && tagNames.includes(node.tagName as string)) {
|
|
610
|
+
firstChildId = node.id;
|
|
611
|
+
return firstChildId;
|
|
612
|
+
}
|
|
613
|
+
this.getChildNodeIDs(node.id).forEach((childId) => {
|
|
614
|
+
const foundId = this.getChildNodeByTagNames(childId, tagNames);
|
|
615
|
+
if (foundId.length > 0 && firstChildId.length === 0) {
|
|
616
|
+
firstChildId = foundId;
|
|
617
|
+
}
|
|
618
|
+
});
|
|
619
|
+
return firstChildId;
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
getParentNodeByTagNames(startNodeId: string, tagNames: string[]): string {
|
|
623
|
+
const node = this.allNodes.get().get(startNodeId);
|
|
624
|
+
if (!node || node.nodeType === 'Root') return '';
|
|
625
|
+
|
|
626
|
+
const parentId = node.parentId || '';
|
|
627
|
+
const parentNode = this.allNodes.get().get(parentId);
|
|
628
|
+
if (
|
|
629
|
+
parentNode &&
|
|
630
|
+
'tagName' in parentNode &&
|
|
631
|
+
tagNames.includes(parentNode.tagName as string)
|
|
632
|
+
) {
|
|
633
|
+
return parentId;
|
|
634
|
+
} else {
|
|
635
|
+
return this.getParentNodeByTagNames(parentId, tagNames);
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
//getStyleByViewport(
|
|
640
|
+
// defaultClasses:
|
|
641
|
+
// | {
|
|
642
|
+
// mobile?: Record<string, string> | undefined;
|
|
643
|
+
// tablet?: Record<string, string> | undefined;
|
|
644
|
+
// desktop?: Record<string, string> | undefined;
|
|
645
|
+
// }
|
|
646
|
+
// | undefined,
|
|
647
|
+
// viewport: ViewportKey
|
|
648
|
+
//): Record<string, string> {
|
|
649
|
+
// switch (viewport) {
|
|
650
|
+
// case "desktop":
|
|
651
|
+
// return defaultClasses?.desktop || {};
|
|
652
|
+
// case "tablet":
|
|
653
|
+
// return defaultClasses?.tablet || {};
|
|
654
|
+
// default:
|
|
655
|
+
// case "mobile":
|
|
656
|
+
// return defaultClasses?.mobile || {};
|
|
657
|
+
// }
|
|
658
|
+
//}
|
|
659
|
+
|
|
660
|
+
getNodeSlug(nodeId: string): string {
|
|
661
|
+
const node = this.allNodes.get().get(nodeId);
|
|
662
|
+
if (!node || !(`slug` in node) || typeof node.slug !== `string`) return '';
|
|
663
|
+
return node.slug;
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
getNodeTagName(nodeId: string): string {
|
|
667
|
+
const node = this.allNodes.get().get(nodeId);
|
|
668
|
+
if (!node || !(`tagName` in node) || typeof node.tagName !== `string`)
|
|
669
|
+
return '';
|
|
670
|
+
return node.tagName;
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
getIsContextPane(nodeId: string): boolean {
|
|
674
|
+
const node = this.allNodes.get().get(nodeId);
|
|
675
|
+
if (!node || !(`isContextPane` in node)) return false;
|
|
676
|
+
return !!node.isContextPane;
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
getMenuNodeById(id: string): MenuNode | null {
|
|
680
|
+
const node = this.allNodes.get().get(id);
|
|
681
|
+
return node?.nodeType === 'Menu' ? (node as MenuNode) : null;
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
getTractStackNodeById(id: string): TractStackNode | null {
|
|
685
|
+
const node = this.allNodes.get().get(id);
|
|
686
|
+
return node?.nodeType === 'TractStack' ? (node as TractStackNode) : null;
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
getStoryFragmentNodeBySlug(slug: string): StoryFragmentNode | null {
|
|
690
|
+
const nodes = Array.from(this.allNodes.get().values());
|
|
691
|
+
return (
|
|
692
|
+
nodes.find(
|
|
693
|
+
(node): node is StoryFragmentNode =>
|
|
694
|
+
node.nodeType === 'StoryFragment' &&
|
|
695
|
+
'slug' in node &&
|
|
696
|
+
node.slug === slug
|
|
697
|
+
) || null
|
|
698
|
+
);
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
getContextPaneNodeBySlug(slug: string): PaneNode | null {
|
|
702
|
+
const nodes = Array.from(this.allNodes.get().values());
|
|
703
|
+
return (
|
|
704
|
+
nodes.find(
|
|
705
|
+
(node): node is PaneNode =>
|
|
706
|
+
node.nodeType === 'Pane' &&
|
|
707
|
+
'slug' in node &&
|
|
708
|
+
node.slug === slug &&
|
|
709
|
+
'isContextPane' in node &&
|
|
710
|
+
node.isContextPane === true
|
|
711
|
+
) || null
|
|
712
|
+
);
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
getImpressionNodesForPanes(paneIds: string[]): ImpressionNode[] {
|
|
716
|
+
const nodes = Array.from(this.impressionNodes.get().values());
|
|
717
|
+
return nodes.filter(
|
|
718
|
+
(node): node is ImpressionNode =>
|
|
719
|
+
node.nodeType === 'Impression' &&
|
|
720
|
+
typeof node.parentId === `string` &&
|
|
721
|
+
paneIds.includes(node.parentId)
|
|
722
|
+
);
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
getPaneSlug(nodeId: string): string | null {
|
|
726
|
+
const node = this.allNodes.get().get(nodeId);
|
|
727
|
+
if (!node || node.nodeType !== 'Pane') {
|
|
728
|
+
return null;
|
|
729
|
+
}
|
|
730
|
+
if (!('slug' in node) || typeof node.slug !== 'string') {
|
|
731
|
+
return null;
|
|
732
|
+
}
|
|
733
|
+
return node.slug;
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
getNodeCodeHookPayload(
|
|
737
|
+
nodeId: string
|
|
738
|
+
): { target: string; params?: Record<string, string> } | null {
|
|
739
|
+
const node = this.allNodes.get().get(nodeId);
|
|
740
|
+
const target =
|
|
741
|
+
node && 'codeHookTarget' in node
|
|
742
|
+
? (node.codeHookTarget as string)
|
|
743
|
+
: undefined;
|
|
744
|
+
const payload =
|
|
745
|
+
node && 'codeHookPayload' in node
|
|
746
|
+
? (node.codeHookPayload as Record<string, string>)
|
|
747
|
+
: undefined;
|
|
748
|
+
|
|
749
|
+
if (target) {
|
|
750
|
+
return {
|
|
751
|
+
target: target,
|
|
752
|
+
...(payload && { params: payload }),
|
|
753
|
+
};
|
|
754
|
+
}
|
|
755
|
+
return null;
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
getPaneIsDecorative(nodeId: string): boolean {
|
|
759
|
+
const paneNode = this.allNodes.get().get(nodeId) as PaneNode;
|
|
760
|
+
if (paneNode.nodeType !== 'Pane') {
|
|
761
|
+
return false;
|
|
762
|
+
}
|
|
763
|
+
if (paneNode.isDecorative) return true;
|
|
764
|
+
return false;
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
getPaneBeliefs(
|
|
768
|
+
nodeId: string
|
|
769
|
+
): { heldBeliefs: BeliefDatum; withheldBeliefs: BeliefDatum } | null {
|
|
770
|
+
const paneNode = this.allNodes.get().get(nodeId) as PaneNode;
|
|
771
|
+
if (paneNode.nodeType !== 'Pane') {
|
|
772
|
+
return null;
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
const beliefs: { heldBeliefs: BeliefDatum; withheldBeliefs: BeliefDatum } =
|
|
776
|
+
{
|
|
777
|
+
heldBeliefs: {},
|
|
778
|
+
withheldBeliefs: {},
|
|
779
|
+
};
|
|
780
|
+
let anyBeliefs = false;
|
|
781
|
+
if ('heldBeliefs' in paneNode) {
|
|
782
|
+
beliefs.heldBeliefs = paneNode.heldBeliefs as BeliefDatum;
|
|
783
|
+
anyBeliefs = true;
|
|
784
|
+
}
|
|
785
|
+
if ('withheldBeliefs' in paneNode) {
|
|
786
|
+
beliefs.withheldBeliefs = paneNode.withheldBeliefs as BeliefDatum;
|
|
787
|
+
anyBeliefs = true;
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
return anyBeliefs ? beliefs : null;
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
getNodeClasses(
|
|
794
|
+
nodeId: string,
|
|
795
|
+
viewport: ViewportKey,
|
|
796
|
+
depth: number = 0
|
|
797
|
+
): string {
|
|
798
|
+
const isPreview = this.rootNodeId.get() === `tmp`;
|
|
799
|
+
const node = this.allNodes.get().get(nodeId);
|
|
800
|
+
if (!node) return '';
|
|
801
|
+
|
|
802
|
+
switch (node.nodeType) {
|
|
803
|
+
case 'Markdown':
|
|
804
|
+
{
|
|
805
|
+
const markdownFragment = node as MarkdownPaneFragmentNode;
|
|
806
|
+
if (markdownFragment.parentClasses) {
|
|
807
|
+
const [all, mobile, tablet, desktop] = processClassesForViewports(
|
|
808
|
+
markdownFragment.parentClasses[depth],
|
|
809
|
+
{}, // No override classes for Markdown parent case
|
|
810
|
+
1
|
|
811
|
+
);
|
|
812
|
+
|
|
813
|
+
if (isPreview) return desktop[0];
|
|
814
|
+
switch (viewport) {
|
|
815
|
+
case 'desktop':
|
|
816
|
+
return desktop[0];
|
|
817
|
+
case 'tablet':
|
|
818
|
+
return tablet[0];
|
|
819
|
+
case 'mobile':
|
|
820
|
+
return mobile[0];
|
|
821
|
+
default:
|
|
822
|
+
return all[0];
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
// Fallback to existing parentCss if needed
|
|
826
|
+
if ('parentCss' in markdownFragment) {
|
|
827
|
+
return (<string[]>markdownFragment.parentCss)[depth];
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
break;
|
|
831
|
+
|
|
832
|
+
case 'TagElement':
|
|
833
|
+
{
|
|
834
|
+
const getButtonClasses = (node: FlatNode) => {
|
|
835
|
+
return {
|
|
836
|
+
mobile: strippedStyles(node.buttonPayload?.buttonClasses || {}),
|
|
837
|
+
tablet: {},
|
|
838
|
+
desktop: {},
|
|
839
|
+
};
|
|
840
|
+
};
|
|
841
|
+
|
|
842
|
+
const getHoverClasses = (node: FlatNode) => {
|
|
843
|
+
return {
|
|
844
|
+
mobile: strippedStyles(
|
|
845
|
+
node.buttonPayload?.buttonHoverClasses || {}
|
|
846
|
+
),
|
|
847
|
+
tablet: {},
|
|
848
|
+
desktop: {},
|
|
849
|
+
};
|
|
850
|
+
};
|
|
851
|
+
|
|
852
|
+
if (hasButtonPayload(node)) {
|
|
853
|
+
const [classesPayload] = processClassesForViewports(
|
|
854
|
+
getButtonClasses(node),
|
|
855
|
+
{},
|
|
856
|
+
1
|
|
857
|
+
);
|
|
858
|
+
const [classesHoverPayload] = processClassesForViewports(
|
|
859
|
+
getHoverClasses(node),
|
|
860
|
+
{},
|
|
861
|
+
1
|
|
862
|
+
);
|
|
863
|
+
return `${classesPayload?.length ? classesPayload[0] : ``} ${
|
|
864
|
+
classesHoverPayload?.length
|
|
865
|
+
? addHoverPrefix(classesHoverPayload[0])
|
|
866
|
+
: ``
|
|
867
|
+
}`;
|
|
868
|
+
}
|
|
869
|
+
const closestPaneId = this.getClosestNodeTypeFromId(
|
|
870
|
+
nodeId,
|
|
871
|
+
'Markdown'
|
|
872
|
+
);
|
|
873
|
+
const paneNode = this.allNodes
|
|
874
|
+
.get()
|
|
875
|
+
.get(closestPaneId) as MarkdownPaneFragmentNode;
|
|
876
|
+
if (paneNode && 'tagName' in node) {
|
|
877
|
+
const tagNameStr = node.tagName as string;
|
|
878
|
+
const styles = paneNode.defaultClasses![tagNameStr];
|
|
879
|
+
if (styles && styles.mobile) {
|
|
880
|
+
const [all, mobile, tablet, desktop] = processClassesForViewports(
|
|
881
|
+
styles,
|
|
882
|
+
(node as FlatNode)?.overrideClasses || {},
|
|
883
|
+
1
|
|
884
|
+
);
|
|
885
|
+
if (isPreview) return desktop[0];
|
|
886
|
+
switch (viewport) {
|
|
887
|
+
case 'desktop':
|
|
888
|
+
return desktop[0];
|
|
889
|
+
case 'tablet':
|
|
890
|
+
return tablet[0];
|
|
891
|
+
case 'mobile':
|
|
892
|
+
return mobile[0];
|
|
893
|
+
default:
|
|
894
|
+
return all[0];
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
break;
|
|
900
|
+
|
|
901
|
+
case 'StoryFragment': {
|
|
902
|
+
const storyFragment = node as StoryFragmentNode;
|
|
903
|
+
return typeof storyFragment?.tailwindBgColour === `string`
|
|
904
|
+
? `bg-${storyFragment?.tailwindBgColour}`
|
|
905
|
+
: ``;
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
return '';
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
nodeToNotify(nodeId: string, nodeType: string) {
|
|
912
|
+
switch (nodeType) {
|
|
913
|
+
case `StoryFragment`:
|
|
914
|
+
return `root`;
|
|
915
|
+
case `Pane`:
|
|
916
|
+
if (this.getIsContextPane(nodeId)) return `root`;
|
|
917
|
+
return this.getClosestNodeTypeFromId(nodeId, 'StoryFragment');
|
|
918
|
+
case `TagElement`:
|
|
919
|
+
case `BgPane`:
|
|
920
|
+
case `Markdown`:
|
|
921
|
+
case `Impression`:
|
|
922
|
+
return this.getClosestNodeTypeFromId(nodeId, 'Pane');
|
|
923
|
+
case `Menu`:
|
|
924
|
+
// do nothing
|
|
925
|
+
break;
|
|
926
|
+
default:
|
|
927
|
+
console.warn(`nodeToNotify missed on`, nodeType);
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
modifyNodes(
|
|
932
|
+
newData: BaseNode[],
|
|
933
|
+
options?: {
|
|
934
|
+
notify?: boolean;
|
|
935
|
+
recordHistory?: boolean;
|
|
936
|
+
}
|
|
937
|
+
) {
|
|
938
|
+
const undoList: ((ctx: NodesContext) => void)[] = [];
|
|
939
|
+
const redoList: ((ctx: NodesContext) => void)[] = [];
|
|
940
|
+
const shouldNotify = options?.notify ?? true;
|
|
941
|
+
const shouldRecordHistory = options?.recordHistory ?? true;
|
|
942
|
+
|
|
943
|
+
for (let i = 0; i < newData.length; i++) {
|
|
944
|
+
const node = newData[i];
|
|
945
|
+
const currentNodeData = this.allNodes.get().get(node.id) as BaseNode;
|
|
946
|
+
if (!currentNodeData) {
|
|
947
|
+
continue;
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
if (isDeepEqual(currentNodeData, node)) {
|
|
951
|
+
continue;
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
const newNodes = new Map(this.allNodes.get());
|
|
955
|
+
newNodes.set(node.id, node);
|
|
956
|
+
this.allNodes.set(newNodes);
|
|
957
|
+
|
|
958
|
+
const deepEqualWithExclusions = isDeepEqual(currentNodeData, node, [
|
|
959
|
+
'isChanged',
|
|
960
|
+
]);
|
|
961
|
+
|
|
962
|
+
if (deepEqualWithExclusions) {
|
|
963
|
+
if (shouldNotify) this.notifyNode(node.id);
|
|
964
|
+
continue;
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
switch (node.nodeType) {
|
|
968
|
+
case `TagElement`:
|
|
969
|
+
case `BgPane`:
|
|
970
|
+
case `Markdown`: {
|
|
971
|
+
const paneNodeId = this.getClosestNodeTypeFromId(node.id, 'Pane');
|
|
972
|
+
const paneNode = cloneDeep(
|
|
973
|
+
this.allNodes.get().get(paneNodeId)
|
|
974
|
+
) as PaneNode;
|
|
975
|
+
this.modifyNodes([{ ...paneNode, isChanged: true }], {
|
|
976
|
+
notify: false,
|
|
977
|
+
});
|
|
978
|
+
this.notifyNode(ROOT_NODE_NAME);
|
|
979
|
+
break;
|
|
980
|
+
}
|
|
981
|
+
case `Menu`:
|
|
982
|
+
case `Pane`:
|
|
983
|
+
case `StoryFragment`:
|
|
984
|
+
break;
|
|
985
|
+
|
|
986
|
+
default:
|
|
987
|
+
console.warn(`must dirty check missed on `, node.nodeType);
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
undoList.push((ctx: NodesContext) => {
|
|
991
|
+
const newNodes = new Map(ctx.allNodes.get());
|
|
992
|
+
newNodes.set(node.id, currentNodeData);
|
|
993
|
+
ctx.allNodes.set(newNodes);
|
|
994
|
+
if (shouldNotify) {
|
|
995
|
+
const parentNode = this.nodeToNotify(node.id, node.nodeType);
|
|
996
|
+
if (parentNode) this.notifyNode(parentNode);
|
|
997
|
+
}
|
|
998
|
+
});
|
|
999
|
+
redoList.push((ctx: NodesContext) => {
|
|
1000
|
+
const newNodes = new Map(ctx.allNodes.get());
|
|
1001
|
+
newNodes.set(node.id, node);
|
|
1002
|
+
ctx.allNodes.set(newNodes);
|
|
1003
|
+
if (shouldNotify) {
|
|
1004
|
+
const parentNode = this.nodeToNotify(node.id, node.nodeType);
|
|
1005
|
+
if (parentNode) this.notifyNode(parentNode);
|
|
1006
|
+
}
|
|
1007
|
+
});
|
|
1008
|
+
|
|
1009
|
+
if (shouldNotify) {
|
|
1010
|
+
if ([`Menu`, `StoryFragment`].includes(node.nodeType))
|
|
1011
|
+
this.notifyNode(ROOT_NODE_NAME);
|
|
1012
|
+
this.notifyNode(node.id);
|
|
1013
|
+
}
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
if (undoList.length > 0 && shouldRecordHistory) {
|
|
1017
|
+
this.history.addPatch({
|
|
1018
|
+
op: PatchOp.REPLACE,
|
|
1019
|
+
undo: (ctx) => {
|
|
1020
|
+
undoList.forEach((fn) => fn(ctx));
|
|
1021
|
+
},
|
|
1022
|
+
redo: (ctx) => {
|
|
1023
|
+
redoList.forEach((fn) => fn(ctx));
|
|
1024
|
+
},
|
|
1025
|
+
});
|
|
1026
|
+
}
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
getNodeStringStyles(nodeId: string): string {
|
|
1030
|
+
const node = this.allNodes.get().get(nodeId);
|
|
1031
|
+
return this.getStringBgColorStyle(node);
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
getNodeCSSPropertiesStyles(nodeId: string): CSSProperties {
|
|
1035
|
+
const node = this.allNodes.get().get(nodeId);
|
|
1036
|
+
return this.getPaneBgColorStyle(node);
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
getPaneBgColorStyle(node: BaseNode | undefined): CSSProperties {
|
|
1040
|
+
if (!node) return {};
|
|
1041
|
+
|
|
1042
|
+
switch (node?.nodeType) {
|
|
1043
|
+
case 'Pane': {
|
|
1044
|
+
const pane = node as PaneFragmentNode;
|
|
1045
|
+
if ('bgColour' in pane) {
|
|
1046
|
+
return { backgroundColor: <string>pane.bgColour };
|
|
1047
|
+
}
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
1050
|
+
return {};
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
getStringBgColorStyle(node: BaseNode | undefined): string {
|
|
1054
|
+
if (!node) return '';
|
|
1055
|
+
switch (node?.nodeType) {
|
|
1056
|
+
case 'Pane': {
|
|
1057
|
+
const pane = node as PaneFragmentNode;
|
|
1058
|
+
if ('bgColour' in pane) {
|
|
1059
|
+
return `background-color: ${<string>pane.bgColour}`;
|
|
1060
|
+
}
|
|
1061
|
+
}
|
|
1062
|
+
}
|
|
1063
|
+
return '';
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
//addPaneToStoryFragment(
|
|
1067
|
+
// nodeId: string,
|
|
1068
|
+
// pane: PaneNode,
|
|
1069
|
+
// location: 'before' | 'after'
|
|
1070
|
+
///) {
|
|
1071
|
+
// const node = this.allNodes.get().get(nodeId) as BaseNode;
|
|
1072
|
+
// if (
|
|
1073
|
+
// !node ||
|
|
1074
|
+
// (node.nodeType !== 'StoryFragment' && node.nodeType !== 'Pane')
|
|
1075
|
+
// ) {
|
|
1076
|
+
// return;
|
|
1077
|
+
// }
|
|
1078
|
+
|
|
1079
|
+
// pane.id = ulid();
|
|
1080
|
+
// this.addNode(pane);
|
|
1081
|
+
|
|
1082
|
+
// if (node.nodeType === 'Pane') {
|
|
1083
|
+
// const storyFragmentId = this.getClosestNodeTypeFromId(
|
|
1084
|
+
// nodeId,
|
|
1085
|
+
// 'StoryFragment'
|
|
1086
|
+
// );
|
|
1087
|
+
// const storyFragment = this.allNodes
|
|
1088
|
+
// .get()
|
|
1089
|
+
// .get(storyFragmentId) as StoryFragmentNode;
|
|
1090
|
+
// if (storyFragment) {
|
|
1091
|
+
// pane.parentId = storyFragmentId;
|
|
1092
|
+
// const originalPaneIndex = storyFragment.paneIds.indexOf(pane.parentId);
|
|
1093
|
+
// let insertIdx = -1;
|
|
1094
|
+
// if (location === 'before')
|
|
1095
|
+
// insertIdx = Math.max(0, originalPaneIndex - 1);
|
|
1096
|
+
// else
|
|
1097
|
+
// insertIdx = Math.min(
|
|
1098
|
+
// storyFragment.paneIds.length - 1,
|
|
1099
|
+
// originalPaneIndex + 1
|
|
1100
|
+
// );
|
|
1101
|
+
// storyFragment.paneIds.splice(insertIdx, 0, pane.id);
|
|
1102
|
+
// }
|
|
1103
|
+
// } else if (node.nodeType !== 'StoryFragment') {
|
|
1104
|
+
// const storyFragment = node as StoryFragmentNode;
|
|
1105
|
+
// if (storyFragment) {
|
|
1106
|
+
// pane.parentId = node.id;
|
|
1107
|
+
// if (location === 'after') {
|
|
1108
|
+
// storyFragment.paneIds.push(pane.id);
|
|
1109
|
+
// } else {
|
|
1110
|
+
// storyFragment.paneIds.unshift(pane.id);
|
|
1111
|
+
// }
|
|
1112
|
+
// }
|
|
1113
|
+
// }
|
|
1114
|
+
//}
|
|
1115
|
+
|
|
1116
|
+
addContextTemplatePane(ownerId: string, pane: TemplatePane) {
|
|
1117
|
+
const ownerNode = this.allNodes.get().get(ownerId);
|
|
1118
|
+
if (ownerNode?.nodeType === 'Pane') {
|
|
1119
|
+
const pane = ownerNode as PaneNode;
|
|
1120
|
+
if (!pane.isContextPane) {
|
|
1121
|
+
return;
|
|
1122
|
+
}
|
|
1123
|
+
}
|
|
1124
|
+
const duplicatedPane = cloneDeep(pane) as TemplatePane;
|
|
1125
|
+
duplicatedPane.id = ownerId;
|
|
1126
|
+
if (
|
|
1127
|
+
ownerNode &&
|
|
1128
|
+
'title' in ownerNode &&
|
|
1129
|
+
typeof ownerNode.title === `string`
|
|
1130
|
+
)
|
|
1131
|
+
duplicatedPane.title = ownerNode.title;
|
|
1132
|
+
if (ownerNode && 'slug' in ownerNode && typeof ownerNode.slug === `string`)
|
|
1133
|
+
duplicatedPane.slug = ownerNode.slug;
|
|
1134
|
+
duplicatedPane.isChanged = true;
|
|
1135
|
+
|
|
1136
|
+
// Track all nodes that need to be added
|
|
1137
|
+
let allNodes: BaseNode[] = [];
|
|
1138
|
+
|
|
1139
|
+
// must generate nodes from markdown
|
|
1140
|
+
if (duplicatedPane.markdown) {
|
|
1141
|
+
duplicatedPane.markdown = cloneDeep(pane.markdown) as TemplateMarkdown;
|
|
1142
|
+
duplicatedPane.markdown.id = pane?.markdown?.id || ulid();
|
|
1143
|
+
duplicatedPane.markdown.markdownId = pane?.markdown?.markdownId || ulid();
|
|
1144
|
+
duplicatedPane.markdown.parentId = ownerId;
|
|
1145
|
+
|
|
1146
|
+
let markdownNodes: TemplateNode[] = [];
|
|
1147
|
+
if (duplicatedPane.markdown.markdownBody) {
|
|
1148
|
+
const markdownGen = new MarkdownGenerator(this);
|
|
1149
|
+
markdownNodes = markdownGen.markdownToFlatNodes(
|
|
1150
|
+
duplicatedPane.markdown.markdownBody,
|
|
1151
|
+
duplicatedPane.markdown.id
|
|
1152
|
+
) as TemplateNode[];
|
|
1153
|
+
allNodes = [...allNodes, duplicatedPane.markdown, ...markdownNodes];
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1156
|
+
// Markdown already as nodes
|
|
1157
|
+
else if (
|
|
1158
|
+
typeof duplicatedPane.markdown !== `undefined` &&
|
|
1159
|
+
typeof duplicatedPane.markdown.id === `string`
|
|
1160
|
+
) {
|
|
1161
|
+
duplicatedPane?.markdown.nodes?.forEach((node) => {
|
|
1162
|
+
const childrenNodes = this.setupTemplateNodeRecursively(
|
|
1163
|
+
node,
|
|
1164
|
+
duplicatedPane?.markdown?.id || ''
|
|
1165
|
+
);
|
|
1166
|
+
markdownNodes.push(...childrenNodes);
|
|
1167
|
+
});
|
|
1168
|
+
allNodes = [...allNodes, duplicatedPane.markdown, ...markdownNodes];
|
|
1169
|
+
}
|
|
1170
|
+
}
|
|
1171
|
+
|
|
1172
|
+
this.addNode(duplicatedPane as PaneNode);
|
|
1173
|
+
this.addNodes(allNodes);
|
|
1174
|
+
this.notifyNode(ownerId);
|
|
1175
|
+
|
|
1176
|
+
return ownerId;
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1179
|
+
addTemplatePane(
|
|
1180
|
+
ownerId: string,
|
|
1181
|
+
pane: TemplatePane,
|
|
1182
|
+
insertPaneId?: string,
|
|
1183
|
+
location?: 'before' | 'after'
|
|
1184
|
+
) {
|
|
1185
|
+
const ownerNode = this.allNodes.get().get(ownerId);
|
|
1186
|
+
if (
|
|
1187
|
+
ownerNode?.nodeType !== 'StoryFragment' &&
|
|
1188
|
+
ownerNode?.nodeType !== 'Root' &&
|
|
1189
|
+
ownerNode?.nodeType !== 'File' &&
|
|
1190
|
+
ownerNode?.nodeType !== 'TractStack'
|
|
1191
|
+
) {
|
|
1192
|
+
return;
|
|
1193
|
+
}
|
|
1194
|
+
const duplicatedPane = cloneDeep(pane) as TemplatePane;
|
|
1195
|
+
const duplicatedPaneId = pane?.id || ulid();
|
|
1196
|
+
duplicatedPane.id = duplicatedPaneId;
|
|
1197
|
+
duplicatedPane.parentId = ownerNode.id;
|
|
1198
|
+
duplicatedPane.isChanged = true;
|
|
1199
|
+
|
|
1200
|
+
if (this.rootNodeId.get() !== 'tmp') {
|
|
1201
|
+
if (
|
|
1202
|
+
ownerNode.nodeType === 'StoryFragment' &&
|
|
1203
|
+
'slug' in ownerNode &&
|
|
1204
|
+
'title' in ownerNode &&
|
|
1205
|
+
typeof ownerNode.title === `string` &&
|
|
1206
|
+
duplicatedPane.slug === '' &&
|
|
1207
|
+
duplicatedPane.title === ''
|
|
1208
|
+
) {
|
|
1209
|
+
duplicatedPane.slug = `${ownerNode.slug}-${duplicatedPaneId.slice(-4)}`;
|
|
1210
|
+
duplicatedPane.title = `${ownerNode.title.slice(0, 20)}-${duplicatedPaneId.slice(-4)}`;
|
|
1211
|
+
}
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1214
|
+
let allNodes: BaseNode[] = [];
|
|
1215
|
+
|
|
1216
|
+
if (duplicatedPane.markdown) {
|
|
1217
|
+
duplicatedPane.markdown = cloneDeep(pane.markdown) as TemplateMarkdown;
|
|
1218
|
+
duplicatedPane.markdown.id = pane?.markdown?.id || ulid();
|
|
1219
|
+
duplicatedPane.markdown.markdownId = pane?.markdown?.markdownId || ulid();
|
|
1220
|
+
duplicatedPane.markdown.parentId = duplicatedPaneId;
|
|
1221
|
+
|
|
1222
|
+
let markdownNodes: TemplateNode[] = [];
|
|
1223
|
+
if (duplicatedPane.markdown.markdownBody) {
|
|
1224
|
+
const markdownGen = new MarkdownGenerator(this);
|
|
1225
|
+
markdownNodes = markdownGen.markdownToFlatNodes(
|
|
1226
|
+
duplicatedPane.markdown.markdownBody,
|
|
1227
|
+
duplicatedPane.markdown.id
|
|
1228
|
+
) as TemplateNode[];
|
|
1229
|
+
allNodes = [...allNodes, duplicatedPane.markdown, ...markdownNodes];
|
|
1230
|
+
} else if (
|
|
1231
|
+
typeof duplicatedPane.markdown !== `undefined` &&
|
|
1232
|
+
typeof duplicatedPane.markdown.id === `string`
|
|
1233
|
+
) {
|
|
1234
|
+
// Create a map to track the original node ID to its duplicated node ID
|
|
1235
|
+
const oldToNewIdMap = new Map<string, string>();
|
|
1236
|
+
// First pass: Clone nodes and generate new IDs
|
|
1237
|
+
const nodesClone =
|
|
1238
|
+
duplicatedPane?.markdown?.nodes?.map((originalNode) => {
|
|
1239
|
+
const newNode = cloneDeep(originalNode);
|
|
1240
|
+
newNode.id = ulid();
|
|
1241
|
+
oldToNewIdMap.set(originalNode.id, newNode.id);
|
|
1242
|
+
return newNode;
|
|
1243
|
+
}) || [];
|
|
1244
|
+
// Second pass: Update parent IDs using the mapping
|
|
1245
|
+
nodesClone.forEach((node) => {
|
|
1246
|
+
// Special case for direct children of markdown
|
|
1247
|
+
if (node.parentId === pane?.markdown?.id) {
|
|
1248
|
+
node.parentId = duplicatedPane?.markdown?.id || '';
|
|
1249
|
+
} else {
|
|
1250
|
+
// For all other nodes, use the mapping to find the new parent ID
|
|
1251
|
+
const newParentId = oldToNewIdMap.get(node.parentId || '');
|
|
1252
|
+
if (newParentId) {
|
|
1253
|
+
node.parentId = newParentId;
|
|
1254
|
+
}
|
|
1255
|
+
}
|
|
1256
|
+
markdownNodes.push(node);
|
|
1257
|
+
});
|
|
1258
|
+
allNodes = [...allNodes, duplicatedPane.markdown, ...markdownNodes];
|
|
1259
|
+
}
|
|
1260
|
+
}
|
|
1261
|
+
|
|
1262
|
+
if (duplicatedPane.bgPane) {
|
|
1263
|
+
const bgPaneId = ulid();
|
|
1264
|
+
|
|
1265
|
+
if (duplicatedPane.bgPane.type === 'visual-break') {
|
|
1266
|
+
const visualBreakPane = duplicatedPane.bgPane as VisualBreakNode;
|
|
1267
|
+
const bgPaneNode: VisualBreakNode = {
|
|
1268
|
+
id: bgPaneId,
|
|
1269
|
+
nodeType: 'BgPane',
|
|
1270
|
+
parentId: duplicatedPaneId,
|
|
1271
|
+
type: 'visual-break',
|
|
1272
|
+
breakDesktop: visualBreakPane.breakDesktop,
|
|
1273
|
+
breakTablet: visualBreakPane.breakTablet,
|
|
1274
|
+
breakMobile: visualBreakPane.breakMobile,
|
|
1275
|
+
};
|
|
1276
|
+
allNodes.push(bgPaneNode);
|
|
1277
|
+
} else if (duplicatedPane.bgPane.type === 'artpack-image') {
|
|
1278
|
+
const artpackBgPane = duplicatedPane.bgPane as ArtpackImageNode;
|
|
1279
|
+
const bgPaneNode: ArtpackImageNode = {
|
|
1280
|
+
id: bgPaneId,
|
|
1281
|
+
nodeType: 'BgPane',
|
|
1282
|
+
parentId: duplicatedPaneId,
|
|
1283
|
+
type: 'artpack-image',
|
|
1284
|
+
collection: artpackBgPane.collection,
|
|
1285
|
+
image: artpackBgPane.image,
|
|
1286
|
+
src: artpackBgPane.src,
|
|
1287
|
+
srcSet: artpackBgPane.srcSet,
|
|
1288
|
+
alt: artpackBgPane.alt || `Artpack image`,
|
|
1289
|
+
objectFit: artpackBgPane.objectFit || 'cover',
|
|
1290
|
+
};
|
|
1291
|
+
allNodes.push(bgPaneNode);
|
|
1292
|
+
}
|
|
1293
|
+
delete duplicatedPane.bgPane;
|
|
1294
|
+
}
|
|
1295
|
+
|
|
1296
|
+
const storyFragmentNode = ownerNode as StoryFragmentNode;
|
|
1297
|
+
let specificIdx = -1;
|
|
1298
|
+
let elIdx = -1;
|
|
1299
|
+
let storyFragmentWasChanged: boolean = false;
|
|
1300
|
+
|
|
1301
|
+
if (
|
|
1302
|
+
insertPaneId &&
|
|
1303
|
+
location &&
|
|
1304
|
+
storyFragmentNode?.nodeType === 'StoryFragment'
|
|
1305
|
+
) {
|
|
1306
|
+
storyFragmentWasChanged = storyFragmentNode.isChanged || false;
|
|
1307
|
+
specificIdx = storyFragmentNode.paneIds.indexOf(insertPaneId);
|
|
1308
|
+
elIdx = specificIdx;
|
|
1309
|
+
if (elIdx === -1) {
|
|
1310
|
+
storyFragmentNode.paneIds.push(duplicatedPane.id);
|
|
1311
|
+
} else {
|
|
1312
|
+
if (location === 'before') {
|
|
1313
|
+
storyFragmentNode.paneIds.splice(elIdx, 0, duplicatedPane.id);
|
|
1314
|
+
specificIdx = Math.max(0, specificIdx - 1);
|
|
1315
|
+
} else {
|
|
1316
|
+
storyFragmentNode.paneIds.splice(elIdx + 1, 0, duplicatedPane.id);
|
|
1317
|
+
specificIdx = Math.min(
|
|
1318
|
+
specificIdx + 1,
|
|
1319
|
+
storyFragmentNode.paneIds.length
|
|
1320
|
+
);
|
|
1321
|
+
}
|
|
1322
|
+
}
|
|
1323
|
+
storyFragmentNode.isChanged = true;
|
|
1324
|
+
}
|
|
1325
|
+
|
|
1326
|
+
this.addNode(duplicatedPane as PaneNode);
|
|
1327
|
+
this.linkChildToParent(
|
|
1328
|
+
duplicatedPane.id,
|
|
1329
|
+
duplicatedPane.parentId,
|
|
1330
|
+
specificIdx
|
|
1331
|
+
);
|
|
1332
|
+
this.addNodes(allNodes);
|
|
1333
|
+
this.notifyNode(ownerId);
|
|
1334
|
+
|
|
1335
|
+
this.history.addPatch({
|
|
1336
|
+
op: PatchOp.ADD,
|
|
1337
|
+
undo: (ctx) => {
|
|
1338
|
+
ctx.deleteNodes(allNodes);
|
|
1339
|
+
|
|
1340
|
+
if (
|
|
1341
|
+
storyFragmentNode &&
|
|
1342
|
+
storyFragmentNode.nodeType === 'StoryFragment' &&
|
|
1343
|
+
Array.isArray(storyFragmentNode.paneIds)
|
|
1344
|
+
) {
|
|
1345
|
+
storyFragmentNode.paneIds = storyFragmentNode.paneIds.filter(
|
|
1346
|
+
(id: string) => id !== duplicatedPane.id
|
|
1347
|
+
);
|
|
1348
|
+
storyFragmentNode.isChanged = storyFragmentWasChanged;
|
|
1349
|
+
}
|
|
1350
|
+
|
|
1351
|
+
ctx.deleteNodes([duplicatedPane]);
|
|
1352
|
+
},
|
|
1353
|
+
redo: (ctx) => {
|
|
1354
|
+
if (storyFragmentNode?.nodeType === 'StoryFragment') {
|
|
1355
|
+
if (elIdx === -1) {
|
|
1356
|
+
storyFragmentNode.paneIds.push(duplicatedPane.id);
|
|
1357
|
+
} else {
|
|
1358
|
+
if (location === 'before') {
|
|
1359
|
+
storyFragmentNode.paneIds.splice(elIdx, 0, duplicatedPane.id);
|
|
1360
|
+
} else {
|
|
1361
|
+
storyFragmentNode.paneIds.splice(elIdx + 1, 0, duplicatedPane.id);
|
|
1362
|
+
}
|
|
1363
|
+
}
|
|
1364
|
+
storyFragmentNode.isChanged = true;
|
|
1365
|
+
}
|
|
1366
|
+
|
|
1367
|
+
ctx.addNodes([duplicatedPane]);
|
|
1368
|
+
ctx.linkChildToParent(
|
|
1369
|
+
duplicatedPane.id,
|
|
1370
|
+
duplicatedPane.parentId,
|
|
1371
|
+
specificIdx
|
|
1372
|
+
);
|
|
1373
|
+
ctx.addNodes(allNodes);
|
|
1374
|
+
},
|
|
1375
|
+
});
|
|
1376
|
+
|
|
1377
|
+
return duplicatedPaneId;
|
|
1378
|
+
}
|
|
1379
|
+
|
|
1380
|
+
handleInsertSignal(tagName: string, nodeId: string) {
|
|
1381
|
+
switch (tagName) {
|
|
1382
|
+
case `a`:
|
|
1383
|
+
settingsPanelStore.set({
|
|
1384
|
+
action: `style-link`,
|
|
1385
|
+
nodeId: nodeId,
|
|
1386
|
+
expanded: true,
|
|
1387
|
+
});
|
|
1388
|
+
break;
|
|
1389
|
+
case `img`:
|
|
1390
|
+
settingsPanelStore.set({
|
|
1391
|
+
action: `style-image`,
|
|
1392
|
+
nodeId: nodeId,
|
|
1393
|
+
expanded: true,
|
|
1394
|
+
});
|
|
1395
|
+
break;
|
|
1396
|
+
case `code`:
|
|
1397
|
+
settingsPanelStore.set({
|
|
1398
|
+
action: `style-widget`,
|
|
1399
|
+
nodeId: nodeId,
|
|
1400
|
+
expanded: true,
|
|
1401
|
+
});
|
|
1402
|
+
break;
|
|
1403
|
+
}
|
|
1404
|
+
this.toolModeValStore.set({ value: 'text' });
|
|
1405
|
+
}
|
|
1406
|
+
|
|
1407
|
+
addTemplateImpressionNode(targetId: string, node: ImpressionNode) {
|
|
1408
|
+
const targetNode = this.allNodes.get().get(targetId) as BaseNode;
|
|
1409
|
+
if (!targetNode || targetNode.nodeType !== 'Pane') {
|
|
1410
|
+
return;
|
|
1411
|
+
}
|
|
1412
|
+
const duplicatedNodes = cloneDeep(node) as TemplateNode;
|
|
1413
|
+
const flattenedNodes = this.setupTemplateNodeRecursively(
|
|
1414
|
+
duplicatedNodes,
|
|
1415
|
+
targetId
|
|
1416
|
+
);
|
|
1417
|
+
this.addNodes(flattenedNodes);
|
|
1418
|
+
this.history.addPatch({
|
|
1419
|
+
op: PatchOp.ADD,
|
|
1420
|
+
undo: (ctx) => ctx.deleteNodes(flattenedNodes),
|
|
1421
|
+
redo: (ctx) => ctx.addNodes(flattenedNodes),
|
|
1422
|
+
});
|
|
1423
|
+
}
|
|
1424
|
+
|
|
1425
|
+
addTemplateNode(
|
|
1426
|
+
targetId: string,
|
|
1427
|
+
node: TemplateNode,
|
|
1428
|
+
insertNodeId?: string,
|
|
1429
|
+
location?: 'before' | 'after'
|
|
1430
|
+
): string | null {
|
|
1431
|
+
let targetNode = this.allNodes.get().get(targetId) as BaseNode;
|
|
1432
|
+
|
|
1433
|
+
// 1. VALIDATE TARGET NODE
|
|
1434
|
+
// Allow Pane, Markdown, or TagElement as valid targets.
|
|
1435
|
+
if (
|
|
1436
|
+
!targetNode ||
|
|
1437
|
+
(targetNode.nodeType !== 'Pane' &&
|
|
1438
|
+
targetNode.nodeType !== 'Markdown' &&
|
|
1439
|
+
targetNode.nodeType !== 'TagElement')
|
|
1440
|
+
) {
|
|
1441
|
+
console.error('addTemplateNode received an invalid targetId.');
|
|
1442
|
+
return null;
|
|
1443
|
+
}
|
|
1444
|
+
|
|
1445
|
+
// 2. PREPARE PARENT AND STATE VARIABLES
|
|
1446
|
+
let parentId =
|
|
1447
|
+
targetNode.nodeType === 'Markdown' || targetNode.nodeType === 'Pane'
|
|
1448
|
+
? targetId
|
|
1449
|
+
: this.getClosestNodeTypeFromId(targetId, 'Markdown');
|
|
1450
|
+
const paneNodeId = this.getClosestNodeTypeFromId(targetId, 'Pane');
|
|
1451
|
+
const originalPaneNode = this.allNodes.get().get(paneNodeId)
|
|
1452
|
+
? cloneDeep(this.allNodes.get().get(paneNodeId) as PaneNode)
|
|
1453
|
+
: null;
|
|
1454
|
+
|
|
1455
|
+
let autoCreatedMarkdownNode: MarkdownPaneFragmentNode | null = null;
|
|
1456
|
+
|
|
1457
|
+
// 3. HANDLE EMPTY PANE BY AUTO-CREATING A MARKDOWN NODE
|
|
1458
|
+
if (targetNode.nodeType === 'Pane') {
|
|
1459
|
+
// Create a minimal markdown node to act as the container
|
|
1460
|
+
const newMarkdownNode: MarkdownPaneFragmentNode = {
|
|
1461
|
+
id: ulid(),
|
|
1462
|
+
nodeType: 'Markdown',
|
|
1463
|
+
parentId: targetId,
|
|
1464
|
+
type: 'markdown',
|
|
1465
|
+
markdownId: ulid(),
|
|
1466
|
+
defaultClasses: {},
|
|
1467
|
+
};
|
|
1468
|
+
|
|
1469
|
+
autoCreatedMarkdownNode = newMarkdownNode;
|
|
1470
|
+
|
|
1471
|
+
// Add the new markdown node to the state
|
|
1472
|
+
this.addNode(newMarkdownNode);
|
|
1473
|
+
|
|
1474
|
+
// Update the parentId to be this new markdown node for the next step
|
|
1475
|
+
parentId = newMarkdownNode.id;
|
|
1476
|
+
}
|
|
1477
|
+
|
|
1478
|
+
// 4. PREPARE THE NEW ELEMENT NODES
|
|
1479
|
+
const duplicatedNodes = cloneDeep(node) as TemplateNode;
|
|
1480
|
+
let flattenedNodes: TemplateNode[] = [];
|
|
1481
|
+
|
|
1482
|
+
if (['img', 'code'].includes(duplicatedNodes.tagName)) {
|
|
1483
|
+
let closestListNode = '';
|
|
1484
|
+
if (
|
|
1485
|
+
'tagName' in targetNode &&
|
|
1486
|
+
['ol', 'ul'].includes(targetNode.tagName as string)
|
|
1487
|
+
) {
|
|
1488
|
+
closestListNode = targetId;
|
|
1489
|
+
} else {
|
|
1490
|
+
closestListNode = this.getParentNodeByTagNames(targetId, ['ol', 'ul']);
|
|
1491
|
+
}
|
|
1492
|
+
|
|
1493
|
+
if (!closestListNode) {
|
|
1494
|
+
const ulNode: TemplateNode = {
|
|
1495
|
+
id: ulid(),
|
|
1496
|
+
nodeType: 'TagElement',
|
|
1497
|
+
tagName: 'ul',
|
|
1498
|
+
parentId: parentId,
|
|
1499
|
+
};
|
|
1500
|
+
const liNode: TemplateNode = {
|
|
1501
|
+
id: ulid(),
|
|
1502
|
+
nodeType: 'TagElement',
|
|
1503
|
+
tagName: 'li',
|
|
1504
|
+
tagNameCustom: duplicatedNodes.tagName,
|
|
1505
|
+
parentId: ulNode.id,
|
|
1506
|
+
};
|
|
1507
|
+
duplicatedNodes.parentId = liNode.id;
|
|
1508
|
+
flattenedNodes = [
|
|
1509
|
+
ulNode,
|
|
1510
|
+
liNode,
|
|
1511
|
+
...this.setupTemplateNodeRecursively(duplicatedNodes, liNode.id),
|
|
1512
|
+
];
|
|
1513
|
+
} else {
|
|
1514
|
+
const liNode: TemplateNode = {
|
|
1515
|
+
id: ulid(),
|
|
1516
|
+
nodeType: 'TagElement',
|
|
1517
|
+
tagName: 'li',
|
|
1518
|
+
tagNameCustom: duplicatedNodes.tagName,
|
|
1519
|
+
parentId: closestListNode,
|
|
1520
|
+
};
|
|
1521
|
+
duplicatedNodes.parentId = liNode.id;
|
|
1522
|
+
flattenedNodes = [
|
|
1523
|
+
liNode,
|
|
1524
|
+
...this.setupTemplateNodeRecursively(duplicatedNodes, liNode.id),
|
|
1525
|
+
];
|
|
1526
|
+
}
|
|
1527
|
+
} else {
|
|
1528
|
+
flattenedNodes = this.setupTemplateNodeRecursively(
|
|
1529
|
+
duplicatedNodes,
|
|
1530
|
+
parentId
|
|
1531
|
+
);
|
|
1532
|
+
}
|
|
1533
|
+
|
|
1534
|
+
// 5. PERFORM REMAINING STATE MUTATIONS
|
|
1535
|
+
if (originalPaneNode) {
|
|
1536
|
+
this.modifyNodes([{ ...originalPaneNode, isChanged: true }], {
|
|
1537
|
+
notify: false,
|
|
1538
|
+
recordHistory: false,
|
|
1539
|
+
});
|
|
1540
|
+
}
|
|
1541
|
+
|
|
1542
|
+
this.addNodes(flattenedNodes);
|
|
1543
|
+
|
|
1544
|
+
const newTopLevelNodes = flattenedNodes.filter(
|
|
1545
|
+
(n) => n.parentId === parentId
|
|
1546
|
+
);
|
|
1547
|
+
const newTopLevelIds = newTopLevelNodes.map((n) => n.id);
|
|
1548
|
+
|
|
1549
|
+
const parentNodesMap = this.parentNodes.get();
|
|
1550
|
+
const parentChildren = parentNodesMap.get(parentId);
|
|
1551
|
+
|
|
1552
|
+
if (insertNodeId && location && parentChildren) {
|
|
1553
|
+
const insertIndex = parentChildren.indexOf(insertNodeId);
|
|
1554
|
+
if (insertIndex !== -1) {
|
|
1555
|
+
const currentChildren = parentChildren.filter(
|
|
1556
|
+
(id) => !newTopLevelIds.includes(id)
|
|
1557
|
+
);
|
|
1558
|
+
if (location === 'before') {
|
|
1559
|
+
currentChildren.splice(insertIndex, 0, ...newTopLevelIds);
|
|
1560
|
+
} else {
|
|
1561
|
+
currentChildren.splice(insertIndex + 1, 0, ...newTopLevelIds);
|
|
1562
|
+
}
|
|
1563
|
+
parentNodesMap.set(parentId, currentChildren);
|
|
1564
|
+
}
|
|
1565
|
+
}
|
|
1566
|
+
|
|
1567
|
+
// 6. RECORD THE ENTIRE ATOMIC OPERATION in a single history patch.
|
|
1568
|
+
this.history.addPatch({
|
|
1569
|
+
op: PatchOp.ADD,
|
|
1570
|
+
undo: (ctx) => {
|
|
1571
|
+
// Undo all changes: delete the element and the auto-created markdown node (if it exists)
|
|
1572
|
+
ctx.deleteNodes(flattenedNodes);
|
|
1573
|
+
if (autoCreatedMarkdownNode) {
|
|
1574
|
+
ctx.deleteNodes([autoCreatedMarkdownNode]);
|
|
1575
|
+
}
|
|
1576
|
+
if (originalPaneNode) {
|
|
1577
|
+
const newNodes = new Map(ctx.allNodes.get());
|
|
1578
|
+
newNodes.set(originalPaneNode.id, originalPaneNode);
|
|
1579
|
+
ctx.allNodes.set(newNodes);
|
|
1580
|
+
}
|
|
1581
|
+
if (paneNodeId) ctx.notifyNode(paneNodeId);
|
|
1582
|
+
},
|
|
1583
|
+
redo: (ctx) => {
|
|
1584
|
+
// Redo all changes in the correct order
|
|
1585
|
+
if (originalPaneNode) {
|
|
1586
|
+
ctx.modifyNodes([{ ...originalPaneNode, isChanged: true }], {
|
|
1587
|
+
notify: false,
|
|
1588
|
+
recordHistory: false,
|
|
1589
|
+
});
|
|
1590
|
+
}
|
|
1591
|
+
if (autoCreatedMarkdownNode) {
|
|
1592
|
+
ctx.addNode(autoCreatedMarkdownNode);
|
|
1593
|
+
}
|
|
1594
|
+
ctx.addNodes(flattenedNodes);
|
|
1595
|
+
|
|
1596
|
+
// Re-apply insertion logic
|
|
1597
|
+
const parentNodesMap = ctx.parentNodes.get();
|
|
1598
|
+
const parentChildren = parentNodesMap.get(parentId);
|
|
1599
|
+
if (insertNodeId && location && parentChildren) {
|
|
1600
|
+
const insertIndex = parentChildren.indexOf(insertNodeId);
|
|
1601
|
+
if (insertIndex !== -1) {
|
|
1602
|
+
const currentChildren = parentChildren.filter(
|
|
1603
|
+
(id) => !newTopLevelIds.includes(id)
|
|
1604
|
+
);
|
|
1605
|
+
if (location === 'before') {
|
|
1606
|
+
currentChildren.splice(insertIndex, 0, ...newTopLevelIds);
|
|
1607
|
+
} else {
|
|
1608
|
+
currentChildren.splice(insertIndex + 1, 0, ...newTopLevelIds);
|
|
1609
|
+
}
|
|
1610
|
+
parentNodesMap.set(parentId, currentChildren);
|
|
1611
|
+
}
|
|
1612
|
+
}
|
|
1613
|
+
if (paneNodeId) ctx.notifyNode(paneNodeId);
|
|
1614
|
+
},
|
|
1615
|
+
});
|
|
1616
|
+
|
|
1617
|
+
// 7. SEND A SINGLE NOTIFICATION to update the UI.
|
|
1618
|
+
if (paneNodeId) {
|
|
1619
|
+
this.notifyNode(paneNodeId);
|
|
1620
|
+
}
|
|
1621
|
+
|
|
1622
|
+
return flattenedNodes.length > 0 ? flattenedNodes[0].id : null;
|
|
1623
|
+
}
|
|
1624
|
+
|
|
1625
|
+
setupTemplateNodeRecursively(node: TemplateNode, parentId: string) {
|
|
1626
|
+
let result: TemplateNode[] = [];
|
|
1627
|
+
if (!node) return result;
|
|
1628
|
+
|
|
1629
|
+
node.id = ulid();
|
|
1630
|
+
node.parentId = parentId;
|
|
1631
|
+
result.push(node);
|
|
1632
|
+
if ('nodes' in node && node.nodes) {
|
|
1633
|
+
for (let i = 0; i < node.nodes.length; ++i) {
|
|
1634
|
+
result = result.concat(
|
|
1635
|
+
this.setupTemplateNodeRecursively(node.nodes[i], node.id)
|
|
1636
|
+
);
|
|
1637
|
+
}
|
|
1638
|
+
}
|
|
1639
|
+
return result;
|
|
1640
|
+
}
|
|
1641
|
+
|
|
1642
|
+
deleteChildren(nodeId: string): BaseNode[] {
|
|
1643
|
+
const node = this.allNodes.get().get(nodeId);
|
|
1644
|
+
if (!node) return [];
|
|
1645
|
+
|
|
1646
|
+
const children = this.getNodesRecursively(node).reverse();
|
|
1647
|
+
children.shift();
|
|
1648
|
+
const deletedNodes = this.deleteNodes(children);
|
|
1649
|
+
this.notifyNode(node.id || '');
|
|
1650
|
+
return deletedNodes;
|
|
1651
|
+
}
|
|
1652
|
+
|
|
1653
|
+
deleteNode(nodeId: string) {
|
|
1654
|
+
// Get the original node
|
|
1655
|
+
const originalNode = this.allNodes.get().get(nodeId) as FlatNode;
|
|
1656
|
+
if (!originalNode) {
|
|
1657
|
+
return;
|
|
1658
|
+
}
|
|
1659
|
+
|
|
1660
|
+
// Track if we're redirecting deletion
|
|
1661
|
+
let targetNodeId = nodeId;
|
|
1662
|
+
let targetNode = originalNode;
|
|
1663
|
+
|
|
1664
|
+
// Case 1: Node is an LI - check if it's the last one in a list
|
|
1665
|
+
if (
|
|
1666
|
+
originalNode.nodeType === 'TagElement' &&
|
|
1667
|
+
'tagName' in originalNode &&
|
|
1668
|
+
originalNode.tagName === 'li' &&
|
|
1669
|
+
originalNode.parentId
|
|
1670
|
+
) {
|
|
1671
|
+
const listNode = this.allNodes
|
|
1672
|
+
.get()
|
|
1673
|
+
.get(originalNode.parentId) as FlatNode;
|
|
1674
|
+
|
|
1675
|
+
if (
|
|
1676
|
+
listNode &&
|
|
1677
|
+
'tagName' in listNode &&
|
|
1678
|
+
(listNode.tagName === 'ul' || listNode.tagName === 'ol')
|
|
1679
|
+
) {
|
|
1680
|
+
// Check if this LI is the last/only one
|
|
1681
|
+
const listChildren = this.getChildNodeIDs(listNode.id);
|
|
1682
|
+
const isLastLi =
|
|
1683
|
+
listChildren.length === 1 && listChildren[0] === nodeId;
|
|
1684
|
+
|
|
1685
|
+
if (isLastLi) {
|
|
1686
|
+
// Redirect deletion to the list
|
|
1687
|
+
targetNodeId = listNode.id;
|
|
1688
|
+
targetNode = listNode;
|
|
1689
|
+
}
|
|
1690
|
+
}
|
|
1691
|
+
}
|
|
1692
|
+
|
|
1693
|
+
// Case 2: Node is an image or code inside an LI
|
|
1694
|
+
else if (
|
|
1695
|
+
originalNode.nodeType === 'TagElement' &&
|
|
1696
|
+
'tagName' in originalNode &&
|
|
1697
|
+
(originalNode.tagName === 'img' || originalNode.tagName === 'code')
|
|
1698
|
+
) {
|
|
1699
|
+
// Find parent LI
|
|
1700
|
+
const liParentId = this.getParentNodeByTagNames(nodeId, ['li']);
|
|
1701
|
+
|
|
1702
|
+
if (liParentId) {
|
|
1703
|
+
const liNode = this.allNodes.get().get(liParentId) as FlatNode;
|
|
1704
|
+
|
|
1705
|
+
// Check if this is the only child of the LI
|
|
1706
|
+
const liChildren = this.getChildNodeIDs(liParentId);
|
|
1707
|
+
|
|
1708
|
+
// Calculate if content is the only significant child
|
|
1709
|
+
// (there might be text nodes with whitespace)
|
|
1710
|
+
const significantChildrenCount = liChildren.filter((childId) => {
|
|
1711
|
+
const child = this.allNodes.get().get(childId) as FlatNode;
|
|
1712
|
+
if (!child) return false;
|
|
1713
|
+
|
|
1714
|
+
// Skip text nodes with only whitespace
|
|
1715
|
+
if (
|
|
1716
|
+
child.tagName === 'text' &&
|
|
1717
|
+
(!child.copy || child.copy.trim() === '')
|
|
1718
|
+
) {
|
|
1719
|
+
return false;
|
|
1720
|
+
}
|
|
1721
|
+
return true;
|
|
1722
|
+
}).length;
|
|
1723
|
+
|
|
1724
|
+
const isOnlySignificantChild = significantChildrenCount === 1;
|
|
1725
|
+
|
|
1726
|
+
if (isOnlySignificantChild && liNode?.parentId) {
|
|
1727
|
+
// Find list container (UL/OL)
|
|
1728
|
+
const listNode = this.allNodes.get().get(liNode.parentId) as FlatNode;
|
|
1729
|
+
|
|
1730
|
+
if (
|
|
1731
|
+
listNode &&
|
|
1732
|
+
'tagName' in listNode &&
|
|
1733
|
+
(listNode.tagName === 'ul' || listNode.tagName === 'ol')
|
|
1734
|
+
) {
|
|
1735
|
+
// Check if this LI is the last/only one
|
|
1736
|
+
const listChildren = this.getChildNodeIDs(listNode.id);
|
|
1737
|
+
const isLastLi =
|
|
1738
|
+
listChildren.length === 1 && listChildren[0] === liParentId;
|
|
1739
|
+
|
|
1740
|
+
if (isLastLi) {
|
|
1741
|
+
// Redirect deletion to the list
|
|
1742
|
+
targetNodeId = listNode.id;
|
|
1743
|
+
targetNode = listNode;
|
|
1744
|
+
} else {
|
|
1745
|
+
// Redirect to the LI instead
|
|
1746
|
+
targetNodeId = liParentId;
|
|
1747
|
+
targetNode = liNode;
|
|
1748
|
+
}
|
|
1749
|
+
}
|
|
1750
|
+
}
|
|
1751
|
+
}
|
|
1752
|
+
}
|
|
1753
|
+
|
|
1754
|
+
// Continue with normal deletion logic using the target node
|
|
1755
|
+
const parentId = targetNode.parentId;
|
|
1756
|
+
const toDelete = this.getNodesRecursively(targetNode).reverse();
|
|
1757
|
+
const closestMarkdownId = this.getClosestNodeTypeFromId(
|
|
1758
|
+
targetNode.id,
|
|
1759
|
+
'Markdown'
|
|
1760
|
+
);
|
|
1761
|
+
|
|
1762
|
+
this.deleteNodes(toDelete);
|
|
1763
|
+
let paneIdx: number = -1;
|
|
1764
|
+
|
|
1765
|
+
// Process based on node type
|
|
1766
|
+
if (parentId !== null) {
|
|
1767
|
+
if (targetNode.nodeType === 'Pane') {
|
|
1768
|
+
const storyFragment = this.allNodes
|
|
1769
|
+
.get()
|
|
1770
|
+
.get(parentId) as StoryFragmentNode;
|
|
1771
|
+
if (storyFragment) {
|
|
1772
|
+
paneIdx = storyFragment.paneIds.indexOf(targetNodeId);
|
|
1773
|
+
storyFragment.paneIds.splice(paneIdx, 1);
|
|
1774
|
+
}
|
|
1775
|
+
} else if (targetNode.nodeType === 'TagElement') {
|
|
1776
|
+
// mark pane as changed
|
|
1777
|
+
const paneNodeId = this.getClosestNodeTypeFromId(
|
|
1778
|
+
closestMarkdownId,
|
|
1779
|
+
'Pane'
|
|
1780
|
+
);
|
|
1781
|
+
if (paneNodeId) {
|
|
1782
|
+
const paneNode = cloneDeep(
|
|
1783
|
+
this.allNodes.get().get(paneNodeId)
|
|
1784
|
+
) as PaneNode;
|
|
1785
|
+
if (paneNode) {
|
|
1786
|
+
this.modifyNodes([{ ...paneNode, isChanged: true }]);
|
|
1787
|
+
}
|
|
1788
|
+
}
|
|
1789
|
+
}
|
|
1790
|
+
} else {
|
|
1791
|
+
if (targetNodeId === this.rootNodeId.get()) {
|
|
1792
|
+
this.rootNodeId.set('');
|
|
1793
|
+
}
|
|
1794
|
+
}
|
|
1795
|
+
|
|
1796
|
+
this.notifyNode(ROOT_NODE_NAME);
|
|
1797
|
+
|
|
1798
|
+
// Add to history for undo/redo
|
|
1799
|
+
this.history.addPatch({
|
|
1800
|
+
op: PatchOp.REMOVE,
|
|
1801
|
+
undo: (ctx) => {
|
|
1802
|
+
ctx.addNodes(toDelete);
|
|
1803
|
+
if (targetNode.nodeType === 'Pane' && parentId !== null) {
|
|
1804
|
+
const storyFragment = this.allNodes
|
|
1805
|
+
.get()
|
|
1806
|
+
.get(parentId) as StoryFragmentNode;
|
|
1807
|
+
if (storyFragment) {
|
|
1808
|
+
storyFragment.paneIds.splice(paneIdx, 0, targetNodeId);
|
|
1809
|
+
this.linkChildToParent(targetNodeId, parentId, paneIdx);
|
|
1810
|
+
}
|
|
1811
|
+
}
|
|
1812
|
+
},
|
|
1813
|
+
redo: (ctx) => ctx.deleteNodes(toDelete),
|
|
1814
|
+
});
|
|
1815
|
+
}
|
|
1816
|
+
|
|
1817
|
+
getNodesRecursively(node: BaseNode | undefined): BaseNode[] {
|
|
1818
|
+
let nodes: BaseNode[] = [];
|
|
1819
|
+
if (!node) return nodes;
|
|
1820
|
+
|
|
1821
|
+
this.getChildNodeIDs(node.id).forEach((id) => {
|
|
1822
|
+
const collectedNodes = this.getNodesRecursively(
|
|
1823
|
+
this.allNodes.get().get(id)
|
|
1824
|
+
);
|
|
1825
|
+
nodes = collectedNodes.concat(nodes);
|
|
1826
|
+
});
|
|
1827
|
+
|
|
1828
|
+
nodes.push(node);
|
|
1829
|
+
return nodes;
|
|
1830
|
+
}
|
|
1831
|
+
|
|
1832
|
+
moveNode(nodeId: string, location: 'before' | 'after') {
|
|
1833
|
+
const node = this.allNodes.get().get(nodeId);
|
|
1834
|
+
if (!node || node.nodeType === 'Root') return;
|
|
1835
|
+
|
|
1836
|
+
if (node.parentId) {
|
|
1837
|
+
const children = this.getChildNodeIDs(node.parentId);
|
|
1838
|
+
const idx = children.indexOf(nodeId);
|
|
1839
|
+
if (idx !== -1) {
|
|
1840
|
+
const newPosNodeId = children.at(
|
|
1841
|
+
location === 'before'
|
|
1842
|
+
? Math.max(idx - 1, 0)
|
|
1843
|
+
: Math.min(idx + 1, children.length - 1)
|
|
1844
|
+
);
|
|
1845
|
+
if (newPosNodeId) {
|
|
1846
|
+
this.moveNodeTo(nodeId, newPosNodeId, location);
|
|
1847
|
+
}
|
|
1848
|
+
}
|
|
1849
|
+
}
|
|
1850
|
+
}
|
|
1851
|
+
|
|
1852
|
+
moveNodeTo(
|
|
1853
|
+
nodeId: string,
|
|
1854
|
+
insertNodeId: string,
|
|
1855
|
+
location: 'before' | 'after'
|
|
1856
|
+
) {
|
|
1857
|
+
const node = this.allNodes.get().get(nodeId);
|
|
1858
|
+
if (!node || node.nodeType === 'Root') return;
|
|
1859
|
+
|
|
1860
|
+
const newLocationNode = this.allNodes.get().get(insertNodeId);
|
|
1861
|
+
if (!newLocationNode) return;
|
|
1862
|
+
|
|
1863
|
+
if (nodeId === insertNodeId) return;
|
|
1864
|
+
|
|
1865
|
+
if (node.nodeType !== newLocationNode.nodeType) {
|
|
1866
|
+
console.warn(
|
|
1867
|
+
`Trying to move nodes ${nodeId} and ${insertNodeId} but they're belong to different types`
|
|
1868
|
+
);
|
|
1869
|
+
return;
|
|
1870
|
+
}
|
|
1871
|
+
|
|
1872
|
+
const oldParentId = node.parentId || '';
|
|
1873
|
+
const oldParentNodes = this.getChildNodeIDs(oldParentId);
|
|
1874
|
+
const originalIdx = oldParentNodes.indexOf(nodeId);
|
|
1875
|
+
|
|
1876
|
+
// Capture original state for history
|
|
1877
|
+
let originalPaneIds: string[] | null = null;
|
|
1878
|
+
if (node.nodeType === 'Pane') {
|
|
1879
|
+
const storyFragmentId = this.getClosestNodeTypeFromId(
|
|
1880
|
+
node.id,
|
|
1881
|
+
'StoryFragment'
|
|
1882
|
+
);
|
|
1883
|
+
const storyFragment = this.allNodes
|
|
1884
|
+
.get()
|
|
1885
|
+
.get(storyFragmentId) as StoryFragmentNode;
|
|
1886
|
+
if (storyFragment) {
|
|
1887
|
+
originalPaneIds = [...storyFragment.paneIds];
|
|
1888
|
+
}
|
|
1889
|
+
}
|
|
1890
|
+
|
|
1891
|
+
moveNodeAtLocationInContext(
|
|
1892
|
+
oldParentNodes,
|
|
1893
|
+
originalIdx,
|
|
1894
|
+
newLocationNode,
|
|
1895
|
+
insertNodeId,
|
|
1896
|
+
nodeId,
|
|
1897
|
+
location,
|
|
1898
|
+
node,
|
|
1899
|
+
this
|
|
1900
|
+
);
|
|
1901
|
+
|
|
1902
|
+
if (node.nodeType === 'Pane') {
|
|
1903
|
+
const storyFragmentId = this.getClosestNodeTypeFromId(
|
|
1904
|
+
node.id,
|
|
1905
|
+
'StoryFragment'
|
|
1906
|
+
);
|
|
1907
|
+
const storyFragment = cloneDeep(
|
|
1908
|
+
this.allNodes.get().get(storyFragmentId)
|
|
1909
|
+
) as StoryFragmentNode;
|
|
1910
|
+
if (storyFragment) {
|
|
1911
|
+
this.modifyNodes([{ ...storyFragment, isChanged: true }]);
|
|
1912
|
+
}
|
|
1913
|
+
} else {
|
|
1914
|
+
const parentNode = this.nodeToNotify(
|
|
1915
|
+
newLocationNode?.parentId || '',
|
|
1916
|
+
newLocationNode.nodeType
|
|
1917
|
+
);
|
|
1918
|
+
this.notifyNode(parentNode || '');
|
|
1919
|
+
}
|
|
1920
|
+
|
|
1921
|
+
this.history.addPatch({
|
|
1922
|
+
op: PatchOp.REPLACE,
|
|
1923
|
+
undo: (ctx) => {
|
|
1924
|
+
const oldParentNodes = ctx.getChildNodeIDs(node.parentId || '');
|
|
1925
|
+
const newParentNodes = ctx.getChildNodeIDs(
|
|
1926
|
+
newLocationNode.parentId || ''
|
|
1927
|
+
);
|
|
1928
|
+
if (newParentNodes) {
|
|
1929
|
+
newParentNodes.splice(newParentNodes.indexOf(nodeId), 1);
|
|
1930
|
+
}
|
|
1931
|
+
if (oldParentNodes) {
|
|
1932
|
+
oldParentNodes.splice(originalIdx, 0, nodeId);
|
|
1933
|
+
}
|
|
1934
|
+
node.parentId = oldParentId;
|
|
1935
|
+
|
|
1936
|
+
if (node.nodeType === 'Pane' && originalPaneIds) {
|
|
1937
|
+
const storyFragmentId = ctx.getClosestNodeTypeFromId(
|
|
1938
|
+
node.id,
|
|
1939
|
+
'StoryFragment'
|
|
1940
|
+
);
|
|
1941
|
+
const storyFragment = cloneDeep(
|
|
1942
|
+
ctx.allNodes.get().get(storyFragmentId)
|
|
1943
|
+
) as StoryFragmentNode;
|
|
1944
|
+
if (storyFragment) {
|
|
1945
|
+
storyFragment.paneIds = [...originalPaneIds];
|
|
1946
|
+
this.modifyNodes([{ ...storyFragment, isChanged: true }]);
|
|
1947
|
+
}
|
|
1948
|
+
}
|
|
1949
|
+
|
|
1950
|
+
//const parentNode = ctx.nodeToNotify(node?.parentId || "", node.nodeType);
|
|
1951
|
+
ctx.notifyNode(node.id || '');
|
|
1952
|
+
},
|
|
1953
|
+
redo: (ctx) => {
|
|
1954
|
+
moveNodeAtLocationInContext(
|
|
1955
|
+
oldParentNodes,
|
|
1956
|
+
originalIdx,
|
|
1957
|
+
newLocationNode,
|
|
1958
|
+
insertNodeId,
|
|
1959
|
+
nodeId,
|
|
1960
|
+
location,
|
|
1961
|
+
node,
|
|
1962
|
+
ctx
|
|
1963
|
+
);
|
|
1964
|
+
|
|
1965
|
+
if (node.nodeType === 'Pane') {
|
|
1966
|
+
const storyFragmentId = ctx.getClosestNodeTypeFromId(
|
|
1967
|
+
node.id,
|
|
1968
|
+
'StoryFragment'
|
|
1969
|
+
);
|
|
1970
|
+
const storyFragment = cloneDeep(
|
|
1971
|
+
ctx.allNodes.get().get(storyFragmentId)
|
|
1972
|
+
) as StoryFragmentNode;
|
|
1973
|
+
if (storyFragment) {
|
|
1974
|
+
this.modifyNodes([{ ...storyFragment, isChanged: true }]);
|
|
1975
|
+
}
|
|
1976
|
+
}
|
|
1977
|
+
},
|
|
1978
|
+
});
|
|
1979
|
+
}
|
|
1980
|
+
|
|
1981
|
+
getPaneImageFileIds(paneId: string): string[] {
|
|
1982
|
+
const paneNode = this.allNodes.get().get(paneId);
|
|
1983
|
+
if (!paneNode || paneNode.nodeType !== 'Pane') return [];
|
|
1984
|
+
|
|
1985
|
+
const allNodes = this.getNodesRecursively(paneNode);
|
|
1986
|
+
|
|
1987
|
+
const embeddedFileIds = allNodes
|
|
1988
|
+
.filter(
|
|
1989
|
+
(node): node is FlatNode =>
|
|
1990
|
+
node.nodeType === 'TagElement' &&
|
|
1991
|
+
'tagName' in node &&
|
|
1992
|
+
node.tagName === 'img' &&
|
|
1993
|
+
'fileId' in node &&
|
|
1994
|
+
typeof node.fileId === 'string'
|
|
1995
|
+
)
|
|
1996
|
+
.map((node) => node.fileId)
|
|
1997
|
+
.filter((id): id is string => id !== undefined);
|
|
1998
|
+
|
|
1999
|
+
const bgFileIds = allNodes
|
|
2000
|
+
.filter(
|
|
2001
|
+
(node): node is any =>
|
|
2002
|
+
node.nodeType === 'BgPane' &&
|
|
2003
|
+
'type' in node &&
|
|
2004
|
+
node.type === 'background-image' &&
|
|
2005
|
+
'fileId' in node &&
|
|
2006
|
+
typeof node.fileId === 'string'
|
|
2007
|
+
)
|
|
2008
|
+
.map((node) => node.fileId)
|
|
2009
|
+
.filter((id): id is string => id !== undefined);
|
|
2010
|
+
|
|
2011
|
+
return [...embeddedFileIds, ...bgFileIds];
|
|
2012
|
+
}
|
|
2013
|
+
|
|
2014
|
+
getPaneImagesMap(): Record<string, string[]> {
|
|
2015
|
+
const paneNodes = Array.from(this.allNodes.get().values()).filter(
|
|
2016
|
+
(node): node is PaneNode => node.nodeType === 'Pane'
|
|
2017
|
+
);
|
|
2018
|
+
const result: Record<string, string[]> = {};
|
|
2019
|
+
paneNodes.forEach((pane) => {
|
|
2020
|
+
const fileIds = this.getPaneImageFileIds(pane.id);
|
|
2021
|
+
if (fileIds.length > 0) {
|
|
2022
|
+
result[pane.id] = fileIds;
|
|
2023
|
+
}
|
|
2024
|
+
});
|
|
2025
|
+
return result;
|
|
2026
|
+
}
|
|
2027
|
+
|
|
2028
|
+
insertPaneId(
|
|
2029
|
+
storyfragmentId: string,
|
|
2030
|
+
paneId: string,
|
|
2031
|
+
insertId?: string,
|
|
2032
|
+
location?: 'before' | 'after'
|
|
2033
|
+
) {
|
|
2034
|
+
const storyfragment = this.allNodes
|
|
2035
|
+
.get()
|
|
2036
|
+
.get(storyfragmentId) as StoryFragmentNode;
|
|
2037
|
+
if (!storyfragment || storyfragment.nodeType !== 'StoryFragment') {
|
|
2038
|
+
console.warn('Invalid storyfragment ID in insertPaneId');
|
|
2039
|
+
return;
|
|
2040
|
+
}
|
|
2041
|
+
|
|
2042
|
+
const newPaneIds = [...storyfragment.paneIds];
|
|
2043
|
+
|
|
2044
|
+
if (!insertId) {
|
|
2045
|
+
newPaneIds.push(paneId);
|
|
2046
|
+
} else {
|
|
2047
|
+
const insertIdx = newPaneIds.indexOf(insertId);
|
|
2048
|
+
if (insertIdx === -1) {
|
|
2049
|
+
console.warn('Insert reference pane not found, adding to end.');
|
|
2050
|
+
newPaneIds.push(paneId);
|
|
2051
|
+
} else {
|
|
2052
|
+
const targetIdx = location === 'before' ? insertIdx : insertIdx + 1;
|
|
2053
|
+
newPaneIds.splice(targetIdx, 0, paneId);
|
|
2054
|
+
}
|
|
2055
|
+
}
|
|
2056
|
+
|
|
2057
|
+
// Create the updated node object with a clear type
|
|
2058
|
+
const updatedStoryFragment: StoryFragmentNode = {
|
|
2059
|
+
...storyfragment,
|
|
2060
|
+
paneIds: newPaneIds,
|
|
2061
|
+
isChanged: true,
|
|
2062
|
+
};
|
|
2063
|
+
|
|
2064
|
+
// Pass the correctly typed object to modifyNodes
|
|
2065
|
+
this.modifyNodes([updatedStoryFragment]);
|
|
2066
|
+
}
|
|
2067
|
+
|
|
2068
|
+
isSlugValid(
|
|
2069
|
+
slug: string,
|
|
2070
|
+
currentNodeId?: string
|
|
2071
|
+
): { isValid: boolean; error?: string } {
|
|
2072
|
+
// Early validation for empty slugs
|
|
2073
|
+
if (!slug || slug.length < 3) {
|
|
2074
|
+
return { isValid: false, error: 'Slug must be at least 3 characters' };
|
|
2075
|
+
}
|
|
2076
|
+
// Check against reserved slugs
|
|
2077
|
+
if (reservedSlugs.includes(slug)) {
|
|
2078
|
+
return {
|
|
2079
|
+
isValid: false,
|
|
2080
|
+
error: 'This URL is reserved and cannot be used',
|
|
2081
|
+
};
|
|
2082
|
+
}
|
|
2083
|
+
// Check if slug contains only valid characters (alphanumeric, hyphens)
|
|
2084
|
+
const validSlugPattern = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
2085
|
+
if (!validSlugPattern.test(slug)) {
|
|
2086
|
+
return {
|
|
2087
|
+
isValid: false,
|
|
2088
|
+
error: 'Slug can only contain lowercase letters, numbers, and hyphens',
|
|
2089
|
+
};
|
|
2090
|
+
}
|
|
2091
|
+
// Check for duplicate slugs
|
|
2092
|
+
const nodes = Array.from(this.allNodes.get().values());
|
|
2093
|
+
const duplicateNode = nodes.find(
|
|
2094
|
+
(node) =>
|
|
2095
|
+
(node.nodeType === 'StoryFragment' || node.nodeType === 'Pane') &&
|
|
2096
|
+
'slug' in node &&
|
|
2097
|
+
node.slug === slug &&
|
|
2098
|
+
node.id !== currentNodeId
|
|
2099
|
+
);
|
|
2100
|
+
if (duplicateNode) {
|
|
2101
|
+
return {
|
|
2102
|
+
isValid: false,
|
|
2103
|
+
error: `This URL is already in use by ${duplicateNode.nodeType === 'Pane' ? 'pane' : 'page'}: ${(duplicateNode as PaneNode).title}`,
|
|
2104
|
+
};
|
|
2105
|
+
}
|
|
2106
|
+
return { isValid: true };
|
|
2107
|
+
}
|
|
2108
|
+
|
|
2109
|
+
generateValidSlug(title: string, currentNodeId?: string): string {
|
|
2110
|
+
// Convert title to lowercase and replace spaces/special chars with hyphens
|
|
2111
|
+
const slug = title
|
|
2112
|
+
.toLowerCase()
|
|
2113
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
2114
|
+
.replace(/^-+|-+$/g, '');
|
|
2115
|
+
// If the base slug is already valid and unique, use it
|
|
2116
|
+
if (this.isSlugValid(slug, currentNodeId)) {
|
|
2117
|
+
return slug;
|
|
2118
|
+
}
|
|
2119
|
+
// Otherwise, append numbers until we find a unique slug
|
|
2120
|
+
let counter = 1;
|
|
2121
|
+
let newSlug = slug;
|
|
2122
|
+
while (!this.isSlugValid(newSlug, currentNodeId)) {
|
|
2123
|
+
newSlug = `${slug}-${counter}`;
|
|
2124
|
+
counter++;
|
|
2125
|
+
}
|
|
2126
|
+
return newSlug;
|
|
2127
|
+
}
|
|
2128
|
+
|
|
2129
|
+
isBunnyVideoNode(node: BaseNode): boolean {
|
|
2130
|
+
if (node.nodeType === 'Pane' && 'codeHookTarget' in node) {
|
|
2131
|
+
return (node as PaneNode).codeHookTarget === 'bunny-video';
|
|
2132
|
+
}
|
|
2133
|
+
if (node.nodeType === 'TagElement' && 'tagName' in node) {
|
|
2134
|
+
const flatNode = node as FlatNode;
|
|
2135
|
+
return (
|
|
2136
|
+
flatNode.tagName === 'code' &&
|
|
2137
|
+
'codeHookParams' in flatNode &&
|
|
2138
|
+
Array.isArray(flatNode.codeHookParams) &&
|
|
2139
|
+
typeof flatNode.copy === 'string' &&
|
|
2140
|
+
flatNode.copy.includes('bunny(')
|
|
2141
|
+
);
|
|
2142
|
+
}
|
|
2143
|
+
return false;
|
|
2144
|
+
}
|
|
2145
|
+
|
|
2146
|
+
getBunnyVideoUrl(nodeId: string): string | string[] | null {
|
|
2147
|
+
const node = this.allNodes.get().get(nodeId);
|
|
2148
|
+
if (!node) return null;
|
|
2149
|
+
|
|
2150
|
+
if (node.nodeType === 'Pane' && 'codeHookPayload' in node) {
|
|
2151
|
+
const paneNode = node as PaneNode;
|
|
2152
|
+
try {
|
|
2153
|
+
if (
|
|
2154
|
+
paneNode.codeHookPayload &&
|
|
2155
|
+
typeof paneNode.codeHookPayload.options === 'string'
|
|
2156
|
+
) {
|
|
2157
|
+
const options = JSON.parse(paneNode.codeHookPayload.options);
|
|
2158
|
+
return options.videoUrl || null;
|
|
2159
|
+
}
|
|
2160
|
+
} catch (error) {
|
|
2161
|
+
console.error('Error parsing Bunny video options:', error);
|
|
2162
|
+
}
|
|
2163
|
+
}
|
|
2164
|
+
|
|
2165
|
+
if (node.nodeType === 'TagElement' && 'codeHookParams' in node) {
|
|
2166
|
+
const flatNode = node as FlatNode;
|
|
2167
|
+
if (
|
|
2168
|
+
Array.isArray(flatNode.codeHookParams) &&
|
|
2169
|
+
flatNode.codeHookParams.length > 0
|
|
2170
|
+
) {
|
|
2171
|
+
return flatNode.codeHookParams[0];
|
|
2172
|
+
}
|
|
2173
|
+
}
|
|
2174
|
+
|
|
2175
|
+
return null;
|
|
2176
|
+
}
|
|
2177
|
+
|
|
2178
|
+
getAllBunnyVideoInfo(): { url: string; title: string; videoId: string }[] {
|
|
2179
|
+
const results: { url: string; title: string; videoId: string }[] = [];
|
|
2180
|
+
const processedVideoIds = new Set<string>();
|
|
2181
|
+
|
|
2182
|
+
// Find panes with bunny-video code hook
|
|
2183
|
+
const allNodes = Array.from(this.allNodes.get().values());
|
|
2184
|
+
const paneNodes = allNodes.filter(
|
|
2185
|
+
(node) =>
|
|
2186
|
+
node.nodeType === 'Pane' &&
|
|
2187
|
+
'codeHookTarget' in node &&
|
|
2188
|
+
node.codeHookTarget === 'bunny-video'
|
|
2189
|
+
) as PaneNode[];
|
|
2190
|
+
|
|
2191
|
+
// Process pane-level bunny videos
|
|
2192
|
+
for (const paneNode of paneNodes) {
|
|
2193
|
+
try {
|
|
2194
|
+
if (
|
|
2195
|
+
paneNode.codeHookPayload &&
|
|
2196
|
+
typeof paneNode.codeHookPayload.options === 'string'
|
|
2197
|
+
) {
|
|
2198
|
+
const options = JSON.parse(paneNode.codeHookPayload.options);
|
|
2199
|
+
const url = options.videoUrl || '';
|
|
2200
|
+
const title = options.title || 'Untitled Video';
|
|
2201
|
+
|
|
2202
|
+
if (url && typeof url === 'string') {
|
|
2203
|
+
let videoId = '';
|
|
2204
|
+
try {
|
|
2205
|
+
const urlObj = new URL(url);
|
|
2206
|
+
if (
|
|
2207
|
+
urlObj.hostname === 'iframe.mediadelivery.net' &&
|
|
2208
|
+
urlObj.pathname.startsWith('/embed/')
|
|
2209
|
+
) {
|
|
2210
|
+
const pathParts = urlObj.pathname.split('/');
|
|
2211
|
+
if (pathParts.length >= 4) {
|
|
2212
|
+
videoId = `${pathParts[2]}/${pathParts[3]}`;
|
|
2213
|
+
}
|
|
2214
|
+
}
|
|
2215
|
+
} catch (error) {
|
|
2216
|
+
console.error('Error extracting video ID from URL:', error);
|
|
2217
|
+
}
|
|
2218
|
+
|
|
2219
|
+
if (videoId && !processedVideoIds.has(videoId)) {
|
|
2220
|
+
results.push({
|
|
2221
|
+
url: url,
|
|
2222
|
+
title: typeof title === 'string' ? title : 'Untitled Video',
|
|
2223
|
+
videoId,
|
|
2224
|
+
});
|
|
2225
|
+
processedVideoIds.add(videoId);
|
|
2226
|
+
}
|
|
2227
|
+
}
|
|
2228
|
+
}
|
|
2229
|
+
} catch (error) {
|
|
2230
|
+
console.error('Error parsing Bunny video options:', error);
|
|
2231
|
+
}
|
|
2232
|
+
}
|
|
2233
|
+
|
|
2234
|
+
// Find inline bunny widgets
|
|
2235
|
+
const codeNodes = allNodes.filter(
|
|
2236
|
+
(node) =>
|
|
2237
|
+
node.nodeType === 'TagElement' &&
|
|
2238
|
+
'tagName' in node &&
|
|
2239
|
+
node.tagName === 'code' &&
|
|
2240
|
+
'codeHookParams' in node &&
|
|
2241
|
+
'copy' in node &&
|
|
2242
|
+
typeof node.copy === 'string' &&
|
|
2243
|
+
node.copy.includes('bunny(')
|
|
2244
|
+
) as FlatNode[];
|
|
2245
|
+
|
|
2246
|
+
// Process inline widgets
|
|
2247
|
+
for (const codeNode of codeNodes) {
|
|
2248
|
+
if (
|
|
2249
|
+
Array.isArray(codeNode.codeHookParams) &&
|
|
2250
|
+
codeNode.codeHookParams.length >= 2
|
|
2251
|
+
) {
|
|
2252
|
+
const urlParam = codeNode.codeHookParams[0];
|
|
2253
|
+
const titleParam = codeNode.codeHookParams[1];
|
|
2254
|
+
|
|
2255
|
+
const url = Array.isArray(urlParam)
|
|
2256
|
+
? urlParam[0]
|
|
2257
|
+
: String(urlParam || '');
|
|
2258
|
+
const title = Array.isArray(titleParam)
|
|
2259
|
+
? titleParam[0]
|
|
2260
|
+
: String(titleParam || 'Untitled Video');
|
|
2261
|
+
|
|
2262
|
+
if (url) {
|
|
2263
|
+
let videoId = '';
|
|
2264
|
+
try {
|
|
2265
|
+
const urlObj = new URL(url);
|
|
2266
|
+
if (
|
|
2267
|
+
urlObj.hostname === 'iframe.mediadelivery.net' &&
|
|
2268
|
+
urlObj.pathname.startsWith('/embed/')
|
|
2269
|
+
) {
|
|
2270
|
+
const pathParts = urlObj.pathname.split('/');
|
|
2271
|
+
if (pathParts.length >= 4) {
|
|
2272
|
+
videoId = `${pathParts[2]}/${pathParts[3]}`;
|
|
2273
|
+
}
|
|
2274
|
+
}
|
|
2275
|
+
} catch (error) {
|
|
2276
|
+
console.error('Error extracting video ID from URL:', error);
|
|
2277
|
+
}
|
|
2278
|
+
|
|
2279
|
+
if (videoId && !processedVideoIds.has(videoId)) {
|
|
2280
|
+
results.push({ url, title, videoId });
|
|
2281
|
+
processedVideoIds.add(videoId);
|
|
2282
|
+
}
|
|
2283
|
+
}
|
|
2284
|
+
}
|
|
2285
|
+
}
|
|
2286
|
+
|
|
2287
|
+
return results;
|
|
2288
|
+
}
|
|
2289
|
+
|
|
2290
|
+
mapNodeHierarchy(nodes: TemplateNode[]) {
|
|
2291
|
+
const nodeMap: Record<string, any> = {};
|
|
2292
|
+
|
|
2293
|
+
// First pass - create entries for all nodes
|
|
2294
|
+
nodes.forEach((node) => {
|
|
2295
|
+
nodeMap[node.id] = {
|
|
2296
|
+
id: node.id,
|
|
2297
|
+
nodeType: node.nodeType,
|
|
2298
|
+
tagName: node.tagName || 'N/A',
|
|
2299
|
+
parentId: node.parentId,
|
|
2300
|
+
children: [],
|
|
2301
|
+
};
|
|
2302
|
+
});
|
|
2303
|
+
|
|
2304
|
+
// Second pass - build the hierarchy
|
|
2305
|
+
nodes.forEach((node) => {
|
|
2306
|
+
if (node.parentId && nodeMap[node.parentId]) {
|
|
2307
|
+
nodeMap[node.parentId].children.push(nodeMap[node.id]);
|
|
2308
|
+
}
|
|
2309
|
+
});
|
|
2310
|
+
|
|
2311
|
+
// Return only the root nodes (those whose parents aren't in our node set)
|
|
2312
|
+
return nodes
|
|
2313
|
+
.filter((node) => !node.parentId || !nodeMap[node.parentId])
|
|
2314
|
+
.map((node) => nodeMap[node.id]);
|
|
2315
|
+
}
|
|
2316
|
+
|
|
2317
|
+
getDirtyNodesClassData(): { dirtyPaneIds: string[]; classes: string[] } {
|
|
2318
|
+
const dirtyNodes = this.getDirtyNodes();
|
|
2319
|
+
const dirtyPaneIds = dirtyNodes
|
|
2320
|
+
.filter((node) => node.nodeType === 'Pane')
|
|
2321
|
+
.map((node) => node.id);
|
|
2322
|
+
const classes = extractClassesFromNodes(dirtyNodes);
|
|
2323
|
+
|
|
2324
|
+
return { dirtyPaneIds, classes };
|
|
2325
|
+
}
|
|
2326
|
+
|
|
2327
|
+
/**
|
|
2328
|
+
* Executes a series of updates on a temporary context and then applies the
|
|
2329
|
+
* results to the main context in a single operation, triggering one UI update.
|
|
2330
|
+
* @param work - An async function that receives the temporary context and performs modifications.
|
|
2331
|
+
*/
|
|
2332
|
+
async applyAtomicUpdate(
|
|
2333
|
+
work: (tmpCtx: NodesContext) => Promise<void>
|
|
2334
|
+
): Promise<void> {
|
|
2335
|
+
// 1. Create a temporary, "off-screen" context
|
|
2336
|
+
const tmpCtx = new NodesContext();
|
|
2337
|
+
// Prime the temp context with the same root ID and other relevant state
|
|
2338
|
+
tmpCtx.rootNodeId.set(this.rootNodeId.get());
|
|
2339
|
+
tmpCtx.allNodes.set(new Map(this.allNodes.get()));
|
|
2340
|
+
tmpCtx.parentNodes.set(new Map(this.parentNodes.get()));
|
|
2341
|
+
|
|
2342
|
+
// 2. Execute the long-running work on the temporary context
|
|
2343
|
+
await work(tmpCtx);
|
|
2344
|
+
|
|
2345
|
+
// 3. Get the results from the temporary context
|
|
2346
|
+
const newNodes = tmpCtx.allNodes.get();
|
|
2347
|
+
const newParentRelations = tmpCtx.parentNodes.get();
|
|
2348
|
+
|
|
2349
|
+
// 4. Swap/Merge the results into the main context
|
|
2350
|
+
this.allNodes.set(newNodes);
|
|
2351
|
+
this.parentNodes.set(newParentRelations);
|
|
2352
|
+
|
|
2353
|
+
// 5. Trigger a single notification to re-render the UI
|
|
2354
|
+
this.notifyNode('root');
|
|
2355
|
+
}
|
|
2356
|
+
|
|
2357
|
+
private deleteNodes(nodesList: BaseNode[]): BaseNode[] {
|
|
2358
|
+
const deletedNodes: BaseNode[] = [];
|
|
2359
|
+
|
|
2360
|
+
nodesList.forEach((node) => {
|
|
2361
|
+
if (!node) return;
|
|
2362
|
+
|
|
2363
|
+
// Remove node
|
|
2364
|
+
const allNodes = this.allNodes.get();
|
|
2365
|
+
if (allNodes.delete(node.id)) {
|
|
2366
|
+
deletedNodes.push(node);
|
|
2367
|
+
}
|
|
2368
|
+
|
|
2369
|
+
// Remove parent link
|
|
2370
|
+
if (node?.parentId !== null) {
|
|
2371
|
+
const parentNodes = this.parentNodes.get();
|
|
2372
|
+
const parentNode = parentNodes.get(node.parentId);
|
|
2373
|
+
if (parentNode) {
|
|
2374
|
+
parentNode.splice(parentNode.indexOf(node.id), 1);
|
|
2375
|
+
this.parentNodes.set(new Map<string, string[]>(parentNodes));
|
|
2376
|
+
}
|
|
2377
|
+
}
|
|
2378
|
+
});
|
|
2379
|
+
|
|
2380
|
+
return deletedNodes;
|
|
2381
|
+
}
|
|
2382
|
+
}
|
|
2383
|
+
|
|
2384
|
+
export const globalCtx: NodesContext = new NodesContext();
|
|
2385
|
+
|
|
2386
|
+
export const getCtx = (
|
|
2387
|
+
props?: NodeProps | CompositorProps | WidgetProps
|
|
2388
|
+
): NodesContext => {
|
|
2389
|
+
return props?.ctx || globalCtx;
|
|
2390
|
+
};
|