Repository: node-projects/web-component-designer
Branch: master
Commit: acd3bbac6007
Files: 669
Total size: 2.5 MB
Directory structure:
gitextract_pnfc9i6h/
├── .editorconfig
├── .github/
│ ├── FUNDING.yml
│ └── copilot-instructions.md
├── .gitignore
├── ACKNOWLEDGMENTS
├── AGENTS.md
├── CLAUDE.md
├── COMPARISON.md
├── LICENSE
├── README.md
├── editor.code-workspace
├── jest.config.js
├── memories/
│ ├── attribute-source-part-jump-note.md
│ ├── default-html-parser-source-parts-note.md
│ ├── freehand-path-interpolation-note.md
│ ├── getboxquads-svg-fast-path-removal.md
│ ├── mermaid-diagram-support-plan.md
│ ├── monaco-selection-coalesce-note.md
│ ├── node-html-parser-source-parts-note.md
│ ├── source-part-selection-coalesce-note.md
│ ├── svg-affine-overlay-point-conversion-note.md
│ ├── svg-geometry-placement-transform-offset-note.md
│ └── unified-geometry-click-selection-no-commit.md
├── memory/
│ ├── context-menu-copy-paste-pattern.md
│ ├── context-menu-popover-anchor-note.md
│ ├── css-numeric-percent-measured-size-note.md
│ ├── css-numeric-preview-lock-shadow-host-note.md
│ ├── css-zoom-placement-preview-note.md
│ ├── cssom-shorthand-test-note.md
│ ├── jest-esm-validation-note.md
│ ├── jest-source-import-style-note.md
│ ├── linked-package-type-compat-note.md
│ ├── manual-collab-snapshot-request.md
│ ├── overlay-refresh-pattern.md
│ ├── pointertool-selected-quad-drag-note.md
│ ├── property-grid-designitem-cache-sync-note.md
│ ├── property-grid-preview-recreation-fix.md
│ ├── resize-left-top-initial-local-axis-note.md
│ ├── straighten-line-screen-y-note.md
│ ├── svg-getctm-double-transform-note.md
│ ├── svg-rect-style-geometry-write-note.md
│ ├── transform-preview-sync-pattern.md
│ ├── transformed-resize-grid-local-point-pattern.md
│ ├── undo-group-content-changed-commit-note.md
│ └── webrtc-cross-machine-ice-config-note.md
├── package.json
├── packages/
│ ├── web-component-designer/
│ │ ├── .npmignore
│ │ ├── README.md
│ │ ├── _esbuild.js
│ │ ├── assets/
│ │ │ ├── designerCanvasIframe.html
│ │ │ └── images/
│ │ │ ├── chromeDevtools/
│ │ │ │ ├── LICENSE
│ │ │ │ └── info.txt
│ │ │ └── treeview/
│ │ │ └── license.txt
│ │ ├── config/
│ │ │ └── elements-native.json
│ │ ├── jest.config.js
│ │ ├── jsr.json
│ │ ├── package.json
│ │ ├── src/
│ │ │ ├── Constants.ts
│ │ │ ├── commandHandling/
│ │ │ │ ├── CommandType.ts
│ │ │ │ ├── IUiCommand.ts
│ │ │ │ └── IUiCommandHandler.ts
│ │ │ ├── elements/
│ │ │ │ ├── controls/
│ │ │ │ │ ├── ColorEditor.ts
│ │ │ │ │ ├── DesignerTabControl.ts
│ │ │ │ │ ├── ImageButtonListSelector.ts
│ │ │ │ │ ├── MetricsEditor.ts
│ │ │ │ │ ├── NumericStyleInput.ts
│ │ │ │ │ ├── NumericStyleInputValueHelpers.ts
│ │ │ │ │ ├── PlainScrollbar.ts
│ │ │ │ │ ├── SimpleSplitView.ts
│ │ │ │ │ └── ThicknessEditor.ts
│ │ │ │ ├── documentContainer.ts
│ │ │ │ ├── helper/
│ │ │ │ │ ├── ArrangeHelper.ts
│ │ │ │ │ ├── Browser.ts
│ │ │ │ │ ├── ClipboardHelper.ts
│ │ │ │ │ ├── CssAttributeParser.ts
│ │ │ │ │ ├── CssCombiner.ts
│ │ │ │ │ ├── CssImportant.ts
│ │ │ │ │ ├── CssUnitConverter.ts
│ │ │ │ │ ├── DesignerStylesheetPatcher.ts
│ │ │ │ │ ├── ElementHelper.ts
│ │ │ │ │ ├── GridHelper.ts
│ │ │ │ │ ├── Helper.ts
│ │ │ │ │ ├── ITextWriter.ts
│ │ │ │ │ ├── IndentedTextWriter.ts
│ │ │ │ │ ├── KeyboardHelper.ts
│ │ │ │ │ ├── LayoutHelper.ts
│ │ │ │ │ ├── NpmPackageHacks.json
│ │ │ │ │ ├── NpmPackageLoader.ts
│ │ │ │ │ ├── ObservedCustomElementsRegistry.ts
│ │ │ │ │ ├── PathDataPolyfill.ts
│ │ │ │ │ ├── PopupHelper.ts
│ │ │ │ │ ├── QuadEdgeHandleHelper.ts
│ │ │ │ │ ├── Screenshot.ts
│ │ │ │ │ ├── SelectionHelper.ts
│ │ │ │ │ ├── SimpleTextWriter.ts
│ │ │ │ │ ├── StylesheetHelper.ts
│ │ │ │ │ ├── SvgHelper.ts
│ │ │ │ │ ├── SwitchContainerHelper.ts
│ │ │ │ │ ├── TextHelper.ts
│ │ │ │ │ ├── TouchGestureHelper.ts
│ │ │ │ │ ├── TransformHelper.ts
│ │ │ │ │ ├── XmlHelper.ts
│ │ │ │ │ ├── contextMenu/
│ │ │ │ │ │ ├── ContextMenu.ts
│ │ │ │ │ │ └── IContextMenuItem.ts
│ │ │ │ │ ├── getBoxQuads.global.d.ts
│ │ │ │ │ ├── getBoxQuads.js
│ │ │ │ │ └── w3color.ts
│ │ │ │ ├── item/
│ │ │ │ │ ├── BindingMode.ts
│ │ │ │ │ ├── BindingTarget.ts
│ │ │ │ │ ├── DesignItem.ts
│ │ │ │ │ ├── IBinding.ts
│ │ │ │ │ ├── IDesignItem.ts
│ │ │ │ │ ├── NodeType.ts
│ │ │ │ │ └── info.txt
│ │ │ │ ├── services/
│ │ │ │ │ ├── BaseServiceContainer.ts
│ │ │ │ │ ├── DefaultServiceBootstrap.ts
│ │ │ │ │ ├── GlobalContext.ts
│ │ │ │ │ ├── IService.ts
│ │ │ │ │ ├── IServiceContainer.ts
│ │ │ │ │ ├── InstanceServiceContainer.ts
│ │ │ │ │ ├── ServiceContainer.ts
│ │ │ │ │ ├── bindableObjectsService/
│ │ │ │ │ │ ├── BindableObjectType.ts
│ │ │ │ │ │ ├── BindableObjectsTarget.ts
│ │ │ │ │ │ ├── IBindableObject.ts
│ │ │ │ │ │ ├── IBindableObjectDragDropService.ts
│ │ │ │ │ │ └── IBindableObjectsService.ts
│ │ │ │ │ ├── bindingsService/
│ │ │ │ │ │ ├── BaseCustomWebcomponentBindingsService.ts
│ │ │ │ │ │ ├── IBindingService.ts
│ │ │ │ │ │ ├── SpecialTagsBindingService.ts
│ │ │ │ │ │ └── VueBindingsService.ts
│ │ │ │ │ ├── collaborationService/
│ │ │ │ │ │ ├── CollaborationNodeIndex.ts
│ │ │ │ │ │ └── ICollaborationService.ts
│ │ │ │ │ ├── configUiService/
│ │ │ │ │ │ └── IConfigUiService.ts
│ │ │ │ │ ├── copyPasteService/
│ │ │ │ │ │ ├── CopyPasteAsJsonService.ts
│ │ │ │ │ │ ├── CopyPasteService.ts
│ │ │ │ │ │ ├── ICopyPasteService.ts
│ │ │ │ │ │ └── PasteFormatSnapshot.ts
│ │ │ │ │ ├── deletionService/
│ │ │ │ │ │ ├── DeletionService.ts
│ │ │ │ │ │ └── IDeletionService.ts
│ │ │ │ │ ├── demoProviderService/
│ │ │ │ │ │ ├── IDemoProviderService.ts
│ │ │ │ │ │ ├── IframeDemoProviderService.ts
│ │ │ │ │ │ └── SimpleDemoProviderService.ts
│ │ │ │ │ ├── designItemDocumentPositionService/
│ │ │ │ │ │ ├── DesignItemDocumentPositionService.ts
│ │ │ │ │ │ └── IDesignItemDocumentPositionService.ts
│ │ │ │ │ ├── designItemService/
│ │ │ │ │ │ ├── BaseCustomWebcomponentDesignItemService.ts
│ │ │ │ │ │ ├── DesignItemService.ts
│ │ │ │ │ │ └── IDesignItemService.ts
│ │ │ │ │ ├── designerAddons/
│ │ │ │ │ │ └── IDesignerAddonJson.ts
│ │ │ │ │ ├── dragDropService/
│ │ │ │ │ │ ├── DragDropService.ts
│ │ │ │ │ │ ├── ExternalDragDropService.ts
│ │ │ │ │ │ ├── IDragDropService.ts
│ │ │ │ │ │ ├── IExternalDragDropService.ts
│ │ │ │ │ │ └── IPropertyGridDragDropService.ts
│ │ │ │ │ ├── elementAtPointService/
│ │ │ │ │ │ ├── ElementAtPointService.ts
│ │ │ │ │ │ └── IElementAtPointService.ts
│ │ │ │ │ ├── elementInteractionService/
│ │ │ │ │ │ └── IElementInteractionService.ts
│ │ │ │ │ ├── elementsService/
│ │ │ │ │ │ ├── IElementDefinition.ts
│ │ │ │ │ │ ├── IElementsJson.ts
│ │ │ │ │ │ ├── IElementsService.ts
│ │ │ │ │ │ ├── JsonFileElementsService.ts
│ │ │ │ │ │ ├── PreDefinedElementsService.ts
│ │ │ │ │ │ └── WebcomponentManifestElementsService.ts
│ │ │ │ │ ├── eventsService/
│ │ │ │ │ │ ├── EventsService.ts
│ │ │ │ │ │ ├── IEvent.ts
│ │ │ │ │ │ ├── IEventsService.ts
│ │ │ │ │ │ └── WebcomponentManifestEventsService.ts
│ │ │ │ │ ├── htmlParserService/
│ │ │ │ │ │ ├── DefaultHtmlParserService.ts
│ │ │ │ │ │ └── IHtmlParserService.ts
│ │ │ │ │ ├── htmlWriterService/
│ │ │ │ │ │ ├── AbstractHtmlWriterService.ts
│ │ │ │ │ │ ├── FormatingHtmlWriterService.ts
│ │ │ │ │ │ ├── HtmlWriterService.ts
│ │ │ │ │ │ ├── IHtmlWriterOptions.ts
│ │ │ │ │ │ ├── IHtmlWriterService.ts
│ │ │ │ │ │ ├── IStringPosition.ts
│ │ │ │ │ │ └── SimpleHtmlWriterService.ts
│ │ │ │ │ ├── initializationService/
│ │ │ │ │ │ └── IIntializationService.ts
│ │ │ │ │ ├── instanceService/
│ │ │ │ │ │ ├── DefaultInstanceService.ts
│ │ │ │ │ │ ├── IDesignerInstance.ts
│ │ │ │ │ │ └── IInstanceService.ts
│ │ │ │ │ ├── manifestParsers/
│ │ │ │ │ │ ├── IOldCustomElementsManifest.ts
│ │ │ │ │ │ ├── OldCustomElementsManifestLoader.ts
│ │ │ │ │ │ └── WebcomponentManifestParserService.ts
│ │ │ │ │ ├── miniatureViewService/
│ │ │ │ │ │ ├── IMiniatureViewService.ts
│ │ │ │ │ │ └── MiniatureViewService.ts
│ │ │ │ │ ├── modelCommandService/
│ │ │ │ │ │ ├── DefaultModelCommandService.ts
│ │ │ │ │ │ └── IModelCommandService.ts
│ │ │ │ │ ├── multiplayerService/
│ │ │ │ │ │ ├── IMultiplayerService.ts
│ │ │ │ │ │ └── MultiplayerService.ts
│ │ │ │ │ ├── placementService/
│ │ │ │ │ │ ├── AbsolutePlacementService.ts
│ │ │ │ │ │ ├── AlwaysAbsolutePlacementService.ts
│ │ │ │ │ │ ├── DefaultPlacementService.ts
│ │ │ │ │ │ ├── FlexBoxPlacementService.ts
│ │ │ │ │ │ ├── GridPlacementService.ts
│ │ │ │ │ │ ├── IPlacementService.ts
│ │ │ │ │ │ ├── ISnaplinesProviderService.ts
│ │ │ │ │ │ └── SnaplinesProviderService.ts
│ │ │ │ │ ├── pngCreatorService/
│ │ │ │ │ │ ├── DisplayMediaPngWriterService.ts
│ │ │ │ │ │ ├── ElectronPngWriterService.ts
│ │ │ │ │ │ └── IPngCreatorService.ts
│ │ │ │ │ ├── propertiesService/
│ │ │ │ │ │ ├── DefaultEditorTypeService.ts
│ │ │ │ │ │ ├── DefaultPropertyEditorTypesService.ts
│ │ │ │ │ │ ├── IEditorTypeService.ts
│ │ │ │ │ │ ├── IPropertiesService.ts
│ │ │ │ │ │ ├── IProperty.ts
│ │ │ │ │ │ ├── IPropertyEditor.ts
│ │ │ │ │ │ ├── IPropertyEditorTypesService.ts
│ │ │ │ │ │ ├── IPropertyGroup.ts
│ │ │ │ │ │ ├── IPropertyGroupsService.ts
│ │ │ │ │ │ ├── PropertyGroupsService.ts
│ │ │ │ │ │ ├── PropertyMutationHandling.ts
│ │ │ │ │ │ ├── PropertyType.ts
│ │ │ │ │ │ ├── ValueType.ts
│ │ │ │ │ │ ├── propertyEditors/
│ │ │ │ │ │ │ ├── AnglePropertyEditor.ts
│ │ │ │ │ │ │ ├── BasePropertyEditor.ts
│ │ │ │ │ │ │ ├── BooleanPropertyEditor.ts
│ │ │ │ │ │ │ ├── ColorPropertyEditor.ts
│ │ │ │ │ │ │ ├── CssPropertyEditor.ts
│ │ │ │ │ │ │ ├── DatePropertyEditor.ts
│ │ │ │ │ │ │ ├── DefaultPropertyEditor.ts
│ │ │ │ │ │ │ ├── FontPropertyEditor.ts
│ │ │ │ │ │ │ ├── ImageButtonListPropertyEditor.ts
│ │ │ │ │ │ │ ├── JsonPropertyEditor.ts
│ │ │ │ │ │ │ ├── JsonPropertyPopupEditor.ts
│ │ │ │ │ │ │ ├── NumberPropertyEditor.ts
│ │ │ │ │ │ │ ├── SelectPropertyEditor.ts
│ │ │ │ │ │ │ ├── TextPropertyEditor.ts
│ │ │ │ │ │ │ ├── ThicknessPropertyEditor.ts
│ │ │ │ │ │ │ ├── UnitPropertyEditor.ts
│ │ │ │ │ │ │ ├── UnitPropertyEditorConfig.ts
│ │ │ │ │ │ │ └── special/
│ │ │ │ │ │ │ ├── GridAssignedRowColumnPropertyEditor.ts
│ │ │ │ │ │ │ └── MetricsPropertyEditor.ts
│ │ │ │ │ │ └── services/
│ │ │ │ │ │ ├── AbstractCssPropertiesService.ts
│ │ │ │ │ │ ├── AbstractPolymerLikePropertiesService.ts
│ │ │ │ │ │ ├── AbstractPropertiesService.ts
│ │ │ │ │ │ ├── AttachedPropertiesService.ts
│ │ │ │ │ │ ├── AttributesPropertiesService.ts
│ │ │ │ │ │ ├── BaseCustomWebComponentPropertiesService.ts
│ │ │ │ │ │ ├── BasicWebcomponentPropertiesService.ts
│ │ │ │ │ │ ├── CommonPropertiesService.ts
│ │ │ │ │ │ ├── ContentAndIdPropertiesService.ts
│ │ │ │ │ │ ├── CssCurrentPropertiesService.ts
│ │ │ │ │ │ ├── CssCustomPropertiesService.ts
│ │ │ │ │ │ ├── CssProperties.json
│ │ │ │ │ │ ├── CssPropertiesService.ts
│ │ │ │ │ │ ├── IJsonPropertyDefinition.ts
│ │ │ │ │ │ ├── IJsonPropertyDefinitions.ts
│ │ │ │ │ │ ├── ListPropertiesService.ts
│ │ │ │ │ │ ├── Lit2PropertiesService.ts
│ │ │ │ │ │ ├── LitElementPropertiesService.ts
│ │ │ │ │ │ ├── MathMLElementsPropertiesService.ts
│ │ │ │ │ │ ├── NativeElementsPropertiesService.ts
│ │ │ │ │ │ ├── PolymerPropertiesService.ts
│ │ │ │ │ │ ├── PropertiesHelper.ts
│ │ │ │ │ │ ├── SVGElementsPropertiesService.ts
│ │ │ │ │ │ ├── UnkownElementsPropertiesService.ts
│ │ │ │ │ │ └── WebcomponentManifestPropertiesService.ts
│ │ │ │ │ ├── refactorService/
│ │ │ │ │ │ ├── BindingsRefactorService.ts
│ │ │ │ │ │ ├── IRefactorService.ts
│ │ │ │ │ │ ├── IRefactoring.ts
│ │ │ │ │ │ └── TextRefactorService.ts
│ │ │ │ │ ├── referencesChangedService/
│ │ │ │ │ │ └── IReferencesChangedService.ts
│ │ │ │ │ ├── renderedDesignItemService/
│ │ │ │ │ │ ├── IRenderedDesignItemService.ts
│ │ │ │ │ │ └── StyleElementRenderedDesignItemService.ts
│ │ │ │ │ ├── searchService/
│ │ │ │ │ │ ├── ISearchResult.ts
│ │ │ │ │ │ ├── ISearchService.ts
│ │ │ │ │ │ └── SearchService.ts
│ │ │ │ │ ├── selectionService/
│ │ │ │ │ │ ├── ISelectionChangedEvent.ts
│ │ │ │ │ │ ├── ISelectionRefreshEvent.ts
│ │ │ │ │ │ ├── ISelectionService.ts
│ │ │ │ │ │ └── SelectionService.ts
│ │ │ │ │ ├── sourceMapService/
│ │ │ │ │ │ ├── ISourceMapProvider.ts
│ │ │ │ │ │ ├── ISourcePart.ts
│ │ │ │ │ │ ├── SvgPathDataSourceMap.ts
│ │ │ │ │ │ └── SvgPathSourceMapProvider.ts
│ │ │ │ │ ├── stylesheetService/
│ │ │ │ │ │ ├── AbstractStylesheetService.ts
│ │ │ │ │ │ ├── IStylesheetService.ts
│ │ │ │ │ │ └── SpecificityCalculator.ts
│ │ │ │ │ ├── treeStructureService/
│ │ │ │ │ │ ├── ITreeStructureChangedEvent.ts
│ │ │ │ │ │ └── ITreeStructureService.ts
│ │ │ │ │ └── undoService/
│ │ │ │ │ ├── ChangeGroup.ts
│ │ │ │ │ ├── ITransactionItem.ts
│ │ │ │ │ ├── IUndoChangeEvent.ts
│ │ │ │ │ ├── IUndoService.ts
│ │ │ │ │ ├── UndoService.ts
│ │ │ │ │ └── transactionItems/
│ │ │ │ │ ├── AttributeAndPropertyChangeAction.ts
│ │ │ │ │ ├── AttributeChangeAction.ts
│ │ │ │ │ ├── CssStyleChangeAction.ts
│ │ │ │ │ ├── DeleteAction.ts
│ │ │ │ │ ├── InsertAction.ts
│ │ │ │ │ ├── InsertChildAction.ts
│ │ │ │ │ ├── PropertyChangeAction.ts
│ │ │ │ │ ├── SelectionChangedAction.ts
│ │ │ │ │ ├── SetDesignItemsAction.ts
│ │ │ │ │ ├── StylesheetChangedAction.ts
│ │ │ │ │ └── TextContentChangeAction.ts
│ │ │ │ └── widgets/
│ │ │ │ ├── bindableObjectsBrowser/
│ │ │ │ │ └── IBindableObjectsBrowser.ts
│ │ │ │ ├── codeView/
│ │ │ │ │ ├── ICodeView.ts
│ │ │ │ │ └── code-view-simple.ts
│ │ │ │ ├── debugView/
│ │ │ │ │ └── debug-view.ts
│ │ │ │ ├── demoView/
│ │ │ │ │ ├── IDemoView.ts
│ │ │ │ │ └── demoView.ts
│ │ │ │ ├── designerView/
│ │ │ │ │ ├── DesignContext.ts
│ │ │ │ │ ├── DomConverter.ts
│ │ │ │ │ ├── IDesignContext.ts
│ │ │ │ │ ├── IDesignerCanvas.ts
│ │ │ │ │ ├── Snaplines.ts
│ │ │ │ │ ├── defaultConfiguredDesignerView.ts
│ │ │ │ │ ├── designerCanvas.ts
│ │ │ │ │ ├── designerView.ts
│ │ │ │ │ ├── extensions/
│ │ │ │ │ │ ├── AbstractExtension.ts
│ │ │ │ │ │ ├── AbstractExtensionBase.ts
│ │ │ │ │ │ ├── AltToEnterContainerExtension.ts
│ │ │ │ │ │ ├── AltToEnterContainerExtensionProvider.ts
│ │ │ │ │ │ ├── BasicStackedToolbarExtension.ts
│ │ │ │ │ │ ├── EditText/
│ │ │ │ │ │ │ ├── EditTextExtension.ts
│ │ │ │ │ │ │ └── EditTextExtensionProvider.ts
│ │ │ │ │ │ ├── ElementDragTitleExtension.ts
│ │ │ │ │ │ ├── ElementDragTitleExtensionProvider.ts
│ │ │ │ │ │ ├── ExtensionManager.ts
│ │ │ │ │ │ ├── ExtensionType.ts
│ │ │ │ │ │ ├── GrayOutDragOverContainerExtension.ts
│ │ │ │ │ │ ├── GrayOutDragOverContainerExtensionProvider.ts
│ │ │ │ │ │ ├── GrayOutExtension.ts
│ │ │ │ │ │ ├── GrayOutExtensionProvider.ts
│ │ │ │ │ │ ├── HighlightElementExtension.ts
│ │ │ │ │ │ ├── HighlightElementExtensionProvider.ts
│ │ │ │ │ │ ├── IDesignerExtension.ts
│ │ │ │ │ │ ├── IDesignerExtensionProvider.ts
│ │ │ │ │ │ ├── IExtensionManger.ts
│ │ │ │ │ │ ├── InvisibleElementExtension.ts
│ │ │ │ │ │ ├── InvisibleElementExtensionProvider.ts
│ │ │ │ │ │ ├── MarginExtension.ts
│ │ │ │ │ │ ├── MarginExtensionProvider.ts
│ │ │ │ │ │ ├── MultipleSelectionRectExtension.ts
│ │ │ │ │ │ ├── MultipleSelectionRectExtensionProvider.ts
│ │ │ │ │ │ ├── OverlayLayer.ts
│ │ │ │ │ │ ├── PaddingExtension.ts
│ │ │ │ │ │ ├── PaddingExtensionProvider.ts
│ │ │ │ │ │ ├── PlacementExtension.ts
│ │ │ │ │ │ ├── PlacementExtensionProvider.ts
│ │ │ │ │ │ ├── PositionExtension.ts
│ │ │ │ │ │ ├── PositionExtensionProvider.ts
│ │ │ │ │ │ ├── PreviousElementSelectExtension.ts
│ │ │ │ │ │ ├── PreviousElementSelectExtensionProvider.ts
│ │ │ │ │ │ ├── ResizeExtension.ts
│ │ │ │ │ │ ├── ResizeExtensionProvider.ts
│ │ │ │ │ │ ├── SelectionDefaultExtension.ts
│ │ │ │ │ │ ├── SelectionDefaultExtensionProvider.ts
│ │ │ │ │ │ ├── block/
│ │ │ │ │ │ │ ├── BlockToolbarExtension.ts
│ │ │ │ │ │ │ └── BlockToolbarExtensionProvider.ts
│ │ │ │ │ │ ├── buttons/
│ │ │ │ │ │ │ ├── AbstractDesignViewConfigButton.ts
│ │ │ │ │ │ │ ├── ButtonSeperatorProvider.ts
│ │ │ │ │ │ │ ├── FlexboxExtensionDesignViewConfigButtons.ts
│ │ │ │ │ │ │ ├── GridExtensionDesignViewConfigButtons.ts
│ │ │ │ │ │ │ ├── IDesignViewConfigButtonsProvider.ts
│ │ │ │ │ │ │ ├── InvisibleElementExtensionDesignViewConfigButtons.ts
│ │ │ │ │ │ │ ├── OptionsContextMenuButton.ts
│ │ │ │ │ │ │ ├── RoundPixelsDesignViewConfigButton.ts
│ │ │ │ │ │ │ ├── StylesheetServiceDesignViewConfigButtons.ts
│ │ │ │ │ │ │ └── ToolbarExtensionsDesignViewConfigButtons.ts
│ │ │ │ │ │ ├── contextMenu/
│ │ │ │ │ │ │ ├── AlignItemsContextMenu.ts
│ │ │ │ │ │ │ ├── BasicContextMenu.ts
│ │ │ │ │ │ │ ├── ChildContextMenu.ts
│ │ │ │ │ │ │ ├── ChildrenContextMenu.ts
│ │ │ │ │ │ │ ├── CopyPasteContextMenu.ts
│ │ │ │ │ │ │ ├── ForceCssContextMenu.ts
│ │ │ │ │ │ │ ├── IContextMenuExtension.ts
│ │ │ │ │ │ │ ├── ItemsBelowContextMenu.ts
│ │ │ │ │ │ │ ├── JumpToElementContextMenu.ts
│ │ │ │ │ │ │ ├── MultipleItemsSelectedContextMenu.ts
│ │ │ │ │ │ │ ├── PasteFormatContextMenu.ts
│ │ │ │ │ │ │ ├── PathContextMenu.ts
│ │ │ │ │ │ │ ├── RectContextMenu.ts
│ │ │ │ │ │ │ ├── RotateLeftAndRightContextMenu.ts
│ │ │ │ │ │ │ ├── SelectAllChildrenContextMenu.ts
│ │ │ │ │ │ │ ├── SeperatorContextMenu.ts
│ │ │ │ │ │ │ ├── ToolWindowsContextMenu.ts
│ │ │ │ │ │ │ ├── ZMoveContextMenu.ts
│ │ │ │ │ │ │ └── ZoomToElementContextMenu.ts
│ │ │ │ │ │ ├── flex/
│ │ │ │ │ │ │ ├── FlexToolbarExtension.ts
│ │ │ │ │ │ │ ├── FlexToolbarExtensionProvider.ts
│ │ │ │ │ │ │ ├── FlexboxExtension.ts
│ │ │ │ │ │ │ └── FlexboxExtensionProvider.ts
│ │ │ │ │ │ ├── grid/
│ │ │ │ │ │ │ ├── DisplayGridExtension.ts
│ │ │ │ │ │ │ ├── DisplayGridExtensionProvider.ts
│ │ │ │ │ │ │ ├── EditGridColumnRowSizesExtension.ts
│ │ │ │ │ │ │ ├── EditGridColumnRowSizesExtensionProvider.ts
│ │ │ │ │ │ │ ├── GridChildResizeExtension.ts
│ │ │ │ │ │ │ ├── GridChildResizeExtensionProvider.ts
│ │ │ │ │ │ │ ├── GridChildToolbarExtension.ts
│ │ │ │ │ │ │ ├── GridChildToolbarExtensionProvider.ts
│ │ │ │ │ │ │ ├── GridToolbarExtension.ts
│ │ │ │ │ │ │ └── GridToolbarExtensionProvider.ts
│ │ │ │ │ │ ├── logic/
│ │ │ │ │ │ │ ├── ApplyFirstMachingExtensionProvider.ts
│ │ │ │ │ │ │ └── ConditionExtensionProvider.ts
│ │ │ │ │ │ ├── pointerExtensions/
│ │ │ │ │ │ │ ├── AbstractDesignerPointerExtension.ts
│ │ │ │ │ │ │ ├── CursorLinePointerExtension.ts
│ │ │ │ │ │ │ ├── CursorLinePointerExtensionProvider.ts
│ │ │ │ │ │ │ ├── IDesignerPointerExtension.ts
│ │ │ │ │ │ │ ├── IDesignerPointerExtensionProvider.ts
│ │ │ │ │ │ │ ├── LinePointerExtension.ts
│ │ │ │ │ │ │ └── LinePointerExtensionProvider.ts
│ │ │ │ │ │ ├── svg/
│ │ │ │ │ │ │ ├── UnifiedGeometryExtension.ts
│ │ │ │ │ │ │ ├── UnifiedGeometryExtensionProvider.ts
│ │ │ │ │ │ │ └── geometry/
│ │ │ │ │ │ │ ├── CssClipPathGeometryReader.ts
│ │ │ │ │ │ │ ├── CssOffsetPathGeometryReader.ts
│ │ │ │ │ │ │ ├── CssShapeOutsideGeometryReader.ts
│ │ │ │ │ │ │ ├── GeometryReaderFactory.ts
│ │ │ │ │ │ │ ├── GeometryWriteHelper.ts
│ │ │ │ │ │ │ ├── IGeometry.ts
│ │ │ │ │ │ │ ├── SvgCircleGeometryReader.ts
│ │ │ │ │ │ │ ├── SvgEllipseGeometryReader.ts
│ │ │ │ │ │ │ ├── SvgLineGeometryReader.ts
│ │ │ │ │ │ │ ├── SvgPathGeometryReader.ts
│ │ │ │ │ │ │ ├── SvgPolygonGeometryReader.ts
│ │ │ │ │ │ │ ├── SvgPolylineGeometryReader.ts
│ │ │ │ │ │ │ └── SvgRectGeometryReader.ts
│ │ │ │ │ │ └── transforms/
│ │ │ │ │ │ ├── ProjectiveTransformExtension.ts
│ │ │ │ │ │ ├── ProjectiveTransformExtensionProvider.ts
│ │ │ │ │ │ ├── RotateExtension.ts
│ │ │ │ │ │ ├── RotateExtensionProvider.ts
│ │ │ │ │ │ ├── RotateGroupExtension.ts
│ │ │ │ │ │ ├── RotateGroupExtensionProvider.ts
│ │ │ │ │ │ ├── SkewExtension.ts
│ │ │ │ │ │ ├── SkewExtensionProvider.ts
│ │ │ │ │ │ ├── TransformOriginExtension.ts
│ │ │ │ │ │ └── TransformOriginExtensionProvider.ts
│ │ │ │ │ ├── overlay/
│ │ │ │ │ │ └── EditTextOverlay.ts
│ │ │ │ │ ├── overlayLayerView.ts
│ │ │ │ │ └── tools/
│ │ │ │ │ ├── DrawElementTool.ts
│ │ │ │ │ ├── DrawEllipsisTool.ts
│ │ │ │ │ ├── DrawLineTool.ts
│ │ │ │ │ ├── DrawPathTool.ts
│ │ │ │ │ ├── DrawRectTool.ts
│ │ │ │ │ ├── ITool.ts
│ │ │ │ │ ├── MagicWandSelectorTool.ts
│ │ │ │ │ ├── MarginTool.ts
│ │ │ │ │ ├── NamedTools.ts
│ │ │ │ │ ├── PaddingTool.ts
│ │ │ │ │ ├── PanTool.ts
│ │ │ │ │ ├── PickColorTool.ts
│ │ │ │ │ ├── PointerTool.ts
│ │ │ │ │ ├── RectangleSelectorTool.ts
│ │ │ │ │ ├── TextTool.ts
│ │ │ │ │ ├── ZoomTool.ts
│ │ │ │ │ └── toolBar/
│ │ │ │ │ ├── DesignerToolbar.ts
│ │ │ │ │ ├── DesignerToolbarButton.ts
│ │ │ │ │ ├── IDesignViewToolbarButtonProvider.ts
│ │ │ │ │ ├── buttons/
│ │ │ │ │ │ ├── DrawToolButtonProvider.ts
│ │ │ │ │ │ ├── PointerToolButtonProvider.ts
│ │ │ │ │ │ ├── SelectorToolButtonProvider.ts
│ │ │ │ │ │ ├── SeperatorToolProvider.ts
│ │ │ │ │ │ ├── SimpleToolButtonProvider.ts
│ │ │ │ │ │ ├── TextToolButtonProvider.ts
│ │ │ │ │ │ ├── TransformToolButtonProvider.ts
│ │ │ │ │ │ └── ZoomToolButtonProvider.ts
│ │ │ │ │ └── popups/
│ │ │ │ │ ├── AbstractBaseToolPopup.ts
│ │ │ │ │ ├── BorderRadiusEditorWindow.ts
│ │ │ │ │ ├── BoxShadowEditorWindow.ts
│ │ │ │ │ ├── DraggableToolWindow.ts
│ │ │ │ │ ├── DrawToolPopup.ts
│ │ │ │ │ ├── GradientEditorWindow.ts
│ │ │ │ │ ├── PointerToolPopup.ts
│ │ │ │ │ ├── SelectionToolPopup.ts
│ │ │ │ │ ├── TextShadowEditorWindow.ts
│ │ │ │ │ └── TransformToolPopup.ts
│ │ │ │ ├── layerDepthView/
│ │ │ │ │ ├── ILayerDepthView.ts
│ │ │ │ │ └── layerDepthView.ts
│ │ │ │ ├── miniatureView/
│ │ │ │ │ ├── IMiniatureView.ts
│ │ │ │ │ └── miniatureView.ts
│ │ │ │ ├── paletteView/
│ │ │ │ │ ├── paletteElements.ts
│ │ │ │ │ └── paletteView.ts
│ │ │ │ ├── propertyGrid/
│ │ │ │ │ ├── PropertyGrid.ts
│ │ │ │ │ ├── PropertyGridPropertyList.ts
│ │ │ │ │ └── PropertyGridWithHeader.ts
│ │ │ │ ├── refactorView/
│ │ │ │ │ └── refactor-view.ts
│ │ │ │ └── treeView/
│ │ │ │ ├── ITreeView.ts
│ │ │ │ └── treeView.ts
│ │ │ ├── enums/
│ │ │ │ ├── EventNames.ts
│ │ │ │ ├── Orientation.ts
│ │ │ │ └── PointerActionType.ts
│ │ │ ├── index-all.ts
│ │ │ ├── index.ts
│ │ │ ├── interfaces/
│ │ │ │ ├── IActivateable.ts
│ │ │ │ ├── IDisposable.ts
│ │ │ │ ├── IPoint.ts
│ │ │ │ ├── IPoint3D.ts
│ │ │ │ ├── IRect.ts
│ │ │ │ └── ISize.ts
│ │ │ └── polyfill/
│ │ │ └── globals.ts
│ │ ├── tests/
│ │ │ ├── ContextMenu.test.ts
│ │ │ ├── CssCombiner.test.ts
│ │ │ ├── CssImportant.test.ts
│ │ │ ├── DesignerStylesheetPatcher.test.ts
│ │ │ ├── GridHelper.test.ts
│ │ │ ├── NumericStyleInput.test.ts
│ │ │ ├── PasteFormatSnapshot.test.ts
│ │ │ ├── PathDataPolyfill.test.ts
│ │ │ ├── PropertyGridRefresh.test.ts
│ │ │ ├── SpecificityCalculator.test.ts
│ │ │ ├── SvgGeometryPlacement.test.ts
│ │ │ └── SvgPathDataSourceMap.test.ts
│ │ └── tsconfig.json
│ ├── web-component-designer-codeview-ace/
│ │ ├── .npmignore
│ │ ├── README.md
│ │ ├── package.json
│ │ ├── src/
│ │ │ └── widgets/
│ │ │ └── codeView/
│ │ │ └── code-view-ace.ts
│ │ └── tsconfig.json
│ ├── web-component-designer-codeview-codemirror/
│ │ ├── .npmignore
│ │ ├── README.md
│ │ ├── package.json
│ │ ├── src/
│ │ │ └── widgets/
│ │ │ └── codeView/
│ │ │ └── code-view-codemirror.ts
│ │ └── tsconfig.json
│ ├── web-component-designer-codeview-codemirror5/
│ │ ├── .npmignore
│ │ ├── README.md
│ │ ├── package.json
│ │ ├── src/
│ │ │ └── widgets/
│ │ │ └── codeView/
│ │ │ └── code-view-codemirror5.ts
│ │ └── tsconfig.json
│ ├── web-component-designer-codeview-monaco/
│ │ ├── .npmignore
│ │ ├── README.md
│ │ ├── package.json
│ │ ├── src/
│ │ │ └── widgets/
│ │ │ └── codeView/
│ │ │ └── code-view-monaco.ts
│ │ └── tsconfig.json
│ ├── web-component-designer-collaboration-service/
│ │ ├── .npmignore
│ │ ├── README.md
│ │ ├── package.json
│ │ ├── src/
│ │ │ ├── extensions/
│ │ │ │ ├── CollaborationCommentsContextMenu.ts
│ │ │ │ ├── CollaborationCursorOverlayExtension.ts
│ │ │ │ ├── CollaborationCursorOverlayExtensionProvider.ts
│ │ │ │ ├── CollaborationOverlayExtension.ts
│ │ │ │ └── CollaborationOverlayExtensionProvider.ts
│ │ │ ├── index.ts
│ │ │ ├── services/
│ │ │ │ ├── DefaultCollaborationService.ts
│ │ │ │ └── WebRtcTabCollaborationTransport.ts
│ │ │ └── setupCollaborationService.ts
│ │ └── tsconfig.json
│ ├── web-component-designer-htmlparserservice-base-custom-webcomponent/
│ │ ├── .npmignore
│ │ ├── README.md
│ │ ├── package.json
│ │ ├── src/
│ │ │ └── service/
│ │ │ └── htmlParserService/
│ │ │ ├── BaseCustomWebcomponentParserService.ts
│ │ │ └── Typescript.d.ts
│ │ └── tsconfig.json
│ ├── web-component-designer-htmlparserservice-lit-element/
│ │ ├── .npmignore
│ │ ├── README.md
│ │ ├── package.json
│ │ ├── src/
│ │ │ └── service/
│ │ │ └── htmlParserService/
│ │ │ └── LitElementParserService.ts
│ │ └── tsconfig.json
│ ├── web-component-designer-htmlparserservice-nodehtmlparser/
│ │ ├── .npmignore
│ │ ├── README.md
│ │ ├── package.json
│ │ ├── src/
│ │ │ └── service/
│ │ │ └── htmlParserService/
│ │ │ └── NodeHtmlParserService.ts
│ │ └── tsconfig.json
│ ├── web-component-designer-mermaid/
│ │ ├── README.md
│ │ ├── package.json
│ │ ├── src/
│ │ │ ├── index.ts
│ │ │ ├── monaco/
│ │ │ │ └── MermaidLanguage.ts
│ │ │ ├── services/
│ │ │ │ ├── MermaidConnectionRouting.ts
│ │ │ │ ├── MermaidDocumentPropertiesService.ts
│ │ │ │ ├── MermaidElementsService.ts
│ │ │ │ ├── MermaidLayoutCopyPasteService.ts
│ │ │ │ ├── MermaidLayoutPlacementService.ts
│ │ │ │ ├── MermaidParserService.ts
│ │ │ │ ├── MermaidPropertyGroupsService.ts
│ │ │ │ └── mermaidGeometry.ts
│ │ │ ├── setupMermaidServiceContainer.ts
│ │ │ ├── toolbar/
│ │ │ │ └── ConnectMermaidNodesTool.ts
│ │ │ └── widgets/
│ │ │ ├── elements.json
│ │ │ ├── mermaid-edge.ts
│ │ │ ├── mermaid-flowchart-directive.ts
│ │ │ ├── mermaid-mindmap-node.ts
│ │ │ ├── mermaid-node.ts
│ │ │ ├── mermaid-requirement-node.ts
│ │ │ ├── mermaid-requirement-relationship.ts
│ │ │ ├── mermaid-sequence-message.ts
│ │ │ ├── mermaid-sequence-participant.ts
│ │ │ ├── mermaid-subgraph.ts
│ │ │ └── views/
│ │ │ └── mermaid-demo-view.ts
│ │ └── tsconfig.json
│ ├── web-component-designer-stylesheetservice-css-parser/
│ │ ├── .npmignore
│ │ ├── README.md
│ │ ├── package.json
│ │ ├── src/
│ │ │ └── service/
│ │ │ └── stylesheetservice/
│ │ │ └── CssParserStylesheetService.ts
│ │ └── tsconfig.json
│ ├── web-component-designer-visualization-addons/
│ │ ├── .npmignore
│ │ ├── README.md
│ │ ├── package.json
│ │ ├── src/
│ │ │ ├── blockly/
│ │ │ │ ├── BlocklyJavascriptHelper.ts
│ │ │ │ ├── BlocklyScriptEditor.ts
│ │ │ │ ├── BlocklyToolbox.ts
│ │ │ │ └── components/
│ │ │ │ ├── Console.ts
│ │ │ │ ├── Debugger.ts
│ │ │ │ ├── Delay.ts
│ │ │ │ ├── GetParameter.ts
│ │ │ │ ├── GetState.ts
│ │ │ │ ├── GetSubProperty.ts
│ │ │ │ ├── OpenScreen.ts
│ │ │ │ ├── QuerySelector.ts
│ │ │ │ ├── QuerySelectorAll.ts
│ │ │ │ ├── Return.ts
│ │ │ │ ├── SetElement.ts
│ │ │ │ ├── SetState.ts
│ │ │ │ ├── StartEvent.ts
│ │ │ │ └── components.ts
│ │ │ ├── components/
│ │ │ │ ├── BindingsEditor.ts
│ │ │ │ ├── BindingsEditorHistoric.ts
│ │ │ │ ├── EventAssignment.ts
│ │ │ │ ├── ParameterEditor.ts
│ │ │ │ ├── SimpleScriptEditor.ts
│ │ │ │ └── VisualizationPropertyGrid.ts
│ │ │ ├── helpers/
│ │ │ │ ├── BindingsHelper.ts
│ │ │ │ └── info.txt
│ │ │ ├── index.ts
│ │ │ ├── interfaces/
│ │ │ │ ├── IScriptMultiplexValue.ts
│ │ │ │ ├── VisualisationElementScript.ts
│ │ │ │ ├── VisualizationBinding.ts
│ │ │ │ ├── VisualizationHandler.ts
│ │ │ │ └── VisualizationShell.ts
│ │ │ ├── scripting/
│ │ │ │ ├── Script.ts
│ │ │ │ ├── ScriptCommands.ts
│ │ │ │ ├── ScriptSystem.ts
│ │ │ │ └── ScriptUpgrader.ts
│ │ │ ├── services/
│ │ │ │ ├── BindableObjectDragDropService.ts
│ │ │ │ ├── PropertyGridDragDropService.ts
│ │ │ │ ├── ScriptRefactorService.ts
│ │ │ │ ├── SignalPropertyEditor.ts
│ │ │ │ ├── VisualizationBindingsRefactorService.ts
│ │ │ │ ├── VisualizationBindingsService.ts
│ │ │ │ └── VisualizationEventsService.ts
│ │ │ └── setupVisuService.ts
│ │ └── tsconfig.json
│ ├── web-component-designer-widgets-wunderbaum/
│ │ ├── .npmignore
│ │ ├── README.md
│ │ ├── package.json
│ │ ├── src/
│ │ │ ├── Constants.ts
│ │ │ ├── index.ts
│ │ │ └── widgets/
│ │ │ ├── WunderbaumOptions.ts
│ │ │ ├── bindableObjectsBrowser/
│ │ │ │ └── bindable-objects-browser.ts
│ │ │ ├── paletteView/
│ │ │ │ └── paletteTreeView.ts
│ │ │ └── treeView/
│ │ │ ├── ExpandCollapseContextMenu.ts
│ │ │ └── treeViewExtended.ts
│ │ └── tsconfig.json
│ └── web-component-designer-zpl/
│ ├── .npmignore
│ ├── README.md
│ ├── package.json
│ ├── src/
│ │ ├── extensions/
│ │ │ └── ZplLayoutResizeExtensionProvider.ts
│ │ ├── index.ts
│ │ ├── jsBarcodeOptions.ts
│ │ ├── monaco/
│ │ │ └── ZplLanguage.ts
│ │ ├── qr.ts
│ │ ├── services/
│ │ │ ├── ZplImageDrop.ts
│ │ │ ├── ZplLayoutCopyPasteService.ts
│ │ │ ├── ZplLayoutPlacementService.ts
│ │ │ └── ZplParserService.ts
│ │ ├── setupZplServiceContainer.ts
│ │ ├── widgets/
│ │ │ ├── elements.json
│ │ │ ├── views/
│ │ │ │ └── zpl-demo-view.ts
│ │ │ ├── zpl-barcode.ts
│ │ │ ├── zpl-comment.ts
│ │ │ ├── zpl-graphic-box.ts
│ │ │ ├── zpl-graphic-circle.ts
│ │ │ ├── zpl-graphic-diagonal-line.ts
│ │ │ ├── zpl-image.ts
│ │ │ └── zpl-text.ts
│ │ └── zplHelper.ts
│ └── tsconfig.json
├── todos/
│ ├── VueParserService.ts
│ └── todo.md
├── tsconfig.build.json
└── tsconfig.json
================================================
FILE CONTENTS
================================================
================================================
FILE: .editorconfig
================================================
root = true
[*]
end_of_line = crlf
insert_final_newline = false
indent_style = space
indent_size = 2
================================================
FILE: .github/FUNDING.yml
================================================
# These are supported funding model platforms
github: jogibear9988
patreon: jogibear9988
================================================
FILE: .github/copilot-instructions.md
================================================
## Project Agent Instructions
- Before starting work, read [AGENTS.md](../AGENTS.md) and follow its guidance.
- Treat [AGENTS.md](../AGENTS.md) as the canonical agent behavior file for this repository.
================================================
FILE: .gitignore
================================================
node_modules/
dist/
/.vs
debug.log
/.vscode
*.tsbuildinfo
================================================
FILE: ACKNOWLEDGMENTS
================================================
# Acknowledgments
- Thanks to @notwaldorf who created the original `wizzywid` project (MIT License).
https://github.com/PolymerLabs/wizzywid
This was a start for this whole project (even if mostly nothing of the original code is left)
- Thanks to @chdh for plain-scrollbar component
https://github.com/chdh/plain-scrollbar
- Thanks to @m-thalmann for contextmenujs
https://github.com/m-thalmann/contextmenujs (also we have heavily modified it)
- Levi Cole for parts of the cssUnits conversion code
https://stackoverflow.com/a/66569574/579623
- Domi for text-width code
https://stackoverflow.com/a/21015393
- gwwar for getClosestStackingContext
https://github.com/gwwar/z-context
- google icons
https://fonts.google.com/icons
================================================
FILE: AGENTS.md
================================================
## Project Agent Instructions
- AI agents must store memories under the repository [memories/](memories) directory.
## Why These Guidelines Exist
- Agents may make unchecked assumptions, hide confusion, and skip clarifications.
- Agents may overengineer solutions with unnecessary abstractions and code bloat.
- Agents may accidentally modify unrelated code or comments as side effects.
## Core Principles
### 1. Think Before Coding
- State assumptions explicitly; if uncertain, ask instead of guessing.
- Surface ambiguity and tradeoffs rather than silently choosing one interpretation.
- Push back when a simpler and safer approach exists.
- Stop and ask for clarification when confusion remains.
### 2. Simplicity First
- Implement the minimum code needed to solve the requested problem.
- Do not add speculative features, configurability, or abstractions not requested.
- Avoid handling impossible scenarios just to look complete.
- If a solution can be significantly simpler, prefer the simpler version.
### 3. Surgical Changes
- Touch only code required by the task.
- Do not refactor or restyle adjacent code that is unrelated to the request.
- Match existing project style and patterns.
- If you see unrelated dead code, mention it; do not remove it unless asked.
- Remove only unused artifacts created by your own changes.
### 4. Goal-Driven Execution
- Define concrete success criteria before implementation.
- Prefer verifiable outcomes, usually with tests or explicit checks.
- For multi-step tasks, use a short plan where each step includes a verification check.
- Continue iterating until criteria are met or a real blocker is identified.
## Success Signals
- Diffs contain only requested changes.
- Solutions are simple and avoid unnecessary rewrites.
- Clarifying questions happen before implementation when needed.
- Pull requests stay minimal and focused.
## Tradeoff
- These rules prioritize caution and correctness over raw speed on non-trivial tasks.
- For obvious small changes, use proportional rigor.
================================================
FILE: CLAUDE.md
================================================
## Project Agent Instructions
- Before starting work, read [AGENTS.md](AGENTS.md) and follow its guidance.
- Treat [AGENTS.md](AGENTS.md) as the canonical agent behavior file for this repository.
================================================
FILE: COMPARISON.md
================================================
## Similar Frameworks
| Name | Licence | Edit Source | Split View | Zooming | Resize Transformed | No Iframe | Iframe | Iframe isolation | Modifies Dom | Multiplayer | URL |
|-------------------------|----------|-------------|------------|---------|--------------------| ----------|--------|------------------|--------------|-------------|----------------------------------------|
| web-component-designer | MIT | yes | yes | yes | yes | yes | yes | allow-same-origin| no | yes no | |
| GrapeJS | BSD-3 | yes | no | no | broken | no | yes | no | yes | no | https://grapesjs.com/ |
| CraftJS | MIT | no | no | no | | no | yes | no | yes | no | https://craft.js.org/ |
| raisins | MIT | - | no | no | | no | yes | yes | yes | no | https://github.com/saasquatch/raisins |
### Description
- Zooming => can zoom Designer Canvas
- No iframe => can Design in a Shadow Root (No Iframe, all Components already loaded are usable)
- Iframe => can Design inside of an Iframe
- Iframe Isolation => iframe can be isolated (against XSS attacks)
- Modifies DOM => inserts it's overlays directly into edited DOM (this could be Bad, when a Component depends on the DOM or css classes collide)
- Multiplayer => Multiple Users can design a document at the same time
================================================
FILE: LICENSE
================================================
MIT License
Copyright (c) 2021 node-projects
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: README.md
================================================
# web-component-designer
A HTML web component for designing web components and HTML pages based on PolymerLabs wizzywid which can easily be integrated in your own software.
Meanwhile polymer is not used anymore.

There is also a preview VSCode addon using the designer: https://github.com/node-projects/vs-code-designer-addon
## NPM Package
https://www.npmjs.com/package/@node-projects/web-component-designer
## Comparison
[Comparison of Designer Frameworks](COMPARISON.md)
## Additional NPM Packages
All Modules which need an external dependency are now extracted to extra NPM packges.
So the designer now should work with bundlers.
| Name | Description |
| ---------------------------------------------------------------------- | -------------------------------------------- |
| web-component-designer-codeview-ace | |
| web-component-designer-codeview-codemirror | |
| web-component-designer-codeview-codemirror5 | |
| web-component-designer-codeview-monaco | atm. stuck at monaco 0.50.0 (webcomp supp) |
| web-component-designer-collaboration-service | |
| web-component-designer-htmlparserservice-base-custom-webcomponent | |
| web-component-designer-htmlparserservice-lit-element | |
| web-component-designer-htmlparserservice-nodehtmlparser | |
| ~~web-component-designer-miniatureview-html2canvas~~ | never worked correctly |
| web-component-designer-stylesheetservice-css-parser | |
| ~~web-component-designer-stylesheetservice-css-tools~~ | deprecated - switched to css-parser |
| ~~web-component-designer-stylesheetservice-css-tree~~ | deprecated - did never work very well |
| web-component-designer-visualization-addons | |
| ~~web-component-designer-texteditextension-stylo~~ | deprecated - stylo is deprecated |
| ~~web-component-designer-widgets-fancytree~~ | deprecated - replaced by widgets-wunderbaum |
| web-component-designer-widgets-wunderbaum | |
## Browser support
- Chrome, Firefox and Safari
## Projects using it
A ZPL-Label Designer:
(https://github.com/node-projects/web-component-designer-zpl-demo)

A material flow layout editor in a comercial application:

A flow chart editor

## Demo
look at: https://node-projects.github.io/web-component-designer-demo/index.html
repository: https://github.com/node-projects/web-component-designer-demo
or a simple one: https://node-projects.github.io/web-component-designer-simple-demo/index.html
repository: https://github.com/node-projects/web-component-designer-simple-demo
## What is needed
- @node-projects/base-custom-webcomponent a very small basic webcomponent library (maybe this will be included directly later, to be dependecy free)
- optional - ace code editor
- optional - monaco code editor (if you use code-view-monaco)
- optional - code mirror code editor (if you use code-view-codemirror) (workin but buggy)
- optional - fancytree (if you use tree-view-extended, palette-tree-view or bindable-objects-browser)
## Features we are working on
https://github.com/node-projects/web-component-designer/issues
## Developing
* This will install all required packages, link all the npm packages and build everyone once. (in mac or linux you need to run the script with sudo, or the "npm link" will not work)
```
$ npm run develop
```
## Using
At first you have to setup a service container providing services for history, properties, elements, ...
## Code Editor
You can select to use one of 3 code editors available (ACE, CodeMirrow, Monaco).
If you use one of the widgets, you need to include the JS lib in your index.html and then use the specific widget.
## TreeView
We have 2 tree components. One independent and one feature rich which uses FancyTree (and cause of this it needs JQuery and JqueryUI).
## DragDrop
If you'd like to use the designer on mobile, you need the mobile-drag-drop npm library.
Your index.html should be extended as follows:
## Copyright notice
The Library uses Images from the Chrome Dev Tools, see
https://github.com/ChromeDevTools/devtools-frontend/tree/main/front_end/Images/src
and
https://github.com/ChromeDevTools/devtools-frontend/blob/main/LICENSE
================================================
FILE: editor.code-workspace
================================================
{
"folders": [
{
"path": "."
},
{
"path": "../web-component-designer-demo"
}
],
"settings": {
"jestrunner.debugOptions": {
"runtimeArgs": [
"--experimental-vm-modules", "--enable-source-maps"
]
}
}
}
================================================
FILE: jest.config.js
================================================
export default {
testMatch: ['**/?(*.)+(spec|test).+(mts|ts|tsx|mjs|js)'],
preset: "ts-jest/presets/default-esm",
testEnvironment: "node",
extensionsToTreatAsEsm: ['.ts', '.mts'],
moduleNameMapper: {
'^(\\.{1,2}/.*)\\.js$': '$1'
},
transform: {
'^.+\\.(mts|ts|tsx|mjs|js)$': [
"ts-jest",
{
"useESM": true,
"tsconfig": {
"allowJs": true
}
}
]
},
transformIgnorePatterns: [
'/node_modules/(?!(?:@node-projects/base-custom-webcomponent)(?:/|$))'
]
}
================================================
FILE: memories/attribute-source-part-jump-note.md
================================================
Attribute property-grid source navigation uses `attribute:${attributeName}` source
parts generated by `AbstractHtmlWriterService`. Set the design item selection
first if needed, then call `selectionService.setSelectedPart(sourcePart)`; the
document container must listen to selection refresh events as well as full
selection changes so same-element source-part jumps update the code selection.
================================================
FILE: memories/default-html-parser-source-parts-note.md
================================================
DefaultHtmlParserService cannot read original source ranges from DOMParser nodes.
After full parses, run the HTML writer with `updatePositions=true` on the created
design items so attribute/source-part navigation works immediately from the
normalized designer HTML, matching what code view receives after switching back.
================================================
FILE: memories/freehand-path-interpolation-note.md
================================================
Freehand `DrawPathTool` can interpolate linear path points during drag with `interpolatePoints: true` and an optional `interpolationDistance`; the stock default tool opts in with 5px maximum spacing. Keep point-to-point path mode separate so its shift/angle snapping behavior stays unchanged.
================================================
FILE: memories/getboxquads-svg-fast-path-removal.md
================================================
# getBoxQuads SVG visual boxes
SVGGraphicsElement quads should be built from a local SVG visual box, then transformed into the requested coordinate system. A viewport `getBoundingClientRect()` shortcut is not generally correct because it is already post-transform and loses orientation under rotated/3D HTML ancestors. A raw `getBBox()` is also not enough because native Firefox includes stroke bounds for SVG lines and paths.
The current polyfill computes a local visual box from `getBBox()` plus stroke inflation, uses `getScreenCTM()` when there is no transformed HTML ancestor, and otherwise uses the existing accumulated transform matrix. This covers plain lines, nested SVG-in-SVG lines, CSS-transformed SVGs, and the i14 stroke-bound path sample.
================================================
FILE: memories/mermaid-diagram-support-plan.md
================================================
# Mermaid diagram support rollout
Goal: extend the Mermaid package from a flowchart-first designer into a Mermaid designer that can preview every Mermaid diagram and progressively offer structured visual editing for each diagram type.
Phases:
1. Flowchart: finish directions, labels, full shape syntax, markdown labels, edge movement, selection mapping, and Mermaid-layout import.
2. Sequence: participants, messages, activations, notes, loops/alt/opt blocks, writer/parser selection mapping.
3. Class: classes/interfaces, members, methods, relationships, annotations, namespaces.
4. State: states, transitions, composite states, notes, forks/joins.
5. ER: entities, attributes, relationship cardinalities.
6. Remaining text-first diagrams: Gantt, journey, pie, quadrant, requirement, gitgraph, mindmap, timeline.
7. Newer visual diagrams: sankey, xy chart, block, packet, kanban, architecture, radar, treemap, venn, ishikawa, wardley, treeview.
Principle: every diagram type should first render in preview through Mermaid, then gain a typed parser/model/writer only when its visual editing semantics are defined.
Current document metadata:
- Root design item attributes store the current editable family: `data-mermaid-diagram-type` and, for flowcharts, `data-mermaid-flowchart-direction`.
- The Mermaid package installs a root property group named `mermaid` so these values are editable when the root item is selected.
- The writer honors root metadata only when doing so will not drop incompatible existing widgets; actual conversion between diagram families is still a separate future task.
- The Mermaid writer opts into root-item serialization so root-only documents can still write `title`, diagram type, and flowchart direction even when there are no child design items.
- Palette entries are filtered by root `diagramType`: flowchart shows flowchart nodes only, sequence shows sequence controls. Flowchart edges are intentionally not in the palette because they are created by the connector tool.
- Mindmap is now a root `diagramType` with a filtered palette entry, root title support, indentation parser/writer, and a visual `mermaid-mindmap-node` widget. Mindmap nodes are real containers: parsed child lines become nested design items, and the writer emits indentation by walking `item.children()`. Import asks Mermaid to render the mindmap SVG first and maps `g.node.mindmap-node` positions back into the designer; the simple hierarchical placement is only a fallback when SVG layout extraction is unavailable. Direct parent nodes automatically draw curved child connection lines with depth-based stroke thickness. Icons/classes/config layout are still future mindmap property work.
- Requirement diagram is now a root `diagramType` with requirement/element palette entries, direction property, block parser/writer, Mermaid-rendered node positioning, editable `mermaid-requirement-node` widgets, and relationship widgets for `contains`, `copies`, `derives`, `satisfies`, `verifies`, `refines`, and `traces` in both forward and reverse syntax forms. Styling/class syntax is still future requirement property work.
- Flowchart frontmatter now preserves non-title content such as `config: htmlLabels: false`; flowchart labels with Markdown markers or embedded newlines are written as Mermaid markdown strings.
- Flowchart support now covers Mermaid v11 expanded `@{ shape: ... }` nodes for the documented shape aliases, subgraph containers with nested node writing, special edge connectors including circle/cross/multidirectional/invisible/min-length forms, edge ids, and raw flowchart directives for `style`, `classDef`, `class`, `click`, and `linkStyle` so styles/classes/animation definitions round-trip. Node rendering is still an editable approximation of Mermaid's SVG, not pixel-identical for every exotic shape.
- Mermaid node widgets render inline Markdown safely in the designer for bold/italic markers and multiline labels, using DOM nodes rather than raw HTML injection.
================================================
FILE: memories/monaco-selection-coalesce-note.md
================================================
# Monaco selection coalescing
When designer selection changes are mirrored into split/code view, the same source range can be requested more than once around a designer content update. `DocumentContainer` now coalesces identical source-range selections before calling the active code view.
`CodeViewMonaco` also tracks the delayed native Monaco selection call. Pending identical selections are ignored, and pending delayed selections are cleared when the model is updated, so a selection queued against the old model is not applied after the refreshed model and followed by the same fresh selection.
================================================
FILE: memories/node-html-parser-source-parts-note.md
================================================
The node-html-parser parser service must populate designItemDocumentPositionService
source parts during parse, not only element positions. Code-to-design transitions
do not run the writer first, so attribute jumps need parser-generated
`attribute:${name}` and `attribute:${name}/value` source parts immediately after
`parseDesignerHTML`.
================================================
FILE: memories/source-part-selection-coalesce-note.md
================================================
When selecting a source part on a design item that may not already be primary,
pass the source part into `selectionService.setSelectedElements(..., sourcePart)`
instead of calling `setSelectedElements()` and then `setSelectedPart()`. Splitting
those calls emits two code-view selections (element range, then source-part range)
and makes the text editor jump.
================================================
FILE: memories/svg-affine-overlay-point-conversion-note.md
================================================
For the getBoxQuads polyfill SVG graphics fast path, diagonal SVG lines need
stroke expansion along the normal vector (`strokeWidth / 2 * abs(dy|dx) / length`)
instead of the generic `strokeWidth * 2` inflation. Keep the legacy generic SVG
inflation for non-line graphics unless replacing it with a fully native-compatible
stroke bbox calculation; the existing path fixture relies on that behavior.
================================================
FILE: memories/svg-geometry-placement-transform-offset-note.md
================================================
- SVG geometry drag commit receives the live preview translation in visual/container
coordinates. Before writing geometry attributes (`x`, `y`, path points, etc.) for
elements that already have a CSS `transform`, map that offset through the inverse
2D linear part of the original transform; otherwise rotated/scaled/skewed SVG
shapes preview correctly but jump on mouseup.
- The conversion intentionally uses only `a/b/c/d` and ignores translation, because
it is transforming an offset vector, not an absolute point.
================================================
FILE: memories/unified-geometry-click-selection-no-commit.md
================================================
# Unified geometry click selection
`UnifiedGeometryExtension` creates drag state on handle pointer-down so it can keep pointer capture and support immediate dragging. A plain click on a handle must not commit geometry on pointer-up. Track `geometryChanged` in the drag state, set it only after a non-zero pointer movement has updated geometry, and commit on pointer-up only when that flag is true.
================================================
FILE: memory/context-menu-copy-paste-pattern.md
================================================
- Designer context menus are registered centrally in DefaultServiceBootstrap and grouped with ChildContextMenu plus SeperatorContextMenu.
- ContextMenu renders an item with title '-' as a divider, so submenu spacers should use that sentinel item.
- Clipboard-derived features can either write richer clipboard formats in copyItems or parse the current clipboard payload on demand; prefer the on-demand path when the behavior should work across browser instances without service-local state.
- For paste-format style transfer, prefer calling getPasteItems() and reading styles() from the first parsed design item instead of adding clipboard-specific methods to copy-paste services.
================================================
FILE: memory/context-menu-popover-anchor-note.md
================================================
- In ContextMenu, popover top-layer rendering becomes non-interactive when combined with CSS anchor positioning in the demo browser check.
- For the popover path, use explicit fixed left/top placement with viewport fallbacks; reserve anchor positioning for non-popover fallback paths only.
- Deferred registration of global mousedown/contextmenu close listeners avoids immediately closing menus opened from mousedown-based triggers like undo/redo hold menus.
================================================
FILE: memory/css-numeric-percent-measured-size-note.md
================================================
- For CSS numeric size properties, `to %` conversion should prefer the element's measured box size (`getBoundingClientRect`) over the raw numeric text value so the unit switch preserves the current rendered size.
- Keep `% -> px` on the normal reference-size path unless a concrete regression shows otherwise.
- Numeric scrub drag is more reliable in the property grid with window-level pointer move/up/cancel listeners than element-local move/up listeners.
================================================
FILE: memory/css-numeric-preview-lock-shadow-host-note.md
================================================
- NumericStyleInput should own its preview display lock internally; editor-level refresh suppression alone is not enough because property-grid refreshes can still race during drag/hold preview.
- Clear the display lock when an explicit new value arrives that differs from the current underlying `_value`; keep it only across repeated stale refreshes.
- Percent conversion for root elements must look past `parentElement` and fall back across shadow boundaries via `getRootNode().host`, then `ownerDocument.body/documentElement`.
- Interactive drag/step changes should snap to step increments and format to step precision, not just use a generic 4-decimal formatter.
================================================
FILE: memory/css-zoom-placement-preview-note.md
================================================
- Drag preview transforms in DefaultPlacementService and AbsolutePlacementService must divide the visual movement by the dragged element's CSS `zoom` factor before composing `translate(...)`.
- `getBoxQuads` must wrap the element transform with the zoom matrix (`zoom * transform`, not `transform * zoom`) so overlay geometry sees the same zoom-scaled translation the browser renders.
- TransformOriginExtension should draw its marker with `getResultingTransformationBetweenElementAndAllAncestors(..., canvas)` so the overlay includes self zoom, but it should keep using `element.convertPointFromNode(..., canvas)` for pointer-up commits so authored `transform-origin` stays in local element units.
- `zoom` scales transform translation during preview, but the final `placeDesignItem(..., 'position')` commit path should stay in layout units.
================================================
FILE: memory/cssom-shorthand-test-note.md
================================================
- Jest node/jsdom in this repo is not reliable for CSS shorthand serialization checks.
- For CssCombiner tests, fake document.createElement/style before importing the module because it creates a helper element at module load.
- Prefer focused fake CSSOM tests or real-browser validation when changing cssText/shorthand behavior.
================================================
FILE: memory/jest-esm-validation-note.md
================================================
- In packages/web-component-designer, Jest currently has unrelated ESM/module-resolution issues for tests that import dist ESM output or dependencies like @node-projects/base-custom-webcomponent from node_modules.
- For new service wiring in this package, npm run tsc is the reliable validation path unless Jest config is widened on purpose.
- For isolated DOM-focused tests, replacing a single `css` helper import with a local `CSSStyleSheet` creator can avoid the Jest ESM boundary without widening package-level Jest transforms.
- For custom-element or editor work in this package, extracting pure parsing/configuration helpers into source files without DOM or @node-projects/base-custom-webcomponent imports gives Jest a stable focused validation target without changing package-wide ESM transforms.
================================================
FILE: memory/jest-source-import-style-note.md
================================================
- In packages/web-component-designer Jest tests, import source modules with the package's existing ts-jest style (for example '../src/.../Module' without a '.js' suffix); using '.js' can fail module resolution in focused test runs.
================================================
FILE: memory/linked-package-type-compat-note.md
================================================
- In this repo's linked-package setup, widening exported structural types like ServiceContainer or InstanceServiceContainer can break demo/package compatibility because helper packages resolve their own nested @node-projects/web-component-designer types.
- Prefer optional public properties or internal `as any` registration for new cross-cutting services when the demo consumes linked packages compiled against older type surfaces.
- The demo HTML loads dist/appShell.js, so when normal demo tsc is blocked by the linked-package type mismatch, `npx tsc --noCheck` is the pragmatic way to refresh runtime artifacts for UI changes.
- In web-component-designer-demo, running `npm i` can replace the local linked `@node-projects/web-component-designer` package with the published node_modules copy; if browser behavior disagrees with passing source/tests, rebuild the package, run `npm link`, then rerun `npm run linkAll` in the demo before debugging further.
================================================
FILE: memory/manual-collab-snapshot-request.md
================================================
- For manual copy/paste WebRTC signaling, do not infer initial snapshot ownership from receiving `hello`; the copied direction can be opposite of the joining direction.
- Carry an explicit `requestInitialSnapshot` flag in `hello` / `hello-ack`, and decide it from the local document state so the empty peer asks for content instead of sending an empty snapshot back.
================================================
FILE: memory/overlay-refresh-pattern.md
================================================
- Designer extensions should prefer passing existing SVG nodes back into _drawLine/_drawCircle/_drawPath and gate refresh work with _valuesHaveChanges.
- Only rebuild overlays when geometry structure changes (segment count/type or control-point presence), otherwise update positions/styles in place to preserve pointer capture and avoid stale handles.
================================================
FILE: memory/pointertool-selected-quad-drag-note.md
================================================
- PointerTool drag start must distinguish click target from drag source.
- After alt-selecting an element underneath another, a plain click should still reselect the topmost hit element.
- A plain drag should move the selected underlying item when the pointer is inside that item's border quad; recompute initial drag offset from the chosen drag source element.
================================================
FILE: memory/property-grid-designitem-cache-sync-note.md
================================================
- PropertyGrid refresh can run correctly while editors still show stale values if external DOM changes bypass DesignItem APIs; properties services read DesignItem `_attributes`/`_styles` caches.
- Fix: add `IDesignItem.refreshAttributesAndStylesFromElement()` and call it before PropertyGrid/PropertyGridWithHeader refresh or rebuild work so `getValue`, `isSet`, `attributes()`, and `getAllStyles()` see live DOM state.
================================================
FILE: memory/property-grid-preview-recreation-fix.md
================================================
# Property Grid Preview Element Recreation Fix
## Root Cause
`CssCurrentPropertiesService.getRefreshMode()` returns `RefreshMode.fullOnValueChange` (value 2).
When NumericStyleInput's preview sets `element.style.setProperty(...)`, the MutationObserver fires
`PropertyGrid._mutationOccured()`, which calls `createElements()` for tabs with `fullOnValueChange`.
This DESTROYS and RECREATES all editors (including the active NumericStyleInput) mid-preview.
## Fix
- PropertyGrid keeps a single `MutationObserver` with `attributeOldValue: true`.
- `IPropertiesService` now has an optional `shouldRecreatePropertyListOnMutation(...)` hook.
- `AbstractPropertiesService` defaults to recreating only when an attribute is added or removed.
- `CssCurrentPropertiesService` and `CssCustomPropertiesService` override that hook to compare old/new inline style declaration names, so value-only style changes refresh editors but declaration add/remove still rebuilds.
- PropertyGrid also needs `designerCanvas.onContentChanged` and relevant `contentService.onContentChanged` subscriptions, because external property changes can arrive through undo/content notifications without a direct selected-element attribute mutation.
- `hasEditorInPreview()` and the schema-signature recreation path were removed.
## Key Enum Values (IPropertiesService.ts)
```
RefreshMode { none=0, full=1, fullOnValueChange=2, fullOnClassChange=3 }
```
- CssCurrentPropertiesService: fullOnValueChange (2) - the "styles" tab
- CssPropertiesService: none (0) - the "layout" tab
- AttributesPropertiesService: fullOnValueChange (2)
- NativeElementsPropertiesService: full (1)
================================================
FILE: memory/resize-left-top-initial-local-axis-note.md
================================================
- Left/top resize handles cannot derive drag deltas by converting the current pointer into the element's current local space, because the element origin moves during the resize and cancels part of the delta.
- Compute resize deltas from the pointer's movement in the initial local-axis basis (initial element-local-to-canvas matrix, translation removed), then apply opposite-corner correction from the current local border-box anchor converted back to canvas/parent space.
- Rereading live getBoxQuads() during the same pointermove turn can add jitter; prefer current local anchor geometry plus convertPointFromNode().
================================================
FILE: memory/straighten-line-screen-y-note.md
================================================
- PathDataPolyfill.straightenLine uses calculateAlpha's clockwise screen-space angle; reconstruct snapped points with `y - Math.sin(rad) * length` or vertical directions invert.
- Added PathDataPolyfill.test.ts with stubbed SVG globals because the helper module patches SVG element prototypes at import time.
================================================
FILE: memory/svg-getctm-double-transform-note.md
================================================
- In the getBoxQuads transform walk, do not seed SVGGraphicsElement (non-root SVG) with getElementTransformWithZoom for self transforms when the loop also multiplies getCTM(); getCTM already includes the element's local SVG/CSS transform and double-applies rotate/scale otherwise.
- A rotated CSS-sized SVG rect can expose this immediately: 90deg rotate produced a 180deg-style quad until the self CSS transform was skipped and getCTM handled it alone.
- For SVG geometry placement commit, the drag preview translation extracted from element.style.transform is already in local geometry coordinates; do not rotate it again before calling placeDesignItem, or rotated shapes jump on mouseup.
================================================
FILE: memory/svg-rect-style-geometry-write-note.md
================================================
- For SVG rect editing in the unified geometry path, read rendered geometry from getBBox() instead of rect.x/rect.width baseVal values when CSS width/height may be the source of truth.
- Preserve style-backed SVG geometry on write by carrying per-property serialization hints (style vs attribute, unit) through the geometry model, then applying writes with setStyle/style.setProperty instead of always setAttribute.
- This avoids stacked rect corner handles for CSS-sized rects and prevents drag/resize from injecting width/height attributes when those properties were authored in inline style.
================================================
FILE: memory/transform-preview-sync-pattern.md
================================================
- When previewing transform edits via direct writes to element.style.transform, always sync or clear that inline transform after commit.
- If the persisted transform stays local, restore the local style value; if it is stylesheet-backed, clear the inline preview so it does not override the stylesheet declaration.
================================================
FILE: memory/transformed-resize-grid-local-point-pattern.md
================================================
- For resize and grid interactions under transformed elements or transformed ancestors, do not derive local deltas from axis-aligned rects or an element-only inverse matrix.
- Convert canvas/overlay points back into element-local coordinates with convertPointFromNode (via the getBoxQuads polyfill), then compute size deltas, grid cell hits, and grid track drags from those local points.
- Grid helpers should expose local cell/gap coordinates separately from overlay coordinates so placement, hover, and resize logic all share the same transform-safe hit-testing path.
- During live resize, getBoxQuads can still reflect the pre-resize box inside the same pointermove turn; for opposite-corner correction, convert the current local fixed-anchor point back to canvas instead of rereading the current quad.
================================================
FILE: memory/undo-group-content-changed-commit-note.md
================================================
- In packages/web-component-designer, grouped undo operations should not emit instanceServiceContainer.onContentChanged from UndoService.execute while a ChangeGroup is open.
- ChangeGroup now accumulates IContentChanged payloads from execute calls and committed subgroups, and UndoService emits them once from commitTransactionItem on the outermost commit.
- This keeps documentContainer, treeView, and propertyGrid from reacting to intermediate states during grouped edits.
================================================
FILE: memory/webrtc-cross-machine-ice-config-note.md
================================================
- Manual WebRTC signaling in collaboration-service worked on the same machine but failed across machines until the transport exposed optional `rtcConfiguration`.
- Root cause: the transport created `RTCPeerConnection()` with no configurable STUN/TURN servers, so cross-machine connections depended only on local host candidates.
- Demo support was added via `collabIceServer` query params and optional JSON `collabRtcConfiguration`; when the demo compiles against an older published package, pass the new option object as `any` at the constructor call site to stay compatible.
- Cloudflare TURN should be integrated through the official server-side `generate-ice-servers` flow from `https://developers.cloudflare.com/realtime/turn/generate-credentials/`; a direct public `https://speed.cloudflare.com/turn-creds` link is not the supported integration path even though the speed test site can fetch credentials internally.
- Cloudflare's official TURN API endpoint at `https://rtc.live.cloudflare.com/v1/turn/keys/{TURN_KEY_ID}/credentials/generate-ice-servers` is callable from the browser with user-supplied key id and API token; for the demo this is acceptable as a dev/test convenience, but it still exposes the long-lived API token to the browser.
- Older static gist TURN hosts like `numb.viagenie.ca`, `turn.bistri.com`, `turn.anyfirewall.com`, and the raw gist `hubl.in` entries did not verify and should not be exposed as presets.
- OpenRelay is still live as a free-tier provider, but it needs signup/API credentials or auth-secret integration, so expose it as a manual provider workflow rather than an unauthenticated preset.
================================================
FILE: package.json
================================================
{
"name": "@node-projects/monorepo",
"private": true,
"type": "module",
"workspaces": [
"packages/*"
],
"scripts": {
"test": "node --experimental-vm-modules node_modules/jest/bin/jest.js",
"develop": "npm i && npm run link && npm run build",
"link": "npm run link --workspaces",
"watch": "npm run watch --workspaces",
"build": "npm run build --workspaces",
"prepublishOnly": "npm run build"
},
"devDependencies": {
"@blockly/zoom-to-fit": "^6.0.9",
"@node-projects/base-custom-webcomponent": ">=0.27.8",
"@node-projects/css-parser": "^5.0.0",
"@node-projects/lean-he-esm": "^3.3.0",
"@node-projects/node-html-parser-esm": "^6.2.0",
"@node-projects/propertygrid.webcomponent": "^1.2.3",
"@types/codemirror": "^5.60.15",
"@types/css-tree": "^2.3.8",
"@types/jest": "^29.5.14",
"@types/jquery": "^3.5.32",
"@types/jquery.fancytree": "0.0.11",
"@types/node": "^22.8.6",
"ace-builds": "^1.36.3",
"blockly": "^11.1.1",
"codemirror": "^6.0.1",
"codemirror5": "npm:codemirror@^5.0.0",
"css-tree": "^3.0.0",
"esprima-next": "^6.0.3",
"html2canvas": "*",
"jest": "^29.7.0",
"jest-environment-jsdom": "^30.3.0",
"jquery": "^3.7.1",
"jquery.fancytree": "^2.38.3",
"jsbarcode": "^3.11.6",
"long": "^5.2.3",
"mdn-data": "^2.12.1",
"monaco-editor": "^0.52.0",
"ts-jest": "^29.2.5",
"typescript": "^5.8.3",
"typescript-lit-html-plugin": "^0.9.0",
"wunderbaum": ">=0.13.0"
}
}
================================================
FILE: packages/web-component-designer/.npmignore
================================================
src/
test/
tests/
node_modules/
tsconfig.json
tsconfig.tsbuildinfo
!dist
================================================
FILE: packages/web-component-designer/README.md
================================================
# web-component-designer
```It's now considered beta. It works, we use it in production, but there are many more features to come```
A HTML web component for designing web components and HTML pages based on PolymerLabs wizzywid which can easily be integrated in your own software.
Meanwhile polymer is not used anymore.

## NPM Package
https://www.npmjs.com/package/@node-projects/web-component-designer
npm i @node-projects/web-component-designer
## Browser support
- Chrome/Firefox & Safari
## Developing
* Install dependencies
```
$ npm install
```
* Compile typescript after doing changes
```
$ npm run build (if you use Visual Studio Code, you can also run the build task via Ctrl + Shift + B > tsc:build - tsconfig.json)
```
* *Link node module*
```
$ npm link
```
## Using
At first you have to setup a service container providing services for history, properties, elements, ...
## Code Editor
You can select to use one of 3 code editors available (ACE, CodeMirrow, Monaco).
If you use one of the widgets, you need to include the JS lib in your index.html and then use the specific widget.
## TreeView
We have 2 tree components. One independent and one feature rich which uses FancyTree (and cause of this it needs JQuery and JqueryUI).
## DragDrop
If you'd like to use the designer on mobile, you need the mobile-drag-drop npm library.
Your index.html should be extended as follows:
## Keys
Pointer Tool:
alt: select element behind
shift: draw selection rect
ctrl: add/remove from selection
ctrl+shift: pan
## Copyright notice
The Library uses Images from the Chrome Dev Tools, see
https://github.com/ChromeDevTools/devtools-frontend/tree/main/front_end/Images/src
and
https://github.com/ChromeDevTools/devtools-frontend/blob/main/LICENSE
================================================
FILE: packages/web-component-designer/_esbuild.js
================================================
import * as esbuild from 'esbuild';
import { minifyHTMLLiteralsPlugin } from 'esbuild-plugin-minify-html-literals';
await esbuild.build({
entryPoints: ['./dist/index-all.js'],
outfile: './dist/index-min.js',
bundle: true,
format: 'esm',
minify: true,
sourcemap: true,
platform: 'neutral',
external: ['@node-projects/base-custom-webcomponent', './NpmPackageHacks.json' ],
plugins: [
minifyHTMLLiteralsPlugin()
]
}).catch(() => process.exit(1));
================================================
FILE: packages/web-component-designer/assets/designerCanvasIframe.html
================================================
================================================
FILE: packages/web-component-designer/assets/images/chromeDevtools/LICENSE
================================================
// Copyright 2014 The Chromium Authors. All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are
// met:
//
// * Redistributions of source code must retain the above copyright
// notice, this list of conditions and the following disclaimer.
// * Redistributions in binary form must reproduce the above
// copyright notice, this list of conditions and the following disclaimer
// in the documentation and/or other materials provided with the
// distribution.
// * Neither the name of Google Inc. nor the names of its
// contributors may be used to endorse or promote products derived from
// this software without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
================================================
FILE: packages/web-component-designer/assets/images/chromeDevtools/info.txt
================================================
Images from
https://github.com/ChromeDevTools/devtools-frontend/tree/main/front_end/Images/src
================================================
FILE: packages/web-component-designer/assets/images/treeview/license.txt
================================================
WPF Designer Icons are taken from
Fugue Icons Library:
License: Creative Commons Attribution 3.0 License
http://p.yusukekamiyamane.com/
Copied from the Fugue Icon Library and left unmodified:
- Icons.16x16.WpfOutline.Eye.png => eyeopen.png
- Icons.16x16.WpfOutline.EyeClosed.png => eyeclosed.png
- lock.png
================================================
FILE: packages/web-component-designer/config/elements-native.json
================================================
{
"elements":
[
"div",
"label",
"input",
"textarea",
"select",
{"tag" : "button", "defaultWidth": "80px", "defaultHeight": "30px", "defaultContent": "Button" },
"img",
"iframe",
{"tag" : "a" },
"p",
"span",
"b",
"i",
"u",
"br",
"em",
"q",
"small",
"strong",
"form",
"h1",
"h2",
"h3",
"h4",
"h5",
"h6",
"ol",
"ul",
"li",
"pre",
"table",
"caption",
"colgroup",
"col",
"thead",
"th",
"tbody",
"tr",
"td",
"tfoot"
]
}
================================================
FILE: packages/web-component-designer/jest.config.js
================================================
export default {
roots: ['/tests'],
testMatch: ['**/?(*.)+(spec|test).+(mts|ts|tsx|mjs|js)'],
preset: "ts-jest/presets/default-esm",
testEnvironment: "node",
extensionsToTreatAsEsm: ['.ts', '.mts'],
moduleNameMapper: {
'^(\\.{1,2}/.*)\\.js$': '$1'
},
transform: {
'^.+\\.(mts|ts|tsx|mjs|js)$': [
"ts-jest",
{
"useESM": true,
"tsconfig": {
"allowJs": true
}
}
]
},
transformIgnorePatterns: [
'/node_modules/(?!(?:@node-projects/base-custom-webcomponent)(?:/|$))'
]
}
================================================
FILE: packages/web-component-designer/jsr.json
================================================
{
"name": "@node-projects/web-component-designer",
"version": "0.1.180",
"exports": "./src/index.ts"
}
================================================
FILE: packages/web-component-designer/package.json
================================================
{
"description": "A WYSIWYG designer webcomponent for html components",
"name": "@node-projects/web-component-designer",
"version": "0.2.24",
"type": "module",
"main": "./dist/index.js",
"author": "jochen.kuehner@gmx.de",
"license": "MIT",
"scripts": {
"test": "node --experimental-vm-modules ../../node_modules/jest/bin/jest.js",
"tsc": "tsc",
"build": "tsc",
"link": "npm link",
"watch": "pm2 start tsc --watch",
"prepublishOnly": "npm run build && npm run bundle",
"bundle": "node _esbuild.js"
},
"dependencies": {
"@node-projects/base-custom-webcomponent": ">=0.27.8"
},
"devDependencies": {
"@types/node": "^22.8.6",
"esbuild": "^0.25.10",
"esbuild-plugin-minify-html-literals": "^3.0.0",
"mdn-data": "^2.4.2"
},
"repository": {
"type": "git",
"url": "git+https://github.com/node-projects/web-component-designer.git"
}
}
================================================
FILE: packages/web-component-designer/src/Constants.ts
================================================
export const dragDropFormatNameElementDefinition = 'text/json/elementdefintion';
export const dragDropFormatNameBindingObject = 'text/json/bindingobject';
export const dragDropFormatNamePropertyGrid = 'text/json/propertydrop';
let imporUrl = new URL((import.meta.url));
export var assetsPath = imporUrl.origin + imporUrl.pathname.split('/').slice(0, -1).join('/') + '/../assets/';
================================================
FILE: packages/web-component-designer/src/commandHandling/CommandType.ts
================================================
export enum CommandType {
'copy' = 'copy',
'paste' = 'paste',
'cut' = 'cut',
'delete' = 'delete',
'undo' = 'undo',
'redo' = 'redo',
'holdUndo' = 'holdUndo',
'holdRedo' = 'holdRedo',
'rotateCounterClockwise' = 'rotateCounterClockwise',
'rotateClockwise' = 'rotateClockwise',
'mirrorHorizontal' = 'mirrorHorizontal',
'mirrorVertical' = 'mirrorVertical',
'selectAll' = 'selectAll',
'moveToFront' = 'moveToFront',
'moveForward' = 'moveForward',
'moveBackward' = 'moveBackward',
'moveToBack' = 'moveToBack',
'arrangeLeft' = 'arrangeLeft',
'arrangeCenter' = 'arrangeCenter',
'arrangeRight' = 'arrangeRight',
'arrangeTop' = 'arrangeTop',
'arrangeMiddle' = 'arrangeMiddle',
'arrangeBottom' = 'arrangeBottom',
'unifyWidth' = 'unifyWidth',
'unifyHeight' = 'unifyHeight',
'distributeHorizontal' = 'distributeHorizontaly',
'distributeVertical' = 'distributeVertical',
'setTool' = 'setTool',
'setStrokeColor' = 'setStrokeColor',
'setFillBrush' = 'setFillBrush',
'setStrokeThickness' = 'setStrokeThickness',
'screenshot' = 'screenshot',
}
================================================
FILE: packages/web-component-designer/src/commandHandling/IUiCommand.ts
================================================
import { CommandType } from './CommandType.js';
export interface IUiCommand {
type: CommandType;
event?: Event;
special?: string;
parameter?: any;
altKey?: boolean;
ctrlKey?: boolean;
metaKey?: boolean;
shiftKey?: boolean;
}
================================================
FILE: packages/web-component-designer/src/commandHandling/IUiCommandHandler.ts
================================================
import { IUiCommand } from './IUiCommand.js';
export interface IUiCommandHandler {
executeCommand: (command: IUiCommand) => void;
canExecuteCommand: (command: IUiCommand) => boolean;
}
================================================
FILE: packages/web-component-designer/src/elements/controls/ColorEditor.ts
================================================
import { BaseCustomWebComponentConstructorAppend, css, html, TypedEvent } from '@node-projects/base-custom-webcomponent';
import { w3color } from '../helper/w3color.js';
export type ColorEditorMode = 'rgb' | 'hsl' | 'cmyk' | 'oklab' | 'oklch';
export type ColorEditorValueChangedEventArgs = { newValue?: string, oldValue?: string };
type RgbaColor = { r: number, g: number, b: number, a: number };
type HsvColor = { h: number, s: number, v: number };
const modes: ColorEditorMode[] = ['rgb', 'hsl', 'cmyk', 'oklab', 'oklch'];
const epsilon = 0.000001;
export class ColorEditor extends BaseCustomWebComponentConstructorAppend {
public static override readonly style = css`
:host {
display: block;
box-sizing: border-box;
width: 280px;
color: var(--property-grid-text-color, #e8edf2);
font: 12px system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
}
#editor {
display: grid;
gap: 10px;
box-sizing: border-box;
padding: 12px;
border: 1px solid rgba(255, 255, 255, .14);
border-radius: 8px;
background: var(--color-editor-background, #20252b);
box-shadow: 0 14px 38px rgba(0, 0, 0, .36);
}
#plane {
position: relative;
height: 150px;
border-radius: 6px;
overflow: hidden;
cursor: crosshair;
background:
linear-gradient(to top, #000, transparent),
linear-gradient(to right, #fff, transparent),
hsl(var(--hue, 0) 100% 50%);
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, .12);
touch-action: none;
}
#plane-handle {
position: absolute;
width: 12px;
height: 12px;
box-sizing: border-box;
border: 2px solid white;
border-radius: 50%;
transform: translate(-6px, -6px);
box-shadow: 0 0 0 1px rgba(0, 0, 0, .85), 0 1px 4px rgba(0, 0, 0, .55);
pointer-events: none;
}
.slider-row {
display: grid;
grid-template-columns: 18px minmax(0, 1fr) 42px;
gap: 8px;
align-items: center;
}
.slider-row span {
color: rgba(232, 237, 242, .68);
font-size: 11px;
text-transform: uppercase;
}
input[type="range"] {
--slider-background: #79b8ff;
-webkit-appearance: none;
appearance: none;
width: 100%;
height: 16px;
margin: 0;
background: transparent;
outline: none;
}
#hue {
--slider-background: linear-gradient(to right, #f00, #ff0, #0f0, #0ff, #00f, #f0f, #f00);
}
#alpha {
--slider-background:
linear-gradient(to right, rgba(var(--rgb-color, 0, 0, 0), 0), var(--opaque-color, #000)),
linear-gradient(45deg, rgba(255, 255, 255, .24) 25%, transparent 25% 75%, rgba(255, 255, 255, .24) 75%),
linear-gradient(45deg, rgba(255, 255, 255, .24) 25%, transparent 25% 75%, rgba(255, 255, 255, .24) 75%),
#2a3037;
--slider-background-position: 0 0, 0 0, 5px 5px, 0 0;
--slider-background-size: auto, 10px 10px, 10px 10px, auto;
}
input[type="range"]::-webkit-slider-runnable-track {
height: 7px;
border-radius: 999px;
background: var(--slider-background);
background-position: var(--slider-background-position, 0 0);
background-size: var(--slider-background-size, auto);
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, .22);
}
input[type="range"]::-moz-range-track {
height: 7px;
border-radius: 999px;
background: var(--slider-background);
background-position: var(--slider-background-position, 0 0);
background-size: var(--slider-background-size, auto);
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, .22);
}
input[type="range"]::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
width: 14px;
height: 14px;
margin-top: -3.5px;
border: 2px solid white;
border-radius: 50%;
background: hsl(var(--hue, 0) 100% 50%);
box-shadow: 0 0 0 1px rgba(0, 0, 0, .75), 0 1px 3px rgba(0, 0, 0, .45);
}
input[type="range"]::-moz-range-thumb {
width: 10px;
height: 10px;
border: 2px solid white;
border-radius: 50%;
background: hsl(var(--hue, 0) 100% 50%);
box-shadow: 0 0 0 1px rgba(0, 0, 0, .75), 0 1px 3px rgba(0, 0, 0, .45);
}
#alpha::-webkit-slider-thumb {
background: var(--color, #000);
}
#alpha::-moz-range-thumb {
background: var(--color, #000);
}
.swatch {
width: 42px;
height: 22px;
border-radius: 5px;
background:
linear-gradient(var(--color, #000), var(--color, #000)),
linear-gradient(45deg, rgba(255, 255, 255, .22) 25%, transparent 25% 75%, rgba(255, 255, 255, .22) 75%),
linear-gradient(45deg, rgba(255, 255, 255, .22) 25%, transparent 25% 75%, rgba(255, 255, 255, .22) 75%);
background-position: 0 0, 0 0, 5px 5px;
background-size: auto, 10px 10px, 10px 10px;
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, .16);
}
#mode {
height: 26px;
min-width: 0;
border: 1px solid rgba(255, 255, 255, .14);
border-radius: 5px;
background: #161a1f;
color: inherit;
font: inherit;
outline: none;
}
#channels {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 6px;
}
label {
display: grid;
gap: 3px;
min-width: 0;
color: rgba(232, 237, 242, .68);
font-size: 10px;
text-transform: uppercase;
}
input[type="number"],
#text {
box-sizing: border-box;
width: 100%;
min-width: 0;
height: 26px;
border: 1px solid rgba(255, 255, 255, .14);
border-radius: 5px;
background: #161a1f;
color: inherit;
font: inherit;
outline: none;
}
input[type="number"] {
padding: 0 4px;
}
#text {
padding: 0 8px;
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
text-transform: none;
}
#text.invalid {
border-color: #e66b6b;
box-shadow: 0 0 0 1px rgba(230, 107, 107, .35);
}
input:focus,
select:focus {
border-color: #79b8ff;
box-shadow: 0 0 0 1px rgba(121, 184, 255, .28);
}
:host([readonly]) input,
:host([readonly]) select,
:host([readonly]) #plane,
:host([disabled]) input,
:host([disabled]) select,
:host([disabled]) #plane {
opacity: .65;
pointer-events: none;
}
`;
public static override readonly template = html`
`;
public valueChanged = new TypedEvent();
public valuePreviewChanged = new TypedEvent();
private _value = '#000000';
private _color: RgbaColor = { r: 0, g: 0, b: 0, a: 1 };
private _mode: ColorEditorMode = 'rgb';
private _hue = 0;
private _plane: HTMLDivElement;
private _planeHandle: HTMLDivElement;
private _hueInput: HTMLInputElement;
private _alphaInput: HTMLInputElement;
private _alphaLabel: HTMLSpanElement;
private _modeSelect: HTMLSelectElement;
private _channels: HTMLDivElement;
private _textInput: HTMLInputElement;
private _dragPointerId: number = null;
public get value() {
return this._value;
}
public set value(value: string) {
const parsed = parseColor(value);
if (parsed) {
this._color = parsed;
this._mode = inferColorMode(value) ?? this._mode;
this._syncHueFromColor();
this._value = this._formatColor();
this._render();
return;
}
this._value = value ?? '';
this._render();
}
public get mode() {
return this._mode;
}
public set mode(value: ColorEditorMode) {
if (modes.includes(value)) {
this._mode = value;
this._value = this._formatColor();
this._render();
}
}
public get readOnly() {
return this.hasAttribute('readonly');
}
public set readOnly(value: boolean) {
this.toggleAttribute('readonly', value);
}
public get disabled() {
return this.hasAttribute('disabled');
}
public set disabled(value: boolean) {
this.toggleAttribute('disabled', value);
}
constructor() {
super();
this._plane = this._getDomElement('plane');
this._planeHandle = this._getDomElement('plane-handle');
this._hueInput = this._getDomElement('hue');
this._alphaInput = this._getDomElement('alpha');
this._alphaLabel = this._getDomElement('alpha-label');
this._modeSelect = this._getDomElement('mode');
this._channels = this._getDomElement('channels');
this._textInput = this._getDomElement('text');
}
ready() {
this._wireEvents();
const attributeValue = this.getAttribute('value');
if (attributeValue != null)
this.value = attributeValue;
const attributeMode = this.getAttribute('mode') as ColorEditorMode;
if (modes.includes(attributeMode)) {
this._mode = attributeMode;
this._value = this._formatColor();
}
this._render();
}
private _wireEvents() {
this._plane.addEventListener('pointerdown', e => this._startPlaneDrag(e));
this._hueInput.addEventListener('input', () => this._applyHue(true));
this._hueInput.addEventListener('change', () => this._commitCurrentValue());
this._alphaInput.addEventListener('input', () => this._applyAlpha(true));
this._alphaInput.addEventListener('change', () => this._commitCurrentValue());
this._modeSelect.addEventListener('change', () => {
this.mode = this._modeSelect.value as ColorEditorMode;
this._commitCurrentValue();
});
this._channels.addEventListener('input', () => this._applyChannelInputs(true));
this._channels.addEventListener('change', () => this._commitCurrentValue());
this._textInput.addEventListener('input', () => this._validateTextInput());
this._textInput.addEventListener('change', () => this._applyText());
this._textInput.addEventListener('keydown', e => {
if (e.key === 'Enter') {
this._applyText();
this._textInput.blur();
}
});
}
private _startPlaneDrag(event: PointerEvent) {
if (this.readOnly || this.disabled)
return;
this._dragPointerId = event.pointerId;
this._plane.setPointerCapture(event.pointerId);
this._applyPlanePointer(event, true);
this._plane.addEventListener('pointermove', this._planePointerMove);
this._plane.addEventListener('pointerup', this._finishPlaneDrag);
this._plane.addEventListener('pointercancel', this._finishPlaneDrag);
event.preventDefault();
}
private _planePointerMove = (event: PointerEvent) => {
if (event.pointerId === this._dragPointerId)
this._applyPlanePointer(event, true);
};
private _finishPlaneDrag = (event: PointerEvent) => {
if (event.pointerId !== this._dragPointerId)
return;
this._dragPointerId = null;
this._plane.removeEventListener('pointermove', this._planePointerMove);
this._plane.removeEventListener('pointerup', this._finishPlaneDrag);
this._plane.removeEventListener('pointercancel', this._finishPlaneDrag);
this._commitCurrentValue();
};
private _applyPlanePointer(event: PointerEvent, preview: boolean) {
const rect = this._plane.getBoundingClientRect();
const s = clamp((event.clientX - rect.left) / rect.width, 0, 1);
const v = clamp(1 - ((event.clientY - rect.top) / rect.height), 0, 1);
const hue = Number(this._hueInput.value) || 0;
this._hue = normalizeHue(hue);
this._setColor({ ...hsvToRgb(hue, s, v), a: this._color.a }, preview);
}
private _applyHue(preview: boolean) {
const hsv = rgbToHsv(this._color);
const hue = Number(this._hueInput.value) || 0;
this._hue = normalizeHue(hue);
this._setColor({ ...hsvToRgb(hue, hsv.s || 1, hsv.v || 1), a: this._color.a }, preview);
}
private _applyAlpha(preview: boolean) {
this._setColor({ ...this._color, a: clamp(Number(this._alphaInput.value) / 100, 0, 1) }, preview);
}
private _applyChannelInputs(preview: boolean) {
const values = [...this._channels.querySelectorAll('input')].reduce((map, input) => {
map[input.name] = Number(input.value);
return map;
}, {} as Record);
let color: RgbaColor;
if (this._mode === 'rgb')
color = { r: values.r, g: values.g, b: values.b, a: values.a / 100 };
else if (this._mode === 'hsl')
color = { ...hslToRgb(values.h, values.s / 100, values.l / 100), a: values.a / 100 };
else if (this._mode === 'cmyk')
color = { ...cmykToRgb(values.c / 100, values.m / 100, values.y / 100, values.k / 100), a: values.a / 100 };
else if (this._mode === 'oklab')
color = { ...oklabToRgb(values.l / 100, values.a1, values.b1), a: values.alpha / 100 };
else
color = { ...oklchToRgb(values.l / 100, values.c, values.h), a: values.a / 100 };
this._setColor(normalizeColor(color), preview, false, true);
}
private _validateTextInput() {
const text = this._textInput.value.trim();
this._textInput.classList.toggle('invalid', text.length > 0 && !parseColor(text));
}
private _applyText() {
const parsed = parseColor(this._textInput.value);
this._textInput.classList.toggle('invalid', !parsed);
if (!parsed)
return;
this._mode = inferColorMode(this._textInput.value) ?? this._mode;
this._setColor(parsed, false, true, true);
}
private _setColor(color: RgbaColor, preview: boolean, renderChannels = true, syncHue = false) {
const oldValue = this._value;
this._color = normalizeColor(color);
if (syncHue)
this._syncHueFromColor();
this._value = this._formatColor();
this._render(renderChannels);
if (preview)
this.valuePreviewChanged.emit({ newValue: this._value, oldValue });
else
this.valueChanged.emit({ newValue: this._value, oldValue });
}
private _commitCurrentValue() {
const oldValue = this._value;
this._value = this._formatColor();
this.valueChanged.emit({ newValue: this._value, oldValue });
}
private _render(renderChannels = true) {
if (!this._textInput)
return;
const hsv = rgbToHsv(this._color);
this.style.setProperty('--hue', String(Math.round(this._hue)));
this.style.setProperty('--color', toCssRgb(this._color));
this.style.setProperty('--opaque-color', `rgb(${this._color.r}, ${this._color.g}, ${this._color.b})`);
this.style.setProperty('--rgb-color', `${this._color.r}, ${this._color.g}, ${this._color.b}`);
this._planeHandle.style.left = `${hsv.s * 100}%`;
this._planeHandle.style.top = `${(1 - hsv.v) * 100}%`;
this._hueInput.value = String(Math.round(this._hue));
this._alphaInput.value = String(Math.round(this._color.a * 100));
this._alphaLabel.textContent = `${Math.round(this._color.a * 100)}%`;
this._modeSelect.value = this._mode;
if (renderChannels)
this._renderChannelInputs();
if (document.activeElement !== this._textInput)
this._textInput.value = this._value;
this._textInput.classList.remove('invalid');
}
private _renderChannelInputs() {
const channels = getChannelValues(this._color, this._mode);
this._channels.innerHTML = channels.map(channel => `
`).join('');
}
private _formatColor() {
return formatColor(this._color, this._mode);
}
private _syncHueFromColor() {
const hsv = rgbToHsv(this._color);
if (hsv.s > epsilon && hsv.v > epsilon)
this._hue = normalizeHue(hsv.h);
}
}
export class ColorInput extends BaseCustomWebComponentConstructorAppend {
public static override readonly style = css`
:host {
display: inline-block;
box-sizing: border-box;
width: 100%;
height: 100%;
padding: 2px;
}
button {
display: block;
box-sizing: border-box;
width: 100%;
height: 100%;
padding: 3px;
border: 1px solid var(--input-border-color, #596c7a);
border-radius: 4px;
background: var(--input-background-color, #1d2228);
cursor: pointer;
outline: none;
}
button:focus {
border-color: #79b8ff;
box-shadow: 0 0 0 1px rgba(121, 184, 255, .28);
}
#swatch {
display: block;
width: 100%;
height: 100%;
border-radius: 2px;
background:
linear-gradient(var(--color, #000), var(--color, #000)),
linear-gradient(45deg, rgba(255, 255, 255, .25) 25%, transparent 25% 75%, rgba(255, 255, 255, .25) 75%),
linear-gradient(45deg, rgba(255, 255, 255, .25) 25%, transparent 25% 75%, rgba(255, 255, 255, .25) 75%);
background-position: 0 0, 0 0, 5px 5px;
background-size: auto, 10px 10px, 10px 10px;
box-shadow: inset 0 0 0 1px rgba(0, 0, 0, .28);
}
:host([readonly]) button,
:host([disabled]) button {
cursor: default;
opacity: .65;
}
`;
public static override readonly template = html`
`;
private _value = '#000000';
private _button: HTMLButtonElement;
private _popup: HTMLDivElement;
private _editor: ColorEditor;
private _ignoreNextClick = false;
private _outsidePointerHandler = (event: PointerEvent) => this._handleOutsidePointer(event);
private _windowKeyHandler = (event: KeyboardEvent) => this._handleWindowKey(event);
public get value() {
return this._value;
}
public set value(value: string) {
this._setValue(value, false);
}
public get readOnly() {
return this.hasAttribute('readonly');
}
public set readOnly(value: boolean) {
this.toggleAttribute('readonly', value);
}
public get disabled() {
return this.hasAttribute('disabled');
}
public set disabled(value: boolean) {
this.toggleAttribute('disabled', value);
if (this._button)
this._button.disabled = value;
}
constructor() {
super();
this._button = this._getDomElement('button');
}
ready() {
const attributeValue = this.getAttribute('value');
if (attributeValue != null)
this._setValue(attributeValue, false);
this._button.disabled = this.disabled;
this._button.addEventListener('pointerdown', e => this._handleButtonPointerDown(e));
this._button.addEventListener('click', e => this._handleButtonClick(e));
this._button.addEventListener('keydown', e => this._handleButtonKeyDown(e));
this._renderSwatch();
}
disconnectedCallback() {
this._closePopup();
}
private _togglePopup() {
if (this.readOnly || this.disabled)
return;
if (this._popup)
this._closePopup();
else
this._openPopup();
}
private _handleButtonPointerDown(event: PointerEvent) {
if (event.button !== 0)
return;
this._ignoreNextClick = true;
this._togglePopup();
event.preventDefault();
event.stopPropagation();
}
private _handleButtonClick(event: MouseEvent) {
if (this._ignoreNextClick) {
this._ignoreNextClick = false;
event.preventDefault();
event.stopPropagation();
return;
}
this._togglePopup();
}
private _handleButtonKeyDown(event: KeyboardEvent) {
if (event.key !== 'Enter' && event.key !== ' ')
return;
this._togglePopup();
event.preventDefault();
event.stopPropagation();
}
private _openPopup() {
this._popup = document.createElement('div');
this._popup.style.position = 'fixed';
this._popup.style.zIndex = '100000';
this._popup.style.width = '280px';
this._editor = document.createElement('node-projects-color-editor') as ColorEditor;
this._editor.value = this._value;
this._editor.valuePreviewChanged.on(e => {
this._setValue(e.newValue, true);
this.dispatchEvent(new Event('input', { bubbles: true, composed: true }));
});
this._editor.valueChanged.on(e => {
this._setValue(e.newValue, true);
this.dispatchEvent(new Event('change', { bubbles: true, composed: true }));
});
this._popup.appendChild(this._editor);
document.body.appendChild(this._popup);
this._positionPopup();
window.addEventListener('resize', () => this._positionPopup(), { once: true });
window.addEventListener('scroll', () => this._positionPopup(), { once: true, capture: true });
window.addEventListener('keydown', this._windowKeyHandler);
requestAnimationFrame(() => window.addEventListener('pointerdown', this._outsidePointerHandler, true));
}
private _positionPopup() {
if (!this._popup)
return;
const rect = this.getBoundingClientRect();
const width = this._popup.offsetWidth || 280;
const height = this._popup.offsetHeight || 360;
let left = rect.left;
let top = rect.bottom + 4;
if (left + width > window.innerWidth - 8)
left = window.innerWidth - width - 8;
if (top + height > window.innerHeight - 8)
top = rect.top - height - 4;
this._popup.style.left = `${Math.max(8, left)}px`;
this._popup.style.top = `${Math.max(8, top)}px`;
}
private _handleOutsidePointer(event: PointerEvent) {
const path = event.composedPath();
if (path.includes(this) || (this._popup && path.includes(this._popup)))
return;
this._closePopup();
}
private _handleWindowKey(event: KeyboardEvent) {
if (event.key === 'Escape')
this._closePopup();
}
private _closePopup() {
window.removeEventListener('pointerdown', this._outsidePointerHandler, true);
window.removeEventListener('keydown', this._windowKeyHandler);
this._popup?.remove();
this._popup = null;
this._editor = null;
}
private _setValue(value: string, updateAttribute: boolean) {
this._value = value ?? '';
if (updateAttribute)
this.setAttribute('value', this._value);
this._renderSwatch();
}
private _renderSwatch() {
const parsed = parseColor(this._value) ?? { r: 0, g: 0, b: 0, a: 1 };
this.style.setProperty('--color', toCssRgb(parsed));
}
}
function parseColor(value: string): RgbaColor {
if (!value)
return null;
const text = value.trim();
if (text.toLowerCase() === 'transparent')
return { r: 0, g: 0, b: 0, a: 0 };
const hex = parseHexColor(text);
if (hex)
return hex;
const oklab = parseOklabColor(text);
if (oklab)
return oklab;
const normalized = normalizeModernColorSyntax(text);
const color = w3color.toColorObject(normalized);
if (color?.valid)
return normalizeColor({ r: color.red, g: color.green, b: color.blue, a: color.opacity });
return null;
}
function inferColorMode(value: string): ColorEditorMode {
const match = /^\s*(rgba?|hsla?|cmyk|oklab|oklch)\s*\(/i.exec(value ?? '');
const colorFunction = match?.[1]?.toLowerCase();
if (colorFunction === 'rgb' || colorFunction === 'rgba')
return 'rgb';
if (colorFunction === 'hsl' || colorFunction === 'hsla')
return 'hsl';
if (colorFunction === 'cmyk')
return 'cmyk';
if (colorFunction === 'oklab')
return 'oklab';
if (colorFunction === 'oklch')
return 'oklch';
return null;
}
function normalizeModernColorSyntax(value: string) {
return value
.replace(/,\s*/g, ',')
.replace(/rgba?\(([^)]*)\)/i, (_, body) => normalizeFunctionBody('rgb', body))
.replace(/hsla?\(([^)]*)\)/i, (_, body) => normalizeFunctionBody('hsl', body));
}
function normalizeFunctionBody(name: string, body: string) {
if (body.includes(',')) {
const parts = body.split(',').map(x => x.trim());
if (parts.length === 4 && parts[3].endsWith('%'))
parts[3] = String(Number(parts[3].slice(0, -1)) / 100);
return `${name}${parts.length === 4 ? 'a' : ''}(${parts.join(',')})`;
}
const [channels, alpha] = body.split('/').map(x => x.trim());
const parts = channels.split(/\s+/).filter(Boolean);
if (alpha)
parts.push(alpha.endsWith('%') ? String(Number(alpha.slice(0, -1)) / 100) : alpha);
return `${name}${parts.length === 4 ? 'a' : ''}(${parts.join(',')})`;
}
function parseHexColor(value: string): RgbaColor {
const match = /^#([0-9a-f]{3,8})$/i.exec(value);
if (!match)
return null;
const hex = match[1];
const read = (part: string) => parseInt(part.length === 1 ? part + part : part, 16);
if (hex.length === 3 || hex.length === 4)
return normalizeColor({ r: read(hex[0]), g: read(hex[1]), b: read(hex[2]), a: hex.length === 4 ? read(hex[3]) / 255 : 1 });
if (hex.length === 6 || hex.length === 8)
return normalizeColor({ r: read(hex.slice(0, 2)), g: read(hex.slice(2, 4)), b: read(hex.slice(4, 6)), a: hex.length === 8 ? read(hex.slice(6, 8)) / 255 : 1 });
return null;
}
function parseOklabColor(value: string): RgbaColor {
const match = /^(oklab|oklch)\((.*)\)$/i.exec(value);
if (!match)
return null;
const mode = match[1].toLowerCase();
const [channels, alphaText] = match[2].split('/').map(x => x.trim());
const parts = channels.replace(/,/g, ' ').split(/\s+/).filter(Boolean);
if (parts.length !== 3)
return null;
const alpha = alphaText ? parseNumberOrPercent(alphaText, 1) : 1;
if (mode === 'oklab')
return normalizeColor({ ...oklabToRgb(parseNumberOrPercent(parts[0], 1), Number(parts[1]), Number(parts[2])), a: alpha });
return normalizeColor({ ...oklchToRgb(parseNumberOrPercent(parts[0], 1), Number(parts[1]), Number(parts[2])), a: alpha });
}
function formatColor(color: RgbaColor, mode: ColorEditorMode) {
const alpha = round(color.a, 3);
if (mode === 'rgb')
return color.a >= 1 ? `rgb(${color.r}, ${color.g}, ${color.b})` : `rgba(${color.r}, ${color.g}, ${color.b}, ${alpha})`;
if (mode === 'hsl') {
const hsl = rgbToHsl(color);
return color.a >= 1
? `hsl(${Math.round(hsl.h)}, ${Math.round(hsl.s * 100)}%, ${Math.round(hsl.l * 100)}%)`
: `hsla(${Math.round(hsl.h)}, ${Math.round(hsl.s * 100)}%, ${Math.round(hsl.l * 100)}%, ${alpha})`;
}
if (mode === 'cmyk') {
const cmyk = rgbToCmyk(color);
const body = `${Math.round(cmyk.c * 100)}%, ${Math.round(cmyk.m * 100)}%, ${Math.round(cmyk.y * 100)}%, ${Math.round(cmyk.k * 100)}%`;
return color.a >= 1 ? `cmyk(${body})` : `cmyk(${body}, ${alpha})`;
}
if (mode === 'oklab') {
const lab = rgbToOklab(color);
return `oklab(${round(lab.l * 100, 2)}% ${round(lab.a, 4)} ${round(lab.b, 4)}${color.a >= 1 ? '' : ` / ${alpha}`})`;
}
const lch = rgbToOklch(color);
return `oklch(${round(lch.l * 100, 2)}% ${round(lch.c, 4)} ${round(lch.h, 2)}${color.a >= 1 ? '' : ` / ${alpha}`})`;
}
function getChannelValues(color: RgbaColor, mode: ColorEditorMode) {
if (mode === 'rgb')
return [
{ label: 'R', name: 'r', value: color.r, min: 0, max: 255, step: 1 },
{ label: 'G', name: 'g', value: color.g, min: 0, max: 255, step: 1 },
{ label: 'B', name: 'b', value: color.b, min: 0, max: 255, step: 1 },
{ label: 'A', name: 'a', value: Math.round(color.a * 100), min: 0, max: 100, step: 1 }
];
if (mode === 'hsl') {
const hsl = rgbToHsl(color);
return [
{ label: 'H', name: 'h', value: Math.round(hsl.h), min: 0, max: 360, step: 1 },
{ label: 'S', name: 's', value: Math.round(hsl.s * 100), min: 0, max: 100, step: 1 },
{ label: 'L', name: 'l', value: Math.round(hsl.l * 100), min: 0, max: 100, step: 1 },
{ label: 'A', name: 'a', value: Math.round(color.a * 100), min: 0, max: 100, step: 1 }
];
}
if (mode === 'cmyk') {
const cmyk = rgbToCmyk(color);
return [
{ label: 'C', name: 'c', value: Math.round(cmyk.c * 100), min: 0, max: 100, step: 1 },
{ label: 'M', name: 'm', value: Math.round(cmyk.m * 100), min: 0, max: 100, step: 1 },
{ label: 'Y', name: 'y', value: Math.round(cmyk.y * 100), min: 0, max: 100, step: 1 },
{ label: 'K', name: 'k', value: Math.round(cmyk.k * 100), min: 0, max: 100, step: 1 },
{ label: 'A', name: 'a', value: Math.round(color.a * 100), min: 0, max: 100, step: 1 }
];
}
if (mode === 'oklab') {
const lab = rgbToOklab(color);
return [
{ label: 'L', name: 'l', value: round(lab.l * 100, 2), min: 0, max: 100, step: .1 },
{ label: 'A', name: 'a1', value: round(lab.a, 4), min: -1, max: 1, step: .001 },
{ label: 'B', name: 'b1', value: round(lab.b, 4), min: -1, max: 1, step: .001 },
{ label: 'Alpha', name: 'alpha', value: Math.round(color.a * 100), min: 0, max: 100, step: 1 }
];
}
const lch = rgbToOklch(color);
return [
{ label: 'L', name: 'l', value: round(lch.l * 100, 2), min: 0, max: 100, step: .1 },
{ label: 'C', name: 'c', value: round(lch.c, 4), min: 0, max: 1, step: .001 },
{ label: 'H', name: 'h', value: round(lch.h, 2), min: 0, max: 360, step: .1 },
{ label: 'A', name: 'a', value: Math.round(color.a * 100), min: 0, max: 100, step: 1 }
];
}
function normalizeColor(color: RgbaColor): RgbaColor {
return {
r: Math.round(clamp(color.r, 0, 255)),
g: Math.round(clamp(color.g, 0, 255)),
b: Math.round(clamp(color.b, 0, 255)),
a: clamp(Number.isFinite(color.a) ? color.a : 1, 0, 1)
};
}
function toCssRgb(color: RgbaColor) {
return `rgba(${color.r}, ${color.g}, ${color.b}, ${round(color.a, 3)})`;
}
function rgbToHsv(color: RgbaColor): HsvColor {
const r = color.r / 255;
const g = color.g / 255;
const b = color.b / 255;
const max = Math.max(r, g, b);
const min = Math.min(r, g, b);
const d = max - min;
let h = 0;
if (d !== 0) {
if (max === r)
h = ((g - b) / d) % 6;
else if (max === g)
h = (b - r) / d + 2;
else
h = (r - g) / d + 4;
h *= 60;
}
return { h: (h + 360) % 360, s: max === 0 ? 0 : d / max, v: max };
}
function hsvToRgb(h: number, s: number, v: number) {
const c = v * s;
const x = c * (1 - Math.abs((h / 60) % 2 - 1));
const m = v - c;
let r = 0;
let g = 0;
let b = 0;
if (h < 60)
[r, g, b] = [c, x, 0];
else if (h < 120)
[r, g, b] = [x, c, 0];
else if (h < 180)
[r, g, b] = [0, c, x];
else if (h < 240)
[r, g, b] = [0, x, c];
else if (h < 300)
[r, g, b] = [x, 0, c];
else
[r, g, b] = [c, 0, x];
return { r: (r + m) * 255, g: (g + m) * 255, b: (b + m) * 255 };
}
function rgbToHsl(color: RgbaColor) {
const r = color.r / 255;
const g = color.g / 255;
const b = color.b / 255;
const max = Math.max(r, g, b);
const min = Math.min(r, g, b);
const l = (max + min) / 2;
const d = max - min;
let h = 0;
let s = 0;
if (d !== 0) {
s = d / (1 - Math.abs(2 * l - 1));
if (max === r)
h = ((g - b) / d) % 6;
else if (max === g)
h = (b - r) / d + 2;
else
h = (r - g) / d + 4;
h *= 60;
}
return { h: (h + 360) % 360, s, l };
}
function hslToRgb(h: number, s: number, l: number) {
const c = (1 - Math.abs(2 * l - 1)) * s;
const x = c * (1 - Math.abs((h / 60) % 2 - 1));
const m = l - c / 2;
let r = 0;
let g = 0;
let b = 0;
if (h < 60)
[r, g, b] = [c, x, 0];
else if (h < 120)
[r, g, b] = [x, c, 0];
else if (h < 180)
[r, g, b] = [0, c, x];
else if (h < 240)
[r, g, b] = [0, x, c];
else if (h < 300)
[r, g, b] = [x, 0, c];
else
[r, g, b] = [c, 0, x];
return { r: (r + m) * 255, g: (g + m) * 255, b: (b + m) * 255 };
}
function rgbToCmyk(color: RgbaColor) {
const r = color.r / 255;
const g = color.g / 255;
const b = color.b / 255;
const k = 1 - Math.max(r, g, b);
if (k >= 1 - epsilon)
return { c: 0, m: 0, y: 0, k: 1 };
return {
c: (1 - r - k) / (1 - k),
m: (1 - g - k) / (1 - k),
y: (1 - b - k) / (1 - k),
k
};
}
function cmykToRgb(c: number, m: number, y: number, k: number) {
return {
r: 255 * (1 - c) * (1 - k),
g: 255 * (1 - m) * (1 - k),
b: 255 * (1 - y) * (1 - k)
};
}
function rgbToOklab(color: RgbaColor) {
const r = srgbToLinear(color.r / 255);
const g = srgbToLinear(color.g / 255);
const b = srgbToLinear(color.b / 255);
const l = Math.cbrt(0.4122214708 * r + 0.5363325363 * g + 0.0514459929 * b);
const m = Math.cbrt(0.2119034982 * r + 0.6806995451 * g + 0.1073969566 * b);
const s = Math.cbrt(0.0883024619 * r + 0.2817188376 * g + 0.6299787005 * b);
return {
l: 0.2104542553 * l + 0.7936177850 * m - 0.0040720468 * s,
a: 1.9779984951 * l - 2.4285922050 * m + 0.4505937099 * s,
b: 0.0259040371 * l + 0.7827717662 * m - 0.8086757660 * s
};
}
function oklabToRgb(lValue: number, aValue: number, bValue: number) {
const l = Math.pow(lValue + 0.3963377774 * aValue + 0.2158037573 * bValue, 3);
const m = Math.pow(lValue - 0.1055613458 * aValue - 0.0638541728 * bValue, 3);
const s = Math.pow(lValue - 0.0894841775 * aValue - 1.2914855480 * bValue, 3);
return {
r: 255 * linearToSrgb(4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s),
g: 255 * linearToSrgb(-1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s),
b: 255 * linearToSrgb(-0.0041960863 * l - 0.7034186147 * m + 1.7076147010 * s)
};
}
function rgbToOklch(color: RgbaColor) {
const lab = rgbToOklab(color);
const c = Math.sqrt(lab.a * lab.a + lab.b * lab.b);
const h = c < epsilon ? 0 : (Math.atan2(lab.b, lab.a) * 180 / Math.PI + 360) % 360;
return { l: lab.l, c, h };
}
function oklchToRgb(l: number, c: number, h: number) {
const radians = h * Math.PI / 180;
return oklabToRgb(l, c * Math.cos(radians), c * Math.sin(radians));
}
function srgbToLinear(value: number) {
return value <= 0.04045 ? value / 12.92 : Math.pow((value + 0.055) / 1.055, 2.4);
}
function linearToSrgb(value: number) {
return value <= 0.0031308 ? 12.92 * value : 1.055 * Math.pow(value, 1 / 2.4) - 0.055;
}
function parseNumberOrPercent(value: string, percentBase: number) {
return value.endsWith('%') ? Number(value.slice(0, -1)) / 100 * percentBase : Number(value);
}
function clamp(value: number, min: number, max: number) {
return Math.min(max, Math.max(min, Number.isFinite(value) ? value : min));
}
function normalizeHue(value: number) {
return clamp(value, 0, 360);
}
function round(value: number, decimals: number) {
const factor = Math.pow(10, decimals);
return Math.round(value * factor) / factor;
}
customElements.define('node-projects-color-editor', ColorEditor);
customElements.define('node-projects-color-input', ColorInput);
================================================
FILE: packages/web-component-designer/src/elements/controls/DesignerTabControl.ts
================================================
import { BaseCustomWebComponentLazyAppend, css, TypedEvent, DomHelper } from '@node-projects/base-custom-webcomponent';
import { IActivateable } from '../../interfaces/IActivateable.js';
export type DesignerTabControlIndexChangedEventArgs = { newIndex: number, oldIndex?: number, changedViaClick?: boolean };
export class DesignerTabControl extends BaseCustomWebComponentLazyAppend {
private _selectedIndex: number = -1;
//private _contentObserver: MutationObserver;
private _panels: HTMLDivElement;
private _headerDiv: HTMLDivElement;
private _moreDiv: HTMLDivElement;
private _moreContainer: HTMLDivElement;
private _elementMap = new WeakMap();
private _firstConnect = true;
static override readonly style = css`
:host {
height: 100%;
}
.outer {
display: flex;
flex-direction: column;
height: 100%;
position: relative;
overflow: hidden;
}
.header {
display: inline-flex;
user-select: none;
-webkit-user-select: none;
flex-direction: row;
cursor: pointer;
height: 30px;
width: calc(100% - 30px);
background-color: var(--dark-grey, #232733);
overflow-x: auto;
scrollbar-width: none; /* Firefox */
}
.header-more {
right: 0;
top: 0;
width: 30px;
position: absolute;
color: white;
display: flex;
justify-content: center;
align-items: center;
font-family: math;
}
.header-more:hover {
background: var(--light-grey, #383f52);
}
.more-container {
z-index: 1;
user-select: none;
-webkit-user-select: none;
background-color: var(--dark-grey, #232733);
right: 0;
top: 30px;
position: absolute;
color: white;
display: flex;
flex-direction: column;
align-items: flex-start;
cursor: pointer;
}
.more-container .tab-header {
width: 100%;
}
.header::-webkit-scrollbar {
display: none; /* Safari and Chrome */
}
.tab-header {
height: 30px;
font-family: Arial;
display: flex;
justify-content: center;
align-items: center;
text-transform: uppercase;
box-sizing: content-box;
padding-left: 5px;
padding-right: 5px;
color: white;
font-size: 12px;
font-weight: 500;
line-height: 1.5;
letter-spacing: 1px;
white-space: nowrap;
}
.tab-header:hover {
background: var(--light-grey, #383f52);
}
.selected {
background: var(--medium-grey, #2f3545);
box-shadow: inset 0 3px 0 var(--highlight-pink, #e91e63);
}
.panels {
z-index: 0;
background: var(--medium-grey, #2f3545);
height: calc(100% - 30px);
}
`;
constructor() {
super();
/*this._contentObserver = new MutationObserver((mut) => {
let refresh = false;
for (let m of mut) {
if (m.type != 'attributes' || m.attributeName == 'style')
refresh = true;
}
if (refresh)
this.refreshItems();
});*/
let outerDiv = document.createElement("div")
outerDiv.className = 'outer';
this.shadowRoot.appendChild(outerDiv);
this._headerDiv = document.createElement("div")
this._headerDiv.className = 'header';
outerDiv.appendChild(this._headerDiv);
this._moreDiv = document.createElement("div");
this._moreDiv.className = "header header-more"
this._moreDiv.innerText = "<<"
outerDiv.appendChild(this._moreDiv);
this._moreContainer = document.createElement("div");
this._moreContainer.className = "more-container";
this._moreContainer.style.visibility = "hidden";
outerDiv.appendChild(this._moreContainer);
this._moreDiv.onclick = () => {
if (this._moreContainer.children.length && this._moreContainer.style.visibility == "hidden")
this._moreContainer.style.visibility = '';
else
this._moreContainer.style.visibility = "hidden";
}
this._panels = document.createElement("div")
this._panels.className = 'panels';
outerDiv.appendChild(this._panels);
let _slot = document.createElement("slot")
_slot.name = 'panels';
this._panels.appendChild(_slot);
const resizeObserver = new ResizeObserver(entries => {
this._showHideHeaderItems();
});
resizeObserver.observe(this._headerDiv);
}
private _showHideHeaderItems() {
this._moreContainer.style.visibility = "hidden";
let w = 0;
DomHelper.removeAllChildnodes(this._moreContainer);
DomHelper.removeAllChildnodes(this._headerDiv);
let reloadOnce = true;
for (let item of this.children) {
if ((item).style.display != 'none') {
let htmlItem = item as HTMLElement;
if (!this._elementMap.has(htmlItem) && reloadOnce) {
this.refreshItems();
reloadOnce = false;
}
const tabHeaderDiv = this._elementMap.get(htmlItem);
this._moreContainer.appendChild(tabHeaderDiv);
if (this._headerDiv.children.length == 0 || (w + (tabHeaderDiv.clientWidth / 2)) < this._headerDiv.clientWidth) {
this._headerDiv.appendChild(tabHeaderDiv);
w += tabHeaderDiv.clientWidth;
}
}
}
}
connectedCallback() {
if (this._firstConnect) {
this.refreshItems();
this._firstConnect = false;
//this._contentObserver.observe(this, { childList: true, subtree: true, attributes: true });
let selectedIndexAttribute = this.getAttribute("selected-index")
if (selectedIndexAttribute) {
this.selectedIndex = parseInt(selectedIndexAttribute);
}
}
}
public get selectedIndex() {
return this._firstConnect ? -1 : this._selectedIndex;
}
public set selectedIndex(value: number) {
let old = this._selectedIndex;
this._selectedIndex = value;
if (this.children.length && old != this._selectedIndex)
this._selectedIndexChanged(old);
}
public refreshItems() {
this._headerDiv.innerHTML = "";
let i = 0;
for (let item of this.children) {
if ((item).style.display != 'none') {
let htmlItem = item as HTMLElement;
let tabHeaderDiv = document.createElement("div")
tabHeaderDiv.innerText = htmlItem.dataset.title || htmlItem.title;
tabHeaderDiv.title = htmlItem.dataset.title || htmlItem.title;
tabHeaderDiv.className = 'tab-header';
let j = i;
tabHeaderDiv.onpointerdown = () => {
let old = this._selectedIndex;
this._selectedIndex = j;
if (this._headerDiv.children.length)
this._selectedIndexChanged(old, true);
this._moreContainer.style.visibility = 'hidden';
}
this._elementMap.set(htmlItem, tabHeaderDiv);
this._headerDiv.appendChild(tabHeaderDiv);
i++;
}
}
this._showHideHeaderItems();
this._selectedIndexChanged();
}
private _selectedIndexChanged(oldIndex?: number, viaClick = false) {
let index = -1;
for (let element of this.children) {
if ((element).style.display != 'none') {
index++;
if (index == this._selectedIndex) {
if (element.slot != "panels")
element.slot = "panels";
const headerEl = this._elementMap.get(element);
if (headerEl) {
headerEl.classList.add('selected');
if ((element).activated)
(element).activated();
}
} else {
element.removeAttribute("slot");
const headerEl = this._elementMap.get(element);
if (headerEl) {
headerEl.classList.remove('selected');
}
}
}
}
this.onSelectedTabChanged.emit({ newIndex: this._selectedIndex, oldIndex: oldIndex, changedViaClick: viaClick });
this._moreContainer.style.visibility = 'hidden';
}
public readonly onSelectedTabChanged = new TypedEvent();
}
customElements.define('node-projects-designer-tab-control', DesignerTabControl);
================================================
FILE: packages/web-component-designer/src/elements/controls/ImageButtonListSelector.ts
================================================
import { BaseCustomWebComponentConstructorAppend, css, html } from '@node-projects/base-custom-webcomponent';
export class ImageButtonListSelector extends BaseCustomWebComponentConstructorAppend {
public static override readonly style = css`
div {
font-size: 10px;
color: white;
}
#property {
color: #00aff0;
}
#value {
color: lightgray;
}
#value.value-set {
color: wheat;
}
.container {
display: flex;
flex-direction: row;
}
::slotted(button) {
min-width: 24px;
height: 24px;
padding: 1px;
background: white;
border: 1px solid lightgray;
}
`;
public static override readonly template = html`
`;
public static properties = {
value: String,
property: String,
unsetValue: String,
noValueInHeader: Boolean
}
constructor() {
super();
this._restoreCachedInititalValues();
}
private _value: string;
public get value() {
return this._value;
}
public set value(value) {
this._value = value;
this._updateValue();
}
public property: string;
public unsetValue: string;
public noValueInHeader: boolean;
_updateValue() {
if (this.value) {
this._getDomElement('value').innerText = this.value;
this._getDomElement('value').classList.add('value-set');
} else {
this._getDomElement('value').classList.remove('value-set');
}
const slot = this._getDomElement('slot');
for (let e of slot.assignedElements()) {
if ((e).dataset.value == this.value)
(e).style.background = "cornflowerblue";
else (e).style.background = "";
}
}
ready() {
this._parseAttributesToProperties();
if (this.property)
this._getDomElement('header').style.display = 'block';
if (this.noValueInHeader)
this._getDomElement('vhd').style.display = 'none';
const slot = this._getDomElement('slot');
slot.onclick = (e) => {
const path = e.composedPath();
for (let e of slot.assignedElements()) {
if (path.indexOf(e) >= 0) {
const oldValue = this._value;
this.value = (e).dataset.value;
const valueChangedEvent = new CustomEvent('value-changed', {
detail: {
newValue: this._value, oldValue: oldValue
}
});
this.dispatchEvent(valueChangedEvent);
}
}
}
this._getDomElement('property').innerText = this.property ?? '';
this._getDomElement('value').innerText = this.unsetValue ?? '';
this._updateValue();
}
}
customElements.define('node-projects-image-button-list-selector', ImageButtonListSelector);
================================================
FILE: packages/web-component-designer/src/elements/controls/MetricsEditor.ts
================================================
import { BaseCustomWebComponentConstructorAppend, css, html } from '@node-projects/base-custom-webcomponent';
export type MetricsEditorArea = 'position' | 'margin' | 'border' | 'padding' | 'content';
export type MetricsEditorSide = 'top' | 'right' | 'bottom' | 'left' | 'width' | 'height';
export type MetricsEditorValueChangedEventArgs = {
property: string,
area: MetricsEditorArea,
side: MetricsEditorSide,
newValue?: string,
oldValue?: string
};
type MetricsEditorValueMap = Partial>>>;
const metricsEditorNaturalWidth = 436;
const cssProperties: Record>> = {
position: {
top: 'top',
right: 'right',
bottom: 'bottom',
left: 'left'
},
margin: {
top: 'margin-top',
right: 'margin-right',
bottom: 'margin-bottom',
left: 'margin-left'
},
border: {
top: 'border-top-width',
right: 'border-right-width',
bottom: 'border-bottom-width',
left: 'border-left-width'
},
padding: {
top: 'padding-top',
right: 'padding-right',
bottom: 'padding-bottom',
left: 'padding-left'
},
content: {
width: 'width',
height: 'height'
}
};
const computedStyleProperties: Record = {
'border-top-width': 'borderTopWidth',
'border-right-width': 'borderRightWidth',
'border-bottom-width': 'borderBottomWidth',
'border-left-width': 'borderLeftWidth',
'margin-top': 'marginTop',
'margin-right': 'marginRight',
'margin-bottom': 'marginBottom',
'margin-left': 'marginLeft',
'padding-top': 'paddingTop',
'padding-right': 'paddingRight',
'padding-bottom': 'paddingBottom',
'padding-left': 'paddingLeft'
};
export class MetricsEditor extends BaseCustomWebComponentConstructorAppend {
public static override readonly style = css`
:host {
display: block;
box-sizing: border-box;
min-width: 0;
color: var(--property-grid-text-color, white);
font: 11px monospace;
overflow: hidden;
}
#box-model {
display: grid;
grid-template-columns: 46px minmax(120px, 1fr) 46px;
grid-template-rows: 22px minmax(29px, auto) minmax(58px, auto) minmax(29px, auto) 22px;
grid-template-areas:
". position-top ."
"position-left margin position-right"
"position-left margin position-right"
"position-left margin position-right"
". position-bottom .";
align-items: center;
justify-items: center;
width: 100%;
min-width: ${metricsEditorNaturalWidth}px;
box-sizing: border-box;
padding: 4px;
transform-origin: top left;
}
.ring {
display: grid;
grid-template-columns: 42px minmax(56px, 1fr) 42px;
grid-template-rows: 20px minmax(34px, auto) 20px;
grid-template-areas:
". top ."
"left inner right"
". bottom .";
align-items: center;
justify-items: center;
position: relative;
box-sizing: border-box;
width: 100%;
min-width: 0;
height: 100%;
min-height: 116px;
border: 1px dashed rgba(0, 0, 0, .55);
}
#margin {
grid-area: margin;
background: #f6c89f;
}
#border {
grid-area: inner;
background: #f7dd9c;
border-style: solid;
min-height: 76px;
}
#padding {
grid-area: inner;
background: #c8d08f;
min-height: 38px;
}
#content {
grid-area: inner;
display: grid;
grid-template-columns: minmax(26px, 1fr) auto minmax(26px, 1fr);
gap: 4px;
align-items: center;
justify-items: center;
width: 100%;
height: 100%;
min-height: 24px;
box-sizing: border-box;
background: #8fb9c3;
border: 1px solid rgba(0, 0, 0, .65);
}
#position-label,
#content-label {
display: none;
}
#content.box,
#border.box {
outline: 2px solid rgba(0, 0, 0, .85);
outline-offset: -2px;
}
.label {
position: absolute;
top: 2px;
left: 4px;
max-width: calc(100% - 8px);
overflow: hidden;
text-overflow: ellipsis;
pointer-events: none;
color: rgba(0, 0, 0, .72);
font-size: 10px;
line-height: 12px;
}
input {
width: 38px;
max-width: 100%;
min-width: 0;
height: 17px;
box-sizing: border-box;
padding: 0 2px;
border: 0;
border-radius: 0;
background: transparent;
color: rgba(0, 0, 0, .85);
font: inherit;
line-height: 17px;
text-align: center;
outline: none;
}
input:hover,
input:focus {
background: rgba(255, 255, 255, .72);
box-shadow: 0 0 0 1px rgba(0, 0, 0, .35);
}
input:disabled {
opacity: .65;
}
[data-side="top"] {
grid-area: top;
}
[data-side="right"] {
grid-area: right;
}
[data-side="bottom"] {
grid-area: bottom;
}
[data-side="left"] {
grid-area: left;
}
[data-area="position"][data-side="top"] {
grid-area: position-top;
}
[data-area="position"][data-side="right"] {
grid-area: position-right;
}
[data-area="position"][data-side="bottom"] {
grid-area: position-bottom;
}
[data-area="position"][data-side="left"] {
grid-area: position-left;
}
`;
public static override readonly template = html`
`;
public property: string;
public unsetValue = '-';
private _borderDiv: HTMLDivElement;
private _boxModelDiv: HTMLDivElement;
private _contentDiv: HTMLDivElement;
private _inputs: HTMLInputElement[] = [];
private _values: MetricsEditorValueMap = {};
private _isRefreshing = false;
private _resizeObserver: ResizeObserver;
constructor() {
super();
this._restoreCachedInititalValues();
this._borderDiv = this._getDomElement('border');
this._boxModelDiv = this._getDomElement('box-model');
this._contentDiv = this._getDomElement('content');
this._inputs = [...this.shadowRoot.querySelectorAll('input[data-area][data-side]')];
}
ready() {
this._parseAttributesToProperties();
this._wireEvents();
this._updateInputs();
requestAnimationFrame(() => this._updateScale());
}
connectedCallback() {
this._resizeObserver ??= new ResizeObserver(() => this._updateScale());
this._resizeObserver.observe(this);
requestAnimationFrame(() => this._updateScale());
}
disconnectedCallback() {
this._resizeObserver?.disconnect();
}
public get values(): MetricsEditorValueMap {
return this._cloneValues(this._values);
}
public set values(value: MetricsEditorValueMap) {
this._values = this._cloneValues(value ?? {});
this._updateInputs();
}
public getPropertyName(area: MetricsEditorArea, side: MetricsEditorSide) {
return cssProperties[area]?.[side];
}
public refresh(element: Element) {
this._contentDiv.classList.remove('box');
this._borderDiv.classList.remove('box');
if (!element) {
this.values = {};
return;
}
const computedStyle = element.ownerDocument.defaultView.getComputedStyle(element);
const nextValues: MetricsEditorValueMap = {};
for (const area of Object.keys(cssProperties) as MetricsEditorArea[]) {
nextValues[area] = {};
for (const side of Object.keys(cssProperties[area]) as MetricsEditorSide[]) {
const propertyName = this.getPropertyName(area, side);
nextValues[area][side] = this._getComputedProperty(computedStyle, propertyName, area);
}
}
if (computedStyle.boxSizing == 'content-box')
this._contentDiv.classList.add('box');
else
this._borderDiv.classList.add('box');
this.values = nextValues;
this._updateScale();
}
private _wireEvents() {
for (const input of this._inputs) {
input.addEventListener('focus', () => input.select());
input.addEventListener('change', () => this._commitInput(input));
input.addEventListener('keydown', event => {
if (event.key === 'Enter') {
this._commitInput(input);
input.blur();
} else if (event.key === 'Escape') {
this._updateInput(input);
input.blur();
}
});
}
}
private _commitInput(input: HTMLInputElement) {
if (this._isRefreshing)
return;
const area = input.dataset['area'] as MetricsEditorArea;
const side = input.dataset['side'] as MetricsEditorSide;
const oldValue = this._values[area]?.[side] ?? '';
const newValue = input.value.trim();
if (!this._values[area])
this._values[area] = {};
this._values[area][side] = newValue;
this._updateInput(input);
if (oldValue === newValue)
return;
this.dispatchEvent(new CustomEvent('value-changed', {
bubbles: true,
composed: true,
detail: {
property: this.getPropertyName(area, side),
area,
side,
newValue,
oldValue
}
}));
}
private _updateInputs() {
this._isRefreshing = true;
try {
for (const input of this._inputs)
this._updateInput(input);
} finally {
this._isRefreshing = false;
}
}
private _updateInput(input: HTMLInputElement) {
const area = input.dataset['area'] as MetricsEditorArea;
const side = input.dataset['side'] as MetricsEditorSide;
input.value = this._values[area]?.[side] ?? this.unsetValue;
}
private _getComputedProperty(computedStyle: CSSStyleDeclaration, propertyName: string, area: MetricsEditorArea) {
const camelName = computedStyleProperties[propertyName] ?? propertyName;
const value = computedStyle.getPropertyValue(propertyName) || computedStyle[camelName];
if (area === 'position' && value === 'auto')
return this.unsetValue;
return value || this.unsetValue;
}
private _cloneValues(values: MetricsEditorValueMap): MetricsEditorValueMap {
const clone: MetricsEditorValueMap = {};
for (const area of Object.keys(values) as MetricsEditorArea[])
clone[area] = { ...values[area] };
return clone;
}
private _updateScale() {
if (!this._boxModelDiv)
return;
const availableWidth = this.clientWidth;
if (availableWidth <= 0)
return;
const scale = Math.min(1, availableWidth / metricsEditorNaturalWidth);
this._boxModelDiv.style.width = scale < 1 ? metricsEditorNaturalWidth + 'px' : '100%';
this._boxModelDiv.style.transform = scale < 1 ? `scale(${scale})` : '';
this.style.height = (this._boxModelDiv.offsetHeight * scale) + 'px';
}
}
customElements.define('node-projects-metrics-editor', MetricsEditor);
================================================
FILE: packages/web-component-designer/src/elements/controls/NumericStyleInput.ts
================================================
import { BaseCustomWebComponentConstructorAppend, css, html, TypedEvent } from '@node-projects/base-custom-webcomponent';
import { combineNumericStyleInputValue, formatNumericStyleInputNumber, getNumericStyleInputUnitLabel, normalizeNumericStyleInputOptionValues, parseNumericStyleInputValue, resolveNumericStyleInputSelectedUnit, resolveNumericStyleInputStep } from './NumericStyleInputValueHelpers.js';
export type { ParsedNumericStyleInputValue } from './NumericStyleInputValueHelpers.js';
export { parseNumericStyleInputValue, formatNumericStyleInputNumber, combineNumericStyleInputValue } from './NumericStyleInputValueHelpers.js';
export type NumericStyleInputValueChangedEventArgs = { newValue?: string, oldValue?: string };
export type NumericStyleInputPreviewFinishedEventArgs = { newValue?: string, oldValue?: string, wasCancelled?: boolean };
export type NumericStyleInputUnitValueConversionArgs = {
value: number,
numberText: string,
rawValue: string,
fromUnit: string,
toUnit: string
};
type NumericStyleInputMode = 'unit' | 'fixed' | 'custom';
type NumericStyleInputDisplayState = {
mode: NumericStyleInputMode,
inputValue: string,
inputVisible: boolean,
inputEnabled: boolean,
selectValue: string,
selectedUnit?: string
};
const customOptionValue = '__node-projects-custom-value__';
const dragHandleGlyph = '⋮';
export class NumericStyleInput extends BaseCustomWebComponentConstructorAppend {
public static override readonly style = css`
:host {
display: block;
width: 100%;
min-width: 0;
}
#container {
display: grid;
gap: 0;
grid-template-columns: minmax(0, 1fr) auto 16px;
width: 100%;
height: 24px;
align-items: stretch;
}
#value-wrapper {
display: grid;
grid-template-columns: 14px minmax(0, 1fr);
min-width: 0;
}
#scrubber,
#input,
#select,
#stepper button {
/* border: 1px solid var(--input-border-color, #596c7a); */
border: none;
box-sizing: border-box;
height: 24px;
min-height: 24px;
background: transparent;
color: inherit;
font: inherit;
outline: none;
box-shadow: none;
}
#scrubber,
#stepper button {
padding: 0;
line-height: 1;
}
#scrubber {
border-right: 0;
cursor: ns-resize;
font-size: 11px;
letter-spacing: -1px;
}
#input {
border-left: 0;
border-right: 0;
min-width: 0;
text-align: right;
}
#input:focus,
#select:focus,
#scrubber:focus,
#stepper button:focus {
outline: none;
box-shadow: none;
border-color: var(--input-border-color, #596c7a);
}
#input[disabled],
#scrubber[disabled] {
cursor: default;
}
#input[disabled] {
color: inherit;
opacity: 1;
-webkit-text-fill-color: currentColor;
}
#select {
border-left: 0;
padding: 0 15px 0 2px;
line-height: 1;
-webkit-appearance: none;
appearance: none;
background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='8' height='4'%3E%3Cpath d='M0 0l4 4 4-4z' fill='%23999'/%3E%3C/svg%3E") no-repeat right 2px center;
}
#measure {
position: absolute;
visibility: hidden;
white-space: nowrap;
font: inherit;
pointer-events: none;
}
#stepper {
display: grid;
height: 24px;
min-height: 24px;
overflow: hidden;
grid-template-rows: repeat(2, minmax(0, 1fr));
}
#stepper button {
height: auto;
min-height: 0;
width: 16px;
min-width: 16px;
font-size: 10px;
}
`;
public static override readonly template = html`
`;
private _value = '';
public get value() {
return this._value;
}
public set value(value) {
this._setValue(value, false, false);
}
public valueChanged = new TypedEvent();
public valuePreviewChanged = new TypedEvent();
public valuePreviewFinished = new TypedEvent();
private _units: string[] = ['px', '%', 'pt'];
public get units() {
return [...this._units];
}
public set units(value: string[]) {
this._units = this._normalizeOptionValues(value);
this._lastNumericUnit = this._units[0] ?? this._lastNumericUnit;
this._updateValue();
}
private _fixedValues: string[] = [];
public get fixedValues() {
return [...this._fixedValues];
}
public set fixedValues(value: string[]) {
this._fixedValues = this._normalizeOptionValues(value);
this._updateValue();
}
private _step = 1;
public get step() {
return this._step;
}
public set step(value: number) {
this._step = Number.isFinite(value) && value > 0 ? value : 1;
}
private _unitSteps: Record = {};
public get unitSteps() {
return { ...this._unitSteps };
}
public set unitSteps(value: Record) {
this._unitSteps = value ?? {};
}
private _min: number = null;
public get min() {
return this._min;
}
public set min(value: number) {
this._min = Number.isFinite(value) ? value : null;
}
private _max: number = null;
public get max() {
return this._max;
}
public set max(value: number) {
this._max = Number.isFinite(value) ? value : null;
}
private _readOnly = false;
public get readOnly() {
return this._readOnly;
}
public set readOnly(value: boolean) {
this._readOnly = value;
this._applyReadonlyState();
}
public get isInPreview(): boolean {
return this._previewStartValue != null;
}
private _allowCustomValue = true;
public get allowCustomValue() {
return this._allowCustomValue;
}
public set allowCustomValue(value: boolean) {
this._allowCustomValue = value;
this._updateValue();
}
private _unitValueConverter: (args: NumericStyleInputUnitValueConversionArgs) => string;
public get unitValueConverter() {
return this._unitValueConverter;
}
public set unitValueConverter(value: (args: NumericStyleInputUnitValueConversionArgs) => string) {
this._unitValueConverter = value;
}
private _input: HTMLInputElement;
private _select: HTMLSelectElement;
private _measure: HTMLSpanElement;
private _scrubberButton: HTMLButtonElement;
private _increaseButton: HTMLButtonElement;
private _decreaseButton: HTMLButtonElement;
private _lastNumericValue = 0;
private _lastNumericUnit = 'px';
private _dragPointerId: number = null;
private _dragStartY = 0;
private _dragStartNumericValue = 0;
private _appliedDragSteps = 0;
private _previewStartValue: string = null;
private _previewChanged = false;
private _displayValueLock: string = null;
private _stepperPointerId: number = null;
private _stepperDirection = 0;
private _stepperRepeatTimeout: number = null;
private _stepperRepeatInterval: number = null;
private _windowDragPointerMoveHandler = (event: PointerEvent) => this._handleWindowDragPointerMove(event);
private _windowDragPointerUpHandler = (event: PointerEvent) => this._finishDragInteractionForPointerEvent(event, false);
private _windowDragPointerCancelHandler = (event: PointerEvent) => this._finishDragInteractionForPointerEvent(event, true);
private _windowDragBlurHandler = () => this._finishDragInteraction(false);
private _windowPointerUpHandler = (event: PointerEvent) => this._handleWindowStepperPointerEnd(event);
private _windowPointerCancelHandler = (event: PointerEvent) => this._handleWindowStepperPointerEnd(event);
private _windowBlurHandler = () => this._finishStepperInteraction(false);
private _preferCustomMode = false;
constructor() {
super();
this._restoreCachedInititalValues();
this._input = this._getDomElement('input');
this._select = this._getDomElement('select');
this._measure = this._getDomElement('measure');
this._scrubberButton = this._getDomElement('scrubber');
this._increaseButton = this._getDomElement('increase');
this._decreaseButton = this._getDomElement('decrease');
}
ready() {
this._parseAttributesToProperties();
this._wireEvents();
this._updateValue();
}
private _wireEvents() {
this._input.addEventListener('change', () => this._applyTypedValue());
this._input.addEventListener('keydown', e => {
if (e.key === 'Enter') {
this._applyTypedValue();
this._input.blur();
}
});
this._scrubberButton.addEventListener('pointerdown', e => this._handlePointerDown(e));
this._select.addEventListener('change', () => this._applySelectedMode());
this._increaseButton.addEventListener('pointerdown', e => this._handleStepperPointerDown(e, 1));
this._decreaseButton.addEventListener('pointerdown', e => this._handleStepperPointerDown(e, -1));
}
private _handlePointerDown(event: PointerEvent) {
if (this._readOnly)
return;
if (!this._isSelectedUnitMode()) {
if (!this._switchToUnitModeForInteraction())
return;
}
this._dragPointerId = event.pointerId;
this._dragStartY = event.clientY;
this._dragStartNumericValue = this._readEditableNumericValue(this._select.value);
this._appliedDragSteps = 0;
this._startPreviewSession();
this._attachDragWindowListeners();
try {
this._scrubberButton.setPointerCapture(event.pointerId);
} catch {
}
event.preventDefault();
}
private _handleWindowDragPointerMove(event: PointerEvent) {
if (this._dragPointerId !== event.pointerId || !this._isSelectedUnitMode())
return;
const stepCount = this._calculateDraggedStepCount(this._dragStartY - event.clientY);
if (stepCount === this._appliedDragSteps)
return;
this._appliedDragSteps = stepCount;
const unit = this._select.value;
const step = this._getEffectiveStep(unit);
this._previewNumericValue(this._getInteractiveStepValue(this._dragStartNumericValue, stepCount, step), unit, step);
event.preventDefault();
}
private _finishDragInteractionForPointerEvent(event: PointerEvent, wasCancelled: boolean) {
if (this._dragPointerId !== event.pointerId)
return;
this._finishDragInteraction(wasCancelled, event.pointerId);
}
private _finishDragInteraction(wasCancelled: boolean, pointerId?: number) {
if (this._dragPointerId == null)
return;
this._dragPointerId = null;
this._appliedDragSteps = 0;
this._detachDragWindowListeners();
if (pointerId != null) {
try {
if (this._scrubberButton.hasPointerCapture(pointerId))
this._scrubberButton.releasePointerCapture(pointerId);
} catch {
}
}
this._finishPreviewSession(wasCancelled);
}
private _handleStepperPointerDown(event: PointerEvent, direction: number) {
if (this._readOnly)
return;
if (!this._isSelectedUnitMode()) {
if (!this._switchToUnitModeForInteraction())
return;
}
this._finishStepperInteraction(true);
this._stepperPointerId = event.pointerId;
this._stepperDirection = direction;
this._attachStepperWindowListeners();
this._startPreviewSession();
this._applyStepPreview(direction);
this._stepperRepeatTimeout = window.setTimeout(() => {
this._stepperRepeatInterval = window.setInterval(() => this._applyStepPreview(this._stepperDirection), 50);
}, 350);
event.preventDefault();
}
private _handleWindowStepperPointerEnd(event: PointerEvent) {
if (this._stepperPointerId !== event.pointerId)
return;
this._finishStepperInteraction(false);
}
private _finishStepperInteraction(wasCancelled: boolean) {
if (this._stepperPointerId == null)
return;
this._stepperPointerId = null;
this._stepperDirection = 0;
this._detachStepperWindowListeners();
this._stopStepperRepeat();
this._finishPreviewSession(wasCancelled);
}
private _applySelectedMode() {
const selectedOption = this._select.selectedOptions.item(0);
if (!selectedOption)
return;
const selectedKind = selectedOption.dataset['kind'];
if (selectedKind === 'fixed') {
this._commitValue(selectedOption.value);
return;
}
if (selectedKind === 'custom') {
this._preferCustomMode = true;
this._updateValue();
requestAnimationFrame(() => {
this._input.focus();
this._input.select();
});
return;
}
const selectedUnit = selectedOption.value;
this._lastNumericUnit = selectedUnit;
this._preferCustomMode = false;
const parsedValue = parseNumericStyleInputValue(this._value);
if (parsedValue.kind === 'numeric') {
const convertedValue = this._convertNumericValue(parsedValue, selectedUnit);
this._commitValue(convertedValue);
return;
}
const resolvedCurrentValue = this._resolveCurrentValueForUnit(selectedUnit);
if (resolvedCurrentValue != null) {
this._commitValue(resolvedCurrentValue);
return;
}
if (parsedValue.kind === 'empty') {
this._updateValue();
this._input.focus();
return;
}
const typedNumericValue = Number(this._input.value);
if (!Number.isNaN(typedNumericValue)) {
this._applyNumericValue(typedNumericValue, selectedUnit);
return;
}
// Switching from a non-numeric value (e.g. unset/inherit) to a unit:
// clear the value so the unit mode is shown with an empty input
this._commitValue('');
this._input.focus();
}
private _applyTypedValue() {
const selectedOption = this._select.selectedOptions.item(0);
const selectedKind = selectedOption?.dataset['kind'];
if (selectedKind === 'fixed')
return;
if (selectedKind === 'custom') {
this._commitValue(this._input.value);
return;
}
const numberText = this._input.value?.trim() ?? '';
if (!numberText) {
this._commitValue('');
return;
}
const numericValue = Number(numberText);
if (!Number.isNaN(numericValue))
this._lastNumericValue = this._clampNumericValue(numericValue);
this._commitValue(combineNumericStyleInputValue(numberText, selectedOption?.value ?? ''));
}
private _updateValue() {
if (!this._input || !this._select)
return;
if (this._previewStartValue == null)
this._renderSelectOptions();
const displayState = this._getDisplayState(this._displayValueLock ?? this._value);
this._input.value = displayState.inputValue;
this._input.style.display = displayState.inputVisible ? '' : 'none';
this._input.disabled = this._readOnly || !displayState.inputEnabled;
this._select.value = displayState.selectValue;
this._autoSizeSelect();
if (displayState.mode === 'unit' && displayState.selectedUnit != null)
this._lastNumericUnit = displayState.selectedUnit;
if (displayState.mode === 'unit' && displayState.inputValue !== '') {
const typedNumericValue = Number(displayState.inputValue);
if (!Number.isNaN(typedNumericValue))
this._lastNumericValue = this._clampNumericValue(typedNumericValue);
}
const hasUnits = this._units.length > 0;
const interactionDisabled = this._readOnly || (!hasUnits || (displayState.mode === 'custom'));
this._scrubberButton.disabled = interactionDisabled;
this._increaseButton.disabled = interactionDisabled;
this._decreaseButton.disabled = interactionDisabled;
this._applyReadonlyState();
}
private _getDisplayState(value: string): NumericStyleInputDisplayState {
const parsedValue = parseNumericStyleInputValue(value);
if (parsedValue.kind === 'numeric') {
if (this._preferCustomMode)
return this._createCustomDisplayState(parsedValue.numberText);
if (parsedValue.unit && this._units.length && !this._units.includes(parsedValue.unit))
return this._createCustomDisplayState(value);
const selectedUnit = resolveNumericStyleInputSelectedUnit(parsedValue.unit, this._lastNumericUnit, this._units) ?? '';
return {
mode: 'unit',
inputValue: parsedValue.numberText,
inputVisible: true,
inputEnabled: true,
selectValue: this._units.includes(selectedUnit) ? selectedUnit : (this._select.value || customOptionValue),
selectedUnit
};
}
if (parsedValue.kind === 'text') {
if (!this._preferCustomMode && this._fixedValues.includes(parsedValue.text)) {
return {
mode: 'fixed',
inputValue: '',
inputVisible: true,
inputEnabled: false,
selectValue: parsedValue.text
};
}
return this._createCustomDisplayState(parsedValue.text);
}
if (this._preferCustomMode)
return this._createCustomDisplayState('');
const selectedUnit = this._lastNumericUnit ?? this._units[0] ?? '';
if (this._units.includes(selectedUnit)) {
return {
mode: 'unit',
inputValue: '',
inputVisible: true,
inputEnabled: true,
selectValue: selectedUnit,
selectedUnit
};
}
return this._createCustomDisplayState('');
}
private _createCustomDisplayState(text: string): NumericStyleInputDisplayState {
if (!this._allowCustomValue && this._units.length) {
return {
mode: 'unit',
inputValue: text,
inputVisible: true,
inputEnabled: true,
selectValue: this._units[0],
selectedUnit: this._units[0]
};
}
return {
mode: 'custom',
inputValue: text,
inputVisible: true,
inputEnabled: true,
selectValue: customOptionValue
};
}
private _renderSelectOptions() {
const selectedValue = this._select.value;
this._select.replaceChildren();
if (this._units.length) {
const group = document.createElement('optgroup');
group.label = 'Units';
for (const unit of this._units)
group.appendChild(this._createOption(getNumericStyleInputUnitLabel(unit), unit, 'unit'));
this._select.appendChild(group);
}
if (this._fixedValues.length) {
const group = document.createElement('optgroup');
group.label = 'Values';
for (const fixedValue of this._fixedValues)
group.appendChild(this._createOption(fixedValue, fixedValue, 'fixed'));
this._select.appendChild(group);
}
if (this._allowCustomValue || this._select.options.length === 0)
this._select.appendChild(this._createOption('custom', customOptionValue, 'custom'));
if (selectedValue !== '')
this._select.value = selectedValue;
else if (this._select.querySelector('option[value=""]'))
this._select.value = '';
}
private _createOption(label: string, value: string, kind: NumericStyleInputMode): HTMLOptionElement {
const option = document.createElement('option');
option.text = label;
option.value = value;
option.dataset['kind'] = kind;
return option;
}
private _autoSizeSelect() {
if (!this._measure || !this._select)
return;
const selectedOption = this._select.selectedOptions.item(0);
this._measure.textContent = selectedOption?.text ?? '';
const textWidth = this._measure.offsetWidth;
if (textWidth > 0) {
// 14px accounts for the custom dropdown arrow + padding
this._select.style.width = (textWidth + 17) + 'px';
} else {
// Element not yet laid out, defer measurement
requestAnimationFrame(() => this._autoSizeSelect());
}
}
private _applyReadonlyState() {
if (!this._input || !this._select)
return;
this._input.readOnly = this._readOnly;
this._select.disabled = this._readOnly;
}
private _convertNumericValue(parsedValue: { numberText: string, value: number, unit: string }, selectedUnit: string) {
if (selectedUnit == null)
return this._value;
const fromUnit = parsedValue.unit;
const convertedValue = this._unitValueConverter?.({
value: parsedValue.value,
numberText: parsedValue.numberText,
rawValue: this._value,
fromUnit,
toUnit: selectedUnit
});
return convertedValue ?? combineNumericStyleInputValue(parsedValue.numberText, selectedUnit);
}
private _resolveCurrentValueForUnit(selectedUnit: string) {
if (selectedUnit == null)
return null;
const convertedValue = this._unitValueConverter?.({
value: Number.NaN,
numberText: '',
rawValue: this._value,
fromUnit: '',
toUnit: selectedUnit
})?.trim();
return convertedValue ? convertedValue : null;
}
private _readEditableNumericValue(selectedUnit: string) {
const parsedValue = parseNumericStyleInputValue(this._value);
if (parsedValue.kind === 'numeric') {
const convertedValue = this._convertNumericValue(parsedValue, selectedUnit);
const convertedParsedValue = parseNumericStyleInputValue(convertedValue);
if (convertedParsedValue.kind === 'numeric')
return convertedParsedValue.value;
return parsedValue.value;
}
if (this._input.value?.trim()) {
const typedNumericValue = Number(this._input.value);
if (!Number.isNaN(typedNumericValue))
return typedNumericValue;
}
return this._lastNumericValue;
}
private _applyNumericValue(value: number, unit: string) {
this._lastNumericValue = this._clampNumericValue(value);
this._lastNumericUnit = unit;
this._commitValue(combineNumericStyleInputValue(formatNumericStyleInputNumber(this._lastNumericValue), unit));
}
private _previewNumericValue(value: number, unit: string, step?: number) {
this._lastNumericValue = this._clampNumericValue(value);
this._lastNumericUnit = unit;
const effectiveStep = step ?? this._getEffectiveStep(unit);
this._setValue(combineNumericStyleInputValue(this._formatInteractiveNumericValue(this._lastNumericValue, effectiveStep), unit), false, true);
}
private _commitValue(value: string) {
this._setValue(value, true, false);
}
private _setValue(value: string, emitCommit: boolean, emitPreview: boolean) {
const normalizedValue = value ?? '';
if (!emitCommit && !emitPreview && this._previewStartValue != null)
return;
const oldValue = this._value;
this._value = normalizedValue;
if (emitPreview)
this._displayValueLock = normalizedValue;
else if (this._displayValueLock != null) {
if (normalizedValue === this._displayValueLock || normalizedValue !== oldValue)
this._displayValueLock = null;
}
this._preferCustomMode = false;
this._updateValue();
if (oldValue !== normalizedValue) {
if (emitPreview) {
this._previewChanged = true;
this.valuePreviewChanged.emit({ newValue: normalizedValue, oldValue: oldValue });
}
if (emitCommit)
this.valueChanged.emit({ newValue: normalizedValue, oldValue: oldValue });
}
}
private _startPreviewSession() {
if (this._previewStartValue == null) {
this._previewStartValue = this._value;
this._previewChanged = false;
}
}
private _finishPreviewSession(wasCancelled: boolean) {
if (this._previewStartValue == null)
return;
const oldValue = this._previewStartValue;
const newValue = this._displayValueLock ?? this._value;
const changed = this._previewChanged && oldValue !== newValue;
const finalWasCancelled = wasCancelled || !changed;
this._previewStartValue = null;
this._previewChanged = false;
if (finalWasCancelled) {
this._displayValueLock = null;
this._value = oldValue;
this._updateValue();
} else {
this._displayValueLock = newValue;
this._updateValue();
}
this.valuePreviewFinished.emit({ newValue, oldValue, wasCancelled: finalWasCancelled });
}
private _applyStepPreview(direction: number) {
const unit = this._select.value;
const step = this._getEffectiveStep(unit);
this._previewNumericValue(this._getInteractiveStepValue(this._readEditableNumericValue(unit), direction, step), unit, step);
}
private _stopStepperRepeat() {
if (this._stepperRepeatTimeout != null) {
window.clearTimeout(this._stepperRepeatTimeout);
this._stepperRepeatTimeout = null;
}
if (this._stepperRepeatInterval != null) {
window.clearInterval(this._stepperRepeatInterval);
this._stepperRepeatInterval = null;
}
}
private _attachStepperWindowListeners() {
window.addEventListener('pointerup', this._windowPointerUpHandler, true);
window.addEventListener('pointercancel', this._windowPointerCancelHandler, true);
window.addEventListener('blur', this._windowBlurHandler);
}
private _attachDragWindowListeners() {
window.addEventListener('pointermove', this._windowDragPointerMoveHandler, true);
window.addEventListener('pointerup', this._windowDragPointerUpHandler, true);
window.addEventListener('pointercancel', this._windowDragPointerCancelHandler, true);
window.addEventListener('blur', this._windowDragBlurHandler);
}
private _detachStepperWindowListeners() {
window.removeEventListener('pointerup', this._windowPointerUpHandler, true);
window.removeEventListener('pointercancel', this._windowPointerCancelHandler, true);
window.removeEventListener('blur', this._windowBlurHandler);
}
private _detachDragWindowListeners() {
window.removeEventListener('pointermove', this._windowDragPointerMoveHandler, true);
window.removeEventListener('pointerup', this._windowDragPointerUpHandler, true);
window.removeEventListener('pointercancel', this._windowDragPointerCancelHandler, true);
window.removeEventListener('blur', this._windowDragBlurHandler);
}
private _calculateDraggedStepCount(pixelDelta: number) {
const absolutePixels = Math.abs(pixelDelta);
const fineSteps = Math.min(absolutePixels, 20) / 5;
const mediumSteps = Math.max(Math.min(absolutePixels - 20, 40), 0) / 3;
const coarseSteps = Math.max(absolutePixels - 60, 0) / 2;
const stepCount = Math.trunc(fineSteps + mediumSteps + coarseSteps);
return Math.sign(pixelDelta) * stepCount;
}
private _getEffectiveStep(unit?: string): number {
return resolveNumericStyleInputStep(this._unitSteps, this._step, unit);
}
private _getInteractiveStepValue(value: number, stepCount: number, step?: number) {
const effectiveStep = step ?? this._step;
const clampedValue = this._clampNumericValue(value);
if (!Number.isFinite(clampedValue) || !Number.isFinite(stepCount) || stepCount === 0)
return clampedValue;
const stepBase = this._min ?? 0;
const quotient = (clampedValue - stepBase) / effectiveStep;
const roundedQuotient = Math.round(quotient);
const isAligned = Math.abs(quotient - roundedQuotient) < 1e-9;
let targetIndex: number;
if (stepCount > 0) {
const firstIndex = isAligned ? roundedQuotient + 1 : Math.ceil(quotient);
targetIndex = firstIndex + stepCount - 1;
} else {
const firstIndex = isAligned ? roundedQuotient - 1 : Math.floor(quotient);
targetIndex = firstIndex + stepCount + 1;
}
const targetValue = stepBase + (targetIndex * effectiveStep);
return this._clampNumericValue(this._roundToStepPrecision(targetValue, effectiveStep));
}
private _formatInteractiveNumericValue(value: number, step?: number) {
const effectiveStep = step ?? this._step;
return formatNumericStyleInputNumber(this._roundToStepPrecision(value, effectiveStep), this._getStepPrecision(effectiveStep));
}
private _roundToStepPrecision(value: number, step?: number) {
const precision = this._getStepPrecision(step);
const factor = 10 ** precision;
return Math.round(value * factor) / factor;
}
private _getStepPrecision(step?: number) {
const stepText = `${step ?? this._step}`.toLowerCase();
if (stepText.includes('e-')) {
const [coefficientText, exponentText] = stepText.split('e-');
const exponent = Number(exponentText);
const decimalPartLength = coefficientText.includes('.') ? coefficientText.length - coefficientText.indexOf('.') - 1 : 0;
return exponent + decimalPartLength;
}
const decimalIndex = stepText.indexOf('.');
return decimalIndex >= 0 ? stepText.length - decimalIndex - 1 : 0;
}
private _switchToUnitModeForInteraction(): boolean {
const targetUnit = this._lastNumericUnit ?? this._units[0];
if (targetUnit == null)
return false;
this._preferCustomMode = false;
const resolved = this._resolveCurrentValueForUnit(targetUnit);
if (resolved != null) {
this._commitValue(resolved);
return this._isSelectedUnitMode();
}
this._applyNumericValue(this._lastNumericValue, targetUnit);
return this._isSelectedUnitMode();
}
private _isSelectedUnitMode() {
return this._select.selectedOptions.item(0)?.dataset['kind'] === 'unit';
}
private _normalizeOptionValues(values: string[]) {
return normalizeNumericStyleInputOptionValues(values);
}
private _clampNumericValue(value: number) {
let result = value;
if (this._min != null)
result = Math.max(this._min, result);
if (this._max != null)
result = Math.min(this._max, result);
return result;
}
}
customElements.define('node-projects-numeric-style-input', NumericStyleInput);
================================================
FILE: packages/web-component-designer/src/elements/controls/NumericStyleInputValueHelpers.ts
================================================
export type ParsedNumericStyleInputValue =
| { kind: 'empty' }
| { kind: 'numeric', numberText: string, value: number, unit: string }
| { kind: 'text', text: string };
export function parseNumericStyleInputValue(value?: string | null): ParsedNumericStyleInputValue {
const text = value?.trim() ?? '';
if (!text)
return { kind: 'empty' };
const match = text.match(/^([+-]?(?:\d+(?:\.\d+)?|\.\d+))(?:([a-z%]+))?$/i);
if (!match)
return { kind: 'text', text };
const numericValue = Number(match[1]);
if (Number.isNaN(numericValue))
return { kind: 'text', text };
return {
kind: 'numeric',
numberText: match[1],
value: numericValue,
unit: match[2]?.toLowerCase() ?? ''
};
}
export function formatNumericStyleInputNumber(value: number, maxDecimalPlaces: number = 4): string {
if (!Number.isFinite(value))
return '0';
const factor = 10 ** Math.max(0, maxDecimalPlaces);
const roundedValue = Math.round(value * factor) / factor;
return Object.is(roundedValue, -0) ? '0' : `${roundedValue}`;
}
export function combineNumericStyleInputValue(numberText: string, unit: string): string {
const trimmedNumberText = numberText?.trim() ?? '';
if (!trimmedNumberText)
return '';
return trimmedNumberText + (unit ?? '');
}
export function getNumericStyleInputUnitLabel(unit: string): string {
return unit === '' ? ' ' : unit;
}
export function normalizeNumericStyleInputOptionValues(values?: string[]): string[] {
const normalizedValues = (values ?? [])
.map(x => x == null ? null : x.trim())
.filter((x): x is string => x != null);
return [...new Set(normalizedValues)];
}
export function resolveNumericStyleInputSelectedUnit(parsedUnit: string | undefined, lastNumericUnit: string | undefined, units: string[]): string | null {
if (parsedUnit != null && units.includes(parsedUnit))
return parsedUnit;
return lastNumericUnit ?? units[0] ?? null;
}
export function resolveNumericStyleInputStep(unitSteps: Record | undefined, defaultStep: number, unit?: string): number {
if (unit != null) {
const unitStep = unitSteps?.[unit];
if (Number.isFinite(unitStep) && unitStep > 0)
return unitStep;
}
return defaultStep;
}
================================================
FILE: packages/web-component-designer/src/elements/controls/PlainScrollbar.ts
================================================
//included from: https://github.com/chdh/plain-scrollbar
import { css, html } from "@node-projects/base-custom-webcomponent";
class Widget {
private host: PlainScrollbar;
private root: HTMLElement;
private trough: HTMLElement;
private button1: HTMLElement; // up/left button
private button2: HTMLElement; // down/right button
private thumb: HTMLElement;
private isConnected: boolean = false;
public thumbSize: number = 0.3; // relative thumb size (0..1)
public value: number = 0; // current scrollbar position (0..1)
public orientation: boolean = false; // false=horizontal, true=vertical
private clickRepeatDelay: number = 300; // click repetition delay time in ms
private clickRepeatInterval: number = 100; // click repetition interval time in ms
private defaultThumbMinSize: number = 25; // default for minimum thumb size in pixels
private dragStartPos: number; // dragging start pointer position (clientX/Y)
private dragStartValue: number; // dragging start scrollbar position
private eventTimeoutId: NodeJS.Timeout | undefined;
private pointerCaptureId: number | undefined; // `undefined` = no capture active
private pointerCaptureElement: HTMLElement;
// User interaction state:
private thumbDragging: boolean; // true while user is dragging the thumb
private button1Active: boolean; // true while user has pointer clicked down on button 1
private button2Active: boolean; // true while user has pointer clicked down on button 2
private troughActive: boolean; // true while user has pointer clicked down on trough
public constructor(host: PlainScrollbar) {
this.host = host;
host.attachShadow({ mode: "open" });
const shadowRoot = host.shadowRoot!;
shadowRoot.appendChild(scrollbarHtmlTemplate.content.cloneNode(true));
shadowRoot.adoptedStyleSheets = [scrollbarStyle];
this.root = shadowRoot.querySelector("#root")!;
this.trough = shadowRoot.querySelector("#trough")!;
this.button1 = shadowRoot.querySelector("#button1")!;
this.button2 = shadowRoot.querySelector("#button2")!;
this.thumb = shadowRoot.querySelector("#thumb")!;
this.trough.addEventListener("pointerdown", this.onTroughPointerDown);
this.trough.addEventListener("pointerup", this.onPointerUp);
this.trough.addEventListener("pointercancel", this.onPointerUp);
this.button1.addEventListener("pointerdown", (event: PointerEvent) => this.onButtonPointerDown(event, 1));
this.button1.addEventListener("pointerup", this.onPointerUp);
this.button1.addEventListener("pointercancel", this.onPointerUp);
this.button1.addEventListener("contextmenu", (e: Event) => e.preventDefault()); // to prevent popup on long touch
this.button2.addEventListener("pointerdown", (event: PointerEvent) => this.onButtonPointerDown(event, 2));
this.button2.addEventListener("pointerup", this.onPointerUp);
this.button2.addEventListener("pointercancel", this.onPointerUp);
this.button2.addEventListener("contextmenu", (e: Event) => e.preventDefault()); // to prevent popup on long touch
this.thumb.addEventListener("pointerdown", this.onThumbPointerDown);
this.thumb.addEventListener("pointerup", this.onPointerUp);
this.thumb.addEventListener("pointercancel", this.onPointerUp);
this.thumb.addEventListener("pointermove", this.onThumbPointerMove);
this.resetInteractionState();
}
private resetInteractionState() {
this.thumbDragging = false;
this.button1Active = false;
this.button2Active = false;
this.troughActive = false;
}
public connectedCallback() {
this.isConnected = true;
this.resetInteractionState();
this.updateLayout();
this.updateStyle();
}
public disconnectedCallback() {
this.isConnected = false;
this.resetInteractionState();
this.stopEventRepetition();
this.stopPointerCapture();
}
public updateLayout() {
if (!this.isConnected) {
return;
}
this.root.classList.toggle("horizontal", !this.orientation);
this.root.classList.toggle("vertical", this.orientation);
this.thumb.style.display = (this.thumbSize == 0) ? "none" : "";
this.thumb.style.height = this.orientation ? percent(this.getEffectiveThumbSize()) : "";
this.thumb.style.width = this.orientation ? "" : percent(this.getEffectiveThumbSize());
this.thumb.style.top = "";
this.thumb.style.left = "";
this.updateThumbPosition();
}
private updateStyle() {
if (!this.isConnected) {
return;
}
this.thumb.classList.toggle("active", this.thumbDragging);
this.button1.classList.toggle("active", this.button1Active);
this.button2.classList.toggle("active", this.button2Active);
void this.troughActive;
} // tslint:disable-line
public updateThumbPosition() {
const v = (1 - this.getEffectiveThumbSize()) * this.value;
if (this.orientation) {
this.thumb.style.top = percent(v);
}
else {
this.thumb.style.left = percent(v);
}
}
private getThroughSize(): number {
return this.orientation ? this.trough.clientHeight : this.trough.clientWidth;
}
private computeThumbMoveValue(distancePixels: number): number {
const troughSlidePixels = this.getThroughSize() * (1 - this.getEffectiveThumbSize());
if (troughSlidePixels < EPS) {
return 0;
}
return distancePixels / troughSlidePixels;
}
public setThumbSize(newThumbSize: number) {
const clippedNewThumbSize = Math.max(0, Math.min(1, newThumbSize));
if (clippedNewThumbSize == this.thumbSize) {
return;
}
this.thumbSize = clippedNewThumbSize;
this.updateLayout();
}
private getThumbMinSize(): number {
const s = this.getCssVar("--plain-scrollbar-thumb-min-size");
if (!s) {
return this.defaultThumbMinSize;
}
const px = decodePxValue(s);
if (!px) {
return this.defaultThumbMinSize;
}
return px;
}
private getEffectiveThumbSize(): number {
const thumbMinSize = this.getThumbMinSize();
const throughSize = this.getThroughSize();
if (!throughSize) {
return this.thumbSize;
}
const min = Math.min(1, thumbMinSize / throughSize);
return Math.max(min, this.thumbSize);
}
public setValue(newValue: number): boolean {
const clippedNewValue = Math.max(0, Math.min(1, newValue));
if (clippedNewValue == this.value) {
return false;
}
this.value = isNaN(clippedNewValue) ? 0 : clippedNewValue;
this.updateThumbPosition();
return true;
}
public setOrientation(newOrientation: boolean): boolean {
if (newOrientation == this.orientation) {
return false;
}
this.orientation = newOrientation;
this.updateLayout();
return true;
}
private getCssVar(varName: string): string | undefined {
const s = getComputedStyle(this.root).getPropertyValue(varName);
if (!s) {
return null;
}
return s.trim();
}
//--- Outgoing events -------------------------------------------------------
private fireEvent(eventSubType: string) {
const event = new CustomEvent("scrollbar-input", { detail: eventSubType });
this.host.dispatchEvent(event);
}
private fireEventRepeatedly(eventSubType: string, repeatDelay: number, repeatInterval: number, repeatCounter = 0) {
this.stopEventRepetition();
this.fireEvent(eventSubType);
const delay = (repeatCounter == 0) ? repeatDelay : repeatInterval;
const f = () => this.fireEventRepeatedly(eventSubType, repeatDelay, repeatInterval, repeatCounter + 1);
this.eventTimeoutId = setTimeout(f, delay);
}
private stopEventRepetition() {
if (this.eventTimeoutId) {
clearTimeout(this.eventTimeoutId);
this.eventTimeoutId = undefined;
}
}
//--- Pointer input ----------------------------------------------------------
private startPointerCapture(element: HTMLElement, pointerId: number) {
this.stopPointerCapture();
element.setPointerCapture(pointerId);
this.pointerCaptureElement = element;
this.pointerCaptureId = pointerId;
}
private stopPointerCapture() {
if (!this.pointerCaptureId) {
return;
}
this.pointerCaptureElement.releasePointerCapture(this.pointerCaptureId);
this.pointerCaptureId = undefined;
}
private onTroughPointerDown = (event: PointerEvent) => {
if (!this.isConnected || this.pointerCaptureId) {
return;
}
if (!event.isPrimary || event.altKey || event.ctrlKey || event.metaKey || event.shiftKey || event.button != 0) {
return;
}
const r = this.trough.getBoundingClientRect();
const pos = this.orientation ? event.clientY - r.top : event.clientX - r.left;
const threshold = (this.orientation ? r.height : r.width) * (1 - this.getEffectiveThumbSize()) * this.value;
const direction = pos > threshold;
const eventSubType = direction ? "incrementLarge" : "decrementLarge";
this.troughActive = true;
event.preventDefault();
this.startPointerCapture(this.trough, event.pointerId);
this.fireEventRepeatedly(eventSubType, this.clickRepeatDelay, this.clickRepeatInterval);
};
private onButtonPointerDown = (event: PointerEvent, buttonNo: number) => {
if (!this.isConnected || this.pointerCaptureId) {
return;
}
if (!event.isPrimary || event.altKey || event.ctrlKey || event.metaKey || event.shiftKey || event.button != 0) {
return;
}
switch (buttonNo) {
case 1: this.button1Active = true; break;
case 2: this.button2Active = true; break;
}
const eventSubType = (buttonNo == 1) ? "decrementSmall" : "incrementSmall";
this.updateStyle();
event.preventDefault();
const buttonElement = (buttonNo == 1) ? this.button1 : this.button2;
this.startPointerCapture(buttonElement, event.pointerId);
this.fireEventRepeatedly(eventSubType, this.clickRepeatDelay, this.clickRepeatInterval);
};
private onThumbPointerDown = (event: PointerEvent) => {
if (!this.isConnected || this.pointerCaptureId) {
return;
}
if (!event.isPrimary || event.altKey || event.ctrlKey || event.metaKey || event.shiftKey || event.button != 0) {
return;
}
this.dragStartPos = this.orientation ? event.clientY : event.clientX;
this.dragStartValue = this.value;
this.thumbDragging = true;
this.updateStyle();
event.preventDefault();
this.startPointerCapture(this.thumb, event.pointerId);
};
private onThumbPointerMove = (event: PointerEvent) => {
if (!this.isConnected) {
return;
}
if (!event.isPrimary || !this.thumbDragging) {
return;
}
const pos = this.orientation ? event.clientY : event.clientX;
const deltaPixels = pos - this.dragStartPos;
const deltaValue = this.computeThumbMoveValue(deltaPixels);
const newValue = this.dragStartValue + deltaValue;
event.preventDefault();
if (this.setValue(newValue)) {
this.fireEvent("value");
}
};
private onPointerUp = (event: PointerEvent) => {
if (!this.isConnected) {
return;
}
if (!event.isPrimary) {
return;
}
this.resetInteractionState();
this.updateStyle();
this.stopEventRepetition();
this.stopPointerCapture();
event.preventDefault();
};
} // end class
//--- Custom Element -----------------------------------------------------------
export class PlainScrollbar extends HTMLElement {
private widget: Widget;
public constructor() {
super();
this.widget = new Widget(this);
const value = parseFloat(this.getAttribute("value"));
if (!isNaN(value))
this.widget.value = value
if (this.hasOwnProperty('value')) {
let value = this.value;
delete this.value;
if (!isNaN(value))
this.widget.value = value;
}
}
/* @Override */ public connectedCallback() {
this.widget.connectedCallback();
}
/* @Override */ public disconnectedCallback() {
this.widget.disconnectedCallback();
}
//--- Element properties ----------------------------------------------------
// Size of the thumb, relative to the trough.
// A value between 0 and 1.
// 0 is used to hide the thumb. Small values greater than 0 are overridden by `plain-scrollbar-thumb-min-size`.
public get thumbSize(): number {
return this.widget.thumbSize;
}
public set thumbSize(v: number) {
this.widget.setThumbSize(v);
}
// The current position of the scrollbar.
// A value between 0 and 1.
public get value(): number {
return this.widget.value;
}
public set value(v: number) {
this.widget.setValue(v);
}
// Orientation of the scrollbar.
// "horizontal" or "vertical".
public get orientation(): string {
return formatOrientation(this.widget.orientation);
}
public set orientation(s: string) {
if (this.widget.setOrientation(decodeOrientation(s))) {
this.setAttribute("orientation", this.orientation);
}
}
// Returns false=horizontal, true=vertical.
public get orientationBoolean(): boolean {
return this.widget.orientation;
}
//--- Element attributes ----------------------------------------------------
/* @Override */ public static get observedAttributes() {
return ["orientation"];
}
/* @Override */ public attributeChangedCallback(attrName: string, _oldValue: string | null, newValue: string | null) {
switch (attrName) {
case "orientation": {
if (newValue) {
this.widget.setOrientation(decodeOrientation(newValue));
}
break;
}
}
}
} // end class
//------------------------------------------------------------------------------
const EPS = 1E-9;
const buttonSize = "var(--plain-scrollbar-button-size, 13px)";
const buttonPath = '';
const scrollbarStyle = css`
:host {
display: block;
contain: content;
background-color: #f8f8f8;
border-style: solid;
border-width: 1px;
border-color: #dddddd;
}
#root {
touch-action: none;
user-select: none;
box-sizing: border-box;
position: relative;
width: 100%;
height: 100%;
}
#trough {
position: absolute;
}
#root.vertical #trough {
width: 100%;
top: ${buttonSize};
bottom: ${buttonSize};
}
#root.horizontal #trough {
height: 100%;
left: ${buttonSize};
right: ${buttonSize};
}
#thumb {
box-sizing: border-box;
position: absolute;
width: 100%;
height: 100%;
background-color: var(--plain-scrollbar-thumb-background-color, #f0f0f0);
border-style: solid;
border-width: var(--plain-scrollbar-thumb-border-width, 1px);
border-color: var(--plain-scrollbar-thumb-border-color, #b8b8b8);
border-radius: var(--plain-scrollbar-thumb-border-radius, 4px);
transition: background-color 50ms linear;
}
#thumb:hover {
background-color: var(--plain-scrollbar-thumb-background-color-hover, #e0e0e0);
}
#thumb.active {
background-color: var(--plain-scrollbar-thumb-background-color-active, #c0c0c0);
}
#button1,
#button2 {
box-sizing: border-box;
position: absolute;
display: block;
fill: var(--plain-scrollbar-button-color, #606060);
}
#root.vertical #button1 {
top: 0;
width: 100%;
height: ${buttonSize};
}
#root.vertical #button2 {
bottom: 0;
width: 100%;
height: ${buttonSize};
}
#root.horizontal #button1 {
left: 0;
height: 100%;
width: ${buttonSize};
}
#root.horizontal #button2 {
right: 0;
height: 100%;
width: ${buttonSize};
}
#upArrow,
#downArrow,
#leftArrow,
#rightArrow {
display: none;
width: 100%;
height: 100%;
}
#root.vertical #upArrow,
#root.vertical #downArrow {
display: block;
}
#root.horizontal #leftArrow,
#root.horizontal #rightArrow {
display: block;
}
#button1:hover,
#button2:hover {
background-color: var(--plain-scrollbar-button-color-hover, #e0e0e0);
}
#button1.active,
#button2.active {
background-color: var(--plain-scrollbar-button-color-active, #c0c0c0);
}
`;
const scrollbarHtmlTemplate = html`
`;
//------------------------------------------------------------------------------
function formatOrientation(b: boolean): string {
return b ? "vertical" : "horizontal";
}
function decodeOrientation(s: string): boolean {
switch (s) {
case "vertical": return true;
case "horizontal": return false;
default: throw new Error("Invalid orientation value \"" + s + "\".");
}
}
function percent(v: number): string {
return (v * 100).toFixed(3) + "%";
}
function decodePxValue(s: string): number | undefined {
if (!s || !s.endsWith("px")) {
return undefined;
}
return Number(s.substring(0, s.length - 2));
}
customElements.define("node-projects-plain-scrollbar", PlainScrollbar);
================================================
FILE: packages/web-component-designer/src/elements/controls/SimpleSplitView.ts
================================================
import { BaseCustomWebComponentConstructorAppend, css, html } from "@node-projects/base-custom-webcomponent";
export class SimpleSplitView extends BaseCustomWebComponentConstructorAppend {
static override readonly style = css`
:host {
display: block;
}
#split {
position: relative;
height: 100%;
width: 100%;
grid-template-rows: calc(var(--split) * 1%) 5px calc(((100 - var(--split)) * 1%) - 5px);
grid-template-columns: 100%;
display: grid;
align-items: center;
}
:host([orientation="horizontal"]) #split {
grid-template-rows: 100%;
grid-template-columns: calc(var(--split) * 1%) 5px calc(((100 - var(--split)) * 1%) - 5px);
}
#splitter {
user-select: none;
-webkit-user-select: none;
}
:host([orientation="horizontal"]) > div > #splitter {
cursor: ew-resize;
width: 5px;
height: 100%;
}
:host([orientation="vertical"]) > div > #splitter {
cursor: ns-resize;
height: 5px;
width: 100%;
}`;
static override readonly template = html`
`;
public static properties = {
orientation: String
}
private _orientation: 'vertical' | 'horizontal' = 'vertical';
public get orientation() {
return this._orientation;
}
public set orientation(value) {
this._orientation = value;
this.setAttribute('orientation', value);
}
constructor() {
super();
this._restoreCachedInititalValues();
}
ready() {
this._parseAttributesToProperties();
this.setAttribute('orientation', this.orientation);
const split = this._getDomElement("split");
const splitter = this._getDomElement("splitter");
let start: boolean = null;
splitter.addEventListener('pointerdown', (e) => {
splitter.setPointerCapture(e.pointerId);
start = true;
});
splitter.addEventListener('pointerup', (e) => {
splitter.releasePointerCapture(e.pointerId);
start = null;
});
splitter.addEventListener('pointermove', (e) => {
if (start !== null) {
let splitValue = parseFloat(split.style.getPropertyValue('--split'));
if (this.orientation === 'horizontal')
splitValue += e.movementX * 100 / split.clientWidth;
else
splitValue += e.movementY * 100 / split.clientHeight;
if (!isNaN(splitValue))
split.style.setProperty("--split", splitValue);
}
});
}
}
customElements.define('node-projects-simple-split-view', SimpleSplitView);
================================================
FILE: packages/web-component-designer/src/elements/controls/ThicknessEditor.ts
================================================
import { BaseCustomWebComponentConstructorAppend, css, html, TypedEvent } from '@node-projects/base-custom-webcomponent';
export type ThicknessEditorValueChangedEventArgs = { newValue?: string, oldValue?: string };
export class ThicknessEditor extends BaseCustomWebComponentConstructorAppend {
public static override readonly style = css`
:host {
margin: 4px;
margin-left: auto;
margin-right: auto;
}
#container {
display: grid;
grid-template-columns: minmax(30px, 40px) minmax(30px, 60px) minmax(30px, 40px);
grid-template-rows: auto;
grid-template-areas:
" . top ."
"left middle right"
" . bottom .";
column-gap: 2px;
row-gap: 2px;
}
input {
width: 20px;
text-align: center;
font-size: 10px;
height: 20px;
padding: 0;
}
#left {
grid-area: left;
justify-self: end;
}
#top {
grid-area: top;
align-self: end;
justify-self: center;
}
#right {
grid-area: right;
justify-self: start;
}
#bottom {
grid-area: bottom;
align-self: start;
justify-self: center;
}
#rect {
grid-area: middle;
border: 1px solid black;
background: lightgray;
}
`;
public static override readonly template = html`
`;
private _leftInput: HTMLInputElement;
private _topInput: HTMLInputElement;
private _rightInput: HTMLInputElement;
private _bottomInput: HTMLInputElement;
private _valueLeft: string;
public get valueLeft() {
return this._valueLeft;
}
public set valueLeft(value) {
const oldValue = this._valueLeft;
this._valueLeft = value;
if (oldValue !== value) {
this._updateValue();
this.valueLeftChanged.emit({ newValue: value, oldValue: oldValue });
}
}
public valueLeftChanged = new TypedEvent();
private _valueTop: string;
public get valueTop() {
return this._valueTop;
}
public set valueTop(value) {
const oldValue = this._valueTop;
this._valueTop = value;
if (oldValue !== value) {
this._updateValue();
this.valueTopChanged.emit({ newValue: value, oldValue: oldValue });
}
}
public valueTopChanged = new TypedEvent();
private _valueRight: string;
public get valueRight() {
return this._valueRight;
}
public set valueRight(value) {
const oldValue = this._valueRight;
this._valueRight = value;
if (oldValue !== value) {
this._updateValue();
this.valueRightChanged.emit({ newValue: value, oldValue: oldValue });
}
}
public valueRightChanged = new TypedEvent();
private _valueBottom: string;
public get valueBottom() {
return this._valueBottom;
}
public set valueBottom(value) {
const oldValue = this._valueBottom;
this._valueBottom = value;
if (oldValue !== value) {
this._updateValue();
this.valueBottomChanged.emit({ newValue: value, oldValue: oldValue });
}
}
public valueBottomChanged = new TypedEvent();
public property: string;
public unsetValue: string;
_updateValue() {
this._leftInput.value = this.valueLeft;
this._topInput.value = this.valueTop;
this._rightInput.value = this.valueRight;
this._bottomInput.value = this._valueBottom;
}
ready() {
this._parseAttributesToProperties();
this._leftInput = this._getDomElement('left');
this._topInput = this._getDomElement('top');
this._rightInput = this._getDomElement('right');
this._bottomInput = this._getDomElement('bottom');
this._leftInput.onkeyup = (e) => { if (e.key === 'Enter') this._valueLeft = this._leftInput.value };
this._topInput.onkeyup = (e) => { if (e.key === 'Enter') this._valueTop = this._topInput.value };
this._rightInput.onkeyup = (e) => { if (e.key === 'Enter') this._valueRight = this._rightInput.value };
this._bottomInput.onkeyup = (e) => { if (e.key === 'Enter') this._valueBottom = this._bottomInput.value };
this._leftInput.onblur = (e) => this._valueLeft = this._leftInput.value;
this._topInput.onblur = (e) => this._valueTop = this._topInput.value;
this._rightInput.onblur = (e) => this._valueRight = this._rightInput.value;
this._bottomInput.onblur = (e) => this._valueBottom = this._bottomInput.value;
this._updateValue();
}
}
customElements.define('node-projects-thickness-editor', ThicknessEditor);
================================================
FILE: packages/web-component-designer/src/elements/documentContainer.ts
================================================
import { BaseCustomWebComponentLazyAppend, css, cssFromString, debounce, TypedEvent } from "@node-projects/base-custom-webcomponent"
import { DesignerTabControl } from './controls/DesignerTabControl.js';
import { DesignerView } from './widgets/designerView/designerView.js';
import { ServiceContainer } from './services/ServiceContainer.js';
import { InstanceServiceContainer } from './services/InstanceServiceContainer.js';
import { ICodeView } from './widgets/codeView/ICodeView.js';
import { IStringPosition } from './services/htmlWriterService/IStringPosition.js';
import { IDemoView } from './widgets/demoView/IDemoView.js';
import { IUiCommandHandler } from '../commandHandling/IUiCommandHandler.js';
import { IUiCommand } from '../commandHandling/IUiCommand.js';
import { IDisposable } from '../interfaces/IDisposable.js';
import { ISelectionChangedEvent } from "./services/selectionService/ISelectionChangedEvent.js";
import { ISelectionRefreshEvent } from './services/selectionService/ISelectionRefreshEvent.js';
import { SimpleSplitView } from './controls/SimpleSplitView.js';
import { IStylesheet } from "./services/stylesheetService/IStylesheetService.js";
import { sleep } from "./helper/Helper.js";
import { ExtensionType } from "./widgets/designerView/extensions/ExtensionType.js";
enum tabIndex {
designer = 0,
code = 1,
split = 2,
preview = 3
}
export class DocumentContainer extends BaseCustomWebComponentLazyAppend implements IUiCommandHandler, IDisposable {
public designerView: DesignerView;
public codeView: ICodeView & HTMLElement;
public demoView: IDemoView & HTMLElement;
public additionalData: any;
private _firstLoad = true;
private _stylesheetChangedEventRegistered: boolean;
private _additionalStyle: string;
public set additionalStyleString(style: string) {
this._additionalStyle = style;
this.designerView.additionalStyles = [cssFromString(style)];
};
public get additionalStyleString() {
return this._additionalStyle;
};
private _additionalStyles: CSSStyleSheet[];
public set additionalStyles(value: CSSStyleSheet[]) {
this._additionalStyles = value;
this.designerView.additionalStyles = this._additionalStyles;
};
public get additionalStyles() {
return this._additionalStyles;
};
private _additionalStylesheets: IStylesheet[];
public set additionalStylesheets(stylesheets: IStylesheet[]) {
this._additionalStylesheets = stylesheets;
if (this.designerView.instanceServiceContainer.stylesheetService) {
this.designerView.instanceServiceContainer.stylesheetService.setStylesheets(stylesheets);
if (!this._stylesheetChangedEventRegistered) {
this._stylesheetChangedEventRegistered = true;
this.designerView.instanceServiceContainer.stylesheetService.stylesheetChanged.on(e => this.additionalStylesheetChanged.emit({ name: e.name, newStyle: e.newStyle, oldStyle: e.oldStyle, changeSource: e.changeSource }));
}
}
};
public get additionalStylesheets() {
return this._additionalStylesheets;
};
public additionalStylesheetChanged = new TypedEvent<{ name: string, newStyle: string, oldStyle: string, changeSource: 'extern' | 'styleupdate' | 'undo' }>;
get readOnly() {
return this.designerView?.readOnly;
}
set readOnly(v) {
if (this.designerView)
this.designerView.readOnly = v;
if (this.codeView)
this.codeView.readOnly = v;
}
public onContentChanged = new TypedEvent<{ source: 'designer' | 'code' }>();
public onTabChanged = new TypedEvent<{ oldTab: 'designer' | 'code' | 'split' | 'preview', newTab: 'designer' | 'code' | 'split' | 'preview' }>();
private _contentChangeSource: 'designer' | 'code' = 'designer';
private _serviceContainer: ServiceContainer;
private _content: string = '';
private _tabControl: DesignerTabControl;
private _selectionPosition: IStringPosition;
private _lastCodeSelectionKey: string;
private _splitDiv: SimpleSplitView;
private _designerDiv: HTMLDivElement;
private _codeDiv: HTMLDivElement;
private refreshInSplitViewDebounced: (...args: any) => any;
private _disableChangeNotificationDesigner: boolean;
private _disableChangeNotificationEditor: boolean;
static override get style() {
return css`
div {
height: 100%;
display: flex;
flex-direction: column;
}
node-projects-designer-view {
height: 100%;
overflow: hidden;
}
`;
}
constructor(serviceContainer: ServiceContainer, content?: string, useIframe: boolean = false) {
super();
this.refreshInSplitViewDebounced = debounce(this.refreshInSplitView, 200)
this._serviceContainer = serviceContainer;
if (content != null)
this._content = content;
let div = document.createElement("div");
this._tabControl = new DesignerTabControl();
div.appendChild(this._tabControl);
this.designerView = new DesignerView(useIframe);
this.designerView.setAttribute('exportparts', 'canvas');
this.designerView.slot = 'top';
this._designerDiv = document.createElement("div");
this._tabControl.appendChild(this._designerDiv);
this._designerDiv.appendChild(this.designerView);
this._designerDiv.dataset.title = 'Designer';
this.designerView.initialize(this._serviceContainer);
this.designerView.instanceServiceContainer.documentContainer = this;
this.designerView.instanceServiceContainer.selectionService.onSelectionChanged.on(e => this.designerSelectionChanged(e))
this.designerView.instanceServiceContainer.selectionService.onSelectionRefresh.on(e => this.designerSelectionChanged(e))
this.designerView.instanceServiceContainer.onContentChanged.on(() => this.designerContentChanged())
this.codeView = new serviceContainer.config.codeViewWidget();
this.codeView.slot = 'bottom';
this.codeView.style.position = 'relative';
this._codeDiv = document.createElement("div");
this._tabControl.appendChild(this._codeDiv);
this._codeDiv.style.position = 'relative';
this._codeDiv.appendChild(this.codeView);
this._codeDiv.dataset.title = 'Code';
this.codeView.onTextChanged.on(text => {
if (!this._disableChangeNotificationDesigner) {
if (this._tabControl.selectedIndex === tabIndex.code || this._tabControl.selectedIndex === tabIndex.split) {
this._disableChangeNotificationEditor = true;
this._content = text;
this.refreshInSplitViewDebounced();
}
}
})
this._splitDiv = new SimpleSplitView();
this._splitDiv.style.height = '100%';
this._splitDiv.dataset.title = 'Split';
this._tabControl.appendChild(this._splitDiv);
if (serviceContainer.config.demoViewWidget) {
this.demoView = new serviceContainer.config.demoViewWidget();
this.demoView.dataset.title = 'Preview';
this._tabControl.appendChild(this.demoView);
}
queueMicrotask(() => {
this.shadowRoot.appendChild(div);
this._tabControl.selectedIndex = tabIndex.designer;
});
}
async refreshInSplitView() {
try {
await this.updateDesignerHtml();
} catch (err) {
console.error(err);
}
this._disableChangeNotificationEditor = false;
}
get currentView(): 'designer' | 'split' | 'code' | 'preview' {
if (this._tabControl.selectedIndex == tabIndex.designer)
return 'designer'
if (this._tabControl.selectedIndex == tabIndex.split)
return 'split'
if (this._tabControl.selectedIndex == tabIndex.code)
return 'code'
if (this._tabControl.selectedIndex == tabIndex.preview)
return 'preview'
return null;
}
set currentView(view: 'designer' | 'split' | 'code' | 'preview') {
if (view == 'designer')
this._tabControl.selectedIndex = tabIndex.designer;
if (view == 'split')
this._tabControl.selectedIndex = tabIndex.split;
if (view == 'code')
this._tabControl.selectedIndex = tabIndex.code;
if (view == 'preview')
this._tabControl.selectedIndex = tabIndex.preview;
}
designerSelectionChanged(e: ISelectionChangedEvent | ISelectionRefreshEvent) {
if (this._tabControl.selectedIndex === tabIndex.split) {
let primarySelection = this.instanceServiceContainer.selectionService.primarySelection;
if (primarySelection) {
if (this.designerView.instanceServiceContainer.designItemDocumentPositionService) {
this._selectionPosition = this.instanceServiceContainer.selectionService.selectedPart?.textRange
?? this.designerView.instanceServiceContainer.designItemDocumentPositionService.getPosition(primarySelection);
if (this._selectionPosition)
this.setCodeViewSelection(this._selectionPosition);
this._selectionPosition = null;
}
}
}
}
designerContentChanged() {
//event wenn text geändert......
this.onContentChanged.emit({ source: this._contentChangeSource });
if (!this._disableChangeNotificationEditor) {
this._disableChangeNotificationDesigner = true;
if (this._tabControl.selectedIndex === tabIndex.code || this._tabControl.selectedIndex === tabIndex.split) {
let primarySelection = this.instanceServiceContainer.selectionService.primarySelection;
this._content = this.designerView.getDesignerHTML();
this.codeView.update(this._content, this.designerView.instanceServiceContainer);
this._lastCodeSelectionKey = null;
if (primarySelection) {
if (this.designerView.instanceServiceContainer.designItemDocumentPositionService) {
this._selectionPosition = this.instanceServiceContainer.selectionService.selectedPart?.textRange
?? this.designerView.instanceServiceContainer.designItemDocumentPositionService.getPosition(primarySelection);
if (this._selectionPosition)
this.setCodeViewSelection(this._selectionPosition);
this._selectionPosition = null;
}
}
}
this._disableChangeNotificationDesigner = false;
}
}
dispose(): void {
if (this.designerView?.instanceServiceContainer?.collaborationService) {
this.designerView.instanceServiceContainer.collaborationService.disconnect();
this.designerView.instanceServiceContainer.collaborationService.detachTransport();
}
this.codeView.dispose();
this.demoView.dispose();
}
executeCommand(command: IUiCommand) {
if (this._tabControl.selectedIndex === tabIndex.designer || this._tabControl.selectedIndex === tabIndex.split)
this.designerView.executeCommand(command);
else if (this._tabControl.selectedIndex === tabIndex.code)
this.codeView.executeCommand(command);
else if (this._tabControl.selectedIndex === tabIndex.preview)
this.demoView.executeCommand(command);
}
canExecuteCommand(command: IUiCommand) {
if (this._tabControl.selectedIndex === tabIndex.designer || this._tabControl.selectedIndex === tabIndex.split) {
if (this.designerView?.canExecuteCommand)
return this.designerView.canExecuteCommand(command);
} else if (this._tabControl.selectedIndex === tabIndex.code) {
if (this.codeView?.canExecuteCommand)
return this.codeView.canExecuteCommand(command);
} else if (this._tabControl.selectedIndex === tabIndex.preview) {
if (this.demoView?.canExecuteCommand)
return this.demoView.canExecuteCommand(command);
}
return false;
}
async setContentAsync(value: string) {
this._content = value;
if (this._tabControl) {
if (this._tabControl.selectedIndex === tabIndex.designer)
await this.updateDesignerHtml();
else if (this._tabControl.selectedIndex === tabIndex.code)
this.codeView.update(this._content, this.designerView.instanceServiceContainer);
else if (this._tabControl.selectedIndex === tabIndex.split) {
}
else if (this._tabControl.selectedIndex === tabIndex.preview)
this.demoView.display(this._serviceContainer, this.designerView.instanceServiceContainer, this._content, this.additionalStyleString);
}
}
set content(value: string) {
this.setContentAsync(value);
}
get content() {
if (this._tabControl) {
if (this._tabControl.selectedIndex === tabIndex.designer)
this._content = this.designerView.getDesignerHTML();
else if (this._tabControl.selectedIndex === tabIndex.code)
this._content = this.codeView.getText();
return this._content;
}
return null;
}
ready() {
this._tabControl.onSelectedTabChanged.on(i => {
if (i.oldIndex === tabIndex.designer) {
let primarySelection = this.instanceServiceContainer.selectionService.primarySelection;
this._content = this.designerView.getDesignerHTML();
if (this.designerView.instanceServiceContainer.designItemDocumentPositionService) {
this._selectionPosition = this.instanceServiceContainer.selectionService.selectedPart?.textRange
?? this.designerView.instanceServiceContainer.designItemDocumentPositionService.getPosition(primarySelection);
}
} else if (i.oldIndex === tabIndex.code) {
this._content = this.codeView.getText();
} else if (i.oldIndex === tabIndex.split) {
this._designerDiv.appendChild(this.designerView);
this._codeDiv.appendChild(this.codeView);
} else if (i.oldIndex === tabIndex.preview) {
if (this.demoView?.stopDisplay)
this.demoView.stopDisplay();
}
if (i.newIndex === tabIndex.designer || i.newIndex === tabIndex.split)
this.updateDesignerHtml();
if (i.newIndex === tabIndex.code || i.newIndex === tabIndex.split) {
this.codeView.update(this._content, this.designerView.instanceServiceContainer);
this._lastCodeSelectionKey = null;
if (this._selectionPosition) {
this.setCodeViewSelection(this._selectionPosition);
sleep(20).then(x => {
if (this._selectionPosition)
this.setCodeViewSelection(this._selectionPosition);
this._selectionPosition = null;
});
}
if (i.changedViaClick) {
this.codeView.focusEditor();
}
}
if (i.newIndex === tabIndex.split) {
this._splitDiv.appendChild(this.designerView);
this._splitDiv.appendChild(this.codeView);
}
if (i.newIndex === tabIndex.preview) {
this.demoView.display(this._serviceContainer, this.designerView.instanceServiceContainer, this._content, this.additionalStyleString);
}
if (this._content) {
this._firstLoad = false;
}
this.onTabChanged.emit({ oldTab: tabIndex[i.oldIndex], newTab: tabIndex[i.newIndex] });
});
if (this._content) {
this.content = this._content;
this._firstLoad = false;
}
}
private async updateDesignerHtml() {
if (this._firstLoad)
return this.designerView.parseDesignerHTML(this._content, this._firstLoad);
else {
const html = this.designerView.getDesignerHTML();
if (html != this._content) {
this._contentChangeSource = 'code';
await this.designerView.parseDesignerHTML(this._content, this._firstLoad);
this._contentChangeSource = 'designer';
return;
} else {
this.instanceServiceContainer.undoService.clearTransactionstackIfNotEmpty();
this.designerView.designerCanvas.overlayLayer.removeAllOverlays();
this.designerView.designerCanvas.extensionManager.reapplyAllAppliedExtentions(null, [ExtensionType.Permanent, ExtensionType.Selection, ExtensionType.PrimarySelection, ExtensionType.PrimarySelectionContainer, ExtensionType.OnlyOneItemSelected, ExtensionType.MultipleItemsSelected]);
}
}
}
private setCodeViewSelection(position: IStringPosition) {
if (!position)
return;
const key = `${position.start}:${position.length}`;
if (this._lastCodeSelectionKey === key)
return;
this._lastCodeSelectionKey = key;
this.codeView.setSelection(position);
}
public get instanceServiceContainer(): InstanceServiceContainer {
return this.designerView.instanceServiceContainer;
}
}
customElements.define("node-projects-document-container", DocumentContainer);
================================================
FILE: packages/web-component-designer/src/elements/helper/ArrangeHelper.ts
================================================
import { Orientation } from '../../enums/Orientation.js';
import { IDesignItem } from '../item/IDesignItem.js';
import { ChangeGroup } from '../services/undoService/ChangeGroup.js';
import { IDesignerCanvas } from '../widgets/designerView/IDesignerCanvas.js';
export abstract class ArrangeHelper {
public static arrangeElements(orientation: Orientation, designerCanvas: IDesignerCanvas, arrangeElements: IDesignItem[]) {
switch (orientation) {
case Orientation.TOP: {
const grp = this.formGroup(ArrangeDirection.TOP, designerCanvas);
const primaryCoordinates = designerCanvas.getNormalizedElementCoordinates(arrangeElements[0].element);
const targetY = primaryCoordinates.y;
for (let elem of arrangeElements) {
let selectedCoordinates = designerCanvas.getNormalizedElementCoordinates(elem.element);
if (targetY != selectedCoordinates.y) {
let parent = designerCanvas.getNormalizedElementCoordinates(elem.parent.element);
if (elem.hasStyle('bottom') && !elem.hasStyle('top')) {
let bottom = parent.y + parent.height - targetY - selectedCoordinates.height;
this.arrange(elem, 'bottom', bottom + "px");
} else {
let top = targetY - parent.y;
this.arrange(elem, 'top', top + "px");
}
}
}
grp.commit();
break;
}
case Orientation.BOTTOM: {
const grp = this.formGroup(ArrangeDirection.BOTTOM, designerCanvas);
const primaryCoordinates = designerCanvas.getNormalizedElementCoordinates(arrangeElements[0].element);
const targetBottom = primaryCoordinates.y + primaryCoordinates.height;
for (let elem of arrangeElements) {
let selectedCoordinates = designerCanvas.getNormalizedElementCoordinates(elem.element);
if (targetBottom != selectedCoordinates.y + selectedCoordinates.height) {
let parent = designerCanvas.getNormalizedElementCoordinates(elem.parent.element);
if (elem.hasStyle('bottom') && !elem.hasStyle('top')) {
let bottom = parent.y + parent.height - targetBottom;
this.arrange(elem, 'bottom', bottom + "px");
} else {
let top = targetBottom - selectedCoordinates.height - parent.y;
this.arrange(elem, 'top', top + "px");
}
}
}
grp.commit();
break;
}
case Orientation.LEFT: {
const grp = this.formGroup(ArrangeDirection.LEFT, designerCanvas);
const primaryCoordinates = designerCanvas.getNormalizedElementCoordinates(arrangeElements[0].element);
const targetX = primaryCoordinates.x;
for (let elem of arrangeElements) {
let selectedCoordinates = designerCanvas.getNormalizedElementCoordinates(elem.element);
if (targetX != selectedCoordinates.x) {
let parent = designerCanvas.getNormalizedElementCoordinates(elem.parent.element);
if (elem.hasStyle('right') && !elem.hasStyle('left')) {
let right = parent.x + parent.width - targetX - selectedCoordinates.width;
this.arrange(elem, 'right', right + "px");
} else {
let left = targetX - parent.x;
this.arrange(elem, 'left', left + "px");
}
}
}
grp.commit();
break;
}
case Orientation.RIGHT: {
const grp = this.formGroup(ArrangeDirection.RIGHT, designerCanvas);
const primaryCoordinates = designerCanvas.getNormalizedElementCoordinates(arrangeElements[0].element);
const targetRight = primaryCoordinates.x + primaryCoordinates.width;
for (let elem of arrangeElements) {
let selectedCoordinates = designerCanvas.getNormalizedElementCoordinates(elem.element);
if (targetRight != selectedCoordinates.x + selectedCoordinates.width) {
let parent = designerCanvas.getNormalizedElementCoordinates(elem.parent.element);
if (elem.hasStyle('right') && !elem.hasStyle('left')) {
let right = parent.x + parent.width - targetRight;
this.arrange(elem, 'right', right + "px");
} else {
let left = targetRight - selectedCoordinates.width - parent.x;
this.arrange(elem, 'left', left + "px");
}
}
}
grp.commit();
break;
}
case Orientation.VERTICAL_CENTER: {
const grp = this.formGroup(ArrangeDirection.VERTICAL_CENTER, designerCanvas);
const primaryCoordinates = designerCanvas.getNormalizedElementCoordinates(arrangeElements[0].element);
const targetCenterY = primaryCoordinates.y + primaryCoordinates.height / 2;
for (let elem of arrangeElements) {
let selectedCoordinates = designerCanvas.getNormalizedElementCoordinates(elem.element);
if (targetCenterY != selectedCoordinates.y + selectedCoordinates.height / 2) {
let parent = designerCanvas.getNormalizedElementCoordinates(elem.parent.element);
if (elem.hasStyle('bottom') && !elem.hasStyle('top')) {
let bottom = parent.y + parent.height - targetCenterY - selectedCoordinates.height / 2;
this.arrange(elem, 'bottom', bottom + "px");
} else {
let top = targetCenterY - selectedCoordinates.height / 2 - parent.y;
this.arrange(elem, 'top', top + "px");
}
}
}
grp.commit();
break;
}
case Orientation.HORIZONTAL_CENTER: {
const grp = this.formGroup(ArrangeDirection.HORIZONTAL_CENTER, designerCanvas);
const primaryCoordinates = designerCanvas.getNormalizedElementCoordinates(arrangeElements[0].element);
const targetCenterX = primaryCoordinates.x + primaryCoordinates.width / 2;
for (let elem of arrangeElements) {
let selectedCoordinates = designerCanvas.getNormalizedElementCoordinates(elem.element);
if (targetCenterX != selectedCoordinates.x + selectedCoordinates.width / 2) {
let parent = designerCanvas.getNormalizedElementCoordinates(elem.parent.element);
if (elem.hasStyle('right') && !elem.hasStyle('left')) {
let right = parent.x + parent.width - targetCenterX - selectedCoordinates.width / 2;
this.arrange(elem, 'right', right + "px");
} else {
let left = targetCenterX - selectedCoordinates.width / 2 - parent.x;
this.arrange(elem, 'left', left + "px");
}
}
}
grp.commit();
break;
}
}
}
private static arrange(element: IDesignItem, attribut: string, value: string) {
element.setStyle(attribut, value);
}
private static formGroup(name: string, designerCanvas: IDesignerCanvas): ChangeGroup {
return designerCanvas.instanceServiceContainer.selectionService.primarySelection.openGroup(name);
}
}
enum ArrangeDirection {
TOP = 'arrangeTop',
RIGHT = 'arrangeRight',
BOTTOM = 'arrangeBottom',
LEFT = 'arrangeLeft',
HORIZONTAL_CENTER = 'arrangeHorizontalCenter',
VERTICAL_CENTER = 'arrangeVerticalCenter',
}
================================================
FILE: packages/web-component-designer/src/elements/helper/Browser.ts
================================================
export const isFirefox = navigator.userAgent.toLowerCase().includes('firefox');
================================================
FILE: packages/web-component-designer/src/elements/helper/ClipboardHelper.ts
================================================
export async function copyTextToClipboard(text) {
copyToClipboard([['text/plain', text]]);
}
//used, so you could copy internal if you have no clipboard access
let internalClipboard = null;
export async function copyToClipboard(items: [format: string, data: string][]) {
if (navigator.clipboard) {
try {
let data = [];
for (let n of items) {
data.push(new ClipboardItem({ [n[0]]: new Blob([n[1]], { type: n[0] }) }));
}
await navigator.clipboard.write(data);
} catch (err) {
await navigator.clipboard.writeText(items[0][1]);
internalClipboard = items[0][1];
}
console.info('Copy to clipboard successful');
} else {
let activeElement: HTMLElement = document.activeElement;
while (activeElement?.shadowRoot?.activeElement)
activeElement = activeElement.shadowRoot.activeElement;
internalClipboard = items[0][1];
const textArea = document.createElement('textarea');
textArea.style.position = 'fixed';
textArea.style.top = '0';
textArea.style.left = '0';
textArea.style.width = '2em';
textArea.style.height = '2em';
textArea.style.padding = '0';
textArea.style.border = 'none';
textArea.style.outline = 'none';
textArea.style.boxShadow = 'none';
textArea.style.background = 'transparent';
textArea.value = items[0][1];
document.body.appendChild(textArea);
textArea.select();
try {
document.execCommand('copy');
} catch (err) {
try {
document.execCommand('copy');
} catch (err) {
console.error(err);
}
}
document.body.removeChild(textArea);
activeElement.focus()
}
}
export async function getTextFromClipboard(): Promise {
if (navigator.clipboard) {
return new Promise(async (resolve, reject) => {
const clipText = await navigator.clipboard.readText();
resolve(clipText);
});
} else {
return new Promise(async (resolve, reject) => {
const textArea = document.createElement('textarea');
textArea.style.position = 'fixed';
textArea.style.top = '0';
textArea.style.left = '0';
textArea.style.width = '2em';
textArea.style.height = '2em';
textArea.style.padding = '0';
textArea.style.border = 'none';
textArea.style.outline = 'none';
textArea.style.boxShadow = 'none';
textArea.style.background = 'transparent';
document.body.appendChild(textArea);
textArea.focus();
textArea.select();
document.execCommand('paste');
let value = textArea.value;
if (!value)
value = internalClipboard;
document.body.removeChild(textArea);
resolve(value);
});
}
}
export async function getFromClipboard() {
if (navigator.clipboard) {
return await navigator.clipboard.read();
} else {
return null;
}
}
================================================
FILE: packages/web-component-designer/src/elements/helper/CssAttributeParser.ts
================================================
enum Token {
Name,
Value,
InQuote
}
export class CssEntry {
constructor(name: string, value: string, important: boolean) {
this.name = name.trim();
this.value = value.trim();
this.important = important;
}
name: string;
value: string;
important: boolean;
}
export class CssAttributeParser {
entries: CssEntry[] = [];
public parse(text: string, quoteType: string = '\'') {
this.entries = [];
let name = '';
let value = '';
let token = Token.Name;
for (let n = 0; n < text.length; n++) {
let c = text[n];
if (token === Token.Name) {
if (c === ':')
token = Token.Value;
else if (c === ';') {
name = '';
} else
name += c;
} else if (token === Token.Value) {
if (c === ';') {
const entry = this.createEntry(name, value);
this.entries.push(entry);
name = '';
value = '';
token = Token.Name;
} else {
if (c === quoteType) {
token = Token.InQuote;
}
value += c;
}
} else if (token === Token.InQuote) {
if (c === '\\') {
value += c;
n++;
c = text[n];
value += c;
} else if (c === quoteType) {
value += c;
token = Token.Value;
} else {
value += c;
}
}
}
if (name.trim() !== '') {
this.entries.push(this.createEntry(name, value));
}
}
private createEntry(name: string, value: string) {
const match = value.match(/\s*!\s*important\s*$/i);
if (!match)
return new CssEntry(name, value, false);
return new CssEntry(name, value.substring(0, match.index), true);
}
}
================================================
FILE: packages/web-component-designer/src/elements/helper/CssCombiner.ts
================================================
export class CssCombiner {
private static _helperElement = document.createElement('div');
static combine(styles: Map, globalStyles?: Map) {
CssCombiner.applyStylesToHelper(styles);
CssCombiner.combineBorder(styles);
CssCombiner.combineMargin(styles);
CssCombiner.combinePadding(styles);
CssCombiner.combineInset(styles);
CssCombiner.combineBackground(styles);
CssCombiner.combineFont(styles);
styles = CssCombiner.combineBrowserSupportedShorthands(styles);
if (globalStyles) {
for (let g of globalStyles) {
if (styles.has(g[0])) {
if (styles.get(g[0]) === g[1])
styles.delete(g[0]);
}
}
}
return styles;
}
private static applyStylesToHelper(styles: Map) {
let e = CssCombiner._helperElement;
e.setAttribute('style', '');
for (let s of styles) {
if (s[0].startsWith('--') || s[0].includes('-'))
e.style.setProperty(s[0], s[1]);
else
(e.style)[s[0]] = s[1];
}
return e;
}
private static combineBrowserSupportedShorthands(styles: Map) {
const originalNormalizedValues = CssCombiner.getNormalizedStyleValues(styles);
let combinedStyles = new Map(styles);
while (true) {
const candidateStyles = CssCombiner.parseStyleDeclarationList(CssCombiner.applyStylesToHelper(combinedStyles).style.cssText);
let bestCandidate: Map | null = null;
let bestSavings = 0;
for (let candidate of candidateStyles) {
const coverage = CssCombiner.getCandidateCoverage(combinedStyles, originalNormalizedValues, candidate[0], candidate[1]);
if (coverage.length === 0)
continue;
let tentativeStyles = new Map(combinedStyles);
for (let coveredStyle of coverage)
tentativeStyles.delete(coveredStyle);
tentativeStyles.set(candidate[0], candidate[1]);
const savings = CssCombiner.getSerializedSize(combinedStyles) - CssCombiner.getSerializedSize(tentativeStyles);
if (savings <= 0)
continue;
if (!CssCombiner.matchesNormalizedStyleValues(tentativeStyles, originalNormalizedValues))
continue;
if (savings > bestSavings) {
bestSavings = savings;
bestCandidate = tentativeStyles;
}
}
if (!bestCandidate)
return combinedStyles;
combinedStyles = bestCandidate;
}
}
private static getNormalizedStyleValues(styles: Map) {
const element = CssCombiner.applyStylesToHelper(styles);
const normalizedValues = new Map();
for (let style of styles)
normalizedValues.set(style[0], CssCombiner.readStyleValue(element.style, style[0]));
return normalizedValues;
}
private static getCandidateCoverage(styles: Map, originalNormalizedValues: Map, candidateName: string, candidateValue: string) {
const element = CssCombiner.applyStylesToHelper(new Map([[candidateName, candidateValue]]));
const coverage: string[] = [];
for (let style of styles) {
const originalValue = originalNormalizedValues.get(style[0]);
if (!originalValue)
continue;
if (CssCombiner.readStyleValue(element.style, style[0]) === originalValue)
coverage.push(style[0]);
}
return coverage;
}
private static matchesNormalizedStyleValues(styles: Map, originalNormalizedValues: Map) {
const element = CssCombiner.applyStylesToHelper(styles);
for (let normalizedValue of originalNormalizedValues) {
if (CssCombiner.readStyleValue(element.style, normalizedValue[0]) !== normalizedValue[1])
return false;
}
return true;
}
private static readStyleValue(style: CSSStyleDeclaration, name: string) {
if (name.startsWith('--') || name.includes('-'))
return style.getPropertyValue(name).trim();
return String((style)[name] ?? '').trim();
}
private static getSerializedSize(styles: Map) {
let size = 0;
for (let style of styles)
size += style[0].length + style[1].length + 2;
return size;
}
private static parseStyleDeclarationList(cssText: string) {
let styles = new Map();
let start = 0;
let quote: string | null = null;
let parenthesisDepth = 0;
for (let i = 0; i < cssText.length; i++) {
const c = cssText[i];
if (quote) {
if (c === quote && cssText[i - 1] !== '\\')
quote = null;
continue;
}
if (c === '"' || c === '\'') {
quote = c;
continue;
}
if (c === '(') {
parenthesisDepth++;
continue;
}
if (c === ')' && parenthesisDepth > 0) {
parenthesisDepth--;
continue;
}
if (c === ';' && parenthesisDepth === 0) {
CssCombiner.addStyleDeclaration(styles, cssText.substring(start, i));
start = i + 1;
}
}
CssCombiner.addStyleDeclaration(styles, cssText.substring(start));
return styles;
}
private static addStyleDeclaration(styles: Map, declaration: string) {
const trimmedDeclaration = declaration.trim();
if (!trimmedDeclaration)
return;
let quote: string | null = null;
let parenthesisDepth = 0;
for (let i = 0; i < trimmedDeclaration.length; i++) {
const c = trimmedDeclaration[i];
if (quote) {
if (c === quote && trimmedDeclaration[i - 1] !== '\\')
quote = null;
continue;
}
if (c === '"' || c === '\'') {
quote = c;
continue;
}
if (c === '(') {
parenthesisDepth++;
continue;
}
if (c === ')' && parenthesisDepth > 0) {
parenthesisDepth--;
continue;
}
if (c === ':' && parenthesisDepth === 0) {
const name = trimmedDeclaration.substring(0, i).trim();
const value = trimmedDeclaration.substring(i + 1).trim();
if (name)
styles.set(name, value);
return;
}
}
}
private static combineBorder(styles: Map) {
if (!CssCombiner.checkIfStyleIsCombinable(styles, 'border-left-style')) return;
if (!CssCombiner.checkIfStyleIsCombinable(styles, 'border-right-style')) return;
if (!CssCombiner.checkIfStyleIsCombinable(styles, 'border-top-style')) return;
if (!CssCombiner.checkIfStyleIsCombinable(styles, 'border-bottom-style')) return;
if (!CssCombiner.checkIfStyleIsCombinable(styles, 'border-left-color')) return;
if (!CssCombiner.checkIfStyleIsCombinable(styles, 'border-right-color')) return;
if (!CssCombiner.checkIfStyleIsCombinable(styles, 'border-top-color')) return;
if (!CssCombiner.checkIfStyleIsCombinable(styles, 'border-bottom-color')) return;
if (!CssCombiner.checkIfStyleIsCombinable(styles, 'border-left-width')) return;
if (!CssCombiner.checkIfStyleIsCombinable(styles, 'border-right-width')) return;
if (!CssCombiner.checkIfStyleIsCombinable(styles, 'border-top-width')) return;
if (!CssCombiner.checkIfStyleIsCombinable(styles, 'border-bottom-width')) return;
if (!CssCombiner.checkIfStyleIsCombinable(styles, 'border-width')) return;
if (!CssCombiner.checkIfStyleIsCombinable(styles, 'border-style')) return;
if (!CssCombiner.checkIfStyleIsCombinable(styles, 'border-color')) return;
if (!CssCombiner.checkIfStyleIsCombinable(styles, 'border-top')) return;
if (!CssCombiner.checkIfStyleIsCombinable(styles, 'border-right')) return;
if (!CssCombiner.checkIfStyleIsCombinable(styles, 'border-left')) return;
if (!CssCombiner.checkIfStyleIsCombinable(styles, 'border-bottom')) return;
if (!CssCombiner.checkIfStyleIsCombinable(styles, 'border-width')) return;
let e = CssCombiner._helperElement;
let bls = e.style.borderLeftStyle;
let blc = e.style.borderLeftColor;
if (bls && blc &&
e.style.borderRightStyle === bls && e.style.borderTopStyle === bls && e.style.borderBottomStyle === bls &&
e.style.borderRightColor === blc && e.style.borderTopColor === blc && e.style.borderBottomColor === blc) {
let btw = e.style.borderTopWidth;
let brw = e.style.borderRightWidth;
let bbw = e.style.borderBottomWidth;
let blw = e.style.borderLeftWidth;
styles.delete('border-left-style');
styles.delete('border-right-style');
styles.delete('border-top-style');
styles.delete('border-bottom-style');
styles.delete('border-left-color');
styles.delete('border-right-color');
styles.delete('border-top-color');
styles.delete('border-bottom-color');
styles.delete('border-left-width');
styles.delete('border-right-width');
styles.delete('border-top-width');
styles.delete('border-bottom-width');
styles.delete('border-width');
styles.delete('border-style');
styles.delete('border-color');
styles.delete('border-top');
styles.delete('border-right');
styles.delete('border-left');
styles.delete('border-bottom');
if (e.style.borderRightWidth == blw && e.style.borderTopWidth === blw && e.style.borderBottomWidth === blw) {
styles.set('border', blw + ' ' + bls + ' ' + blc);
} else {
styles.set('border', bls + ' ' + blc);
if (btw === bbw && brw === blw) {
styles.set('border-width', btw + ' ' + brw);
} else {
styles.set('border-width', btw + ' ' + brw + ' ' + bbw + ' ' + blw);
}
}
}
if (e.style.borderImageSource === 'initial')
styles.delete('border-image-source');
if (e.style.borderImageSlice === 'initial')
styles.delete('border-image-slice');
if (e.style.borderImageWidth === 'initial')
styles.delete('border-image-width');
if (e.style.borderImageOutset === 'initial')
styles.delete('border-image-outset');
if (e.style.borderImageRepeat === 'initial')
styles.delete('border-image-repeat');
}
private static combineMargin(styles: Map) {
if (!CssCombiner.checkIfStyleIsCombinable(styles, 'margin-top')) return;
if (!CssCombiner.checkIfStyleIsCombinable(styles, 'margin-right')) return;
if (!CssCombiner.checkIfStyleIsCombinable(styles, 'margin-bottom')) return;
if (!CssCombiner.checkIfStyleIsCombinable(styles, 'margin-left')) return;
if (!CssCombiner.checkIfStyleIsCombinable(styles, 'margin')) return;
let e = CssCombiner._helperElement;
if (e.style.marginTop && e.style.marginRight && e.style.marginBottom && e.style.marginLeft) {
styles.delete('margin-top');
styles.delete('margin-right');
styles.delete('margin-bottom');
styles.delete('margin-left');
if (e.style.marginTop == e.style.marginRight && e.style.marginTop == e.style.marginBottom && e.style.marginTop == e.style.marginLeft) {
styles.set('margin', e.style.marginTop);
} else
styles.set('margin', e.style.marginTop + ' ' + e.style.marginRight + ' ' + e.style.marginBottom + ' ' + e.style.marginLeft);
}
}
private static combinePadding(styles: Map) {
if (!CssCombiner.checkIfStyleIsCombinable(styles, 'padding-top')) return;
if (!CssCombiner.checkIfStyleIsCombinable(styles, 'padding-right')) return;
if (!CssCombiner.checkIfStyleIsCombinable(styles, 'padding-bottom')) return;
if (!CssCombiner.checkIfStyleIsCombinable(styles, 'padding-left')) return;
if (!CssCombiner.checkIfStyleIsCombinable(styles, 'padding')) return;
let e = CssCombiner._helperElement;
if (e.style.paddingTop && e.style.paddingRight && e.style.paddingBottom && e.style.paddingLeft) {
styles.delete('padding-top');
styles.delete('padding-right');
styles.delete('padding-bottom');
styles.delete('padding-left');
if (e.style.paddingTop == e.style.paddingRight && e.style.paddingTop == e.style.paddingBottom && e.style.paddingTop == e.style.paddingLeft) {
styles.set('padding', e.style.paddingTop);
} else
styles.set('padding', e.style.paddingTop + ' ' + e.style.paddingRight + ' ' + e.style.paddingBottom + ' ' + e.style.paddingLeft);
}
}
private static combineInset(styles: Map) {
if (!CssCombiner.checkIfStyleIsCombinable(styles, 'top')) return;
if (!CssCombiner.checkIfStyleIsCombinable(styles, 'right')) return;
if (!CssCombiner.checkIfStyleIsCombinable(styles, 'bottom')) return;
if (!CssCombiner.checkIfStyleIsCombinable(styles, 'left')) return;
if (!CssCombiner.checkIfStyleIsCombinable(styles, 'inset')) return;
let e = CssCombiner._helperElement;
if (e.style.top && e.style.right && e.style.bottom && e.style.left) {
styles.delete('top');
styles.delete('right');
styles.delete('bottom');
styles.delete('left');
styles.set('inset', e.style.top + ' ' + e.style.right + ' ' + e.style.bottom + ' ' + e.style.left);
}
}
private static combineBackground(styles: Map) {
if (!CssCombiner.checkIfStyleIsCombinable(styles, 'background-image')) return;
if (!CssCombiner.checkIfStyleIsCombinable(styles, 'background-position')) return;
if (!CssCombiner.checkIfStyleIsCombinable(styles, 'background-position-x')) return;
if (!CssCombiner.checkIfStyleIsCombinable(styles, 'background-position-y')) return;
if (!CssCombiner.checkIfStyleIsCombinable(styles, 'background-size')) return;
if (!CssCombiner.checkIfStyleIsCombinable(styles, 'background-repeat')) return;
if (!CssCombiner.checkIfStyleIsCombinable(styles, 'background-repeat-x')) return;
if (!CssCombiner.checkIfStyleIsCombinable(styles, 'background-repeat-y')) return;
if (!CssCombiner.checkIfStyleIsCombinable(styles, 'background-attachment')) return;
if (!CssCombiner.checkIfStyleIsCombinable(styles, 'background-origin')) return;
if (!CssCombiner.checkIfStyleIsCombinable(styles, 'background-clip')) return;
if (!CssCombiner.checkIfStyleIsCombinable(styles, 'background-color')) return;
if (!CssCombiner.checkIfStyleIsCombinable(styles, 'background')) return;
let e = CssCombiner._helperElement;
styles.delete('background-image');
styles.delete('background-position');
styles.delete('background-position-x'); //TODO
styles.delete('background-position-y'); //TODO
styles.delete('background-size');
styles.delete('background-repeat');
styles.delete('background-repeat-x'); //TODO
styles.delete('background-repeat-y'); //TODO
styles.delete('background-attachment');
styles.delete('background-origin');
styles.delete('background-clip');
styles.delete('background-color');
styles.delete('background');
let background = '';
if (e.style.backgroundImage && e.style.backgroundImage !== 'initial')
background += (background === '' ? '' : ' ') + e.style.backgroundImage;
if (e.style.backgroundPosition && e.style.backgroundPosition !== 'initial')
background += (background === '' ? '' : ' ') + e.style.backgroundPosition;
if (e.style.backgroundSize && e.style.backgroundSize !== 'initial')
background += (background === '' ? '' : ' / ') + e.style.backgroundSize;
if (e.style.backgroundRepeat && e.style.backgroundRepeat !== 'initial')
background += (background === '' ? '' : ' ') + e.style.backgroundRepeat;
if (e.style.backgroundAttachment && e.style.backgroundAttachment !== 'initial')
background += (background === '' ? '' : ' ') + e.style.backgroundAttachment;
if (e.style.backgroundOrigin && e.style.backgroundOrigin !== 'initial')
background += (background === '' ? '' : ' ') + e.style.backgroundOrigin;
if (e.style.backgroundClip && e.style.backgroundClip !== 'initial')
background += (background === '' ? '' : ' ') + e.style.backgroundClip;
if (e.style.backgroundColor && e.style.backgroundColor !== 'initial')
background += (background === '' ? '' : ' ') + e.style.backgroundColor;
if (background)
styles.set('background', background);
}
private static combineFont(styles: Map) {
if (!CssCombiner.checkIfStyleIsCombinable(styles, 'font-style')) return;
if (!CssCombiner.checkIfStyleIsCombinable(styles, 'font-weight')) return;
if (!CssCombiner.checkIfStyleIsCombinable(styles, 'font-size')) return;
if (!CssCombiner.checkIfStyleIsCombinable(styles, 'line-height')) return;
if (!CssCombiner.checkIfStyleIsCombinable(styles, 'font-family')) return;
if (!CssCombiner.checkIfStyleIsCombinable(styles, 'font')) return;
let e = CssCombiner._helperElement;
if (e.style.fontFamily) {
styles.delete('font-style');
styles.delete('font-weight');
styles.delete('font-size');
styles.delete('line-height');
styles.delete('font-family');
styles.delete('font');
let font = '';
if (e.style.fontStyle)
font += (font === '' ? '' : ' ') + e.style.fontStyle;
if (e.style.fontWeight)
font += (font === '' ? '' : ' ') + e.style.fontWeight;
if (e.style.fontSize)
font += (font === '' ? '' : ' ') + e.style.fontSize;
if (e.style.lineHeight)
font += '/' + e.style.lineHeight;
if (e.style.fontFamily)
font += (font === '' ? '' : ' ') + e.style.fontFamily;
styles.set('font', font);
}
}
private static checkIfStyleIsCombinable(styles: Map, name: string) {
if (styles.has(name)) {
const st = styles.get(name);
if (typeof st == 'string') {
if (st.startsWith('var('))
return false;
return true;
}
return false;
}
return true;
}
}
================================================
FILE: packages/web-component-designer/src/elements/helper/CssImportant.ts
================================================
export function splitCssImportant(value: string): { value: string, important: boolean } {
const match = value.match(/\s*!\s*important\s*$/i);
if (!match)
return { value, important: false };
return { value: value.substring(0, match.index).trimEnd(), important: true };
}
export function appendCssImportant(value: string, important: boolean) {
if (!important)
return value;
return value + ' !important';
}
================================================
FILE: packages/web-component-designer/src/elements/helper/CssUnitConverter.ts
================================================
//unsupported: ex, ch, svw, svh, vw, lvh, dvw, dvh, vi, ic, ric
const units = ['px', 'cm', 'mm', 'q', 'in', 'pc', 'pt', 'rem', 'em', 'vw', 'vh', 'vmin', 'vmax', 'lh', 'rlh', '%', 'ms', 's', 'deg', 'rad', 'grad', 'turn', 'cqw', 'cqh', 'cqi', 'cqb', 'cqmin', 'cqmax', 'fr'];
const pattern = new RegExp(`^([\-\+]?(?:\\d+(?:\\.\\d+)?))(${units.join('|')})$`, 'i');
export function convertCssUnitToPixel(cssValue: string, target: HTMLElement, percentTarget: 'width' | 'height'): number {
if (!cssValue)
return null;
const supportedUnits = {
// Absolute sizes
'px': value => value,
'cm': value => value * 38,
'mm': value => value * 3.8,
'q': value => value * 0.95,
'in': value => value * 96,
'pc': value => value * 16,
'pt': value => value * 1.333333,
// Relative sizes
'rem': value => value * parseFloat(getComputedStyle(document.documentElement).fontSize),
'em': value => value * parseFloat(getComputedStyle(target).fontSize),
'vw': value => value / 100 * window.innerWidth,
'vh': value => value / 100 * window.innerHeight,
'vmin': value => value / 100 * (window.innerHeight < window.innerWidth ? window.innerHeight : window.innerWidth),
'vmax': value => value / 100 * (window.innerHeight > window.innerWidth ? window.innerHeight : window.innerWidth),
'lh': value => value * parseFloat(getComputedStyle(target).lineHeight),
'rlh': value => value * parseFloat(getComputedStyle(document.documentElement).lineHeight),
'%': value => value / 100 * (percentTarget == 'height' ? getOriginalSizeBeforeTransformation(target).height : getOriginalSizeBeforeTransformation(target).width),
/*TODO: container units
//find parent with computed style where container-type is inline-size or size (regarding to query type)
//use this size for calculation
'cqw':
'cqh':
'cqi':
'cqb':
'cqmin':
'cqmax':
*/
// Times
'ms': value => value,
's': value => value * 1000,
// Angles
'deg': value => value,
'rad': value => value * (180 / Math.PI),
'grad': value => value * (180 / 200),
'turn': value => value * 360
};
// If is a match, return example: [ "-2.75rem", "-2.75", "rem" ]
const matches = cssValue.trim().match(pattern);
if (matches) {
const value = Number(matches[1]);
const unit = matches[2].toLowerCase();
// Sanity check, make sure unit conversion function exists
if (unit in supportedUnits) {
return supportedUnits[unit](value);
}
}
//@ts-ignore
return cssValue;
}
export function getCssUnit(cssValue: string) {
const matches = cssValue.trim().match(pattern);
if (matches)
return matches[2].toLowerCase();
return null;
}
export function convertCssUnit(cssValue: string | number, target: HTMLElement, percentTarget: 'width' | 'height', unit: string, roundFunc?: (val: number) => string): string {
if (!cssValue)
return null;
const supportedUnits = {
// Absolute sizes
'px': value => value,
'cm': value => value / 38,
'mm': value => value / 3.8,
'q': value => value / 0.95,
'in': value => value / 96,
'pc': value => value / 16,
'pt': value => value / 1.333333,
// Relative sizes
'rem': value => value / parseFloat(getComputedStyle(document.documentElement).fontSize),
'em': value => value / parseFloat(getComputedStyle(target).fontSize),
'vw': value => value * 100 / window.innerWidth,
'vh': value => value * 100 / window.innerHeight,
'vmin': value => value * 100 / (window.innerHeight < window.innerWidth ? window.innerHeight : window.innerWidth),
'vmax': value => value * 100 / (window.innerHeight > window.innerWidth ? window.innerHeight : window.innerWidth),
'lh': value => value / parseFloat(getComputedStyle(target).lineHeight),
'rlh': value => value / parseFloat(getComputedStyle(document.documentElement).lineHeight),
'%': value => value * 100 / (percentTarget == 'height' ? getOriginalSizeBeforeTransformation(target).height : getOriginalSizeBeforeTransformation(target).width),
// Times
'ms': value => value,
's': value => value / 1000,
// Angles
'deg': value => value,
'rad': value => value / (180 / Math.PI),
'grad': value => value / (180 / 200),
'turn': value => value / 360
};
if (typeof cssValue == 'string')
cssValue = convertCssUnitToPixel(cssValue, target, percentTarget);
if (unit in supportedUnits) {
const val = supportedUnits[unit](cssValue);
if (roundFunc)
return roundFunc(val) + unit
return val + unit;
}
if (roundFunc)
return roundFunc(cssValue);
return cssValue;
}
function getOriginalSizeBeforeTransformation(element: HTMLElement): { width: number, height: number } {
return { width: element.offsetWidth, height: element.offsetHeight };
}
export function splitCssGridColumnSizes(sizes: String) {
const parts = sizes.split(' ');
const ret: string[] = [];
for (let i = 0; i < parts.length; i++) {
let p = parts[i];
if (p.startsWith('repeat(')) {
while (!p.includes(")")) {
i++;
if (!parts[i])
continue;
p += parts[i];
}
}
ret.push(p);
}
return ret;
}
export function getExpandedCssGridColumnSizes(sizes: String) {
const parts = splitCssGridColumnSizes(sizes);
const ret: string[] = [];
for (let p of parts) {
if (p.startsWith('repeat(')) {
const prt = p.split(',');
for (let i = 0; i < parseInt(prt[0].substring(7)); i++)
ret.push(prt[1].substring(0, prt[1].length - 1));
} else
ret.push(p);
}
return ret.map(x => getCssUnit(x));
}
================================================
FILE: packages/web-component-designer/src/elements/helper/DesignerStylesheetPatcher.ts
================================================
export interface IDesignerStylesheetPatchAttributes {
forceHoverAttributeName: string;
forceActiveAttributeName: string;
forceVisitedAttributeName: string;
forceFocusAttributeName: string;
forceFocusWithinAttributeName: string;
forceFocusVisibleAttributeName: string;
}
export function patchStylesheetSelectorForDesigner(text: string, attributes: IDesignerStylesheetPatchAttributes) {
return text
.replaceAll(/:root\b/g, ':host')
.replaceAll(':focus-within', '[' + attributes.forceFocusWithinAttributeName + ']')
.replaceAll(':focus-visible', '[' + attributes.forceFocusVisibleAttributeName + ']')
.replaceAll(':hover', '[' + attributes.forceHoverAttributeName + ']')
.replaceAll(':active', '[' + attributes.forceActiveAttributeName + ']')
.replaceAll(':visited', '[' + attributes.forceVisitedAttributeName + ']')
.replaceAll(':focus', '[' + attributes.forceFocusAttributeName + ']');
}
================================================
FILE: packages/web-component-designer/src/elements/helper/ElementHelper.ts
================================================
import { IPoint } from '../../interfaces/IPoint.js';
import { IRect } from '../../interfaces/IRect.js';
import { IDesignItem } from '../item/IDesignItem.js';
import { NodeType } from '../item/NodeType.js';
import { IDesignerCanvas } from '../widgets/designerView/IDesignerCanvas.js';
export function inDesigner(element: Element): boolean {
let node = element.getRootNode();
if ((node)?.host?.localName == "node-projects-designer-canvas")
return true;
return false;
}
export function newElementFromString(text, document: Document): Element {
const range = document.createRange();
//@ts-ignore
const fragment = range.createContextualFragment(text, { includeShadowRoots: true });
return fragment.firstChild as Element;
}
export enum ElementDisplayType {
none,
inline,
block,
}
export function instanceOf(node: any, fnc: T): node is T {
if (node instanceof fnc || node instanceof (node.ownerDocument.defaultView ?? window)[fnc.name])
return true;
return false;
}
export function instanceOfAny(node: Node, ...fnc: Function[]) {
for (const f of fnc)
if (node instanceof f || node instanceof (node.ownerDocument.defaultView ?? window)[f.name])
return true;
return false;
}
export function isInline(element: HTMLElement): boolean {
if (element == null)
return false;
if (instanceOfAny(element, SVGElement, HTMLHtmlElement, HTMLHeadElement, HTMLBodyElement, HTMLSelectElement, HTMLOptionElement))
return false;
return (element.ownerDocument.defaultView ?? window).getComputedStyle(element).display.startsWith('inline');
}
export function isInlineAfter(element: HTMLElement): boolean {
if (element == null)
return false;
if (instanceOfAny(element, SVGElement, HTMLHtmlElement, HTMLHeadElement, HTMLBodyElement, HTMLSelectElement, HTMLOptionElement))
return false;
return (element.ownerDocument.defaultView ?? window).getComputedStyle(element).display.startsWith('inline');
}
export function getElementDisplaytype(element: HTMLElement): ElementDisplayType {
if (instanceOfAny(element, SVGElement, HTMLHtmlElement, HTMLHeadElement, HTMLBodyElement))
return ElementDisplayType.block;
if (instanceOf(element, MathMLElement))
return ElementDisplayType.block;
const display = (element.ownerDocument.defaultView ?? window).getComputedStyle(element).display;
return display == 'none' ? ElementDisplayType.none : display.startsWith('inline') ? ElementDisplayType.inline : ElementDisplayType.block;
}
export function isEmptyTextNode(node: Node): boolean {
return node.textContent.trim() == '' && node.textContent.indexOf('\xa0' /* */) < 0;
}
export function getActiveElement(): Element {
let activeElement = document.activeElement;
let lastActive = null;
while (activeElement != lastActive) {
lastActive = activeElement;
if (activeElement.shadowRoot != null && activeElement.shadowRoot.activeElement)
activeElement = activeElement.shadowRoot.activeElement;
}
return activeElement;
}
export function getElementOffsetsInContainer(element: Element) {
if (instanceOf(element, HTMLElement)) {
//@ts-ignore
return { x: element.offsetLeft, y: element.offsetTop };
} else {
//const cs = (element.ownerDocument.defaultView ?? window).getComputedStyle(element);
//todo: this will not work correctly with transformed SVGs or MathML Elements
const r1 = getBoundingClientRectAlsoForDisplayContents(element);
const r2 = getBoundingClientRectAlsoForDisplayContents(element.parentElement);
return { x: r1.x - r2.x, y: r1.y - r2.y }
}
}
export function getBoundingClientRectAlsoForDisplayContents(element: Element): DOMRect {
let r = element.getBoundingClientRect();
if (r.width == 0 && r.height == 0) {
const cs = (element.ownerDocument.defaultView ?? window).getComputedStyle(element);
if (cs.display == 'contents') {
if (element.shadowRoot) {
for (let c of element.shadowRoot.children) {
const rc = getBoundingClientRectAlsoForDisplayContents(c);
r = new DOMRect(
Math.min(r.x, rc.x),
Math.min(r.y, rc.y),
Math.max(r.width, rc.width),
Math.max(r.height, rc.height)
);
}
} else {
for (let c of element.children) {
const rc = getBoundingClientRectAlsoForDisplayContents(c);
r = new DOMRect(
Math.min(r.x, rc.x),
Math.min(r.y, rc.y),
Math.max(r.width, rc.width),
Math.max(r.height, rc.height)
);
}
}
}
}
return r;
}
export function getElementZoomFactor(element: Element): number {
const zoom = (element.ownerDocument.defaultView ?? window).getComputedStyle(element).zoom;
if (!zoom || zoom === 'normal')
return 1;
if (zoom.endsWith('%')) {
const percentage = parseFloat(zoom);
return Number.isFinite(percentage) && percentage > 0 ? percentage / 100 : 1;
}
const value = parseFloat(zoom);
return Number.isFinite(value) && value > 0 ? value : 1;
}
export function getContentBoxContentOffsets(element): IPoint {
let xOffset = parseInt(getComputedStyle(element).paddingLeft.replace('px', ''))
+ parseInt(getComputedStyle(element).marginLeft.replace('px', ''))
+ parseInt(getComputedStyle(element).borderLeft.replace('px', ''))
+ parseInt(getComputedStyle(element).paddingRight.replace('px', ''))
+ parseInt(getComputedStyle(element).marginRight.replace('px', ''))
+ parseInt(getComputedStyle(element).borderRight.replace('px', ''));
let yOffset = parseInt(getComputedStyle(element).paddingTop.replace('px', ''))
+ parseInt(getComputedStyle(element).marginTop.replace('px', ''))
+ parseInt(getComputedStyle(element).borderTop.replace('px', ''))
+ parseInt(getComputedStyle(element).paddingBottom.replace('px', ''))
+ parseInt(getComputedStyle(element).marginBottom.replace('px', ''))
+ parseInt(getComputedStyle(element).borderBottom.replace('px', ''));
return { x: xOffset, y: yOffset };
}
export function calculateOuterRect(designItems: IDesignItem[], designerCanvas: IDesignerCanvas): IRect {
let min: IPoint = { x: Number.MAX_VALUE, y: Number.MAX_VALUE };
let max: IPoint = { x: Number.MIN_VALUE, y: Number.MIN_VALUE };
let elementRect: IRect;
for (let s of designItems) {
if (s.nodeType == NodeType.TextNode || s.nodeType == NodeType.Comment)
continue;
elementRect = {
x: designerCanvas.getNormalizedElementCoordinates(s.element).x,
y: designerCanvas.getNormalizedElementCoordinates(s.element).y,
width: designerCanvas.getNormalizedElementCoordinates(s.element).width,
height: designerCanvas.getNormalizedElementCoordinates(s.element).height
}
// calculate min and max of selection
if (elementRect.x < min.x)
min.x = elementRect.x;
if (elementRect.y < min.y)
min.y = elementRect.y;
if (elementRect.x + elementRect.width > max.x)
max.x = elementRect.x + elementRect.width;
if (elementRect.y + elementRect.height > max.y)
max.y = elementRect.y + elementRect.height;
}
// calculate reckt around selection
return {
x: min.x,
y: min.y,
width: max.x - min.x,
height: max.y - min.y
}
}
================================================
FILE: packages/web-component-designer/src/elements/helper/GridHelper.ts
================================================
import { IPoint } from "../../interfaces/IPoint.js";
import { IDesignItem } from "../item/IDesignItem.js";
import { getElementSize } from "./getBoxQuads.js";
export interface IGridCellInformation {
x: number;
y: number;
width: number;
height: number;
name: string;
localX: number;
localY: number;
}
export interface IGridGapInformation {
x: number;
y: number;
width: number;
height: number;
localX: number;
localY: number;
column?: number;
row?: number;
type: 'h' | 'v';
}
export interface IGridInformation {
cells: IGridCellInformation[][];
gaps: IGridGapInformation[];
xGap: number;
yGap: number;
}
export interface IGridCellHitResult {
row: number;
column: number;
cell: IGridCellInformation;
localPoint: IPoint;
}
export function getElementGridInformation(element: HTMLElement) {
let cs = getComputedStyle(element);
let rowSpan = 1;
let colSpan = 1;
if (cs.gridRowEnd == 'auto')
rowSpan = 1
else if (cs.gridRowEnd.startsWith('span'))
rowSpan = parseInt(cs.gridRowEnd.substring(4));
else
rowSpan = parseInt(cs.gridRowEnd) - parseInt(cs.gridRowStart);
if (cs.gridColumnEnd == 'auto')
colSpan = 1
else if (cs.gridColumnEnd.startsWith('span'))
colSpan = parseInt(cs.gridColumnEnd.substring(4));
else
colSpan = parseInt(cs.gridColumnEnd) - parseInt(cs.gridColumnStart);
return { colSpan, rowSpan };
}
export function getGridLocalPoint(designItem: IDesignItem, point: IPoint): IPoint {
const designerCanvas = designItem.instanceServiceContainer.designerCanvas;
const localPoint = designItem.element.convertPointFromNode(new DOMPoint(point.x, point.y), designerCanvas.canvas, { iframes: designerCanvas.iframes });
return { x: localPoint.x, y: localPoint.y };
}
export function getElementLocalToCanvasMatrix(designItem: IDesignItem): DOMMatrix {
const designerCanvas = designItem.instanceServiceContainer.designerCanvas;
const origin = designerCanvas.canvas.convertPointFromNode(new DOMPoint(0, 0), designItem.element, { iframes: designerCanvas.iframes });
const xAxis = designerCanvas.canvas.convertPointFromNode(new DOMPoint(1, 0), designItem.element, { iframes: designerCanvas.iframes });
const yAxis = designerCanvas.canvas.convertPointFromNode(new DOMPoint(0, 1), designItem.element, { iframes: designerCanvas.iframes });
return new DOMMatrix([
xAxis.x - origin.x,
xAxis.y - origin.y,
yAxis.x - origin.x,
yAxis.y - origin.y,
origin.x,
origin.y,
]);
}
export function getGridColumnIndexFromLocalX(gridInformation: IGridInformation, localX: number): number {
const columns = gridInformation.cells[0];
if (!columns?.length || !Number.isFinite(localX))
return 0;
let column = 0;
for (let i = 0; i < columns.length; i++) {
const cell = columns[i];
if (localX > cell.localX + cell.width / 2) {
column = i;
}
}
return column;
}
export function getGridColumnStartLineFromLocalX(gridInformation: IGridInformation, localX: number): number {
const columns = gridInformation.cells[0];
if (!columns?.length || !Number.isFinite(localX))
return 1;
let line = 1;
for (let i = 0; i < columns.length; i++) {
const cell = columns[i];
if (localX > cell.localX + cell.width / 2) {
line = i + 2;
}
}
return line;
}
export function getGridRowIndexFromLocalY(gridInformation: IGridInformation, localY: number): number {
if (!gridInformation.cells.length || !Number.isFinite(localY))
return 0;
let row = 0;
for (let i = 0; i < gridInformation.cells.length; i++) {
const cell = gridInformation.cells[i][0];
if (localY > cell.localY + cell.height / 2) {
row = i;
}
}
return row;
}
export function getGridRowStartLineFromLocalY(gridInformation: IGridInformation, localY: number): number {
if (!gridInformation.cells.length || !Number.isFinite(localY))
return 1;
let line = 1;
for (let i = 0; i < gridInformation.cells.length; i++) {
const cell = gridInformation.cells[i][0];
if (localY > cell.localY + cell.height / 2) {
line = i + 2;
}
}
return line;
}
export function getGridCellFromPoint(designItem: IDesignItem, point: IPoint, gridInformation: IGridInformation = calculateGridInformation(designItem)): IGridCellHitResult {
const localPoint = getGridLocalPoint(designItem, point);
if (!Number.isFinite(localPoint.x) || !Number.isFinite(localPoint.y))
return null;
for (let row = 0; row < gridInformation.cells.length; row++) {
for (let column = 0; column < gridInformation.cells[row].length; column++) {
const cell = gridInformation.cells[row][column];
if (localPoint.x >= cell.localX && localPoint.x <= cell.localX + cell.width && localPoint.y >= cell.localY && localPoint.y <= cell.localY + cell.height) {
return { row, column, cell, localPoint };
}
}
}
return null;
}
export function calculateGridInformation(designItem: IDesignItem): IGridInformation {
//TODO: same name should combine columns/rows
const designerCanvas = designItem.instanceServiceContainer.designerCanvas;
const transformedCornerPoints: DOMQuad = designItem.element.getBoxQuads({ relativeTo: designerCanvas.canvas, iframes: designerCanvas.iframes })[0];
const itemSize = getElementSize(designItem.element);
const computedStyle = getComputedStyle(designItem.element);
const rows = computedStyle.gridTemplateRows.split(' ');
const columns = computedStyle.gridTemplateColumns.split(' ');
const paddingLeft = Number.parseFloat(computedStyle.paddingLeft);
const paddingTop = Number.parseFloat(computedStyle.paddingTop);
const borderLeft = Number.parseFloat(computedStyle.borderLeftWidth);
const borderTop = Number.parseFloat(computedStyle.borderTopWidth);
let y = 0;
let xGap = 0;
let yGap = 0;
let rw = 0;
let xOffset = transformedCornerPoints.p1.x + borderLeft;
let yOffset = transformedCornerPoints.p1.y + borderTop;
let localXOffset = borderLeft;
let localYOffset = borderTop;
let gridA: string[] = null;
if (computedStyle.gridTemplateAreas && computedStyle.gridTemplateAreas !== 'none')
gridA = computedStyle.gridTemplateAreas.split('\"');
if (computedStyle.columnGap && computedStyle.columnGap != 'normal')
xGap = Number.parseFloat(computedStyle.columnGap.replace('px', ''));
if (computedStyle.rowGap && computedStyle.rowGap != 'normal')
yGap = Number.parseFloat(computedStyle.rowGap.replace('px', ''));
let gesX = 0;
let gesY = 0;
for (let c of columns) {
const currX = Number.parseFloat(c.replace('px', ''));
gesX += currX + xGap;
}
gesX -= xGap;
for (let r of rows) {
const currY = Number.parseFloat(r.replace('px', ''));
gesY += currY + yGap;
}
gesY -= yGap;
if (computedStyle.justifyContent == 'center') {
const diff = (itemSize.width - gesX) / 2;
xOffset += diff;
localXOffset += diff;
} else if (computedStyle.justifyContent == 'end') {
const diff = itemSize.width - gesX;
xOffset += diff;
localXOffset += diff;
} else if (computedStyle.justifyContent == 'space-between') {
xGap += (itemSize.width - gesX) / (columns.length - 1);
} else if (computedStyle.justifyContent == 'space-around') {
let gp = (itemSize.width - gesX) / (columns.length * 2);
xGap += gp * 2;
xOffset += gp;
localXOffset += gp;
} else if (computedStyle.justifyContent == 'space-evenly') {
let gp = (itemSize.width - gesX) / (columns.length + 1);
xGap += gp;
xOffset += gp;
localXOffset += gp;
}
if (computedStyle.alignContent == 'center') {
const diff = (itemSize.height - gesY) / 2;
yOffset += diff;
localYOffset += diff;
} else if (computedStyle.alignContent == 'end') {
const diff = itemSize.height - gesY;
yOffset += diff;
localYOffset += diff;
} else if (computedStyle.alignContent == 'space-between') {
yGap += (itemSize.height - gesY) / (rows.length - 1);
} else if (computedStyle.alignContent == 'space-around') {
let gp = (itemSize.height - gesY) / (rows.length * 2);
yGap += gp * 2;
yOffset += gp;
localYOffset += gp;
} else if (computedStyle.alignContent == 'space-evenly') {
let gp = (itemSize.height - gesY) / (rows.length + 1);
yGap += gp;
yOffset += gp;
localYOffset += gp;
}
const retVal: IGridInformation = { cells: [], gaps: [], xGap, yGap };
for (let rowIdx = 0; rowIdx < rows.length; rowIdx++) {
const r = rows[rowIdx];
let areas: string[] = null;
if (gridA && gridA[rw + 1]) {
areas = gridA[rw + 1].split(' ');
}
let x = 0;
let cl = 0;
const currY = Number.parseFloat(r.replace('px', ''));
let cellList: IGridCellInformation[] = [];
retVal.cells.push(cellList);
for (let colIdx = 0; colIdx < columns.length; colIdx++) {
const c = columns[colIdx];
if (colIdx > 0) {
retVal.gaps.push({ x: x + xOffset + paddingLeft, y: y + yOffset + paddingTop, width: xGap, height: currY, localX: x + localXOffset + paddingLeft, localY: y + localYOffset + paddingTop, column: colIdx, row: rowIdx, type: 'v' });
x += xGap
}
const currX = Number.parseFloat(c.replace('px', ''));
if (rowIdx > 0) {
retVal.gaps.push({ x: x + xOffset + paddingLeft, y: y + yOffset - yGap + paddingTop, width: currX, height: yGap, localX: x + localXOffset + paddingLeft, localY: y + localYOffset - yGap + paddingTop, column: colIdx, row: rowIdx, type: 'h' });
}
let name = null;
if (areas && areas[cl]) {
const nm = areas[cl].trim();
if (nm != '.') {
name = nm;
}
}
const cell = { x: x + xOffset + paddingLeft, y: y + yOffset + paddingTop, width: currX, height: currY, name: name, localX: x + localXOffset + paddingLeft, localY: y + localYOffset + paddingTop };
cellList.push(cell);
x += currX;
cl++;
}
y += currY + yGap;
rw += 2;
}
return retVal;
}
================================================
FILE: packages/web-component-designer/src/elements/helper/Helper.ts
================================================
import { IPoint } from "../../interfaces/IPoint.js";
import { IRect } from "../../interfaces/IRect.js";
export function htmlAsString(strings: TemplateStringsArray, ...values: any[]) {
return strings.reduce((result, str, i) => {
return result + str + (values[i] ?? '');
}, '');
}
export function isAppleDevice() {
return window.navigator.platform?.startsWith("Mac") || window.navigator.platform === "iPhone" || window.navigator.platform === "iPad" || window.navigator.platform === "iPod";
}
export function sleep(ms): Promise {
return new Promise(resolve => setTimeout(resolve, ms));
}
export async function exportData(blob: Blob, fileName: string): Promise {
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.style.display = 'none';
a.download = fileName;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
await sleep(300);
}
export function dataURItoBlob(dataURI) {
var mime = dataURI.split(',')[0].split(':')[1].split(';')[0];
var binary = atob(dataURI.split(',')[1]);
var array = [];
for (var i = 0; i < binary.length; i++) {
array.push(binary.charCodeAt(i));
}
return new Blob([new Uint8Array(array)], { type: mime });
}
export function pointInRect(point: IPoint, rect: IRect) {
return point.x >= rect.x && point.x <= rect.x + rect.width && point.y >= rect.y && point.y <= rect.y + rect.height;
}
export function removeTrailing(text: string, char: string) {
if (text.endsWith(char ?? '/'))
return text.substring(0, text.length - 1);
return text;
}
export function removeLeading(text: string, char: string) {
if (text.startsWith(char ?? '/'))
return text.substring(1);
return text;
}
export function requestAnimationFramePromise() {
return new Promise(resolve => requestAnimationFrame(resolve));
}
export function arraysEqual(a: T[], b: T[]) {
if (a === b) return true;
if (a == null || b == null) return false;
if (a.length !== b.length) return false;
for (var i = 0; i < a.length; ++i) {
if (a[i] !== b[i]) return false;
}
return true;
}
let nullObject: {};
export function deepValue(obj, path: string, returnNullObject = false, splitter = '.') {
if (path === undefined || path === null) {
return obj;
}
const pathParts = path.split(splitter);
for (let i = 0; i < pathParts.length; i++) {
if (obj != null) {
obj = obj[pathParts[i]];
} else {
return returnNullObject ? nullObject : null;
}
}
return obj;
}
export function setDeepValue(obj, path: string, value, splitter = '.') {
if (path === undefined || path === null) {
return;
}
const pathParts = path.split(splitter);
for (let i = 0; i < pathParts.length - 1; i++) {
if (obj != null) {
let newObj = obj[pathParts[i]];
if (newObj == null) {
newObj = {};
obj[pathParts[i]] = newObj;
}
obj = newObj;
}
}
if (obj != null)
obj[pathParts[pathParts.length - 1]] = value;
}
================================================
FILE: packages/web-component-designer/src/elements/helper/ITextWriter.ts
================================================
export interface ITextWriter {
get position(): number;
isLastCharNewline(): boolean;
levelRaise(): void
levelShrink(): void
write(text: string): void
writeLine(text: string): void
writeIndent(): void
writeNewline(): void
getString(): string
}
================================================
FILE: packages/web-component-designer/src/elements/helper/IndentedTextWriter.ts
================================================
import { ITextWriter } from './ITextWriter.js';
export class IndentedTextWriter implements ITextWriter {
private _textHolder: string = ''
public readonly indent: number = 4;
public level: number = 0;
public get position(): number {
return this._textHolder.length;
}
public isLastCharNewline() {
return this._textHolder[this._textHolder.length - 1] === '\n';
}
public levelRaise() {
this.level++;
}
public levelShrink() {
this.level--;
}
public write(text: string) {
this._textHolder += text;
}
public writeLine(text: string) {
this.writeIndent();
this._textHolder += text;
this.writeNewline();
}
public writeIndent() {
this._textHolder += ''.padEnd(this.level * this.indent, ' ');
}
public writeNewline() {
this._textHolder += '\n';
}
public getString() {
return this._textHolder;
}
}
================================================
FILE: packages/web-component-designer/src/elements/helper/KeyboardHelper.ts
================================================
import { isAppleDevice } from "./Helper.js";
export function hasCommandKey(event: KeyboardEvent | MouseEvent | PointerEvent | DragEvent | WheelEvent) {
if (isAppleDevice())
return event.metaKey;
return event.ctrlKey;
}
================================================
FILE: packages/web-component-designer/src/elements/helper/LayoutHelper.ts
================================================
//TODO: this function should return the correct property to change a layout,
// for example left/right when left or right is used,
//maybe margin on grid? or transform??
import { IPoint } from "../../interfaces/IPoint.js";
import { IDesignItem } from "../item/IDesignItem.js";
import { getBoundingClientRectAlsoForDisplayContents } from "./ElementHelper.js";
import { getGeometryReader } from "../widgets/designerView/extensions/svg/geometry/GeometryReaderFactory.js";
import { applyGeometryWritesToDesignItem } from '../widgets/designerView/extensions/svg/geometry/GeometryWriteHelper.js';
/**
* This function filters a items list, so only the outer elments are used for example in a move
*/
export function filterChildPlaceItems(items: IDesignItem[]) {
const filterdPlaceItems: IDesignItem[] = [];
next:
for (let i of items) {
let par = i.parent;
while (par != null && !par.isRootItem) {
if (items.indexOf(par) >= 0)
continue next;
par = par.parent;
}
filterdPlaceItems.push(i);
}
return filterdPlaceItems;
}
export function getDesignItemCurrentPos(designItem: IDesignItem, mode: 'position' | 'transform' | 'margin' | 'padding'): IPoint {
if (mode === 'position') {
const computedStyleMovedElement = getComputedStyle(designItem.element);
let oldLeft: number | null = parseFloat(computedStyleMovedElement.left);
oldLeft = Number.isNaN(oldLeft) ? null : oldLeft;
let oldTop: number | null = parseFloat(computedStyleMovedElement.top);
oldTop = Number.isNaN(oldTop) ? null : oldTop;
return { x: oldLeft ?? 0, y: oldTop ?? 0 }
}
return { x: 0, y: 0 }
}
export function placeDesignItem(container: IDesignItem, designItem: IDesignItem, offset: IPoint, mode: 'position' | 'transform' | 'margin' | 'padding') {
const movedElement = designItem.element;
const computedStyleMovedElement = getComputedStyle(movedElement);
if (mode === 'position') {
if (_placeSvgDesignItem(designItem, offset)) {
return;
}
let positionedContainerElement: Element | null = container.element;
let computedStylePositionedContainer = container.getComputedStyle();
if (computedStylePositionedContainer.position !== 'relative' && computedStylePositionedContainer.position !== 'absolute' && positionedContainerElement && (positionedContainerElement).offsetParent) {
positionedContainerElement = (positionedContainerElement).offsetParent;
if (positionedContainerElement)
computedStylePositionedContainer = container.window.getComputedStyle(positionedContainerElement);
}
let oldLeft = null;
let oldRight = null;
let oldTop = null;
let oldBottom = null;
let containerLeft = 0;
let containerRight = 0;
let containerTop = 0;
let containerBottom = 0;
let hasPositionedLayout = false;
if (computedStyleMovedElement.position === 'relative' || computedStyleMovedElement.position === 'absolute') {
oldLeft = parseFloat((movedElement).style.left);
oldLeft = Number.isNaN(oldLeft) ? null : oldLeft;
oldTop = parseFloat((movedElement).style.top);
oldTop = Number.isNaN(oldTop) ? null : oldTop;
oldRight = parseFloat((movedElement).style.right);
oldRight = Number.isNaN(oldRight) ? null : oldRight;
oldBottom = parseFloat((movedElement).style.bottom);
oldBottom = Number.isNaN(oldBottom) ? null : oldBottom;
hasPositionedLayout = true;
} else {
if (positionedContainerElement && positionedContainerElement !== container.element) {
let posContainerRect = getBoundingClientRectAlsoForDisplayContents(positionedContainerElement);
let elementRect = getBoundingClientRectAlsoForDisplayContents(designItem.element);
containerLeft = elementRect.left - posContainerRect.left;
containerRight = elementRect.right - posContainerRect.right;
containerTop = elementRect.top - posContainerRect.top;
containerBottom = elementRect.bottom - posContainerRect.bottom;
}
}
if (!hasPositionedLayout)
designItem.setStyle('position', 'absolute');
if (oldLeft || oldRight == null)
designItem.setStyle('left', roundValue(designItem, offset.x + (oldLeft ?? 0) + containerLeft) + "px");
if (oldTop || oldBottom == null)
designItem.setStyle('top', roundValue(designItem, offset.y + (oldTop ?? 0) + containerTop) + "px");
if (oldRight != null)
designItem.setStyle('right', roundValue(designItem, (oldRight ?? 0) - offset.x + containerRight) + "px");
if (oldBottom != null)
designItem.setStyle('bottom', roundValue(designItem, (oldBottom ?? 0) - offset.y + containerBottom) + "px");
}
}
export function transformOffsetByInverseLinearMatrix(offset: IPoint, matrix: Pick): IPoint {
const determinant = matrix.a * matrix.d - matrix.b * matrix.c;
if (Math.abs(determinant) < 1e-10) {
return offset;
}
return {
x: (matrix.d * offset.x - matrix.c * offset.y) / determinant,
y: (-matrix.b * offset.x + matrix.a * offset.y) / determinant,
};
}
function _placeSvgDesignItem(designItem: IDesignItem, offset: IPoint): boolean {
const element = designItem.element;
if (!(element instanceof SVGGraphicsElement) || element instanceof SVGSVGElement) {
return false;
}
const reader = getGeometryReader(element);
if (!reader) {
return false;
}
const geometry = reader.read(element);
for (const segment of geometry.segments) {
if (segment.point) {
segment.point.x += offset.x;
segment.point.y += offset.y;
}
if (segment.cp1) {
segment.cp1.x += offset.x;
segment.cp1.y += offset.y;
}
if (segment.cp2) {
segment.cp2.x += offset.x;
segment.cp2.y += offset.y;
}
}
const attrs = reader.serialize(geometry);
if (!attrs.length) {
return false;
}
const group = designItem.openGroup('place svg geometry');
applyGeometryWritesToDesignItem(designItem, attrs);
group.commit();
return true;
}
export function roundValue(designItem: IDesignItem, value: number) {
if (designItem.serviceContainer.options.roundPixelsToDecimalPlaces >= 0) {
return value.toFixed(designItem.serviceContainer.options.roundPixelsToDecimalPlaces);
}
return value.toString();
}
/*function placeViaPosition(container: IDesignItem, designItem: IDesignItem, offset: IPoint, mode: 'position' | 'transform' | 'margin' | 'padding') {
}*/
================================================
FILE: packages/web-component-designer/src/elements/helper/NpmPackageHacks.json
================================================
{
"@shoelace-style/shoelace": {
"html": "\n"
},
"@microsoft/fast-components": {
"script": "let res = await import('@microsoft/fast-components');\nres.provideFASTDesignSystem().register(res.allComponents);"
},
"@zooplus/zoo-web-components": {
"script": "let res = await import('@zooplus/zoo-web-components');\nres.registerComponents(res);",
"style":":root {\n--primary-mid: #3C9700;\n--primary-light: #66B100;\n--primary-dark: #286400;\n--primary-ultralight: #EBF4E5;\n--secondary-mid: #FF6200;\n--secondary-light: #F80;\n--secondary-dark: #CC4E00;\n--info-ultralight: #ECF5FA;\n--info-mid: #459FD0;\n--warning-ultralight: #FDE8E9;\n--warning-mid: #ED1C24;\n}"
},
"@material/web": {
"import": "@material/web/all.js"
},
"@stencil/core": {
"map": {
"@stencil/core/internal/client": "internal/client/index.js",
"@stencil/core/internal/app-data": "internal/app-data/index.js"
}
}
}
================================================
FILE: packages/web-component-designer/src/elements/helper/NpmPackageLoader.ts
================================================
import { IDesignerAddonJson } from "../services/designerAddons/IDesignerAddonJson.js";
import { IElementsJson } from "../services/elementsService/IElementsJson.js";
import { PreDefinedElementsService } from "../services/elementsService/PreDefinedElementsService.js";
import { WebcomponentManifestElementsService } from "../services/elementsService/WebcomponentManifestElementsService.js";
import { WebcomponentManifestPropertiesService } from "../services/propertiesService/services/WebcomponentManifestPropertiesService.js";
import { ServiceContainer } from "../services/ServiceContainer.js";
import { removeLeading, removeTrailing } from "./Helper.js";
import { ObservedCustomElementsRegistry } from "./ObservedCustomElementsRegistry.js";
export class NpmPackageLoader {
private static registryPatchedTohandleErrors: boolean;
private static packageHacks;
//packageSource = '//unpkg.com/';
private _packageSource: string;
private _dependecies = new Map();
constructor(packageSource: string = '//cdn.jsdelivr.net/npm/') {
this._packageSource = packageSource;
NpmPackageLoader.patchCustomElementsRegistryToHandleErrors();
}
static patchCustomElementsRegistryToHandleErrors() {
if (!NpmPackageLoader.registryPatchedTohandleErrors) {
NpmPackageLoader.registryPatchedTohandleErrors = true;
let customElementsRegistry = window.customElements;
const registry: any = {};
registry.define = function (name, constructor, options) {
try {
customElementsRegistry.define(name, constructor, options);
}
catch (err) {
console.warn(err);
}
}
registry.get = function (name) {
return customElementsRegistry.get(name);
}
registry.upgrade = function (node) {
return customElementsRegistry.upgrade(node);
}
registry.whenDefined = function (name) {
return customElementsRegistry.whenDefined(name);
}
Object.defineProperty(window, "customElements", {
get() {
return registry
}
});
}
}
//TODO: remove paletteTree form params. elements should be added to serviceconatiner, and the container should notify
async loadNpmPackage(pkg: string, serviceContainer?: ServiceContainer, paletteTree?: any, loadAllImports?: boolean, reportState?: (state: string) => void): Promise<{ html: string, style: string }> {
if (!NpmPackageLoader.packageHacks) {
NpmPackageLoader.packageHacks = (await import("./NpmPackageHacks.json", { assert: { type: 'json' } })).default;
}
const baseUrl = window.location.protocol + this._packageSource + pkg + '/';
const packageJsonUrl = baseUrl + 'package.json';
if (reportState)
reportState(pkg + ": loading package.json");
const packageJson = await fetch(packageJsonUrl);
const packageJsonObj = await packageJson.json();
this.addToImportmap(baseUrl, packageJsonObj);
const depPromises: Promise[] = []
if (packageJsonObj.dependencies) {
for (let d in packageJsonObj.dependencies) {
depPromises.push(this.loadDependency(d, packageJsonObj.dependencies[d]));
}
}
await Promise.all(depPromises)
let customElementsUrl = baseUrl + 'custom-elements.json';
let elementsRootPath = baseUrl;
if (packageJsonObj.customElements) {
customElementsUrl = baseUrl + removeTrailing(packageJsonObj.customElements, '/');
if (customElementsUrl.includes('/')) {
let idx = customElementsUrl.lastIndexOf('/');
elementsRootPath = customElementsUrl.substring(0, idx + 1);
}
}
let webComponentDesignerUrl = baseUrl + 'web-component-designer.json';
if (packageJsonObj.webComponentDesigner) {
webComponentDesignerUrl = baseUrl + removeLeading(packageJsonObj.webComponentDesigner, '/');
}
if (reportState)
reportState(pkg + ": loading custom-elements.json");
let customElementsJson = await fetch(customElementsUrl);
if (!customElementsJson.ok && packageJsonObj.homepage) {
try {
const url = new URL(packageJsonObj.homepage);
const newurl = 'https://raw.githubusercontent.com/' + url.pathname + '/master/custom-elements.json';
customElementsJson = await fetch(newurl);
console.warn("custom-elements.json was missing from npm package, but was loaded from github as a fallback.")
}
catch (err) {
console.warn("github custom elments json fallback", err);
}
}
if (serviceContainer) {
fetch(webComponentDesignerUrl).then(async x => {
if (x.ok) {
const webComponentDesignerJson = await x.json();
if (webComponentDesignerJson.services) {
for (let o in webComponentDesignerJson.services) {
for (let s of webComponentDesignerJson.services[o]) {
if (s.startsWith('./'))
s = s.substring(2);
//@ts-ignore
const classDefinition = (await importShim(baseUrl + s)).default;
//@ts-ignore
serviceContainer.register(o, new classDefinition());
}
}
}
}
});
}
if (customElementsJson.ok) {
const customElementsJsonObj = await customElementsJson.json();
let elements = new WebcomponentManifestElementsService(packageJsonObj.name, elementsRootPath, customElementsJsonObj);
if (serviceContainer)
serviceContainer.register('elementsService', elements);
if (serviceContainer) {
let properties = new WebcomponentManifestPropertiesService(packageJsonObj.name, customElementsJsonObj);
serviceContainer.register('propertyService', properties);
}
if (loadAllImports) {
for (let e of await elements.getElements()) {
//@ts-ignore
importShim(e.import);
}
}
if (serviceContainer && paletteTree) {
//TODO: should be retriggered by service container, or changeing list in container
paletteTree.loadControls(serviceContainer, serviceContainer.elementsServices);
}
/* Package Hacks */
if (NpmPackageLoader.packageHacks[pkg]?.import) {
import(NpmPackageLoader.packageHacks[pkg]?.import);
}
if (NpmPackageLoader.packageHacks[pkg]?.script) {
const scriptUrl = URL.createObjectURL(new Blob([NpmPackageLoader.packageHacks[pkg]?.script], { type: 'application/javascript' }));
import(scriptUrl);
}
} else {
console.warn('npm package: ' + pkg + ' - no custom-elements.json found, only loading javascript module');
const observedCustomElementsRegistry = new ObservedCustomElementsRegistry();
if (packageJsonObj.module) {
//@ts-ignore
await importShim(baseUrl + removeLeading(packageJsonObj.module, '/'))
} else if (packageJsonObj.main) {
//@ts-ignore
await importShim(baseUrl + removeLeading(packageJsonObj.main, '/'))
} else if (packageJsonObj.unpkg) {
//@ts-ignore
await importShim(baseUrl + removeLeading(packageJsonObj.unpkg, '/'))
} else {
console.warn('npm package: ' + pkg + ' - no entry point in package found.');
}
/* Package Hacks */
if (NpmPackageLoader.packageHacks[pkg]?.import) {
await import(NpmPackageLoader.packageHacks[pkg]?.import);
}
if (NpmPackageLoader.packageHacks[pkg]?.script) {
const scriptUrl = URL.createObjectURL(new Blob([NpmPackageLoader.packageHacks[pkg]?.script], { type: 'application/javascript' }));
await import(scriptUrl);
}
const newElements = observedCustomElementsRegistry.getNewElements();
if (newElements.length > 0 && serviceContainer && paletteTree) {
const elementsCfg: IElementsJson = {
elements: newElements
}
let elService = new PreDefinedElementsService(pkg, elementsCfg)
serviceContainer.register('elementsService', elService);
paletteTree.loadControls(serviceContainer, serviceContainer.elementsServices);
}
observedCustomElementsRegistry.dispose();
}
if (reportState)
reportState(pkg + ": done");
let retVal: any = {};
if (NpmPackageLoader.packageHacks[pkg]?.html) {
retVal.html = (NpmPackageLoader.packageHacks[pkg]?.html).replaceAll("${baseUrl}", baseUrl);
}
if (NpmPackageLoader.packageHacks[pkg]?.style) {
retVal.style = (NpmPackageLoader.packageHacks[pkg]?.style).replaceAll("${baseUrl}", baseUrl);
}
return retVal;
}
async loadDependency(dependency: string, version?: string, reportState?: (state: string) => void) {
if (this._dependecies.has(dependency))
return;
this._dependecies.set(dependency, true);
if (dependency.startsWith('@types')) {
console.warn('ignoring wrong dependency: ', dependency);
return;
}
if (reportState)
reportState(dependency + ": loading dependency: " + dependency);
const baseUrl = window.location.protocol + this._packageSource + dependency + '/';
const packageJsonUrl = baseUrl + 'package.json';
const packageJson = await fetch(packageJsonUrl);
const packageJsonObj = await packageJson.json();
const depPromises: Promise[] = []
if (packageJsonObj.dependencies) {
for (let d in packageJsonObj.dependencies) {
depPromises.push(this.loadDependency(d, packageJsonObj.dependencies[d]));
}
}
await Promise.all(depPromises)
this.addToImportmap(baseUrl, packageJsonObj);
}
async addToImportmap(baseUrl: string, packageJsonObj: { name?: string, module?: string, main?: string, unpkg?: string, exports?: Record }) {
//@ts-ignore
const map = importShim.getImportMap().imports;
const importMap = { imports: {}, scopes: {} };
if (!map.hasOwnProperty(packageJsonObj.name)) {
//TODO: use exports of package.json for importMap
if (packageJsonObj.exports) {
/* "exports": {
".": {
"browser": "./index.browser.js",
"default": "./index.js"
},
"./async": {
"browser": "./async/index.browser.js",
"default": "./async/index.js"
},
"./non-secure": "./non-secure/index.js",
"./package.json": "./package.json"
}
"exports": {
"node": {
"import": "./feature-node.mjs",
"require": "./feature-node.cjs"
},
"default": "./feature.mjs"
}
"exports": {
".": "./index.js",
"./feature.js": {
"node": "./feature-node.js",
"default": "./feature.js"
}
}
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": {
"browser": {
"development": "./dist/composed-offset-position.browser.mjs",
"default": "./dist/composed-offset-position.browser.min.mjs"
},
"default": "./dist/composed-offset-position.mjs"
},
"module": "./dist/composed-offset-position.esm.js",
"default": "./dist/composed-offset-position.umd.js"
},
"./package.json": "./package.json"
}
*/
/*
"exports": {
"import": "./index-module.js",
"require": "./index-require.cjs"
},
*/
let getImport = (obj: any) => {
if (obj?.browser)
return obj.browser;
if (obj?.import)
return obj.import;
if (obj?.module)
return obj.module;
if (obj?.default)
return obj.default;
return obj?.node;
}
/*
for support of this:
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": {
"browser": {
"development": "./dist/composed-offset-position.browser.mjs",
"default": "./dist/composed-offset-position.browser.min.mjs"
},
*/
let getImportFlat = (obj: any) => {
let i = getImport(obj);
if (!(typeof i == 'string'))
i = getImport(i);
if (!(typeof i == 'string'))
i = getImport(i);
if (!(typeof i == 'string'))
i = null;
return i;
}
//Names to use: browser, import, default, node
let imp = getImportFlat(packageJsonObj.exports);
if (imp) {
importMap.imports[packageJsonObj.name] = baseUrl + removeLeading(removeLeading(imp, '.'), '/');
} else if (imp = getImportFlat(packageJsonObj.exports?.['.'])) {
importMap.imports[packageJsonObj.name] = baseUrl + removeLeading(removeLeading(imp, '.'), '/');
}
}
let mainImport = packageJsonObj.main;
if (packageJsonObj.module)
mainImport = packageJsonObj.module;
if (packageJsonObj.unpkg && !mainImport)
mainImport = packageJsonObj.unpkg;
if (!importMap.imports[packageJsonObj.name]) {
if (mainImport)
importMap.imports[packageJsonObj.name] = baseUrl + removeLeading(removeLeading(mainImport, '.'), '/');
else
console.warn('package: ' + baseUrl + 'no main import found');
}
importMap.imports[packageJsonObj.name + '/'] = baseUrl;
if (NpmPackageLoader.packageHacks[packageJsonObj.name]?.map) {
for (let h in NpmPackageLoader.packageHacks[packageJsonObj.name]?.map) [
importMap.imports[h] = baseUrl + NpmPackageLoader.packageHacks[packageJsonObj.name].map[h]
]
}
//@ts-ignore
importShim.addImportMap(importMap);
}
}
}
================================================
FILE: packages/web-component-designer/src/elements/helper/ObservedCustomElementsRegistry.ts
================================================
import { IDisposable } from "../../interfaces/IDisposable.js";
export class ObservedCustomElementsRegistry implements IDisposable {
private _originalCustomElementsRegistry: CustomElementRegistry
private _newElements: string[] = [];
constructor() {
this._originalCustomElementsRegistry = window.customElements;
const registry: any = {};
const originalCustomElementsRegistry = this._originalCustomElementsRegistry
const newElements = this._newElements
registry.define = function (name, constructor, options) {
newElements.push(name);
originalCustomElementsRegistry.define(name, constructor, options);
}
registry.get = function (name) {
return originalCustomElementsRegistry.get(name);
}
registry.upgrade = function (node) {
return originalCustomElementsRegistry.upgrade(node);
}
registry.whenDefined = function (name) {
return originalCustomElementsRegistry.whenDefined(name);
}
Object.defineProperty(window, "customElements", {
get() {
return registry;
}
});
}
dispose(): void {
const orgReg = this._originalCustomElementsRegistry;
Object.defineProperty(window, "customElements", {
get() {
return orgReg;
}
});
}
getNewElements(): string[] {
const newElements = this._newElements;
this._newElements = [];
return newElements;
}
}
================================================
FILE: packages/web-component-designer/src/elements/helper/PathDataPolyfill.ts
================================================
// https://github.com/jarek-foksa/path-data-polyfill
// @info
// Polyfill for SVG getPathData() and setPathData() methods. Based on:
// - SVGPathSeg polyfill by Philip Rogers (MIT License)
// https://github.com/progers/pathseg
// - SVGPathNormalizer by Tadahisa Motooka (MIT License)
// https://github.com/motooka/SVGPathNormalizer/tree/master/src
// - arcToCubicCurves() by Dmitry Baranovskiy (MIT License)
// https://github.com/DmitryBaranovskiy/raphael/blob/v2.1.1/raphael.core.js#L1837
// @author
// Jarosław Foksa
// @license
import { IPoint } from "../../interfaces/IPoint.js";
// MIT License
if (!SVGPathElement.prototype.getPathData || !SVGPathElement.prototype.setPathData) {
(function () {
var commandsMap = {
"Z": "Z", "M": "M", "L": "L", "C": "C", "Q": "Q", "A": "A", "H": "H", "V": "V", "S": "S", "T": "T",
"z": "Z", "m": "m", "l": "l", "c": "c", "q": "q", "a": "a", "h": "h", "v": "v", "s": "s", "t": "t"
};
var Source = function (string) {
//@ts-ignore
this._string = string;
//@ts-ignore
this._currentIndex = 0;
//@ts-ignore
this._endIndex = this._string.length;
//@ts-ignore
this._prevCommand = null;
//@ts-ignore
this._skipOptionalSpaces();
};
Source.prototype = {
parseSegment: function () {
var char = this._string[this._currentIndex];
var command = commandsMap[char] ? commandsMap[char] : null;
if (command === null) {
// Possibly an implicit command. Not allowed if this is the first command.
if (this._prevCommand === null) {
return null;
}
// Check for remaining coordinates in the current command.
if (
(char === "+" || char === "-" || char === "." || (char >= "0" && char <= "9")) && this._prevCommand !== "Z"
) {
if (this._prevCommand === "M") {
command = "L";
}
else if (this._prevCommand === "m") {
command = "l";
}
else {
command = this._prevCommand;
}
}
else {
command = null;
}
if (command === null) {
return null;
}
}
else {
this._currentIndex += 1;
}
this._prevCommand = command;
var values = null;
var cmd = command.toUpperCase();
if (cmd === "H" || cmd === "V") {
values = [this._parseNumber()];
}
else if (cmd === "M" || cmd === "L" || cmd === "T") {
values = [this._parseNumber(), this._parseNumber()];
}
else if (cmd === "S" || cmd === "Q") {
values = [this._parseNumber(), this._parseNumber(), this._parseNumber(), this._parseNumber()];
}
else if (cmd === "C") {
values = [
this._parseNumber(),
this._parseNumber(),
this._parseNumber(),
this._parseNumber(),
this._parseNumber(),
this._parseNumber()
];
}
else if (cmd === "A") {
values = [
this._parseNumber(),
this._parseNumber(),
this._parseNumber(),
this._parseArcFlag(),
this._parseArcFlag(),
this._parseNumber(),
this._parseNumber()
];
}
else if (cmd === "Z") {
this._skipOptionalSpaces();
values = [];
}
if (values === null || values.indexOf(null) >= 0) {
// Unknown command or known command with invalid values
return null;
}
else {
return { type: command, values: values };
}
},
hasMoreData: function () {
return this._currentIndex < this._endIndex;
},
peekSegmentType: function () {
var char = this._string[this._currentIndex];
return commandsMap[char] ? commandsMap[char] : null;
},
initialCommandIsMoveTo: function () {
// If the path is empty it is still valid, so return true.
if (!this.hasMoreData()) {
return true;
}
var command = this.peekSegmentType();
// Path must start with moveTo.
return command === "M" || command === "m";
},
_isCurrentSpace: function () {
var char = this._string[this._currentIndex];
return char <= " " && (char === " " || char === "\n" || char === "\t" || char === "\r" || char === "\f");
},
_skipOptionalSpaces: function () {
while (this._currentIndex < this._endIndex && this._isCurrentSpace()) {
this._currentIndex += 1;
}
return this._currentIndex < this._endIndex;
},
_skipOptionalSpacesOrDelimiter: function () {
if (
this._currentIndex < this._endIndex &&
!this._isCurrentSpace() &&
this._string[this._currentIndex] !== ","
) {
return false;
}
if (this._skipOptionalSpaces()) {
if (this._currentIndex < this._endIndex && this._string[this._currentIndex] === ",") {
this._currentIndex += 1;
this._skipOptionalSpaces();
}
}
return this._currentIndex < this._endIndex;
},
// Parse a number from an SVG path. This very closely follows genericParseNumber(...) from
// Source/core/svg/SVGParserUtilities.cpp.
// Spec: http://www.w3.org/TR/SVG11/single-page.html#paths-PathDataBNF
_parseNumber: function () {
var exponent = 0;
var integer = 0;
var frac = 1;
var decimal = 0;
var sign = 1;
var expsign = 1;
var startIndex = this._currentIndex;
this._skipOptionalSpaces();
// Read the sign.
if (this._currentIndex < this._endIndex && this._string[this._currentIndex] === "+") {
this._currentIndex += 1;
}
else if (this._currentIndex < this._endIndex && this._string[this._currentIndex] === "-") {
this._currentIndex += 1;
sign = -1;
}
if (
this._currentIndex === this._endIndex ||
(
(this._string[this._currentIndex] < "0" || this._string[this._currentIndex] > "9") &&
this._string[this._currentIndex] !== "."
)
) {
// The first character of a number must be one of [0-9+-.].
return null;
}
// Read the integer part, build right-to-left.
var startIntPartIndex = this._currentIndex;
while (
this._currentIndex < this._endIndex &&
this._string[this._currentIndex] >= "0" &&
this._string[this._currentIndex] <= "9"
) {
this._currentIndex += 1; // Advance to first non-digit.
}
if (this._currentIndex !== startIntPartIndex) {
var scanIntPartIndex = this._currentIndex - 1;
var multiplier = 1;
while (scanIntPartIndex >= startIntPartIndex) {
integer += multiplier * (this._string[scanIntPartIndex] - "0");
scanIntPartIndex -= 1;
multiplier *= 10;
}
}
// Read the decimals.
if (this._currentIndex < this._endIndex && this._string[this._currentIndex] === ".") {
this._currentIndex += 1;
// There must be a least one digit following the .
if (
this._currentIndex >= this._endIndex ||
this._string[this._currentIndex] < "0" ||
this._string[this._currentIndex] > "9"
) {
return null;
}
while (
this._currentIndex < this._endIndex &&
this._string[this._currentIndex] >= "0" &&
this._string[this._currentIndex] <= "9"
) {
frac *= 10;
decimal += (this._string.charAt(this._currentIndex) - "0") / frac;
this._currentIndex += 1;
}
}
// Read the exponent part.
if (
this._currentIndex !== startIndex &&
this._currentIndex + 1 < this._endIndex &&
(this._string[this._currentIndex] === "e" || this._string[this._currentIndex] === "E") &&
(this._string[this._currentIndex + 1] !== "x" && this._string[this._currentIndex + 1] !== "m")
) {
this._currentIndex += 1;
// Read the sign of the exponent.
if (this._string[this._currentIndex] === "+") {
this._currentIndex += 1;
}
else if (this._string[this._currentIndex] === "-") {
this._currentIndex += 1;
expsign = -1;
}
// There must be an exponent.
if (
this._currentIndex >= this._endIndex ||
this._string[this._currentIndex] < "0" ||
this._string[this._currentIndex] > "9"
) {
return null;
}
while (
this._currentIndex < this._endIndex &&
this._string[this._currentIndex] >= "0" &&
this._string[this._currentIndex] <= "9"
) {
exponent *= 10;
exponent += (this._string[this._currentIndex] - "0");
this._currentIndex += 1;
}
}
var number = integer + decimal;
number *= sign;
if (exponent) {
number *= Math.pow(10, expsign * exponent);
}
if (startIndex === this._currentIndex) {
return null;
}
this._skipOptionalSpacesOrDelimiter();
return number;
},
_parseArcFlag: function () {
if (this._currentIndex >= this._endIndex) {
return null;
}
var flag = null;
var flagChar = this._string[this._currentIndex];
this._currentIndex += 1;
if (flagChar === "0") {
flag = 0;
}
else if (flagChar === "1") {
flag = 1;
}
else {
return null;
}
this._skipOptionalSpacesOrDelimiter();
return flag;
}
};
var parsePathDataString = function (string): PathData[] {
if (!string || string.length === 0) return [];
var source = new Source(string);
var pathData = [];
if (source.initialCommandIsMoveTo()) {
while (source.hasMoreData()) {
var pathSeg = source.parseSegment();
if (pathSeg === null) {
break;
}
else {
pathData.push(pathSeg);
}
}
}
return pathData;
}
// @info
// Get an array of corresponding cubic bezier curve parameters for given arc curve paramters.
var arcToCubicCurves = function (x1, y1, x2, y2, r1, r2, angle, largeArcFlag, sweepFlag, _recursive?) {
var degToRad = function (degrees) {
return (Math.PI * degrees) / 180;
};
var rotate = function (x, y, angleRad) {
var X = x * Math.cos(angleRad) - y * Math.sin(angleRad);
var Y = x * Math.sin(angleRad) + y * Math.cos(angleRad);
return { x: X, y: Y };
};
var angleRad = degToRad(angle);
var params = [];
var f1, f2, cx, cy;
if (_recursive) {
f1 = _recursive[0];
f2 = _recursive[1];
cx = _recursive[2];
cy = _recursive[3];
}
else {
var p1 = rotate(x1, y1, -angleRad);
x1 = p1.x;
y1 = p1.y;
var p2 = rotate(x2, y2, -angleRad);
x2 = p2.x;
y2 = p2.y;
var x = (x1 - x2) / 2;
var y = (y1 - y2) / 2;
var h = (x * x) / (r1 * r1) + (y * y) / (r2 * r2);
if (h > 1) {
h = Math.sqrt(h);
r1 = h * r1;
r2 = h * r2;
}
var sign;
if (largeArcFlag === sweepFlag) {
sign = -1;
}
else {
sign = 1;
}
var r1Pow = r1 * r1;
var r2Pow = r2 * r2;
var left = r1Pow * r2Pow - r1Pow * y * y - r2Pow * x * x;
var right = r1Pow * y * y + r2Pow * x * x;
var k = sign * Math.sqrt(Math.abs(left / right));
cx = k * r1 * y / r2 + (x1 + x2) / 2;
cy = k * -r2 * x / r1 + (y1 + y2) / 2;
f1 = Math.asin(parseFloat(((y1 - cy) / r2).toFixed(9)));
f2 = Math.asin(parseFloat(((y2 - cy) / r2).toFixed(9)));
if (x1 < cx) {
f1 = Math.PI - f1;
}
if (x2 < cx) {
f2 = Math.PI - f2;
}
if (f1 < 0) {
f1 = Math.PI * 2 + f1;
}
if (f2 < 0) {
f2 = Math.PI * 2 + f2;
}
if (sweepFlag && f1 > f2) {
f1 = f1 - Math.PI * 2;
}
if (!sweepFlag && f2 > f1) {
f2 = f2 - Math.PI * 2;
}
}
var df = f2 - f1;
if (Math.abs(df) > (Math.PI * 120 / 180)) {
var f2old = f2;
var x2old = x2;
var y2old = y2;
if (sweepFlag && f2 > f1) {
f2 = f1 + (Math.PI * 120 / 180) * (1);
}
else {
f2 = f1 + (Math.PI * 120 / 180) * (-1);
}
x2 = cx + r1 * Math.cos(f2);
y2 = cy + r2 * Math.sin(f2);
params = arcToCubicCurves(x2, y2, x2old, y2old, r1, r2, angle, 0, sweepFlag, [f2, f2old, cx, cy]);
}
df = f2 - f1;
var c1 = Math.cos(f1);
var s1 = Math.sin(f1);
var c2 = Math.cos(f2);
var s2 = Math.sin(f2);
var t = Math.tan(df / 4);
var hx = 4 / 3 * r1 * t;
var hy = 4 / 3 * r2 * t;
var m1 = [x1, y1];
var m2 = [x1 + hx * s1, y1 - hy * c1];
var m3 = [x2 + hx * s2, y2 - hy * c2];
var m4 = [x2, y2];
m2[0] = 2 * m1[0] - m2[0];
m2[1] = 2 * m1[1] - m2[1];
if (_recursive) {
return [m2, m3, m4].concat(params);
}
else {
params = [m2, m3, m4].concat(params);
var curves = [];
for (var i = 0; i < params.length; i += 3) {
let r1 = rotate(params[i][0], params[i][1], angleRad);
let r2 = rotate(params[i + 1][0], params[i + 1][1], angleRad);
let r3 = rotate(params[i + 2][0], params[i + 2][1], angleRad);
curves.push([r1.x, r1.y, r2.x, r2.y, r3.x, r3.y]);
}
return curves;
}
};
/*var clonePathData = function (pathData) {
return pathData.map(function (seg) {
return { type: seg.type, values: Array.prototype.slice.call(seg.values) }
});
};*/
// @info
// Takes any path data, returns path data that consists only from absolute commands.
var absolutizePathData = function (pathData) {
var absolutizedPathData = [];
var currentX = null;
var currentY = null;
var subpathX = null;
var subpathY = null;
pathData.forEach(function (seg) {
var type = seg.type;
if (type === "M") {
var x = seg.values[0];
var y = seg.values[1];
absolutizedPathData.push({ type: "M", values: [x, y] });
subpathX = x;
subpathY = y;
currentX = x;
currentY = y;
}
else if (type === "m") {
var x = currentX + seg.values[0];
var y = currentY + seg.values[1];
absolutizedPathData.push({ type: "M", values: [x, y] });
subpathX = x;
subpathY = y;
currentX = x;
currentY = y;
}
else if (type === "L") {
var x = seg.values[0];
var y = seg.values[1];
absolutizedPathData.push({ type: "L", values: [x, y] });
currentX = x;
currentY = y;
}
else if (type === "l") {
var x = currentX + seg.values[0];
var y = currentY + seg.values[1];
absolutizedPathData.push({ type: "L", values: [x, y] });
currentX = x;
currentY = y;
}
else if (type === "C") {
var x1 = seg.values[0];
var y1 = seg.values[1];
var x2 = seg.values[2];
var y2 = seg.values[3];
var x = seg.values[4];
var y = seg.values[5];
absolutizedPathData.push({ type: "C", values: [x1, y1, x2, y2, x, y] });
currentX = x;
currentY = y;
}
else if (type === "c") {
var x1 = currentX + seg.values[0];
var y1 = currentY + seg.values[1];
var x2 = currentX + seg.values[2];
var y2 = currentY + seg.values[3];
var x = currentX + seg.values[4];
var y = currentY + seg.values[5];
absolutizedPathData.push({ type: "C", values: [x1, y1, x2, y2, x, y] });
currentX = x;
currentY = y;
}
else if (type === "Q") {
var x1 = seg.values[0];
var y1 = seg.values[1];
var x = seg.values[2];
var y = seg.values[3];
absolutizedPathData.push({ type: "Q", values: [x1, y1, x, y] });
currentX = x;
currentY = y;
}
else if (type === "q") {
var x1 = currentX + seg.values[0];
var y1 = currentY + seg.values[1];
var x = currentX + seg.values[2];
var y = currentY + seg.values[3];
absolutizedPathData.push({ type: "Q", values: [x1, y1, x, y] });
currentX = x;
currentY = y;
}
else if (type === "A") {
var x = seg.values[5];
var y = seg.values[6];
absolutizedPathData.push({
type: "A",
values: [seg.values[0], seg.values[1], seg.values[2], seg.values[3], seg.values[4], x, y]
});
currentX = x;
currentY = y;
}
else if (type === "a") {
var x = currentX + seg.values[5];
var y = currentY + seg.values[6];
absolutizedPathData.push({
type: "A",
values: [seg.values[0], seg.values[1], seg.values[2], seg.values[3], seg.values[4], x, y]
});
currentX = x;
currentY = y;
}
else if (type === "H") {
var x = seg.values[0];
absolutizedPathData.push({ type: "H", values: [x] });
currentX = x;
}
else if (type === "h") {
var x = currentX + seg.values[0];
absolutizedPathData.push({ type: "H", values: [x] });
currentX = x;
}
else if (type === "V") {
var y = seg.values[0];
absolutizedPathData.push({ type: "V", values: [y] });
currentY = y;
}
else if (type === "v") {
var y = currentY + seg.values[0];
absolutizedPathData.push({ type: "V", values: [y] });
currentY = y;
}
else if (type === "S") {
var x2 = seg.values[0];
var y2 = seg.values[1];
var x = seg.values[2];
var y = seg.values[3];
absolutizedPathData.push({ type: "S", values: [x2, y2, x, y] });
currentX = x;
currentY = y;
}
else if (type === "s") {
var x2 = currentX + seg.values[0];
var y2 = currentY + seg.values[1];
var x = currentX + seg.values[2];
var y = currentY + seg.values[3];
absolutizedPathData.push({ type: "S", values: [x2, y2, x, y] });
currentX = x;
currentY = y;
}
else if (type === "T") {
var x = seg.values[0];
var y = seg.values[1]
absolutizedPathData.push({ type: "T", values: [x, y] });
currentX = x;
currentY = y;
}
else if (type === "t") {
var x = currentX + seg.values[0];
var y = currentY + seg.values[1]
absolutizedPathData.push({ type: "T", values: [x, y] });
currentX = x;
currentY = y;
}
else if (type === "Z" || type === "z") {
absolutizedPathData.push({ type: "Z", values: [] });
currentX = subpathX;
currentY = subpathY;
}
});
return absolutizedPathData;
};
// @info
// Takes path data that consists only from absolute commands, returns path data that consists only from
// "M", "L", "C" and "Z" commands.
var reducePathData = function (pathData) {
var reducedPathData = [];
var lastType = null;
var lastControlX = null;
var lastControlY = null;
var currentX = null;
var currentY = null;
var subpathX = null;
var subpathY = null;
pathData.forEach(function (seg) {
if (seg.type === "M") {
var x = seg.values[0];
var y = seg.values[1];
reducedPathData.push({ type: "M", values: [x, y] });
subpathX = x;
subpathY = y;
currentX = x;
currentY = y;
}
else if (seg.type === "C") {
var x1 = seg.values[0];
var y1 = seg.values[1];
var x2 = seg.values[2];
var y2 = seg.values[3];
var x = seg.values[4];
var y = seg.values[5];
reducedPathData.push({ type: "C", values: [x1, y1, x2, y2, x, y] });
lastControlX = x2;
lastControlY = y2;
currentX = x;
currentY = y;
}
else if (seg.type === "L") {
var x = seg.values[0];
var y = seg.values[1];
reducedPathData.push({ type: "L", values: [x, y] });
currentX = x;
currentY = y;
}
else if (seg.type === "H") {
var x = seg.values[0];
reducedPathData.push({ type: "L", values: [x, currentY] });
currentX = x;
}
else if (seg.type === "V") {
var y = seg.values[0];
reducedPathData.push({ type: "L", values: [currentX, y] });
currentY = y;
}
else if (seg.type === "S") {
var x2 = seg.values[0];
var y2 = seg.values[1];
var x = seg.values[2];
var y = seg.values[3];
var cx1, cy1;
if (lastType === "C" || lastType === "S") {
cx1 = currentX + (currentX - lastControlX);
cy1 = currentY + (currentY - lastControlY);
}
else {
cx1 = currentX;
cy1 = currentY;
}
reducedPathData.push({ type: "C", values: [cx1, cy1, x2, y2, x, y] });
lastControlX = x2;
lastControlY = y2;
currentX = x;
currentY = y;
}
else if (seg.type === "T") {
var x = seg.values[0];
var y = seg.values[1];
var x1, y1;
if (lastType === "Q" || lastType === "T") {
x1 = currentX + (currentX - lastControlX);
y1 = currentY + (currentY - lastControlY);
}
else {
x1 = currentX;
y1 = currentY;
}
var cx1 = currentX + 2 * (x1 - currentX) / 3;
var cy1 = currentY + 2 * (y1 - currentY) / 3;
var cx2 = x + 2 * (x1 - x) / 3;
var cy2 = y + 2 * (y1 - y) / 3;
reducedPathData.push({ type: "C", values: [cx1, cy1, cx2, cy2, x, y] });
lastControlX = x1;
lastControlY = y1;
currentX = x;
currentY = y;
}
else if (seg.type === "Q") {
var x1 = seg.values[0];
var y1 = seg.values[1];
var x = seg.values[2];
var y = seg.values[3];
var cx1 = currentX + 2 * (x1 - currentX) / 3;
var cy1 = currentY + 2 * (y1 - currentY) / 3;
var cx2 = x + 2 * (x1 - x) / 3;
var cy2 = y + 2 * (y1 - y) / 3;
reducedPathData.push({ type: "C", values: [cx1, cy1, cx2, cy2, x, y] });
lastControlX = x1;
lastControlY = y1;
currentX = x;
currentY = y;
}
else if (seg.type === "A") {
let r1 = Math.abs(seg.values[0]);
let r2 = Math.abs(seg.values[1]);
var angle = seg.values[2];
var largeArcFlag = seg.values[3];
var sweepFlag = seg.values[4];
var x = seg.values[5];
var y = seg.values[6];
if (r1 === 0 || r2 === 0) {
reducedPathData.push({ type: "C", values: [currentX, currentY, x, y, x, y] });
currentX = x;
currentY = y;
}
else {
if (currentX !== x || currentY !== y) {
var curves = arcToCubicCurves(currentX, currentY, x, y, r1, r2, angle, largeArcFlag, sweepFlag);
curves.forEach(function (curve) {
reducedPathData.push({ type: "C", values: curve });
});
currentX = x;
currentY = y;
}
}
}
else if (seg.type === "Z") {
reducedPathData.push(seg);
currentX = subpathX;
currentY = subpathY;
}
lastType = seg.type;
});
return reducedPathData;
};
SVGPathElement.prototype.getPathData = function (options) {
if (options && options.normalize) {
/*if (this[$cachedNormalizedPathData]) {
return clonePathData(this[$cachedNormalizedPathData]);
}
else */ {
let pathData;
/*if (this[$cachedPathData]) {
pathData = clonePathData(this[$cachedPathData]);
}
else */{
pathData = parsePathDataString(this.getAttribute("d") || "");
//this[$cachedPathData] = clonePathData(pathData);
}
let normalizedPathData = reducePathData(absolutizePathData(pathData));
//this[$cachedNormalizedPathData] = clonePathData(normalizedPathData);
return normalizedPathData;
}
}
else {
/*if (this[$cachedPathData]) {
return clonePathData(this[$cachedPathData]);
}
else*/ {
let pathData = parsePathDataString(this.getAttribute("d") || "");
//this[$cachedPathData] = clonePathData(pathData);
return pathData;
}
}
};
SVGPathElement.prototype.setPathData = function (pathData) {
if (pathData.length === 0) {
this.removeAttribute("d");
}
else {
let d = "";
for (let i = 0, l = pathData.length; i < l; i += 1) {
let seg: any = pathData[i];
if (i > 0) {
d += " ";
}
d += seg.type;
if (seg.values && seg.values.length > 0) {
d += " " + seg.values.join(" ");
}
}
this.setAttribute("d", d);
}
};
SVGRectElement.prototype.getPathData = function (options) {
var x = this.x.baseVal.value;
var y = this.y.baseVal.value;
var width = this.width.baseVal.value;
var height = this.height.baseVal.value;
var rx = this.hasAttribute("rx") ? this.rx.baseVal.value : this.ry.baseVal.value;
var ry = this.hasAttribute("ry") ? this.ry.baseVal.value : this.rx.baseVal.value;
if (rx > width / 2) {
rx = width / 2;
}
if (ry > height / 2) {
ry = height / 2;
}
var pathData: any = [
{ type: "M", values: [x + rx, y] },
{ type: "H", values: [x + width - rx] },
{ type: "A", values: [rx, ry, 0, 0, 1, x + width, y + ry] },
{ type: "V", values: [y + height - ry] },
{ type: "A", values: [rx, ry, 0, 0, 1, x + width - rx, y + height] },
{ type: "H", values: [x + rx] },
{ type: "A", values: [rx, ry, 0, 0, 1, x, y + height - ry] },
{ type: "V", values: [y + ry] },
{ type: "A", values: [rx, ry, 0, 0, 1, x + rx, y] },
{ type: "Z", values: [] }
];
// Get rid of redundant "A" segs when either rx or ry is 0
pathData = pathData.filter(function (s) {
return s.type === "A" && (s.values[0] === 0 || s.values[1] === 0) ? false : true;
});
if (options && options.normalize === true) {
pathData = reducePathData(pathData);
}
return pathData;
};
SVGCircleElement.prototype.getPathData = function (options) {
var cx = this.cx.baseVal.value;
var cy = this.cy.baseVal.value;
var r = this.r.baseVal.value;
var pathData: any = [
{ type: "M", values: [cx + r, cy] },
{ type: "A", values: [r, r, 0, 0, 1, cx, cy + r] },
{ type: "A", values: [r, r, 0, 0, 1, cx - r, cy] },
{ type: "A", values: [r, r, 0, 0, 1, cx, cy - r] },
{ type: "A", values: [r, r, 0, 0, 1, cx + r, cy] },
{ type: "Z", values: [] }
];
if (options && options.normalize === true) {
pathData = reducePathData(pathData);
}
return pathData;
};
SVGEllipseElement.prototype.getPathData = function (options) {
var cx = this.cx.baseVal.value;
var cy = this.cy.baseVal.value;
var rx = this.rx.baseVal.value;
var ry = this.ry.baseVal.value;
var pathData: any = [
{ type: "M", values: [cx + rx, cy] },
{ type: "A", values: [rx, ry, 0, 0, 1, cx, cy + ry] },
{ type: "A", values: [rx, ry, 0, 0, 1, cx - rx, cy] },
{ type: "A", values: [rx, ry, 0, 0, 1, cx, cy - ry] },
{ type: "A", values: [rx, ry, 0, 0, 1, cx + rx, cy] },
{ type: "Z", values: [] }
];
if (options && options.normalize === true) {
pathData = reducePathData(pathData);
}
return pathData;
};
SVGLineElement.prototype.getPathData = function () {
return [
{ type: "M", values: [this.x1.baseVal.value, this.y1.baseVal.value] },
{ type: "L", values: [this.x2.baseVal.value, this.y2.baseVal.value] }
];
};
SVGPolylineElement.prototype.getPathData = function () {
var pathData = [];
for (var i = 0; i < this.points.numberOfItems; i += 1) {
var point = this.points.getItem(i);
pathData.push({
type: (i === 0 ? "M" : "L"),
values: [point.x, point.y]
});
}
return pathData;
};
SVGPolygonElement.prototype.getPathData = function () {
var pathData = [];
for (var i = 0; i < this.points.numberOfItems; i += 1) {
var point = this.points.getItem(i);
pathData.push({
type: (i === 0 ? "M" : "L"),
values: [point.x, point.y]
});
}
pathData.push({
type: "Z",
values: []
});
return pathData;
};
})();
}
export { }
export declare type PathDataM = { type: 'M' | 'm', values: [x: number, y: number] }
export declare type PathDataL = { type: 'L' | 'l', values: [x: number, y: number] }
export declare type PathDataT = { type: 'T' | 't', values: [x: number, y: number] }
export declare type PathDataH = { type: 'H' | 'h', values: [x: number] }
export declare type PathDataV = { type: 'V' | 'v', values: [y: number] }
export declare type PathDataZ = { type: 'Z' | 'z', values?: [] }
export declare type PathDataC = { type: 'C' | 'c', values: [x1: number, y1: number, x2: number, y2: number, x: number, y: number] }
export declare type PathDataS = { type: 'S' | 's', values: [x2: number, y2: number, x: number, y: number] }
export declare type PathDataQ = { type: 'Q' | 'q', values: [x1: number, y1: number, x: number, y: number] }
export declare type PathDataA = { type: 'A' | 'a', values: [rx: number, ry: number, ang: number, flag1: 0 | 1, flag2: 0 | 1, x: number, y: number] }
export declare type PathData = { type: string } & (PathDataM | PathDataL | PathDataH | PathDataV | PathDataZ | PathDataC | PathDataS | PathDataQ | PathDataT | PathDataA)[];
export function straightenLine(p1: IPoint, p2: IPoint, stepDeg = 45): IPoint {
const alpha = calculateAlpha(p1, p2);
const snapped = Math.round(alpha / stepDeg) * stepDeg;
const angle = (snapped + 360) % 360;
const rad = angle * Math.PI / 180;
const dx = p2.x - p1.x;
const dy = p2.y - p1.y;
const length = Math.sqrt(dx * dx + dy * dy);
const newX = p1.x + Math.cos(rad) * length;
const newY = p1.y - Math.sin(rad) * length;
return { x: newX, y: newY };
}
export function calculateNormLegth(p1: IPoint, p2: IPoint): number {
let normLenght;
let currentLength = Math.sqrt(Math.pow(p2.x - p1.x, 2) + Math.pow(p2.y - p1.y, 2));
let alpha = calculateAlpha(p1, p2);
let beta = alpha - ((Math.floor(alpha / 90) * 90) + 45);
normLenght = currentLength * Math.cos(beta * (Math.PI / 180)) / Math.sqrt(2);
return normLenght;
}
export function calculateAlpha(p1: IPoint, p2: IPoint): number {
let alpha = - 1 * Math.atan2(p2.y - p1.y, p2.x - p1.x) * 180 / Math.PI;
if (alpha < 0)
alpha += 360;
return alpha;
}
export function interpolateLinePoints(p1: IPoint, p2: IPoint, maxDistance: number): IPoint[] {
const dx = p2.x - p1.x;
const dy = p2.y - p1.y;
const distance = Math.sqrt(dx * dx + dy * dy);
if (distance === 0) {
return [];
}
if (maxDistance <= 0) {
return [p2];
}
const segments = Math.max(1, Math.ceil(distance / maxDistance));
const points: IPoint[] = [];
for (let i = 1; i <= segments; i++) {
points.push({
x: p1.x + dx * i / segments,
y: p1.y + dy * i / segments
});
}
return points;
}
export function moveSVGPath(path: SVGPathElement, xFactor: number, yFactor: number): string {
let newPathData = "";
let pd = path.getPathData({ normalize: true });
{
for (let p of pd) {
switch (p.type) {
case ('M'):
case ('m'):
case ('L'):
case ('l'):
case ('T'):
case ('t'):
newPathData += p.type + " " + (p.values[0] - xFactor) + " " + (p.values[1] - yFactor) + " ";
break;
case ('Z'):
case ('z'):
newPathData += p.type + " ";
break;
case ('C'):
case ('c'):
newPathData += p.type + " " + (p.values[0] - xFactor) + " " + (p.values[1] - yFactor) + " " + (p.values[2] - xFactor) + " " + (p.values[3] - yFactor) + " " + (p.values[4] - xFactor) + " " + (p.values[5] - yFactor) + " ";
break;
case ('S'):
case ('s'):
case ('Q'):
case ('q'):
newPathData += p.type + " " + (p.values[0] - xFactor) + " " + (p.values[1] - yFactor) + " " + (p.values[2] - xFactor) + " " + (p.values[3] - yFactor) + " ";
break;
case ('A'):
case ('a'):
newPathData += p.type + " " + (p.values[0] - xFactor) + " " + (p.values[1] - yFactor) + " " + p.values[2] + " " + p.values[3] + " " + p.values[4] + " " + (p.values[5] - xFactor) + " " + (p.values[6] - yFactor) + " ";
break;
}
}
}
return newPathData;
}
export function createPathD(path: PathData[]) {
let pathD: string = "";
for (let p of path) {
pathD += p.type + " ";
for (var i = 0; i < p.values.length; i++) {
if (p.values[i] != null && !isNaN(p.values[i])) {
pathD += p.values[i] + " ";
}
}
}
return pathD;
}
declare global {
interface SVGGraphicsElement {
getPathData(options?: { normalize?: boolean }): PathData[]
isPointInStroke(point: { x: number, y: number })
isPointInFill(point: { x: number, y: number })
}
interface SVGPathElement {
getPathData(options?: { normalize?: boolean }): PathData[]
setPathData(pathData: PathData[])
}
interface SVGRectElement {
getPathData(options?: { normalize?: boolean }): PathData[]
}
interface SVGCircleElement {
getPathData(options?: { normalize?: boolean }): PathData[]
}
interface SVGEllipseElement {
getPathData(options?: { normalize?: boolean }): PathData[]
}
interface SVGLineElement {
getPathData(options?: { normalize?: boolean }): PathData[]
}interface SVGPolylineElement {
getPathData(options?: { normalize?: boolean }): PathData[]
}
interface SVGPolygonElement {
getPathData(options?: { normalize?: boolean }): PathData[]
}
}
================================================
FILE: packages/web-component-designer/src/elements/helper/PopupHelper.ts
================================================
export function showPopup(content: Element, anchorEl: HTMLElement, closedCallback?: () => void): () => void {
const root = anchorEl.getRootNode();
// 🧱 Create popup element
const popupEl = document.createElement('div');
// Enable Popup API
popupEl.popover = 'auto';
// 📦 Insert content
if (typeof content === 'string') {
popupEl.innerHTML = content;
} else {
popupEl.append(content);
}
// 📍 Mount into SAME root (required for anchor positioning)
if (root instanceof ShadowRoot) {
root.appendChild(popupEl);
} else {
document.body.appendChild(popupEl);
}
Object.assign(popupEl.style, {
positionArea: 'right bottom',
positionTryFallbacks: `
flip-block,
flip-inline,
top,
bottom,
left,
right
`
});
// 🧹 cleanup when closed
popupEl.addEventListener('toggle', () => {
if (!popupEl.matches(':popover-open')) {
popupEl.remove();
closedCallback?.();
}
}, { once: true });
//@ts-ignore
// 🚀 Open via Popup API
popupEl.showPopover({ source: anchorEl });
// 🔙 return close callback
return () => {
if (popupEl.matches(':popover-open')) {
popupEl.hidePopover();
}
popupEl.remove();
closedCallback?.();
};
}
================================================
FILE: packages/web-component-designer/src/elements/helper/QuadEdgeHandleHelper.ts
================================================
import { IPoint } from '../../interfaces/IPoint.js';
function normalize(vector: IPoint) {
const length = Math.hypot(vector.x, vector.y);
if (length < 1e-8) {
return { x: 0, y: 0 };
}
return { x: vector.x / length, y: vector.y / length };
}
export function getQuadCenter(quad: DOMQuad): IPoint {
return {
x: (quad.p1.x + quad.p2.x + quad.p3.x + quad.p4.x) / 4,
y: (quad.p1.y + quad.p2.y + quad.p3.y + quad.p4.y) / 4
};
}
export function getEdgeMidpoint(start: IPoint, end: IPoint): IPoint {
return {
x: (start.x + end.x) / 2,
y: (start.y + end.y) / 2
};
}
export function getOutwardNormal(start: IPoint, end: IPoint, quadCenter: IPoint, fallback: IPoint): IPoint {
const dx = end.x - start.x;
const dy = end.y - start.y;
let normal = normalize({ x: -dy, y: dx });
if (normal.x === 0 && normal.y === 0) {
normal = normalize(fallback);
}
const midpoint = getEdgeMidpoint(start, end);
const toCenter = { x: quadCenter.x - midpoint.x, y: quadCenter.y - midpoint.y };
if ((normal.x * toCenter.x) + (normal.y * toCenter.y) > 0) {
normal = { x: -normal.x, y: -normal.y };
}
return normal;
}
export function getEdgeOffsetPoint(start: IPoint, end: IPoint, quadCenter: IPoint, distance: number, fallback: IPoint): IPoint {
const midpoint = getEdgeMidpoint(start, end);
const normal = getOutwardNormal(start, end, quadCenter, fallback);
return {
x: midpoint.x + (normal.x * distance),
y: midpoint.y + (normal.y * distance)
};
}
================================================
FILE: packages/web-component-designer/src/elements/helper/Screenshot.ts
================================================
import { getBoundingClientRectAlsoForDisplayContents } from "./ElementHelper.js";
// for screenshots to be genrated properly, you need to select the current tab only in media source selector
export class Screenshot {
private static _canvas: HTMLCanvasElement;
private static _context: CanvasRenderingContext2D;
private static _video: HTMLVideoElement;
private static _captureStream: MediaStream;
private static _disableStream() {
Screenshot._captureStream.getTracks().forEach(track => track.stop());
Screenshot._canvas = null;
}
static get screenshotsEnabled() {
return Screenshot._captureStream && Screenshot._captureStream.active;
}
static async enableScreenshots(elementHostForVideo: Element = document.body) {
if (Screenshot._captureStream && !Screenshot._captureStream.active) {
Screenshot._disableStream();
}
if (Screenshot._canvas == null) {
Screenshot._canvas = document.createElement("canvas");
Screenshot._context = Screenshot._canvas.getContext("2d");
Screenshot._video = document.createElement("video");
const gdmOptions = {
video: {
cursor: "never",
displaySurface: 'browser'
},
audio: false,
selfBrowserSurface: "include",
preferCurrentTab: true
}
Screenshot._video.style.display = "none";
elementHostForVideo.appendChild(Screenshot._video);
try {
//@ts-ignore
Screenshot._captureStream = await navigator.mediaDevices.getDisplayMedia(gdmOptions);
} catch (e) {
Screenshot._canvas = null;
throw e;
}
//@ts-ignore
const captureType = Screenshot._captureStream.getVideoTracks()[0].getSettings().displaySurface
if (captureType != 'browser') {
Screenshot._disableStream();
alert('You need to share the current Tab, for the screenshot API to work');
throw 'You need to share the current Tab, for the screenshot API to work';
}
Screenshot._video.srcObject = Screenshot._captureStream;
Screenshot._video.play();
await Screenshot._sleep(2000);
}
}
static async takeScreenshot(element: Element, width: number = 100, height: number = 100, elementHostForVideo: Element = document.body): Promise {
await Screenshot.enableScreenshots(elementHostForVideo);
const rect = getBoundingClientRectAlsoForDisplayContents(element);
Screenshot._canvas.width = width;
Screenshot._canvas.height = height;
Screenshot._context.drawImage(Screenshot._video, 0, 0, 1, 1, 0, 0, width, height);
const factorX = Screenshot._video.videoWidth / window.innerWidth;
const factorY = Screenshot._video.videoHeight / window.innerHeight;
Screenshot._context.drawImage(Screenshot._video, rect.left * factorX, rect.top * factorY, rect.width * factorX, rect.height * factorY, 0, 0, width, height);
const frame = Screenshot._canvas.toDataURL("image/png");
return frame;
}
private static _sleep(timeout: number) {
let resolve = null
const promise = new Promise(r => resolve = r)
window.setTimeout(resolve, timeout)
return promise
}
}
================================================
FILE: packages/web-component-designer/src/elements/helper/SelectionHelper.ts
================================================
export function shadowrootGetSelection(shadowRoot: ShadowRoot): Selection | ArrayLike | null {
let selection = document.getSelection();
if ((shadowRoot).getSelection)
selection = (shadowRoot).getSelection()
else if ((selection).getComposedRanges)
selection = (selection).getComposedRanges(shadowRoot);
return selection;
}
function wrapTextNodesInSpan(range: Range, spans: HTMLSpanElement[]) {
function wrapNode(node: Node) {
const parent = node.parentNode;
if (!parent)
return;
const span = document.createElement('span');
spans.push(span);
parent.insertBefore(span, node);
span.appendChild(node);
}
function canReuseSpan(node: Node): node is HTMLSpanElement {
return node instanceof HTMLSpanElement && Array.from(node.childNodes).every(x => x.nodeType === Node.TEXT_NODE);
}
function processNode(node: Node) {
switch (node.nodeType) {
case Node.TEXT_NODE:
wrapNode(node);
break;
case Node.DOCUMENT_FRAGMENT_NODE:
case Node.ELEMENT_NODE:
if (canReuseSpan(node)) {
spans.push(node);
break;
}
Array.from(node.childNodes).forEach(processNode);
break;
}
}
const fragment = range.extractContents();
processNode(fragment);
range.insertNode(fragment);
}
function staticRangeToRange(staticRange: StaticRange) {
const range = document.createRange();
range.setStart(staticRange.startContainer, staticRange.startOffset);
range.setEnd(staticRange.endContainer, staticRange.endOffset);
return range;
}
export function wrapSelectionInSpans(selection: Selection | ArrayLike) {
const spans: HTMLSpanElement[] = [];
if ('getRangeAt' in selection) {
if (!selection.rangeCount)
return spans;
const range = selection.getRangeAt(0);
wrapTextNodesInSpan(range, spans);
} else {
const staticRange = selection[0];
if (!staticRange)
return spans;
wrapTextNodesInSpan(staticRangeToRange(staticRange), spans);
}
if ('removeAllRanges' in selection && selection.removeAllRanges)
selection.removeAllRanges();
return spans;
}
================================================
FILE: packages/web-component-designer/src/elements/helper/SimpleTextWriter.ts
================================================
import { ITextWriter } from './ITextWriter.js';
export class SimpleTextWriter implements ITextWriter {
private _textHolder: string = ''
public get position(): number {
return this._textHolder.length;
}
public isLastCharNewline() {
return this._textHolder[this._textHolder.length - 1] === '\n';
}
public levelRaise() {
}
public levelShrink() {
}
public write(text: string) {
this._textHolder += text;
}
public writeLine(text: string) {
this._textHolder += text;
}
public writeIndent() {
}
public writeNewline() {
}
public getString() {
return this._textHolder;
}
}
================================================
FILE: packages/web-component-designer/src/elements/helper/StylesheetHelper.ts
================================================
export function stylesheetFromString(window: Window, text: string) {
//@ts-ignore
const newStylesheet = new window.CSSStyleSheet();
newStylesheet.replaceSync(text);
return newStylesheet;
}
export function stylesheetToString(stylesheet: CSSStyleSheet) {
return Array.from(stylesheet.cssRules).map(rule => rule.cssText).join('\n');
}
================================================
FILE: packages/web-component-designer/src/elements/helper/SvgHelper.ts
================================================
import { IPoint } from '../../interfaces/IPoint.js';
import { IDesignerCanvas } from '../widgets/designerView/IDesignerCanvas.js';
import './PathDataPolyfill.js';
import { createPathD, PathData } from './PathDataPolyfill.js';
type ProjectiveMatrix = [number, number, number, number, number, number, number, number, number];
interface SvgOverlayTransform {
bbox: DOMRect;
matrix: ProjectiveMatrix;
inverseMatrix: ProjectiveMatrix;
}
export interface SvgOverlayPathOptions {
includeMarkers?: boolean;
}
interface NormalizedPathSegment {
start: IPoint;
end: IPoint;
startAngle: number;
endAngle: number;
}
interface MarkerPlacement {
marker: SVGMarkerElement;
point: IPoint;
angle: number;
}
interface SvgViewBoxTransform {
transform: string;
scaleX: number;
scaleY: number;
translateX: number;
translateY: number;
}
export function isVisualSvgElement(element: SVGElement) {
let el: Element = element;
while (el) {
if (el instanceof (el.ownerDocument.defaultView ?? window).SVGSVGElement)
return true;
if (el instanceof (el.ownerDocument.defaultView ?? window).SVGDefsElement)
return false;
if (el instanceof (el.ownerDocument.defaultView ?? window).SVGMetadataElement)
return false;
el = el.parentElement;
}
return true;
}
export function svg(strings: TemplateStringsArray, ...values: any[]) {
const svgEl = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
svgEl.innerHTML = svgAsString(strings, ...values);
return svgEl;
}
export function svgAsString(strings: TemplateStringsArray, ...values: any[]) {
if (strings.length === 1)
return strings.raw[0];
else {
let r = ''
for (let i = 0; i < strings.length; i++) {
r += strings[i] + (values[i] ?? '');
}
return r;
}
}
export function isSupportedSvgGeometryElement(element: Element): element is SVGGraphicsElement {
return element instanceof SVGPathElement ||
element instanceof SVGRectElement ||
element instanceof SVGLineElement ||
element instanceof SVGCircleElement ||
element instanceof SVGEllipseElement ||
element instanceof SVGPolygonElement ||
element instanceof SVGPolylineElement;
}
export function toOverlayPointFromSvgUserSpace(element: Element, designerCanvas: IDesignerCanvas, point: IPoint): IPoint {
const transform = _getSvgOverlayTransform(element, designerCanvas);
if (transform) {
const local = _toElementReferenceBoxPoint(element, point);
return _applyProjectiveMatrix(transform.matrix, local.x / transform.bbox.width, local.y / transform.bbox.height);
}
const fallbackLocalPoint = _toElementReferenceBoxPoint(element, point);
const tp = designerCanvas.canvas.convertPointFromNode(fallbackLocalPoint, element, { iframes: designerCanvas.iframes });
return { x: tp.x, y: tp.y };
}
export function fromOverlayPointToSvgUserSpace(element: Element, designerCanvas: IDesignerCanvas, point: IPoint): IPoint {
const transform = _getSvgOverlayTransform(element, designerCanvas);
if (transform) {
const normalized = _applyProjectiveMatrix(transform.inverseMatrix, point.x, point.y);
return {
x: transform.bbox.x + normalized.x * transform.bbox.width,
y: transform.bbox.y + normalized.y * transform.bbox.height,
};
}
const tp = element.convertPointFromNode({ x: point.x, y: point.y }, designerCanvas.canvas, { iframes: designerCanvas.iframes });
return _fromElementReferenceBoxPoint(element, { x: tp.x, y: tp.y });
}
export function createOverlayPathDataFromSvgGeometryElement(element: Element, designerCanvas: IDesignerCanvas, options?: SvgOverlayPathOptions): string | null {
const basePathData = _createOverlayPathDataWithoutMarkers(element, designerCanvas);
const includeMarkers = options?.includeMarkers !== false;
if (!includeMarkers) {
return basePathData;
}
const markerPathData = _createMarkerOverlayPathData(element, designerCanvas);
if (basePathData && markerPathData) {
return `${basePathData} ${markerPathData}`;
}
return basePathData ?? markerPathData;
}
function _createOverlayPathDataWithoutMarkers(element: Element, designerCanvas: IDesignerCanvas): string | null {
if (!isSupportedSvgGeometryElement(element)) {
return null;
}
const getPathData = (element as any).getPathData as ((options?: { normalize?: boolean }) => PathData[]) | undefined;
if (!getPathData) {
return null;
}
const sourcePathData = getPathData.call(element, { normalize: true }) ?? [];
if (!sourcePathData.length) {
return null;
}
const transformedPathData: PathData[] = [];
let currentPoint: IPoint = { x: 0, y: 0 };
let subPathStart: IPoint = { x: 0, y: 0 };
for (const command of sourcePathData) {
const type = command.type.toUpperCase();
const isRelative = command.type !== type;
const values = command.values ?? [];
const toAbsolute = (x: number, y: number): IPoint => {
if (isRelative) {
return { x: currentPoint.x + x, y: currentPoint.y + y };
}
return { x, y };
};
const toOverlay = (x: number, y: number): IPoint => {
const absolutePoint = toAbsolute(x, y);
return toOverlayPointFromSvgUserSpace(element, designerCanvas, absolutePoint);
};
switch (type) {
case 'M': {
const overlayPoint = toOverlay(values[0], values[1]);
transformedPathData.push({ type: 'M', values: [overlayPoint.x, overlayPoint.y] } as any);
currentPoint = toAbsolute(values[0], values[1]);
subPathStart = { ...currentPoint };
break;
}
case 'L': {
const overlayPoint = toOverlay(values[0], values[1]);
transformedPathData.push({ type: 'L', values: [overlayPoint.x, overlayPoint.y] } as any);
currentPoint = toAbsolute(values[0], values[1]);
break;
}
case 'H': {
const target = isRelative ? { x: currentPoint.x + values[0], y: currentPoint.y } : { x: values[0], y: currentPoint.y };
const overlayPoint = toOverlayPointFromSvgUserSpace(element, designerCanvas, target);
transformedPathData.push({ type: 'L', values: [overlayPoint.x, overlayPoint.y] } as any);
currentPoint = target;
break;
}
case 'V': {
const target = isRelative ? { x: currentPoint.x, y: currentPoint.y + values[0] } : { x: currentPoint.x, y: values[0] };
const overlayPoint = toOverlayPointFromSvgUserSpace(element, designerCanvas, target);
transformedPathData.push({ type: 'L', values: [overlayPoint.x, overlayPoint.y] } as any);
currentPoint = target;
break;
}
case 'C': {
const cp1 = toOverlay(values[0], values[1]);
const cp2 = toOverlay(values[2], values[3]);
const end = toOverlay(values[4], values[5]);
transformedPathData.push({ type: 'C', values: [cp1.x, cp1.y, cp2.x, cp2.y, end.x, end.y] } as any);
currentPoint = toAbsolute(values[4], values[5]);
break;
}
case 'S': {
const cp2 = toOverlay(values[0], values[1]);
const end = toOverlay(values[2], values[3]);
transformedPathData.push({ type: 'S', values: [cp2.x, cp2.y, end.x, end.y] } as any);
currentPoint = toAbsolute(values[2], values[3]);
break;
}
case 'Q': {
const cp1 = toOverlay(values[0], values[1]);
const end = toOverlay(values[2], values[3]);
transformedPathData.push({ type: 'Q', values: [cp1.x, cp1.y, end.x, end.y] } as any);
currentPoint = toAbsolute(values[2], values[3]);
break;
}
case 'T': {
const end = toOverlay(values[0], values[1]);
transformedPathData.push({ type: 'T', values: [end.x, end.y] } as any);
currentPoint = toAbsolute(values[0], values[1]);
break;
}
case 'A': {
const end = toOverlay(values[5], values[6]);
transformedPathData.push({ type: 'A', values: [values[0], values[1], values[2], values[3], values[4], end.x, end.y] } as any);
currentPoint = toAbsolute(values[5], values[6]);
break;
}
case 'Z': {
transformedPathData.push({ type: 'Z', values: [] } as any);
currentPoint = { ...subPathStart };
break;
}
}
}
return createPathD(transformedPathData);
}
function _createMarkerOverlayPathData(element: Element, designerCanvas: IDesignerCanvas): string | null {
if (!(element instanceof SVGGraphicsElement)) {
return null;
}
const placements = _collectMarkerPlacements(element);
if (!placements.length) {
return null;
}
const ownerSvg = element.ownerSVGElement;
if (!ownerSvg) {
return null;
}
const tempGroup = document.createElementNS('http://www.w3.org/2000/svg', 'g');
tempGroup.setAttribute('visibility', 'hidden');
tempGroup.setAttribute('pointer-events', 'none');
ownerSvg.appendChild(tempGroup);
try {
const pathParts: string[] = [];
for (const placement of placements) {
const markerInstance = _createMarkerInstanceGroup(element, placement);
if (!markerInstance) {
continue;
}
tempGroup.appendChild(markerInstance);
const markerGraphics = markerInstance.querySelectorAll('path, rect, line, circle, ellipse, polygon, polyline');
for (const markerGraphic of markerGraphics) {
const pathData = _createOverlayPathDataWithoutMarkers(markerGraphic, designerCanvas);
if (pathData) {
pathParts.push(pathData);
}
}
}
return pathParts.length ? pathParts.join(' ') : null;
} finally {
tempGroup.remove();
}
}
function _collectMarkerPlacements(element: SVGGraphicsElement): MarkerPlacement[] {
const segments = _getNormalizedPathSegments(element);
if (!segments.length) {
return [];
}
const placements: MarkerPlacement[] = [];
const startMarker = _resolveMarkerReference(element, 'marker-start');
const midMarker = _resolveMarkerReference(element, 'marker-mid');
const endMarker = _resolveMarkerReference(element, 'marker-end');
if (startMarker) {
placements.push({
marker: startMarker,
point: segments[0].start,
angle: _resolveMarkerAngle(startMarker, segments[0].startAngle, undefined, true)
});
}
if (midMarker) {
for (let i = 0; i < segments.length - 1; i++) {
if (!_pointsEqual(segments[i].end, segments[i + 1].start)) {
continue;
}
placements.push({
marker: midMarker,
point: segments[i].end,
angle: _resolveMarkerAngle(midMarker, segments[i].endAngle, segments[i + 1].startAngle, false)
});
}
if (_pointsEqual(segments[segments.length - 1].end, segments[0].start) && segments.length > 1) {
placements.push({
marker: midMarker,
point: segments[segments.length - 1].end,
angle: _resolveMarkerAngle(midMarker, segments[segments.length - 1].endAngle, segments[0].startAngle, false)
});
}
}
if (endMarker) {
const lastSegment = segments[segments.length - 1];
placements.push({
marker: endMarker,
point: lastSegment.end,
angle: _resolveMarkerAngle(endMarker, lastSegment.endAngle, undefined, false)
});
}
return placements;
}
function _getNormalizedPathSegments(element: SVGGraphicsElement): NormalizedPathSegment[] {
const getPathData = (element as any).getPathData as ((options?: { normalize?: boolean }) => PathData[]) | undefined;
if (!getPathData) {
return [];
}
const pathData = getPathData.call(element, { normalize: true }) ?? [];
const segments: NormalizedPathSegment[] = [];
let currentPoint: IPoint = { x: 0, y: 0 };
let subPathStart: IPoint = { x: 0, y: 0 };
for (const command of pathData) {
switch (command.type) {
case 'M': {
currentPoint = { x: command.values[0], y: command.values[1] };
subPathStart = { ...currentPoint };
break;
}
case 'L': {
const endPoint = { x: command.values[0], y: command.values[1] };
const angle = _angleBetween(currentPoint, endPoint);
if (angle != null) {
segments.push({ start: { ...currentPoint }, end: endPoint, startAngle: angle, endAngle: angle });
}
currentPoint = endPoint;
break;
}
case 'C': {
const cp1 = { x: command.values[0], y: command.values[1] };
const cp2 = { x: command.values[2], y: command.values[3] };
const endPoint = { x: command.values[4], y: command.values[5] };
const startAngle = _getFirstUsableAngle([currentPoint, cp1, cp2, endPoint]);
const endAngle = _getLastUsableAngle([currentPoint, cp1, cp2, endPoint]);
if (startAngle != null && endAngle != null) {
segments.push({ start: { ...currentPoint }, end: endPoint, startAngle, endAngle });
}
currentPoint = endPoint;
break;
}
case 'Z': {
const angle = _angleBetween(currentPoint, subPathStart);
if (angle != null) {
segments.push({ start: { ...currentPoint }, end: { ...subPathStart }, startAngle: angle, endAngle: angle });
}
currentPoint = { ...subPathStart };
break;
}
}
}
return segments;
}
function _resolveMarkerReference(element: SVGGraphicsElement, attributeName: 'marker-start' | 'marker-mid' | 'marker-end'): SVGMarkerElement | null {
const reference = element.getAttribute(attributeName)?.trim();
if (!reference) {
return null;
}
const urlMatch = /^url\((['"]?)(.*?)\1\)$/.exec(reference);
const url = urlMatch?.[2] ?? reference;
if (!url.startsWith('#')) {
return null;
}
const rootNode = element.getRootNode() as ParentNode;
const marker = typeof rootNode.querySelector === 'function' ? rootNode.querySelector(url) : null;
return marker instanceof SVGMarkerElement ? marker : null;
}
function _createMarkerInstanceGroup(sourceElement: SVGGraphicsElement, placement: MarkerPlacement): SVGGElement | null {
const marker = placement.marker;
if (!marker.childNodes.length) {
return null;
}
const markerScale = _getMarkerUnitsScale(sourceElement, marker);
const outerGroup = document.createElementNS('http://www.w3.org/2000/svg', 'g');
const transformParts = [
`translate(${placement.point.x} ${placement.point.y})`,
`rotate(${placement.angle})`
];
if (markerScale !== 1) {
transformParts.push(`scale(${markerScale})`);
}
outerGroup.setAttribute('transform', transformParts.join(' '));
const viewBoxTransform = _createViewBoxTransform(marker);
const mappedRefPoint = _mapMarkerPointToViewport(marker, viewBoxTransform, {
x: _getAnimatedLengthValue(marker.refX),
y: _getAnimatedLengthValue(marker.refY)
});
const refGroup = document.createElementNS('http://www.w3.org/2000/svg', 'g');
refGroup.setAttribute('transform', `translate(${-mappedRefPoint.x} ${-mappedRefPoint.y})`);
outerGroup.appendChild(refGroup);
const viewBoxGroup = document.createElementNS('http://www.w3.org/2000/svg', 'g');
if (viewBoxTransform) {
viewBoxGroup.setAttribute('transform', viewBoxTransform.transform);
}
refGroup.appendChild(viewBoxGroup);
for (const childNode of Array.from(marker.childNodes)) {
viewBoxGroup.appendChild(childNode.cloneNode(true));
}
return outerGroup;
}
function _getMarkerUnitsScale(sourceElement: SVGGraphicsElement, marker: SVGMarkerElement): number {
if (marker.getAttribute('markerUnits') === 'userSpaceOnUse') {
return 1;
}
const strokeWidth = parseFloat(getComputedStyle(sourceElement).strokeWidth);
return Number.isFinite(strokeWidth) && strokeWidth > 0 ? strokeWidth : 1;
}
function _createViewBoxTransform(marker: SVGMarkerElement): SvgViewBoxTransform | null {
const viewBox = marker.viewBox.baseVal;
if (viewBox == null || (viewBox.width === 0 && viewBox.height === 0)) {
return null;
}
const viewportWidth = _getAnimatedLengthValue(marker.markerWidth);
const viewportHeight = _getAnimatedLengthValue(marker.markerHeight);
if (!viewportWidth || !viewportHeight || !viewBox.width || !viewBox.height) {
return null;
}
const preserveAspectRatio = marker.preserveAspectRatio.baseVal;
let scaleX = viewportWidth / viewBox.width;
let scaleY = viewportHeight / viewBox.height;
let translateX = -viewBox.x * scaleX;
let translateY = -viewBox.y * scaleY;
if (preserveAspectRatio.align !== SVGPreserveAspectRatio.SVG_PRESERVEASPECTRATIO_NONE) {
const uniformScale = preserveAspectRatio.meetOrSlice === SVGPreserveAspectRatio.SVG_MEETORSLICE_SLICE
? Math.max(scaleX, scaleY)
: Math.min(scaleX, scaleY);
scaleX = uniformScale;
scaleY = uniformScale;
const extraWidth = viewportWidth - viewBox.width * uniformScale;
const extraHeight = viewportHeight - viewBox.height * uniformScale;
const { xAlign, yAlign } = _getAlignFactors(preserveAspectRatio.align);
translateX = -viewBox.x * uniformScale + extraWidth * xAlign;
translateY = -viewBox.y * uniformScale + extraHeight * yAlign;
}
return {
transform: `matrix(${scaleX} 0 0 ${scaleY} ${translateX} ${translateY})`,
scaleX,
scaleY,
translateX,
translateY,
};
}
function _mapMarkerPointToViewport(marker: SVGMarkerElement, viewBoxTransform: SvgViewBoxTransform | null, point: IPoint): IPoint {
if (!viewBoxTransform) {
return point;
}
return {
x: point.x * viewBoxTransform.scaleX + viewBoxTransform.translateX,
y: point.y * viewBoxTransform.scaleY + viewBoxTransform.translateY,
};
}
function _getAlignFactors(align: number): { xAlign: number; yAlign: number } {
switch (align) {
case SVGPreserveAspectRatio.SVG_PRESERVEASPECTRATIO_XMIDYMIN:
return { xAlign: 0.5, yAlign: 0 };
case SVGPreserveAspectRatio.SVG_PRESERVEASPECTRATIO_XMAXYMIN:
return { xAlign: 1, yAlign: 0 };
case SVGPreserveAspectRatio.SVG_PRESERVEASPECTRATIO_XMINYMID:
return { xAlign: 0, yAlign: 0.5 };
case SVGPreserveAspectRatio.SVG_PRESERVEASPECTRATIO_XMIDYMID:
return { xAlign: 0.5, yAlign: 0.5 };
case SVGPreserveAspectRatio.SVG_PRESERVEASPECTRATIO_XMAXYMID:
return { xAlign: 1, yAlign: 0.5 };
case SVGPreserveAspectRatio.SVG_PRESERVEASPECTRATIO_XMINYMAX:
return { xAlign: 0, yAlign: 1 };
case SVGPreserveAspectRatio.SVG_PRESERVEASPECTRATIO_XMIDYMAX:
return { xAlign: 0.5, yAlign: 1 };
case SVGPreserveAspectRatio.SVG_PRESERVEASPECTRATIO_XMAXYMAX:
return { xAlign: 1, yAlign: 1 };
default:
return { xAlign: 0, yAlign: 0 };
}
}
function _resolveMarkerAngle(marker: SVGMarkerElement, incomingAngle: number, outgoingAngle?: number, isStartMarker?: boolean): number {
const orient = marker.orientType.baseVal === SVGMarkerElement.SVG_MARKER_ORIENT_AUTO ? 'auto' : marker.getAttribute('orient')?.trim() ?? '0';
if (orient === 'auto' || orient === 'auto-start-reverse') {
let angle = outgoingAngle == null ? incomingAngle : _bisectAngles(incomingAngle, outgoingAngle);
if (isStartMarker && orient === 'auto-start-reverse') {
angle += 180;
}
return angle;
}
return _parseAngle(orient);
}
function _parseAngle(value: string): number {
if (value.endsWith('rad')) {
return parseFloat(value) * 180 / Math.PI;
}
if (value.endsWith('turn')) {
return parseFloat(value) * 360;
}
if (value.endsWith('grad')) {
return parseFloat(value) * 0.9;
}
return parseFloat(value);
}
function _bisectAngles(angleA: number, angleB: number): number {
const vectorA = { x: Math.cos(angleA * Math.PI / 180), y: Math.sin(angleA * Math.PI / 180) };
const vectorB = { x: Math.cos(angleB * Math.PI / 180), y: Math.sin(angleB * Math.PI / 180) };
const sum = { x: vectorA.x + vectorB.x, y: vectorA.y + vectorB.y };
if (Math.abs(sum.x) < 1e-10 && Math.abs(sum.y) < 1e-10) {
return angleB;
}
return Math.atan2(sum.y, sum.x) * 180 / Math.PI;
}
function _getFirstUsableAngle(points: IPoint[]): number | null {
for (let i = 0; i < points.length - 1; i++) {
const angle = _angleBetween(points[i], points[i + 1]);
if (angle != null) {
return angle;
}
}
return null;
}
function _getLastUsableAngle(points: IPoint[]): number | null {
for (let i = points.length - 1; i > 0; i--) {
const angle = _angleBetween(points[i - 1], points[i]);
if (angle != null) {
return angle;
}
}
return null;
}
function _angleBetween(start: IPoint, end: IPoint): number | null {
const dx = end.x - start.x;
const dy = end.y - start.y;
if (Math.abs(dx) < 1e-10 && Math.abs(dy) < 1e-10) {
return null;
}
return Math.atan2(dy, dx) * 180 / Math.PI;
}
function _pointsEqual(a: IPoint, b: IPoint): boolean {
return Math.abs(a.x - b.x) < 1e-10 && Math.abs(a.y - b.y) < 1e-10;
}
function _getAnimatedLengthValue(length: SVGAnimatedLength): number {
return length?.baseVal?.value ?? 0;
}
function _toElementReferenceBoxPoint(element: Element, point: IPoint): IPoint {
const bbox = _getSvgGeometryBBoxOffset(element);
return { x: point.x - bbox.x, y: point.y - bbox.y };
}
function _fromElementReferenceBoxPoint(element: Element, point: IPoint): IPoint {
const bbox = _getSvgGeometryBBoxOffset(element);
return { x: point.x + bbox.x, y: point.y + bbox.y };
}
function _getSvgGeometryBBoxOffset(element: Element): IPoint {
if (!(element instanceof SVGGraphicsElement) || element instanceof SVGSVGElement) {
return { x: 0, y: 0 };
}
const bbox = element.getBBox();
return { x: bbox.x, y: bbox.y };
}
function _getSvgOverlayTransform(element: Element, designerCanvas: IDesignerCanvas): SvgOverlayTransform | null {
if (!(element instanceof SVGGraphicsElement) || element instanceof SVGSVGElement) {
return null;
}
const bbox = element.getBBox();
if (Math.abs(bbox.width) < 1e-10 || Math.abs(bbox.height) < 1e-10) {
return null;
}
const quad = element.getBoxQuads({ relativeTo: designerCanvas.canvas, iframes: designerCanvas.iframes })[0];
if (!quad) {
return null;
}
const matrix = _createProjectiveMatrixForQuad(quad);
if (!matrix) {
return null;
}
const inverseMatrix = _invertProjectiveMatrix(matrix);
if (!inverseMatrix) {
return null;
}
return { bbox, matrix, inverseMatrix };
}
function _createProjectiveMatrixForQuad(quad: DOMQuad): ProjectiveMatrix | null {
const x1 = quad.p1.x;
const y1 = quad.p1.y;
const x2 = quad.p2.x;
const y2 = quad.p2.y;
const x3 = quad.p3.x;
const y3 = quad.p3.y;
const x4 = quad.p4.x;
const y4 = quad.p4.y;
const sx = x1 - x2 + x3 - x4;
const sy = y1 - y2 + y3 - y4;
const dx1 = x2 - x3;
const dy1 = y2 - y3;
const dx2 = x4 - x3;
const dy2 = y4 - y3;
let g = 0;
let h = 0;
if (Math.abs(sx) >= 1e-10 || Math.abs(sy) >= 1e-10) {
const denominator = dx1 * dy2 - dy1 * dx2;
if (Math.abs(denominator) < 1e-10) {
return null;
}
g = (sx * dy2 - sy * dx2) / denominator;
h = (dx1 * sy - dy1 * sx) / denominator;
}
return [
x2 - x1 + g * x2,
x4 - x1 + h * x4,
x1,
y2 - y1 + g * y2,
y4 - y1 + h * y4,
y1,
g,
h,
1,
];
}
function _applyProjectiveMatrix(matrix: ProjectiveMatrix, x: number, y: number): IPoint {
const projectedX = matrix[0] * x + matrix[1] * y + matrix[2];
const projectedY = matrix[3] * x + matrix[4] * y + matrix[5];
const projectedW = matrix[6] * x + matrix[7] * y + matrix[8];
const safeW = Math.abs(projectedW) < 1e-10 ? (projectedW < 0 ? -1e-10 : 1e-10) : projectedW;
return {
x: projectedX / safeW,
y: projectedY / safeW,
};
}
function _invertProjectiveMatrix(matrix: ProjectiveMatrix): ProjectiveMatrix | null {
const determinant =
matrix[0] * (matrix[4] * matrix[8] - matrix[5] * matrix[7]) -
matrix[1] * (matrix[3] * matrix[8] - matrix[5] * matrix[6]) +
matrix[2] * (matrix[3] * matrix[7] - matrix[4] * matrix[6]);
if (Math.abs(determinant) < 1e-10) {
return null;
}
const inverseDeterminant = 1 / determinant;
return [
(matrix[4] * matrix[8] - matrix[5] * matrix[7]) * inverseDeterminant,
(matrix[2] * matrix[7] - matrix[1] * matrix[8]) * inverseDeterminant,
(matrix[1] * matrix[5] - matrix[2] * matrix[4]) * inverseDeterminant,
(matrix[5] * matrix[6] - matrix[3] * matrix[8]) * inverseDeterminant,
(matrix[0] * matrix[8] - matrix[2] * matrix[6]) * inverseDeterminant,
(matrix[2] * matrix[3] - matrix[0] * matrix[5]) * inverseDeterminant,
(matrix[3] * matrix[7] - matrix[4] * matrix[6]) * inverseDeterminant,
(matrix[1] * matrix[6] - matrix[0] * matrix[7]) * inverseDeterminant,
(matrix[0] * matrix[4] - matrix[1] * matrix[3]) * inverseDeterminant,
];
}
================================================
FILE: packages/web-component-designer/src/elements/helper/SwitchContainerHelper.ts
================================================
import { IDesignItem } from '../item/IDesignItem.js';
import { NodeType } from '../item/NodeType.js';
export function switchContainer(items: IDesignItem[], newContainer: IDesignItem, resizeNewContainer: boolean = false, newContainerOffset: number = 0) {
//TODO: switch to other containers like grid, flexbox, ...
//TODO: position non absolute, or absolute from bottom or right
for (let i of items) {
if (i == newContainer || i.element.contains(newContainer.element)) {
console.warn('could not move items into of itself or a child');
return;
}
}
const firstItem = items[0];
const grp = firstItem.openGroup('switchContainerHelper');
const designerCanvas = firstItem.instanceServiceContainer.designerCanvas;
let minX = Number.MAX_VALUE;
let minY = Number.MAX_VALUE;
let maxX = 0;
let maxY = 0;
for (let e of items) {
let rect = designerCanvas.getNormalizedElementCoordinates(e.element);
if (rect.x < minX)
minX = rect.x;
if (rect.y < minY)
minY = rect.y;
if (rect.x + rect.width > maxX)
maxX = rect.x + rect.width;
if (rect.y + rect.height > maxY)
maxY = rect.y + rect.height;
}
let rectNewContainer = designerCanvas.getNormalizedElementCoordinates(newContainer.element);
for (let e of items) {
let rect = designerCanvas.getNormalizedElementCoordinates(e.element);
if (e.nodeType == NodeType.Element) {
//TODO: use container service or the helper wich this uses, not fixed left and top.
if (resizeNewContainer) {
e.setStyle('left', (rect.x - minX + newContainerOffset).toString() + 'px');
e.setStyle('top', (rect.y - minY + newContainerOffset).toString() + 'px');
} else {
e.setStyle('left', (rect.x - rectNewContainer.x).toString() + 'px');
e.setStyle('top', (rect.y - rectNewContainer.y).toString() + 'px');
}
}
newContainer.insertChild(e);
}
if (resizeNewContainer) {
newContainer.setStyle('position', 'absolute');
newContainer.setStyle('left', (minX - newContainerOffset).toString() + 'px');
newContainer.setStyle('top', (minY - newContainerOffset).toString() + 'px');
newContainer.setStyle('width', (maxX - minX + 2 * newContainerOffset).toString() + 'px');
newContainer.setStyle('height', (maxY - minY + 2 * newContainerOffset).toString() + 'px');
}
grp.commit();
}
================================================
FILE: packages/web-component-designer/src/elements/helper/TextHelper.ts
================================================
let canvas: HTMLCanvasElement;
/**
* Uses canvas.measureText to compute and return the width of the given text of given font in pixels.
*
* @param {String} text The text to be rendered.
* @param {String} font The css font descriptor that text is to be rendered with (e.g. "bold 14px verdana").
*
* @see https://stackoverflow.com/questions/118241/calculate-text-width-with-javascript/21015393#21015393
*/
export function getTextWidth(text: string, font: string) {
if (!canvas)
canvas = document.createElement("canvas");
const context = canvas.getContext("2d");
context.font = font;
const metrics = context.measureText(text);
return metrics.width;
}
export function getFont(el: Element) {
const fontWeight = getComputedStyle(el).fontWeight || 'normal';
const fontSize = getComputedStyle(el).fontSize || '16px';
const fontFamily = getComputedStyle(el).fontFamily || 'Times New Roman';
return `${fontWeight} ${fontSize} ${fontFamily}`;
}
================================================
FILE: packages/web-component-designer/src/elements/helper/TouchGestureHelper.ts
================================================
const panThreshold = 10;
const zoomThreshold = 10;
//const rotateThreshold = 0;
export class TouchGestureHelper {
public static addTouchEvents(element: HTMLElement) {
return new TouchGestureHelper(element);
}
private constructor(element: HTMLElement) {
this._target = element;
element.addEventListener('touchstart', (e) => this._touchStart(e));
element.addEventListener('touchmove', (e) => this._touchMove(e));
element.addEventListener('touchend', (e) => this._touchEnd(e));
element.addEventListener('touchcancel', (e) => this._touchEnd(e));
}
private _target: HTMLElement;
private _started: boolean;
private _startX_0: number;
private _startY_0: number;
//private _startX_1: number;
//private _startY_1: number;
private _lastZoom: number;
private _lastPanDistanceX: number;
private _lastPanDistanceY: number;
private _startZoomDistance: number;
public multitouchEventActive: boolean;
private _mode: 'pan' | 'zoom' | 'rotate' = null;
_touchStart(e: TouchEvent) {
if (e.touches.length === 2) {
this.multitouchEventActive = true;
this._mode = null;
this._started = true;
this._startX_0 = e.touches[0].screenX;
this._startY_0 = e.touches[0].screenY;
//this._startX_1 = e.touches[1].screenX;
//this._startY_1 = e.touches[1].screenY;
this._lastZoom = 0;
this._lastPanDistanceX = 0;
this._lastPanDistanceY = 0;
this._startZoomDistance = Math.hypot(
e.touches[0].screenX - e.touches[1].screenX,
e.touches[0].screenY - e.touches[1].screenY);
} else {
this.multitouchEventActive = false;
this._started = false;
}
}
_touchMove(e: TouchEvent) {
if (e.touches.length !== 2) {
this.multitouchEventActive = false;
this._started = false;
}
if (this._started) {
e.preventDefault();
let newZoomDistance = Math.hypot(
e.touches[0].screenX - e.touches[1].screenX,
e.touches[0].screenY - e.touches[1].screenY);
const newPanDistanceX = this._startX_0 - e.touches[0].screenX;
const newPanDistanceY = this._startY_0 - e.touches[0].screenY;
const panDiffX = newPanDistanceX - this._lastPanDistanceX;
const panDiffY = newPanDistanceY - this._lastPanDistanceY;
this._lastPanDistanceX = newPanDistanceX;
this._lastPanDistanceY = newPanDistanceY;
const zoom = newZoomDistance - this._startZoomDistance;
const zoomDiff = zoom - this._lastZoom;
this._lastZoom = zoom;
this._lastZoom
if (!this._mode) {
if (Math.abs(zoom) > zoomThreshold) {
this._mode = 'zoom'
}
if (Math.abs(newPanDistanceX) > panThreshold || Math.abs(newPanDistanceY) > panThreshold) {
this._mode = 'pan'
}
}
if (this._mode) {
if (this._mode == 'zoom') {
const event = new CustomEvent("zoom", { detail: { factor: zoom, diff: zoomDiff } });
this._target.dispatchEvent(event);
} else if (this._mode == 'pan') {
const event = new CustomEvent("pan", { detail: { x: newPanDistanceX, deltaX: panDiffX, y: newPanDistanceY, deltaY: panDiffY } });
this._target.dispatchEvent(event);
}
}
}
}
_touchEnd(e: TouchEvent) {
this.multitouchEventActive = false;
if (e.touches.length !== 2) {
}
}
}
================================================
FILE: packages/web-component-designer/src/elements/helper/TransformHelper.ts
================================================
let identityMatrix: number[] = [
1, 0, 0, 0,
0, 1, 0, 0,
0, 0, 1, 0,
0, 0, 0, 1
];
export function combineTransforms(element: HTMLElement, actualTransforms: string, requestedTransformation: string) {
if (actualTransforms == null || actualTransforms == '') {
element.style.transform = requestedTransformation;
return;
}
const actualTransformationMatrix = new DOMMatrix(actualTransforms);
const requestedTransformationMatrix = new DOMMatrix(requestedTransformation);
const newTransformationMatrix = requestedTransformationMatrix.multiply(actualTransformationMatrix);
element.style.transform = newTransformationMatrix.toString();
}
export function transformPointByInverseMatrix(point: DOMPoint, matrix: DOMMatrix) {
const inverse = matrix.inverse();
return point.matrixTransform(inverse);
}
export function getRotationMatrix3d(axisOfRotation: 'x' | 'y' | 'z' | 'X' | 'Y' | 'Z', angle: number) {
const angleInRadians = angle / 180 * Math.PI;
const sin = Math.sin;
const cos = Math.cos;
let rotationMatrix3d = [];
switch (axisOfRotation.toLowerCase()) {
case 'x':
rotationMatrix3d = [
1, 0, 0, 0,
0, cos(angleInRadians), -sin(angleInRadians), 0,
0, sin(angleInRadians), cos(angleInRadians), 0,
0, 0, 0, 1
];
break;
case 'y':
rotationMatrix3d = [
cos(angleInRadians), 0, sin(angleInRadians), 0,
0, 1, 0, 0,
-sin(angleInRadians), 0, cos(angleInRadians), 0,
0, 0, 0, 1
];
break;
case 'z':
rotationMatrix3d = [
cos(angleInRadians), -sin(angleInRadians), 0, 0,
sin(angleInRadians), cos(angleInRadians), 0, 0,
0, 0, 1, 0,
0, 0, 0, 1
];
break;
default:
rotationMatrix3d = null;
break;
}
return rotationMatrix3d;
}
export function rotateElementByMatrix3d(element: HTMLElement, matrix: number[]) {
element.style.transform = "matrix3d(" + matrix.join(',') + ")"
}
//maybe remove -> refactor rotate extension
export function cssMatrixToMatrixArray(cssMatrix: string) {
if (!cssMatrix.includes('matrix')) {
if (cssMatrix != 'none')
console.error('cssMatrixToMatrixArray: no css matrix passed');
return identityMatrix;
}
let matrixArray: number[] = cssMatrix.match(/^matrix.*\((.*)\)/)[1].split(',').map(Number);
return matrixArray;
}
export function getRotationAngleFromMatrix(matrixArray: number[], domMatrix: DOMMatrix) {
let angle = null;
const a = domMatrix != null ? domMatrix.a : matrixArray[0];
const b = domMatrix != null ? domMatrix.b : matrixArray[1];
angle = Math.round(Math.atan2(b, a) * (180 / Math.PI));
return angle;
}
export function extractTranslationFromDOMMatrix(matrix: DOMMatrix): DOMPoint {
//TODO: maybe we also need m43 here??
return new DOMPoint(matrix.m41, matrix.m42, 0, 0);
}
export function extractRotationAngleFromDOMMatrix(matrix: DOMMatrix): number {
return getRotationAngleFromMatrix(null, matrix);
}
================================================
FILE: packages/web-component-designer/src/elements/helper/XmlHelper.ts
================================================
export function encodeXMLChars(value: string) {
return value.replace(/&/g, '&')
.replace(//g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''');
};
export function decodeXMLChars(value: string) {
return value.replace(/'/g, "'")
.replace(/"/g, '"')
.replace(/>/g, '>')
.replace(/</g, '<')
.replace(/&/g, '&');
};
================================================
FILE: packages/web-component-designer/src/elements/helper/contextMenu/ContextMenu.ts
================================================
import { css } from '@node-projects/base-custom-webcomponent';
import { IContextMenu, IContextMenuItem } from './IContextMenuItem.js';
export interface IContextMenuOptions {
defaultIcon?: string,
subIcon?: string,
mouseOffset?: number,
shadowRoot?: ShadowRoot | Document,
mode?: 'normal' | 'undo'
}
export class ContextMenu implements IContextMenu {
private static _contextMenuCss = css`
.context_menu {
position: fixed;
inset: auto;
margin: 0;
border: none;
background: transparent;
overflow: visible;
opacity: 0;
transform: scale(0);
transition: transform 0.1s;
transform-origin: top left;
padding: 0;
z-index: 2147483647;
color: black;
}
.context_menu.context_menu_display {
opacity: 1;
transform: scale(1);
}
.context_menu,
.context_menu * {
box-sizing: border-box;
}
.context_menu * {
position: relative;
}
.context_menu ul {
list-style-type: none;
padding: 3px;
margin: 0;
border: none;
background-color: #f5f7f7;
box-shadow: 0 0 5px #333;
max-inline-size: calc(100vw - 8px);
max-block-size: calc(100vh - 8px);
overflow: auto;
overscroll-behavior: contain;
isolation: isolate;
}
.context_menu ul[popover] {
inset: auto;
margin: 0;
border: none;
}
.context_menu li {
padding: 0;
padding-right: 1.7em;
cursor: pointer;
white-space: nowrap;
display: flex;
align-items: center;
}
.context_menu li:hover {
background-color: #bbb;
}
.context_menu li .context_menu_icon_span {
width: 28px;
display: inline-flex;
align-items: center;
justify-content: center;
}
.context_menu li .context_menu_icon_span img {
height: 18px;
}
.context_menu li .context_menu_text {
padding-left: 2px;
vertical-align: middle;
}
.context_menu li .context_menu_sub_span {
width: 1em;
display: inline-block;
text-align: center;
position: absolute;
top: 50%;
right: 0.5em;
transform: translateY(-50%);
}
.context_menu li>ul {
position: absolute;
inset: auto;
top: 0;
left: 100%;
opacity: 0;
transition: opacity 0.2s;
visibility: hidden;
}
.context_menu li>ul.context_menu_submenu_popover {
position: fixed;
}
.context_menu li>ul:popover-open {
opacity: 1;
visibility: visible;
}
.context_menu li.context_menu_divider {
border-bottom: 1px solid #aaa;
margin: 5px;
padding: 0;
cursor: default;
}
.context_menu li.context_menu_divider:hover {
background-color: inherit;
}
.context_menu li[disabled=""] {
color: #777;
cursor: default;
}
.context_menu li[disabled=""]:hover {
background-color: inherit;
}
.context_menu li.context_menu_marked {
background-color: #5ebdec;
}`;
static count = 0;
private static _openedContextMenus = new Set();
menu: IContextMenuItem[];
public options?: IContextMenuOptions;
public context: any
private num: number;
private _menuElement!: HTMLDivElement;
constructor(menu: IContextMenuItem[], options?: IContextMenuOptions, context?: any) {
this.num = ContextMenu.count++;
this.menu = menu;
this.options = options;
this.context = context;
this.reload();
this._windowDown = this._windowDown.bind(this);
this._windowKeyUp = this._windowKeyUp.bind(this);
this._windowResize = this._windowResize.bind(this);
}
reload() {
let shadowRoot = this.options?.shadowRoot ?? document;
if (this._menuElement == null) {
this._menuElement = document.createElement("div");
this._menuElement.className = "context_menu";
this._menuElement.id = "context_menu_" + this.num;
this._menuElement.setAttribute('popover', 'manual');
if (shadowRoot === document)
document.body.appendChild(this._menuElement);
else
shadowRoot.appendChild(this._menuElement);
}
this._menuElement.innerHTML = "";
if (shadowRoot.adoptedStyleSheets.indexOf(ContextMenu._contextMenuCss) < 0) {
shadowRoot.adoptedStyleSheets = [...shadowRoot.adoptedStyleSheets, ContextMenu._contextMenuCss];
}
this._menuElement.appendChild(this.renderLevel(this.menu));
}
renderLevel(level: IContextMenuItem[]) {
let ul_outer = document.createElement("ul");
let addDivider = false;
level.forEach((item) => {
if (item.title !== '-') {
if (addDivider) {
let li = document.createElement("li");
li.className = "context_menu_divider";
addDivider = false;
ul_outer.appendChild(li);
}
let li = document.createElement("li");
let icon_span = document.createElement("span");
icon_span.className = 'context_menu_icon_span';
if (item.checked === true) {
icon_span.innerHTML = '✔';
} else if ((item.icon ?? '') != '') {
icon_span.innerHTML = item.icon ?? '';
} else {
icon_span.innerHTML = this.options?.defaultIcon ?? '';
}
let text_span = document.createElement("span");
text_span.className = 'context_menu_text';
text_span.innerHTML = item.title ?? '';
let sub_span = document.createElement("span");
sub_span.className = 'context_menu_sub_span';
if (item.children != null) {
sub_span.innerHTML = this.options?.subIcon ?? '';
}
li.appendChild(icon_span);
li.appendChild(text_span);
li.appendChild(sub_span);
if (item.disabled) {
li.setAttribute("disabled", "");
} else {
if (item.checkable) {
li.addEventListener('click', (e) => {
e.stopPropagation();
e.preventDefault();
item.checked = !item.checked;
icon_span.innerHTML = item.checked ? '✔' : (item.icon ?? this.options?.defaultIcon ?? '');
if (item.action)
item.action(e, item, this.context, this);
});
} else if (item.action)
li.addEventListener('click', (e) => {
e.stopPropagation();
e.preventDefault();
item.action?.(e, item, this.context, this);
this.close();
});
if (this.options?.mode == 'undo') {
li.addEventListener('mouseup', (e) => {
e.stopPropagation();
item.action?.(e, item, this.context, this);
this.close();
});
}
li.addEventListener('mouseenter', () => {
this.closeSiblingSubmenus(li);
if (this.options?.mode == 'undo') {
this.markUndoItems(li);
}
});
if (item.children != null) {
let childmenu = this.renderLevel(item.children);
this.configurePopoverSubmenu(childmenu);
li.appendChild(childmenu);
li.addEventListener('mouseenter', () => {
this.openPopoverSubmenu(li, childmenu);
});
}
}
ul_outer.appendChild(li);
} else {
addDivider = true;
}
});
return ul_outer;
}
public display(event: MouseEvent) {
let menu = this._menuElement;
let mouseOffset = this.options?.mouseOffset != null ? this.options.mouseOffset : 2;
this.showPopover(menu);
this.positionMenu(menu, event.clientX, event.clientY, mouseOffset);
menu.classList.add("context_menu_display");
event.preventDefault();
ContextMenu._openedContextMenus.add(this);
window.addEventListener("keyup", this._windowKeyUp);
window.addEventListener("resize", this._windowResize);
window.addEventListener("mousedown", this._windowDown);
setTimeout(() => {
if (!ContextMenu._openedContextMenus.has(this))
return;
window.addEventListener("contextmenu", this._windowDown);
}, 150);
}
_windowResize() {
this.close();
}
_windowDown(e: MouseEvent) {
e.preventDefault();
if (!(e.target instanceof Node) || !this._menuElement.contains(e.target))
this.close();
return false;
}
_windowKeyUp(e: KeyboardEvent) {
if (e.key === 'Escape') {
this.close();
}
}
static show(menu: IContextMenuItem[], event: MouseEvent, options?: IContextMenuOptions, context?: any) {
let ctxMenu = new ContextMenu(menu, options, context);
ctxMenu.display(event);
return ctxMenu;
}
close() {
this.hideDescendantSubmenus(this._menuElement);
this.hidePopover(this._menuElement);
this._menuElement.remove();
window.removeEventListener("mousedown", this._windowDown);
window.removeEventListener("resize", this._windowResize);
window.removeEventListener("keyup", this._windowKeyUp);
setTimeout(() => window.removeEventListener("contextmenu", this._windowDown), 10);
ContextMenu._openedContextMenus.delete(this);
}
static closeAll() {
for (const c of ContextMenu._openedContextMenus.values())
c.close();
}
private configurePopoverSubmenu(childmenu: HTMLUListElement) {
childmenu.classList.add('context_menu_submenu_popover');
childmenu.setAttribute('popover', 'manual');
}
private markUndoItems(li: HTMLLIElement) {
if (li.parentElement == null)
return;
let select = true;
for (let node of li.parentElement.children) {
if (select)
(node).classList.add('context_menu_marked')
else
(node).classList.remove('context_menu_marked')
if (node == li)
select = false
}
}
private closeSiblingSubmenus(li: HTMLLIElement) {
if (li.parentElement == null)
return;
for (const node of li.parentElement.children) {
if (node !== li) {
this.hideDescendantSubmenus(node as HTMLElement);
}
}
}
private hideDescendantSubmenus(element: HTMLElement) {
const submenus = element.querySelectorAll('ul[popover]');
for (const submenu of submenus) {
this.hidePopover(submenu as HTMLElement);
}
}
private openPopoverSubmenu(li: HTMLLIElement, childmenu: HTMLUListElement) {
this.showPopover(childmenu);
this.positionSubmenuPopover(li, childmenu);
}
private showPopover(element: HTMLElement) {
if (this.isPopoverOpen(element))
return;
element.showPopover();
}
private hidePopover(element?: HTMLElement) {
if (element == null || !this.isPopoverOpen(element))
return;
element.hidePopover();
}
private isPopoverOpen(element: HTMLElement) {
return element.matches(':popover-open');
}
private positionMenu(menu: HTMLDivElement, clickCoordsX: number, clickCoordsY: number, mouseOffset: number) {
const menuWidth = menu.offsetWidth + 4;
const menuHeight = menu.offsetHeight + 4;
const spaceRight = window.innerWidth - clickCoordsX;
const spaceLeft = clickCoordsX;
const spaceBelow = window.innerHeight - clickCoordsY;
const spaceAbove = clickCoordsY;
let left = clickCoordsX + mouseOffset;
if (spaceRight < menuWidth && spaceLeft > spaceRight) {
left = clickCoordsX - menuWidth - mouseOffset;
}
let top = clickCoordsY + mouseOffset;
if (spaceBelow < menuHeight && spaceAbove > spaceBelow) {
top = clickCoordsY - menuHeight - mouseOffset;
}
menu.style.left = `${Math.max(0, Math.min(left, window.innerWidth - menuWidth))}px`;
menu.style.top = `${Math.max(0, Math.min(top, window.innerHeight - menuHeight))}px`;
}
private positionSubmenuPopover(li: HTMLLIElement, childmenu: HTMLUListElement) {
const parentRect = li.getBoundingClientRect();
const childRect = childmenu.getBoundingClientRect();
const menuWidth = childmenu.offsetWidth || childRect.width;
const menuHeight = childmenu.offsetHeight || childRect.height;
const spaceRight = window.innerWidth - parentRect.right;
const spaceLeft = parentRect.left;
let left = parentRect.right;
if (spaceRight < menuWidth && spaceLeft > spaceRight) {
left = parentRect.left - menuWidth;
}
const spaceBelow = window.innerHeight - parentRect.top;
const spaceAbove = parentRect.bottom;
let top = parentRect.top;
if (spaceBelow < menuHeight && spaceAbove > spaceBelow) {
top = parentRect.bottom - menuHeight;
}
childmenu.style.left = `${Math.max(0, Math.min(left, window.innerWidth - menuWidth))}px`;
childmenu.style.top = `${Math.max(0, Math.min(top, window.innerHeight - menuHeight))}px`;
childmenu.style.right = 'auto';
childmenu.style.bottom = 'auto';
}
}
================================================
FILE: packages/web-component-designer/src/elements/helper/contextMenu/IContextMenuItem.ts
================================================
export interface IContextMenu {
close: () => void;
};
export interface IContextMenuItem {
readonly id?: string,
readonly title?: string,
readonly icon?: string,
readonly children?: IContextMenuItem[],
readonly disabled?: boolean,
readonly shortCut?: string;
readonly checkable?: boolean;
checked?: boolean;
action?: (event: MouseEvent, item: IContextMenuItem, context?: any, menu?: IContextMenu) => void
};
================================================
FILE: packages/web-component-designer/src/elements/helper/getBoxQuads.global.d.ts
================================================
export { };
declare global {
interface Node {
convertQuadFromNode(quad: DOMQuadInit, from: Element, options?: { fromBox?: 'margin' | 'border' | 'padding' | 'content', toBox?: 'margin' | 'border' | 'padding' | 'content', iframes?: HTMLIFrameElement[] }): DOMQuad
convertRectFromNode(rect: {x: number, y: number, width: number, height: number}, from: Element, options?: { fromBox?: 'margin' | 'border' | 'padding' | 'content', toBox?: 'margin' | 'border' | 'padding' | 'content', iframes?: HTMLIFrameElement[] }): DOMQuad
convertPointFromNode(point: DOMPointInit, from: Element, options?: { fromBox?: 'margin' | 'border' | 'padding' | 'content', toBox?: 'margin' | 'border' | 'padding' | 'content', iframes?: HTMLIFrameElement[] }): DOMPoint
getBoxQuads(options?: { box?: 'margin' | 'border' | 'padding' | 'content', relativeTo?: Element, iframes?: HTMLIFrameElement[] }): DOMQuad[]
}
}
================================================
FILE: packages/web-component-designer/src/elements/helper/getBoxQuads.js
================================================
//todo:
//transform-box (SVGs) https://developer.mozilla.org/en-US/docs/Web/CSS/transform-box
// =============================================================================
// PERFORMANCE FIXES APPLIED (14 total):
//
// FIX 1 – getElementCombinedTransform: check parent perspective inside fast-path
// so identity elements return immediately on non-3D pages.
// FIX 2 – getResultingTransformationBetweenElementAndAllAncestors: guard initial
// self-transform multiply with isIdentity check.
// FIX 3 – All translate matrix calls: skip new DOMMatrix().translateSelf(0,0)
// when both offsets are zero (hot loop, flat layouts).
// FIX 4 – getElementOffsetsInContainer/HTMLElement: defer getCachedComputedStyle
// until inside the `includeScroll` branch where it's actually needed.
// FIX 5 – getResultingTransformationBetweenElementAndAllAncestors: guard
// projectTo2D calls with !is2D check to skip style reads on 2D pages.
// FIX 6 – getResultingTransformationBetweenElementAndAllAncestors: cache the
// result on the early-return (ancestor found) path, not just fallthrough.
// FIX 7 – getBoxQuads: reuse the Range already created for the multi-fragment
// text check; pass clientRects/boundingRect into getElementSize to
// avoid creating a second Range for Text nodes.
// FIX 8 – getBoxQuads: split the per-point `!o` branch out of the loop into
// two separate loops to eliminate the branch test on every iteration.
// FIX 9 – getBoxQuads: hoist parseFloat calls for box offsets (margin/padding/
// content) so values are computed once, not 4x inside the loop.
// FIX 10 – getElementCombinedTransform: check hasTransform first (most common
// non-identity case) so cheaper checks fire before heavier ones.
// FIX 11 – getElementCombinedTransform: skip transform-origin wrap/unwrap when
// origin is (0, 0, 0), saving two DOMMatrix allocations.
// FIX 12 – getResultingTransformationBetweenElementAndAllAncestors: reuse
// getElementCombinedTransform result across loop iterations (carry
// parent transform forward instead of recomputing next iteration).
// FIX 13 – Repeated cross-realm instanceof checks: cache constructors from the
// node's window at function entry to avoid repeated property lookups.
// (Applied in getElementSize and getBoxQuads hot paths.)
// FIX 14 – transformPointBox: parse style values once per call, not once per
// property access (avoids repeated parseFloat on shared string props).
// =============================================================================
/**
* @param {globalThis} windowObj?
* @param {boolean=} force
*/
export function addPolyfill(windowObj = window, force = false) {
windowObj.__getBoxQuadsPolyfillFns ??= {};
windowObj.__getBoxQuadsPolyfillFns.getBoxQuads = getBoxQuads;
windowObj.__getBoxQuadsPolyfillFns.convertQuadFromNode = convertQuadFromNode;
windowObj.__getBoxQuadsPolyfillFns.convertRectFromNode = convertRectFromNode;
windowObj.__getBoxQuadsPolyfillFns.convertPointFromNode = convertPointFromNode;
if (force || !windowObj.Node.prototype.getBoxQuads) {
//@ts-ignore
windowObj.Node.prototype.getBoxQuads = function (options) {
return windowObj.__getBoxQuadsPolyfillFns.getBoxQuads(this, options)
}
}
if (force || !windowObj.Node.prototype.convertQuadFromNode) {
//@ts-ignore
windowObj.Node.prototype.convertQuadFromNode = function (quad, from, options) {
return windowObj.__getBoxQuadsPolyfillFns.convertQuadFromNode(this, quad, from, options)
}
}
if (force || !windowObj.Node.prototype.convertRectFromNode) {
//@ts-ignore
windowObj.Node.prototype.convertRectFromNode = function (rect, from, options) {
return windowObj.__getBoxQuadsPolyfillFns.convertRectFromNode(this, rect, from, options)
}
}
if (force || !windowObj.Node.prototype.convertPointFromNode) {
//@ts-ignore
windowObj.Node.prototype.convertPointFromNode = function (point, from, options) {
return windowObj.__getBoxQuadsPolyfillFns.convertPointFromNode(this, point, from, options)
}
}
}
/**
* @param {globalThis} windowObj?
*/
export function patchAdoptNode(windowObj = window) {
if (!windowObj.Node.prototype.getBoxQuads) {
//@ts-ignore
windowObj.Node.prototype.getBoxQuads = function (options) {
return getBoxQuads(this, options)
}
}
if (!windowObj.Node.prototype.convertQuadFromNode) {
//@ts-ignore
windowObj.Node.prototype.convertQuadFromNode = function (quad, from, options) {
return convertQuadFromNode(this, quad, from, options)
}
}
if (!windowObj.Node.prototype.convertRectFromNode) {
//@ts-ignore
windowObj.Node.prototype.convertRectFromNode = function (rect, from, options) {
return convertRectFromNode(this, rect, from, options)
}
}
if (!windowObj.Node.prototype.convertPointFromNode) {
//@ts-ignore
windowObj.Node.prototype.convertPointFromNode = function (point, from, options) {
return convertPointFromNode(this, point, from, options)
}
}
}
/**
* @param {Node} node
* @param {DOMQuadInit} quad
* @param {Element} from
* @param {{fromBox?: 'margin'|'border'|'padding'|'content', toBox?: 'margin'|'border'|'padding'|'content', iframes?: HTMLIFrameElement[]}=} options
* @returns {DOMQuad}
*/
export function convertQuadFromNode(node, quad, from, options) {
const ancestor = (node.ownerDocument.defaultView ?? window).document.body;
const m1 = getResultingTransformationBetweenElementAndAllAncestors(from, ancestor, options?.iframes, true);
const m2 = getResultingTransformationBetweenElementAndAllAncestors(node, ancestor, options?.iframes, true).inverse();
if (options?.fromBox && options?.fromBox !== 'border') {
const fromStyle = getCachedComputedStyle(from);
quad = new DOMQuad(transformPointBox(quad.p1, options.fromBox, fromStyle, -1), transformPointBox(quad.p2, options.fromBox, fromStyle, -1), transformPointBox(quad.p3, options.fromBox, fromStyle, -1), transformPointBox(quad.p4, options.fromBox, fromStyle, -1))
}
let res = new DOMQuad(m2.transformPoint(m1.transformPoint(quad.p1)), m2.transformPoint(m1.transformPoint(quad.p2)), m2.transformPoint(m1.transformPoint(quad.p3)), m2.transformPoint(m1.transformPoint(quad.p4)));
if (options?.toBox && options?.toBox !== 'border' && (node instanceof Element || node instanceof (node.ownerDocument.defaultView ?? window).Element)) {
const nodeStyle = getCachedComputedStyle(node);
res = new DOMQuad(transformPointBox(res.p1, options.toBox, nodeStyle, -1), transformPointBox(res.p2, options.toBox, nodeStyle, -1), transformPointBox(res.p3, options.toBox, nodeStyle, -1), transformPointBox(res.p4, options.toBox, nodeStyle, -1))
}
return res;
}
/**
* @param {Node} node
* @param {{x: number, y: number, width: number, height: number}} rect
* @param {Element} from
* @param {{fromBox?: 'margin'|'border'|'padding'|'content', toBox?: 'margin'|'border'|'padding'|'content', iframes?: HTMLIFrameElement[]}=} options
* @returns {DOMQuad}
*/
export function convertRectFromNode(node, rect, from, options) {
const ancestor = (node.ownerDocument.defaultView ?? window).document.body.parentElement;
const m1 = getResultingTransformationBetweenElementAndAllAncestors(from, ancestor, options?.iframes, true);
const m2 = getResultingTransformationBetweenElementAndAllAncestors(node, ancestor, options?.iframes, true).inverse();
if (options?.fromBox && options?.fromBox !== 'border') {
const p = transformPointBox(new DOMPoint(rect.x, rect.y), options.fromBox, getCachedComputedStyle(from), 1);
rect = new DOMRect(p.x, p.y, rect.width, rect.height);
}
let res = new DOMQuad(m2.transformPoint(m1.transformPoint(new DOMPoint(rect.x, rect.y))), m2.transformPoint(m1.transformPoint(new DOMPoint(rect.x + rect.width, rect.y))), m2.transformPoint(m1.transformPoint(new DOMPoint(rect.x + rect.width, rect.y + rect.height))), m2.transformPoint(m1.transformPoint(new DOMPoint(rect.x, rect.y + rect.height))));
if (options?.toBox && options?.toBox !== 'border' && (node instanceof Element || node instanceof (node.ownerDocument.defaultView ?? window).Element)) {
const nodeStyle = getCachedComputedStyle(node);
res = new DOMQuad(transformPointBox(res.p1, options.toBox, nodeStyle, -1), transformPointBox(res.p2, options.toBox, nodeStyle, -1), transformPointBox(res.p3, options.toBox, nodeStyle, -1), transformPointBox(res.p4, options.toBox, nodeStyle, -1))
}
return res;
}
/**
* @param {Node} node
* @param {DOMPointInit} point
* @param {Element} from
* @param {{fromBox?: 'margin'|'border'|'padding'|'content', toBox?: 'margin'|'border'|'padding'|'content', iframes?: HTMLIFrameElement[]}=} options
* @returns {DOMPoint}
*/
export function convertPointFromNode(node, point, from, options) {
const ancestor = (node.ownerDocument.defaultView ?? window).document.body.parentElement;
const m1 = getResultingTransformationBetweenElementAndAllAncestors(from, ancestor, options?.iframes, true);
const m2 = getResultingTransformationBetweenElementAndAllAncestors(node, ancestor, options?.iframes, true).inverse();
if (options?.fromBox && options?.fromBox !== 'border') {
point = transformPointBox(point, options.fromBox, getCachedComputedStyle(from), 1);
}
let res = m2.transformPoint(m1.transformPoint(point));
if (options?.toBox && options?.toBox !== 'border' && (node instanceof Element || node instanceof (node.ownerDocument.defaultView ?? window).Element)) {
res = transformPointBox(res, options.toBox, getCachedComputedStyle(node), -1);
}
return res;
}
/**
* @param {DOMPointInit} point
* @param {'margin'|'border'|'padding'|'content'} box
* @param {CSSStyleDeclaration} style
* @param {number} operator
* @returns {DOMPoint}
*
* FIX 14: Parse each style value once and reuse, rather than calling parseFloat
* multiple times on the same property (e.g. borderLeftWidth used twice in 'content').
*/
function transformPointBox(point, box, style, operator) {
if (box === 'margin') {
const mLeft = parseFloat(style.marginLeft);
const mTop = parseFloat(style.marginTop);
return new DOMPoint(point.x - operator * mLeft, point.y - operator * mTop);
} else if (box === 'padding') {
const bLeft = parseFloat(style.borderLeftWidth);
const bTop = parseFloat(style.borderTopWidth);
return new DOMPoint(point.x + operator * bLeft, point.y + operator * bTop);
} else if (box === 'content') {
const bLeft = parseFloat(style.borderLeftWidth);
const bTop = parseFloat(style.borderTopWidth);
const pLeft = parseFloat(style.paddingLeft);
const pTop = parseFloat(style.paddingTop);
return new DOMPoint(point.x + operator * (bLeft + pLeft), point.y + operator * (bTop + pTop));
}
//@ts-ignore
return point;
}
/** @type { WeakMap } */
let hash;
/** @type { Map } */
let boxQuadsCache;
/** @type { Map } */
let transformCache;
/** @type { WeakMap } */
let computedStyleCache;
let hashId = 0;
export function clearCache() {
boxQuadsCache.clear();
transformCache.clear();
computedStyleCache = new WeakMap();
}
export function useCache() {
hash = new WeakMap();
boxQuadsCache = new Map();
transformCache = new Map();
computedStyleCache = new WeakMap();
}
/**
* @param {Element} element
* @returns {CSSStyleDeclaration}
*/
function getCachedComputedStyle(element) {
if (!computedStyleCache) {
return (element.ownerDocument.defaultView ?? window).getComputedStyle(element);
}
let style = computedStyleCache.get(element);
if (!style) {
style = (element.ownerDocument.defaultView ?? window).getComputedStyle(element);
computedStyleCache.set(element, style);
}
return style;
}
/**
* @param {Node} node
* @returns {boolean}
*/
function isElementNode(node) {
return !!node && node.nodeType === Node.ELEMENT_NODE;
}
/**
* @param {Node} element
* @returns {number}
*/
function getElementZoom(element) {
if (!isElementNode(element)) {
return 1;
}
/** @type {Element} */
// @ts-ignore
const actualElement = element;
const zoom = getCachedComputedStyle(actualElement).zoom;
if (!zoom || zoom === 'normal') {
return 1;
}
if (zoom.endsWith('%')) {
const percentage = parseFloat(zoom);
return Number.isFinite(percentage) && percentage > 0 ? percentage / 100 : 1;
}
const value = parseFloat(zoom);
return Number.isFinite(value) && value > 0 ? value : 1;
}
/**
* `zoom` scales the element's internal coordinate space while also shifting the
* element within its parent from the top-center anchor in the default writing-mode.
* For descendant coordinates we keep the scale in the matrix pipeline and apply
* the parent-position shift separately in the layout translation step.
* @param {Node} element
* @returns {DOMMatrix}
*/
function getElementZoomScaleTransform(element) {
if (!isElementNode(element)) {
return new DOMMatrix();
}
const zoom = getElementZoom(element);
if (zoom === 1) {
return new DOMMatrix();
}
return new DOMMatrix().scaleSelf(zoom);
}
/**
* @param {Node} element
* @param {HTMLIFrameElement[]=} iframes
* @param {boolean=} includeZoom
* @returns {DOMMatrix}
*/
function getElementTransformWithZoom(element, iframes, includeZoom = true) {
const transform = getElementCombinedTransform(element, iframes);
if (!includeZoom || !isElementNode(element)) {
return transform;
}
const zoomTransform = getElementZoomScaleTransform(element);
if (zoomTransform.isIdentity) {
return transform;
}
// `zoom` scales transform translations as well as the element's local axes,
// so it needs to wrap the transform matrix instead of being appended to it.
return zoomTransform.multiply(transform);
}
/**
* @param {Node} node
* @param {{box?: 'margin'|'border'|'padding'|'content', relativeTo?: Element, iframes?: HTMLIFrameElement[]}=} options
* @returns {DOMQuad[]}
*/
export function getBoxQuads(node, options) {
const defaultRelativeTo = node.ownerDocument.documentElement ?? node.ownerDocument.body;
const relativeTo = options?.relativeTo ?? defaultRelativeTo;
let key;
if (boxQuadsCache) {
let i1 = hash.get(node);
if (i1 === undefined)
hash.set(node, i1 = hashId++);
let i2 = hash.get(relativeTo);
if (i2 === undefined)
hash.set(relativeTo, i2 = hashId++);
key = i1 + '_' + i2 + '_' + (options?.box ?? 'border');
const q = boxQuadsCache.get(key);
if (q)
return q;
}
/** @type {DOMMatrix} */
let originalElementAndAllParentsMultipliedMatrix = getResultingTransformationBetweenElementAndAllAncestors(node, relativeTo, options?.iframes);
// FIX 13: Cache cross-realm constructors once per call.
const win = node.ownerDocument.defaultView ?? window;
const _Text = win.Text;
const _SVGGraphicsElement = win.SVGGraphicsElement;
const _SVGSVGElement = win.SVGSVGElement;
// For text nodes, check for multiple fragments (multi-column layout, line-wrapping).
// getClientRects() returns one rect per line-box fragment; getBoundingClientRect()
// only returns the union AABB, so without this we'd always get one quad.
if ((node instanceof Text || node instanceof _Text)) {
const canUseViewportTextRects = originalElementAndAllParentsMultipliedMatrix.is2D
&& Math.abs(originalElementAndAllParentsMultipliedMatrix.b) < 1e-10
&& Math.abs(originalElementAndAllParentsMultipliedMatrix.c) < 1e-10;
// FIX 7: Create the Range once here and reuse it for both the multi-fragment
// check and the single-fragment size fallback, avoiding a second Range
// allocation inside getElementSize for Text nodes.
const range = node.ownerDocument.createRange();
range.selectNodeContents(node);
const clientRects = range.getClientRects();
const viewportRoot = node.ownerDocument.documentElement ?? node.ownerDocument.body;
const viewportRootRect = viewportRoot?.getBoundingClientRect();
const convertViewportRectToRelativeQuad = (rect) => {
if (relativeTo === viewportRoot) {
return new DOMQuad(
new DOMPoint(rect.x, rect.y),
new DOMPoint(rect.x + rect.width, rect.y),
new DOMPoint(rect.x + rect.width, rect.y + rect.height),
new DOMPoint(rect.x, rect.y + rect.height),
);
}
const rectInViewportRoot = new DOMRect(
rect.x - (viewportRootRect?.x ?? 0),
rect.y - (viewportRootRect?.y ?? 0),
rect.width,
rect.height,
);
return convertRectFromNode(relativeTo, rectInViewportRoot, viewportRoot, {
iframes: options?.iframes,
});
};
if (clientRects.length > 1) {
if (canUseViewportTextRects) {
const quads = [];
for (const cr of clientRects) {
if (cr.width < 1 && cr.height < 1) continue;
quads.push(convertViewportRectToRelativeQuad(cr));
}
if (quads.length > 0) {
if (boxQuadsCache) boxQuadsCache.set(key, quads);
return quads;
}
}
// Work via the parent element so rotation is handled correctly.
// Each fragment's viewport rect (from getClientRects) is an AABB;
// its center equals the actual geometric center regardless of rotation.
// We convert that center to parent-local space, recover the fragment's
// local dimensions via the 2x2 AABB system, then apply the parent's
// accumulated matrix to build proper (rotated) quads in relativeTo-space.
const parent = getParentElementIncludingSlots(node, options?.iframes);
const M_parent = getResultingTransformationBetweenElementAndAllAncestors(parent, relativeTo, options?.iframes);
const parentCss = getElementCombinedTransform(parent, options?.iframes);
const pr = parent.getBoundingClientRect();
const pa = parentCss.a, pb = parentCss.b, pc = parentCss.c, pd = parentCss.d;
// AABB center of the transformed parent equals its geometric center.
// geometric_center_screen = screen(0,0) + L * (pw/2, ph/2)
// => screen(0,0) = AABB_center - L * (pw/2, ph/2)
//@ts-ignore
const pw = parent.offsetWidth;
//@ts-ignore
const ph = parent.offsetHeight;
const parentOriginX = (pr.x + pr.width / 2) - (pa * pw / 2 + pc * ph / 2);
const parentOriginY = (pr.y + pr.height / 2) - (pb * pw / 2 + pd * ph / 2);
const linearDet = pa * pd - pb * pc;
const absA = Math.abs(pa), absB = Math.abs(pb);
const absDet = absA * absA - absB * absB;
const quads = [];
for (const cr of clientRects) {
if (cr.width < 1 && cr.height < 1) continue;
// Fragment AABB center -> parent-local center via inverse CSS transform
const dx = cr.x + cr.width / 2 - parentOriginX;
const dy = cr.y + cr.height / 2 - parentOriginY;
let lcx, lcy;
if (Math.abs(linearDet) > 1e-10) {
lcx = (pd * dx - pc * dy) / linearDet;
lcy = (pa * dy - pb * dx) / linearDet;
} else {
lcx = dx; lcy = dy;
}
// Fragment dimensions in parent-local via 2x2 AABB system
let tw, th;
if (Math.abs(absDet) > 1e-6) {
tw = Math.max(0, (absA * cr.width - absB * cr.height) / absDet);
th = Math.max(0, (absA * cr.height - absB * cr.width) / absDet);
} else {
// Singular (~45 deg): use CSS line-height as th
const cs = getCachedComputedStyle(parent);
th = Math.max(0, parseFloat(cs.lineHeight) || parseFloat(cs.fontSize) * 1.2 || 16);
const denom = Math.max(absA, absB);
tw = denom > 1e-6 ? Math.max(0, (cr.width - th * absB) / denom) : cr.width;
}
// Fragment top-left in parent-local, then transform all 4 corners via M_parent
const lx = lcx - tw / 2, ly = lcy - th / 2;
const quad = new DOMQuad(
M_parent.transformPoint(new DOMPoint(lx, ly)),
M_parent.transformPoint(new DOMPoint(lx + tw, ly)),
M_parent.transformPoint(new DOMPoint(lx + tw, ly + th)),
M_parent.transformPoint(new DOMPoint(lx, ly + th))
);
quads.push(toViewportRelativeDocumentElementQuad(quad, node, relativeTo, options?.iframes));
}
if (quads.length > 0) {
if (boxQuadsCache) boxQuadsCache.set(key, quads);
return quads;
}
}
// FIX 7 (continued): Single-fragment text — reuse the already-fetched
// bounding rect so getElementSize doesn't create a second Range.
const textBoundingRect = range.getBoundingClientRect();
if (canUseViewportTextRects) {
const tQuad = [convertViewportRectToRelativeQuad(textBoundingRect)];
if (boxQuadsCache) boxQuadsCache.set(key, tQuad);
return tQuad;
}
const { width: tw, height: th } = _getTextNodeSize(originalElementAndAllParentsMultipliedMatrix, textBoundingRect, node);
const is2Dt = originalElementAndAllParentsMultipliedMatrix.is2D;
const tCorners = [
new DOMPoint(0, 0),
new DOMPoint(tw, 0),
new DOMPoint(tw, th),
new DOMPoint(0, th),
];
/** @type {[DOMPoint,DOMPoint,DOMPoint,DOMPoint]} */
//@ts-ignore
const tPoints = Array(4);
if (is2Dt) {
for (let i = 0; i < 4; i++)
tPoints[i] = tCorners[i].matrixTransform(originalElementAndAllParentsMultipliedMatrix);
} else {
for (let i = 0; i < 4; i++) {
tPoints[i] = projectPoint(tCorners[i], originalElementAndAllParentsMultipliedMatrix).matrixTransform(originalElementAndAllParentsMultipliedMatrix);
tPoints[i] = as2DPoint(tPoints[i]);
}
}
const tQuad = [toViewportRelativeDocumentElementQuad(new DOMQuad(tPoints[0], tPoints[1], tPoints[2], tPoints[3]), node, relativeTo, options?.iframes)];
if (boxQuadsCache) boxQuadsCache.set(key, tQuad);
return tQuad;
}
if ((node instanceof SVGGraphicsElement || node instanceof _SVGGraphicsElement)
&& !((node instanceof SVGSVGElement || node instanceof _SVGSVGElement))) {
const bbox = node.getBBox();
const visualBox = getSvgVisualBox(node, bbox);
const x0 = visualBox.x - bbox.x;
const y0 = visualBox.y - bbox.y;
const x1 = x0 + visualBox.width;
const y1 = y0 + visualBox.height;
const screenPts = [
new DOMPoint(visualBox.x, visualBox.y),
new DOMPoint(visualBox.x + visualBox.width, visualBox.y),
new DOMPoint(visualBox.x + visualBox.width, visualBox.y + visualBox.height),
new DOMPoint(visualBox.x, visualBox.y + visualBox.height),
];
const pts = [
new DOMPoint(x0, y0),
new DOMPoint(x1, y0),
new DOMPoint(x1, y1),
new DOMPoint(x0, y1),
];
const screenCtm = !hasTransformedHtmlAncestor(node, relativeTo, options?.iframes) ? node.getScreenCTM() : null;
if (screenCtm) {
const screenQuad = new DOMQuad(
screenPts[0].matrixTransform(screenCtm),
screenPts[1].matrixTransform(screenCtm),
screenPts[2].matrixTransform(screenCtm),
screenPts[3].matrixTransform(screenCtm),
);
const svgQuad = [convertViewportQuadToRelativeNode(screenQuad, node, relativeTo, options?.iframes)];
if (boxQuadsCache) boxQuadsCache.set(key, svgQuad);
return svgQuad;
}
/** @type {[DOMPoint,DOMPoint,DOMPoint,DOMPoint]} */
//@ts-ignore
const points = Array(4);
if (originalElementAndAllParentsMultipliedMatrix.is2D) {
for (let i = 0; i < 4; i++)
points[i] = pts[i].matrixTransform(originalElementAndAllParentsMultipliedMatrix);
} else {
for (let i = 0; i < 4; i++) {
points[i] = projectPoint(pts[i], originalElementAndAllParentsMultipliedMatrix).matrixTransform(originalElementAndAllParentsMultipliedMatrix);
points[i] = as2DPoint(points[i]);
}
}
const svgQuad = [toViewportRelativeDocumentElementQuad(new DOMQuad(points[0], points[1], points[2], points[3]), node, relativeTo, options?.iframes)];
if (boxQuadsCache) boxQuadsCache.set(key, svgQuad);
return svgQuad;
}
if (
(node instanceof win.Element)
&& relativeTo === node.ownerDocument.documentElement
&& (!options?.box || options.box === 'border')
&& originalElementAndAllParentsMultipliedMatrix.is2D
&& Math.abs(originalElementAndAllParentsMultipliedMatrix.b) < 1e-10
&& Math.abs(originalElementAndAllParentsMultipliedMatrix.c) < 1e-10
) {
const rect = node.getBoundingClientRect();
const viewportQuad = [new DOMQuad(
new DOMPoint(rect.x, rect.y),
new DOMPoint(rect.x + rect.width, rect.y),
new DOMPoint(rect.x + rect.width, rect.y + rect.height),
new DOMPoint(rect.x, rect.y + rect.height),
)];
if (boxQuadsCache) boxQuadsCache.set(key, viewportQuad);
return viewportQuad;
}
let { width, height } = getElementSize(node, originalElementAndAllParentsMultipliedMatrix);
// FIX 13: cache cross-realm Element constructor.
const _Element = win.Element;
const is2D = originalElementAndAllParentsMultipliedMatrix.is2D;
// FIX 8 + FIX 9: Split the `!o` branch out of the point loop into two
// separate code paths. In the box-offset path, parse style values once
// (FIX 9) and use them directly, eliminating 4x redundant parseFloat calls.
if ((node instanceof Element || node instanceof _Element)) {
const box = options?.box;
if (box === 'margin' || box === 'padding' || box === 'content') {
const cs = getCachedComputedStyle(node);
let x0, y0, x1, y1, x2, y2, x3, y3;
if (box === 'margin') {
const mL = parseFloat(cs.marginLeft);
const mT = parseFloat(cs.marginTop);
const mR = parseFloat(cs.marginRight);
const mB = parseFloat(cs.marginBottom);
x0 = -mL; y0 = -mT;
x1 = width + mR; y1 = -mT;
x2 = width + mR; y2 = height + mB;
x3 = -mL; y3 = height + mB;
} else if (box === 'padding') {
const bL = parseFloat(cs.borderLeftWidth);
const bT = parseFloat(cs.borderTopWidth);
const bR = parseFloat(cs.borderRightWidth);
const bB = parseFloat(cs.borderBottomWidth);
x0 = bL; y0 = bT;
x1 = width - bR; y1 = bT;
x2 = width - bR; y2 = height - bB;
x3 = bL; y3 = height - bB;
} else { // content
const bL = parseFloat(cs.borderLeftWidth);
const bT = parseFloat(cs.borderTopWidth);
const bR = parseFloat(cs.borderRightWidth);
const bB = parseFloat(cs.borderBottomWidth);
const pL = parseFloat(cs.paddingLeft);
const pT = parseFloat(cs.paddingTop);
const pR = parseFloat(cs.paddingRight);
const pB = parseFloat(cs.paddingBottom);
x0 = bL + pL; y0 = bT + pT;
x1 = width - bR - pR; y1 = bT + pT;
x2 = width - bR - pR; y2 = height - bB - pB;
x3 = bL + pL; y3 = height - bB - pB;
}
const pts = [
new DOMPoint(x0, y0),
new DOMPoint(x1, y1),
new DOMPoint(x2, y2),
new DOMPoint(x3, y3),
];
/** @type {[DOMPoint,DOMPoint,DOMPoint,DOMPoint]} */
//@ts-ignore
const points = Array(4);
if (is2D) {
for (let i = 0; i < 4; i++)
points[i] = pts[i].matrixTransform(originalElementAndAllParentsMultipliedMatrix);
} else {
for (let i = 0; i < 4; i++) {
points[i] = projectPoint(pts[i], originalElementAndAllParentsMultipliedMatrix).matrixTransform(originalElementAndAllParentsMultipliedMatrix);
points[i] = as2DPoint(points[i]);
}
}
const quad = [toViewportRelativeDocumentElementQuad(new DOMQuad(points[0], points[1], points[2], points[3]), node, relativeTo, options?.iframes)];
if (boxQuadsCache) boxQuadsCache.set(key, quad);
return quad;
}
}
// FIX 8: No-offset path — plain loop, no `!o` branch test per iteration.
const corners = [
new DOMPoint(0, 0),
new DOMPoint(width, 0),
new DOMPoint(width, height),
new DOMPoint(0, height),
];
/** @type {[DOMPoint,DOMPoint,DOMPoint,DOMPoint]} */
//@ts-ignore
const points = Array(4);
if (is2D) {
for (let i = 0; i < 4; i++)
points[i] = corners[i].matrixTransform(originalElementAndAllParentsMultipliedMatrix);
} else {
for (let i = 0; i < 4; i++) {
points[i] = projectPoint(corners[i], originalElementAndAllParentsMultipliedMatrix).matrixTransform(originalElementAndAllParentsMultipliedMatrix);
points[i] = as2DPoint(points[i]);
}
}
const quad = [toViewportRelativeDocumentElementQuad(new DOMQuad(points[0], points[1], points[2], points[3]), node, relativeTo, options?.iframes)];
if (boxQuadsCache)
boxQuadsCache.set(key, quad);
return quad;
}
function convertViewportQuadToRelativeNode(quad, node, relativeTo, iframes) {
const viewportRoot = node.ownerDocument.documentElement ?? node.ownerDocument.body;
if (relativeTo === viewportRoot) {
return quad;
}
const win = node.ownerDocument.defaultView ?? window;
if (relativeTo instanceof win.SVGElement) {
const relativeScreenCtm = relativeTo.getScreenCTM();
if (relativeScreenCtm) {
const inverse = relativeScreenCtm.inverse();
return new DOMQuad(
quad.p1.matrixTransform(inverse),
quad.p2.matrixTransform(inverse),
quad.p3.matrixTransform(inverse),
quad.p4.matrixTransform(inverse),
);
}
}
if (relativeTo.ownerDocument === node.ownerDocument && relativeTo instanceof win.HTMLElement) {
const htmlQuad = convertViewportQuadToHtmlElement(quad, relativeTo, iframes);
if (htmlQuad) {
return htmlQuad;
}
}
if (relativeTo === node.ownerDocument.body) {
const relativeRect = relativeTo.getBoundingClientRect();
const scrollLeft = relativeTo.scrollLeft || 0;
const scrollTop = relativeTo.scrollTop || 0;
return new DOMQuad(
new DOMPoint(quad.p1.x - relativeRect.x + scrollLeft, quad.p1.y - relativeRect.y + scrollTop),
new DOMPoint(quad.p2.x - relativeRect.x + scrollLeft, quad.p2.y - relativeRect.y + scrollTop),
new DOMPoint(quad.p3.x - relativeRect.x + scrollLeft, quad.p3.y - relativeRect.y + scrollTop),
new DOMPoint(quad.p4.x - relativeRect.x + scrollLeft, quad.p4.y - relativeRect.y + scrollTop),
);
}
return convertQuadFromNode(relativeTo, quad, viewportRoot, { iframes });
}
function convertViewportQuadToHtmlElement(quad, relativeTo, iframes) {
const scale = getPositiveAxisAlignedViewportScale(relativeTo, iframes);
if (!scale) {
return null;
}
const relativeRect = relativeTo.getBoundingClientRect();
const scaleX = scale.x;
const scaleY = scale.y;
if (!Number.isFinite(scaleX) || !Number.isFinite(scaleY) || Math.abs(scaleX) < 1e-10 || Math.abs(scaleY) < 1e-10) {
return null;
}
const scrollLeft = relativeTo.scrollLeft || 0;
const scrollTop = relativeTo.scrollTop || 0;
const convertPoint = (point) => new DOMPoint(
(point.x - relativeRect.x) / scaleX + scrollLeft,
(point.y - relativeRect.y) / scaleY + scrollTop,
);
return new DOMQuad(
convertPoint(quad.p1),
convertPoint(quad.p2),
convertPoint(quad.p3),
convertPoint(quad.p4),
);
}
function getPositiveAxisAlignedViewportScale(element, iframes) {
const win = element.ownerDocument.defaultView ?? window;
const scale = { x: 1, y: 1 };
let current = element;
while (current && current !== element.ownerDocument.documentElement) {
if (current instanceof win.Element) {
const style = getCachedComputedStyle(current);
const transformScale = getPositiveAxisAlignedTransformScale(style);
if (!transformScale) {
return null;
}
const zoom = getElementZoom(current);
scale.x *= transformScale.x * zoom;
scale.y *= transformScale.y * zoom;
}
current = getParentElementIncludingSlots(current, iframes);
}
return scale;
}
function getPositiveAxisAlignedTransformScale(style) {
if (style.perspective && style.perspective !== 'none') {
return null;
}
if (style.rotate && style.rotate !== 'none' && !isZeroAngleValue(style.rotate)) {
return null;
}
const individualScale = parsePositiveScaleValue(style.scale);
if (!individualScale) {
return null;
}
const transform = style.transform;
if (!transform || transform === 'none') {
return individualScale;
}
if (transform.startsWith('matrix3d(')) {
const values = parseCssMatrixValues(transform);
if (values?.length === 16
&& values[0] > 0
&& values[5] > 0
&& Math.abs(values[1]) < 1e-10
&& Math.abs(values[4]) < 1e-10
&& Math.abs(values[3]) < 1e-10
&& Math.abs(values[7]) < 1e-10
&& Math.abs(values[11]) < 1e-10) {
return { x: individualScale.x * values[0], y: individualScale.y * values[5] };
}
return null;
}
if (transform.startsWith('matrix(')) {
const values = parseCssMatrixValues(transform);
if (values?.length === 6
&& values[0] > 0
&& values[3] > 0
&& Math.abs(values[1]) < 1e-10
&& Math.abs(values[2]) < 1e-10) {
return { x: individualScale.x * values[0], y: individualScale.y * values[3] };
}
return null;
}
return null;
}
function parsePositiveScaleValue(value) {
if (!value || value === 'none') {
return { x: 1, y: 1 };
}
const parts = value.split(/\s+/).map(part => parseFloat(part)).filter(Number.isFinite);
if (parts.length === 0 || parts[0] <= 0 || (parts[1] != null && parts[1] <= 0)) {
return null;
}
return { x: parts[0], y: parts[1] ?? parts[0] };
}
function parseCssMatrixValues(transform) {
const start = transform.indexOf('(');
const end = transform.lastIndexOf(')');
if (start < 0 || end <= start) {
return null;
}
const values = transform.slice(start + 1, end).split(',').map(value => parseFloat(value.trim()));
return values.every(Number.isFinite) ? values : null;
}
function isZeroAngleValue(value) {
const matches = value.match(/-?\d*\.?\d+(?:e[-+]?\d+)?/gi);
if (!matches?.length) {
return false;
}
const angle = parseFloat(matches[matches.length - 1]);
return Number.isFinite(angle) && Math.abs(angle) < 1e-10;
}
function hasTransformedHtmlAncestor(node, relativeTo, iframes) {
const win = node.ownerDocument.defaultView ?? window;
let element = getParentElementIncludingSlots(node, iframes);
while (element && element !== relativeTo && element !== node.ownerDocument.documentElement) {
if (element instanceof win.HTMLElement) {
const css = getCachedComputedStyle(element);
if (transformProperties.some((value) => css[value] ? css[value] !== 'none' : false)) {
return true;
}
}
element = getParentElementIncludingSlots(element, iframes);
}
return false;
}
function toViewportRelativeDocumentElementQuad(quad, node, relativeTo, iframes) {
if (relativeTo !== node.ownerDocument.documentElement) {
return quad;
}
if (isViewportFixedAnchoredNode(node, iframes)) {
return quad;
}
const win = node.ownerDocument.defaultView ?? window;
const scrollX = win.scrollX ?? node.ownerDocument.documentElement.scrollLeft ?? 0;
const scrollY = win.scrollY ?? node.ownerDocument.documentElement.scrollTop ?? 0;
if (scrollX === 0 && scrollY === 0) {
return quad;
}
return new DOMQuad(
new DOMPoint(quad.p1.x - scrollX, quad.p1.y - scrollY),
new DOMPoint(quad.p2.x - scrollX, quad.p2.y - scrollY),
new DOMPoint(quad.p3.x - scrollX, quad.p3.y - scrollY),
new DOMPoint(quad.p4.x - scrollX, quad.p4.y - scrollY),
);
}
function isViewportFixedAnchoredNode(node, iframes) {
const win = node.ownerDocument.defaultView ?? window;
let element = null;
if (node instanceof win.Text) {
element = getParentElementIncludingSlots(node, iframes);
} else if (node instanceof win.Element) {
element = node;
}
while (element && element !== node.ownerDocument.documentElement) {
const cs = getCachedComputedStyle(element);
if (cs.position === 'fixed') {
return getNearestFixedContainingBlock(element, iframes) == null;
}
element = getParentElementIncludingSlots(element, iframes);
}
return false;
}
/**
* Compute width/height for a Text node given an already-fetched bounding rect.
* Extracted from getElementSize so getBoxQuads (FIX 7) can reuse the Range it
* already created, avoiding a second Range allocation.
* @param {DOMMatrix} matrix
* @param {DOMRect} targetRect
* @param {Text} node
*/
function _getTextNodeSize(matrix, targetRect, node) {
const absA = Math.abs(matrix?.a ?? 1);
const absB = Math.abs(matrix?.b ?? 0);
const det = absA * absA - absB * absB; // cos(2*angle)*scale^2
let width, height;
if (Math.abs(det) > 1e-6) {
width = Math.max(0, (absA * targetRect.width - absB * targetRect.height) / det);
height = Math.max(0, (absA * targetRect.height - absB * targetRect.width) / det);
} else {
// Singular (~45 deg rotation): use CSS line-height as the known height
const parentEl = node.parentElement;
let lineH = 16;
if (parentEl) {
const cs = getCachedComputedStyle(parentEl);
lineH = parseFloat(cs.lineHeight) || parseFloat(cs.fontSize) * 1.2 || 16;
}
height = Math.max(0, lineH);
const denom = Math.max(absA, absB);
width = denom > 1e-6 ? Math.max(0, (targetRect.width - height * absB) / denom) : targetRect.width;
}
return { width, height };
}
//todo: https://drafts.csswg.org/css-transforms-2/#accumulated-3d-transformation-matrix-computation
// also good for writing a spec
// Find a value for z that will transform to 0. (from firefox matrix.h)
// or chromium https://github.com/chromium/chromium/blob/main/ui/gfx/geometry/transform.cc#L849
/**
* @param {DOMPoint} point
*/
function projectPoint(point, m) {
const z = -(point.x * m.m13 + point.y * m.m23 + m.m43) / m.m33;
return new DOMPoint(point.x, point.y, z, 1);
}
/**
* convert a DOM-Point to 2D
* @param {DOMPoint} point
*/
function as2DPoint(point) {
return new DOMPoint(
point.x / point.w,
point.y / point.w
);
}
/**
* @param {Node} node
* @param {DOMMatrix=} matrix
*/
export function getElementSize(node, matrix) {
let width = 0;
let height = 0;
// FIX 13: Cache cross-realm constructors once.
const win = node.ownerDocument.defaultView ?? window;
if ((node instanceof HTMLElement || node instanceof win.HTMLElement)) {
width = node.offsetWidth;
height = node.offsetHeight;
} else if ((node instanceof SVGSVGElement || node instanceof win.SVGSVGElement)) {
width = node.width.baseVal.value
height = node.height.baseVal.value
} else if ((node instanceof SVGGraphicsElement || node instanceof win.SVGGraphicsElement)) {
const bbox = node.getBBox()
width = bbox.width;
height = bbox.height;
} else if ((node instanceof MathMLElement || node instanceof win.MathMLElement)) {
const bbox = node.getBoundingClientRect()
width = bbox.width / (matrix?.a ?? 1);
height = bbox.height / (matrix?.d ?? 1);
} else if ((node instanceof Text || node instanceof win.Text)) {
// Note: getBoxQuads passes an already-fetched rect via _getTextNodeSize to
// avoid this Range creation. This path serves external callers of getElementSize.
const range = node.ownerDocument.createRange();
range.selectNodeContents(node);
const targetRect = range.getBoundingClientRect();
const result = _getTextNodeSize(matrix, targetRect, node);
width = result.width;
height = result.height;
}
return { width, height }
}
function getSvgVisualBox(node, bbox) {
const svgStyle = getCachedComputedStyle(node);
const strokeWidth = svgStyle.stroke !== 'none' ? parseFloat(svgStyle.strokeWidth) || 0 : 0;
if (strokeWidth <= 0) {
return bbox;
}
const strokeInflation = getSvgStrokeInflation(node, bbox, strokeWidth);
return new DOMRect(
bbox.x - strokeInflation.left,
bbox.y - strokeInflation.top,
bbox.width + strokeInflation.left + strokeInflation.right,
bbox.height + strokeInflation.top + strokeInflation.bottom,
);
}
function getSvgStrokeInflation(node, bbox, strokeWidth) {
const halfStrokeWidth = strokeWidth / 2;
if ((node instanceof SVGLineElement || node instanceof (node.ownerDocument.defaultView ?? window).SVGLineElement)) {
const x1 = node.x1.baseVal.value;
const y1 = node.y1.baseVal.value;
const x2 = node.x2.baseVal.value;
const y2 = node.y2.baseVal.value;
const dx = x2 - x1;
const dy = y2 - y1;
const length = Math.hypot(dx, dy);
if (length > 1e-10) {
let inflateX = halfStrokeWidth * Math.abs(dy) / length;
let inflateY = halfStrokeWidth * Math.abs(dx) / length;
const lineCap = getCachedComputedStyle(node).strokeLinecap;
if (lineCap === 'round' || lineCap === 'square') {
inflateX += halfStrokeWidth * Math.abs(dx) / length;
inflateY += halfStrokeWidth * Math.abs(dy) / length;
}
return { left: inflateX, right: inflateX, top: inflateY, bottom: inflateY };
}
}
const genericInflation = strokeWidth * 2;
return { left: genericInflation, right: genericInflation, top: genericInflation, bottom: genericInflation };
}
/**
* @param {Node} node
* @param {boolean} includeScroll
* @param {HTMLIFrameElement[]} iframes
*/
function getElementOffsetsInContainer(node, includeScroll, iframes) {
if ((node instanceof HTMLElement || node instanceof (node.ownerDocument.defaultView ?? window).HTMLElement)) {
// When appears inside a shadow DOM canvas the browser reflects
// body's top margin into html.offsetTop (but not offsetLeft), causing
// a Y-only shift in the transform walk. The html element has no real
// layout offset relative to its shadow-host container, so return 0,0.
if ((node instanceof HTMLHtmlElement || node instanceof (node.ownerDocument.defaultView ?? window).HTMLHtmlElement)) {
return new DOMPoint(0, 0);
}
const cs = getCachedComputedStyle(node);
if (cs.position === 'fixed') {
const fixedContainer = getNearestFixedContainingBlock(node, iframes);
if (!fixedContainer) {
const left = cs.left && cs.left !== 'auto' ? parseFloat(cs.left) : node.offsetLeft;
const top = cs.top && cs.top !== 'auto' ? parseFloat(cs.top) : node.offsetTop;
return new DOMPoint(left, top);
}
const m = getResultingTransformationBetweenElementAndAllAncestors(fixedContainer, node.ownerDocument.body, iframes, true).inverse();
const r1 = node.getBoundingClientRect();
const r1t = m.transformPoint(r1);
const r2 = fixedContainer.getBoundingClientRect();
const r2t = m.transformPoint(r2);
return new DOMPoint(r1t.x - r2t.x, r1t.y - r2t.y);
}
// FIX 4: Only call getCachedComputedStyle when includeScroll is true —
// cs is unused in the plain offsetLeft/offsetTop path.
if (includeScroll) {
return new DOMPoint(node.offsetLeft - (node.scrollLeft - parseFloat(cs.borderLeftWidth)), node.offsetTop - (node.scrollTop - parseFloat(cs.borderTopWidth)));
} else {
return new DOMPoint(node.offsetLeft, node.offsetTop);
}
} else if ((node instanceof Text || node instanceof (node.ownerDocument.defaultView ?? window).Text)) {
const range = node.ownerDocument.createRange();
range.selectNodeContents(node);
const r1 = range.getBoundingClientRect();
/** @type {HTMLElement} */
//@ts-ignore
const parent = getParentElementIncludingSlots(node, iframes);
const r2 = parent.getBoundingClientRect();
// Get the parent's CSS transform so we can work in local space even when rotated.
const pt = getElementCombinedTransform(parent, iframes);
const pa = pt.a, pb = pt.b, pc = pt.c, pd = pt.d;
// AABB center of the transformed parent equals its geometric center.
// geometric_center_screen = screen(0,0) + L * (pw/2, ph/2)
// => screen(0,0) = AABB_center - L * (pw/2, ph/2)
const pw = parent.offsetWidth;
const ph = parent.offsetHeight;
const parentOriginX = (r2.x + r2.width / 2) - (pa * pw / 2 + pc * ph / 2);
const parentOriginY = (r2.y + r2.height / 2) - (pb * pw / 2 + pd * ph / 2);
// Delta from parent origin to text AABB center in screen space
const dx = (r1.x + r1.width / 2) - parentOriginX;
const dy = (r1.y + r1.height / 2) - parentOriginY;
// Apply inverse of CSS transform linear part: local_center = L^-1 * screen_delta
const transformDet = pa * pd - pb * pc;
let localCenterX, localCenterY;
if (Math.abs(transformDet) > 1e-10) {
localCenterX = (pd * dx - pc * dy) / transformDet;
localCenterY = (pa * dy - pb * dx) / transformDet;
} else {
localCenterX = dx;
localCenterY = dy;
}
// Recover tw and th in local space from the AABB using the 2x2 system:
// aabb_w = tw*|cos| + th*|sin|, aabb_h = tw*|sin| + th*|cos|
const absA = Math.abs(pa), absB = Math.abs(pb);
const absDet = absA * absA - absB * absB;
let tw, th;
if (Math.abs(absDet) > 1e-6) {
tw = Math.max(0, (absA * r1.width - absB * r1.height) / absDet);
th = Math.max(0, (absA * r1.height - absB * r1.width) / absDet);
} else {
// Singular (~45 deg): use CSS line-height as th
const cs = getCachedComputedStyle(parent);
th = parseFloat(cs.lineHeight) || parseFloat(cs.fontSize) * 1.2 || 16;
th = Math.max(0, th);
const denom = Math.max(absA, absB);
tw = denom > 1e-6 ? Math.max(0, (r1.width - th * absB) / denom) : r1.width;
}
// local origin = center minus half-dimensions
return new DOMPoint(localCenterX - tw / 2, localCenterY - th / 2);
} else if ((node instanceof Element || node instanceof (node.ownerDocument.defaultView ?? window).Element)) {
if ((node instanceof SVGGraphicsElement || node instanceof (node.ownerDocument.defaultView ?? window).SVGGraphicsElement) && !((node instanceof SVGSVGElement || node instanceof (node.ownerDocument.defaultView ?? window).SVGSVGElement))) {
const bb = node.getBBox();
return new DOMPoint(bb.x, bb.y);
}
const cs = getCachedComputedStyle(node);
if (cs.position === 'absolute') {
return new DOMPoint(parseFloat(cs.left), parseFloat(cs.top));
}
const par = getParentElementIncludingSlots(node, iframes);
const m = getResultingTransformationBetweenElementAndAllAncestors(par, document.body, iframes, true).inverse();
const r1 = node.getBoundingClientRect();
const r1t = m.transformPoint(r1);
const r2 = par.getBoundingClientRect();
const r2t = m.transformPoint(r2);
return new DOMPoint(r1t.x - r2t.x, r1t.y - r2t.y);
}
}
/**
* @param {Node} node
* @param {Element} ancestor
* @param {HTMLIFrameElement[]} iframes
* @param {boolean=} excludeSelfZoom
*/
export function getResultingTransformationBetweenElementAndAllAncestors(node, ancestor, iframes, excludeSelfZoom = false) {
let key;
if (transformCache) {
let i1 = hash.get(node);
if (i1 === undefined)
hash.set(node, i1 = hashId++);
let i2 = hash.get(ancestor);
if (i2 === undefined)
hash.set(ancestor, i2 = hashId++);
key = i1 + '_' + i2 + '_' + (excludeSelfZoom ? 'no-self-zoom' : 'full');
const q = transformCache.get(key);
if (q)
return q;
}
/** @type {Element } */
//@ts-ignore
let actualElement = node;
/** @type {DOMMatrix } */
let parentElementMatrix;
// FIX 12: Compute self-transform once; we'll carry parent transforms forward
// each iteration instead of recomputing them.
const useOwnSvgCtm =
(actualElement instanceof SVGGraphicsElement || actualElement instanceof (actualElement.ownerDocument.defaultView ?? window).SVGGraphicsElement) &&
!((actualElement instanceof SVGSVGElement || actualElement instanceof (actualElement.ownerDocument.defaultView ?? window).SVGSVGElement));
// SVGGraphicsElement.getCTM() already includes the element's local SVG/CSS transform.
// Starting with getElementTransformWithZoom here would double-apply self rotate/scale.
let currentElementTransform = useOwnSvgCtm
? new DOMMatrix()
: getElementTransformWithZoom(actualElement, iframes, !excludeSelfZoom);
/** @type {DOMMatrix } */
// FIX 2: Only use a non-identity starting matrix when the element itself has
// a CSS transform. Most plain elements have identity, avoiding a multiply.
let originalElementAndAllParentsMultipliedMatrix = currentElementTransform.isIdentity
? new DOMMatrix()
: currentElementTransform;
let perspectiveParentElement = getParentElementIncludingSlots(actualElement, iframes);
if (perspectiveParentElement) {
// FIX 5: Guard transformStyle read behind is2D — on a standard 2D page
// the matrix is always 2D here so we skip the style read entirely.
if (!originalElementAndAllParentsMultipliedMatrix.is2D) {
const s = getCachedComputedStyle(perspectiveParentElement);
if (s.transformStyle !== 'preserve-3d') {
projectTo2D(originalElementAndAllParentsMultipliedMatrix);
}
}
}
let lastOffsetParent = null;
while (actualElement != ancestor && actualElement != null) {
let parentElement = getParentElementIncludingSlots(actualElement, iframes);
if ((actualElement instanceof HTMLElement || actualElement instanceof (actualElement.ownerDocument.defaultView ?? window).HTMLElement)) {
const fixedStyle = getCachedComputedStyle(actualElement);
if (fixedStyle.position === 'fixed') {
const fixedContainer = getNearestFixedContainingBlock(actualElement, iframes);
parentElement = fixedContainer ?? ancestor;
}
}
if (actualElement.assignedSlot != null) {
if (actualElement.nodeType === Node.ELEMENT_NODE) {
const slotOffsetParent = offsetParentPolyfill(actualElement);
const shouldApplySlottedOffset = lastOffsetParent !== slotOffsetParent
&& (lastOffsetParent === null || actualElement === lastOffsetParent || !isFlatTreeInclusiveAncestor(lastOffsetParent, actualElement));
if (shouldApplySlottedOffset) {
const l = offsetTopLeftPolyfill(actualElement, 'offsetLeft');
const t = offsetTopLeftPolyfill(actualElement, 'offsetTop');
// FIX 3: Skip zero-translation matrix allocations.
if (l !== 0 || t !== 0) {
const mvMat = new DOMMatrix().translateSelf(l, t);
originalElementAndAllParentsMultipliedMatrix = mvMat.multiplySelf(originalElementAndAllParentsMultipliedMatrix);
}
lastOffsetParent = slotOffsetParent;
}
if (lastOffsetParent === null)
lastOffsetParent = slotOffsetParent;
} else if (actualElement.nodeType === Node.TEXT_NODE) {
const offsets = getElementOffsetsInContainer(actualElement, actualElement !== node, iframes);
// FIX 3
if (offsets.x !== 0 || offsets.y !== 0) {
const mvMat = new DOMMatrix().translateSelf(offsets.x, offsets.y);
originalElementAndAllParentsMultipliedMatrix = mvMat.multiplySelf(originalElementAndAllParentsMultipliedMatrix);
}
}
/*
following code should be used instead of above to fix:
but it does not work with:
*/
/*
const l = offsetTopLeftPolyfill(actualElement, 'offsetLeft');
const t = offsetTopLeftPolyfill(actualElement, 'offsetTop');
const mvMat = new DOMMatrix().translateSelf(l, t);
originalElementAndAllParentsMultipliedMatrix = mvMat.multiplySelf(originalElementAndAllParentsMultipliedMatrix);
*/
} else {
if (!(actualElement instanceof SVGSVGElement) && !(actualElement instanceof (actualElement.ownerDocument.defaultView ?? window).SVGSVGElement) &&
(actualElement instanceof SVGGraphicsElement || actualElement instanceof (actualElement.ownerDocument.defaultView ?? window).SVGGraphicsElement)) {
const ctm = actualElement.getCTM();
const bb = actualElement.getBBox();
// FIX 3
if (bb.x !== 0 || bb.y !== 0) {
const mvMat = new DOMMatrix().translateSelf(bb.x, bb.y);
originalElementAndAllParentsMultipliedMatrix = mvMat.multiplySelf(originalElementAndAllParentsMultipliedMatrix);
}
originalElementAndAllParentsMultipliedMatrix = new DOMMatrix([ctm.a, ctm.b, ctm.c, ctm.d, ctm.e, ctm.f]).multiplySelf(originalElementAndAllParentsMultipliedMatrix);
parentElement = actualElement.ownerSVGElement;
} else if ((actualElement instanceof HTMLElement || actualElement instanceof (actualElement.ownerDocument.defaultView ?? window).HTMLElement)) {
const actualStyle = getCachedComputedStyle(actualElement);
const isFixedSelf = actualStyle.position === 'fixed' && actualElement === node;
if ((isFixedSelf || lastOffsetParent !== actualElement.offsetParent) && !((actualElement instanceof HTMLSlotElement || actualElement instanceof (actualElement.ownerDocument.defaultView ?? window).HTMLSlotElement))
&& (lastOffsetParent === null || actualElement === lastOffsetParent || !isFlatTreeInclusiveAncestor(lastOffsetParent, actualElement))) {
const offsets = getElementOffsetsInContainer(actualElement, actualElement !== node, iframes);
const zoom = getElementZoom(actualElement);
lastOffsetParent = actualElement.offsetParent;
// FIX 3
if (offsets.x !== 0 || offsets.y !== 0) {
const mvMat = new DOMMatrix().translateSelf(offsets.x * zoom, offsets.y * zoom);
originalElementAndAllParentsMultipliedMatrix = mvMat.multiplySelf(originalElementAndAllParentsMultipliedMatrix);
}
}
} else {
const offsets = getElementOffsetsInContainer(actualElement, actualElement !== node, iframes);
lastOffsetParent = null;
// FIX 3
if (offsets.x !== 0 || offsets.y !== 0) {
const mvMat = new DOMMatrix().translateSelf(offsets.x, offsets.y);
originalElementAndAllParentsMultipliedMatrix = mvMat.multiplySelf(originalElementAndAllParentsMultipliedMatrix);
}
}
}
if (parentElement) {
if (parentElement === ancestor) {
// The ancestor's own transform is excluded from the returned matrix,
// so avoid computing it on the hot return path.
if (lastOffsetParent !== null &&
(parentElement instanceof HTMLElement || parentElement instanceof (parentElement.ownerDocument.defaultView ?? window).HTMLElement) &&
parentElement.offsetParent === lastOffsetParent) {
const ancOff = getElementOffsetsInContainer(parentElement, false, iframes);
// FIX 3
if (ancOff.x !== 0 || ancOff.y !== 0) {
originalElementAndAllParentsMultipliedMatrix = new DOMMatrix().translate(-ancOff.x, -ancOff.y).multiply(originalElementAndAllParentsMultipliedMatrix);
}
}
// FIX 15: Do NOT subtract the scroll of documentElement (the viewport/window
// scroll). The offsetLeft/offsetTop walk already yields document-absolute
// coordinates; subtracting documentElement.scrollTop would wrongly
// shift positions to viewport-space when the page is scrolled.
// Only subtract scroll for a non-root ancestor that is itself a
// scroll container (e.g. an overflow:scroll div used as relativeTo).
const isViewportScrollContainer = parentElement === parentElement.ownerDocument.documentElement;
if (!isViewportScrollContainer && (parentElement.scrollTop || parentElement.scrollLeft))
originalElementAndAllParentsMultipliedMatrix = new DOMMatrix().translate(-parentElement.scrollLeft, -parentElement.scrollTop).multiply(originalElementAndAllParentsMultipliedMatrix);
const ancestorZoom = getElementZoomScaleTransform(parentElement);
if (!ancestorZoom.isIdentity)
originalElementAndAllParentsMultipliedMatrix = ancestorZoom.multiply(originalElementAndAllParentsMultipliedMatrix);
// FIX 6: Cache result on the early-return path. Originally, the
// cache.set() only ran after the while-loop (the null/root
// fallthrough), so the most common case — element IS a
// descendant of ancestor — was NEVER cached.
if (transformCache)
transformCache.set(key, originalElementAndAllParentsMultipliedMatrix);
return originalElementAndAllParentsMultipliedMatrix;
}
// FIX 12: parentElementMatrix computed here; in the next iteration this
// becomes the element's own transform, so we can reuse it without
// calling getElementCombinedTransform again.
parentElementMatrix = getElementTransformWithZoom(parentElement, iframes);
if (!parentElementMatrix.isIdentity)
originalElementAndAllParentsMultipliedMatrix = parentElementMatrix.multiply(originalElementAndAllParentsMultipliedMatrix);
perspectiveParentElement = getParentElementIncludingSlots(parentElement, iframes);
if (perspectiveParentElement) {
// FIX 5: Skip transformStyle read when matrix is already 2D.
if (!originalElementAndAllParentsMultipliedMatrix.is2D) {
const s = getCachedComputedStyle(perspectiveParentElement);
if (s.transformStyle !== 'preserve-3d') {
projectTo2D(originalElementAndAllParentsMultipliedMatrix);
}
}
}
}
actualElement = parentElement;
}
if (transformCache) {
transformCache.set(key, originalElementAndAllParentsMultipliedMatrix);
}
return originalElementAndAllParentsMultipliedMatrix;
}
/*
getResultingTransformationBetweenElementAndAllAncestors -> but with extra layout matrix (does not work yet....)
export function getResultingTransformationBetweenElementAndAllAncestors(node, ancestor, iframes) {
let key;
if (transformCache) {
let i1 = hash.get(node);
if (i1 === undefined)
hash.set(node, i1 = hashId++);
let i2 = hash.get(ancestor);
if (i2 === undefined)
hash.set(ancestor, i2 = hashId++);
key = i1 + '_' + i2;
const q = transformCache.get(key);
if (q)
return q;
}
// NEW - two matrices instead of one
let layoutMatrix = new DOMMatrix();
let actualElement = node;
let transformMatrix = getElementCombinedTransform(actualElement, iframes); //.multiplySelf(transformMatrix);
const perspectiveParent = getParentElementIncludingSlots(actualElement, iframes);
if (perspectiveParent) {
const s = getCachedComputedStyle(perspectiveParent);
if (s.transformStyle !== "preserve-3d")
projectTo2D(transformMatrix);
}
let lastOffsetParent = null;
while (actualElement !== ancestor && actualElement != null) {
const parentElement = getParentElementIncludingSlots(actualElement, iframes);
// ------------------------
// LAYOUT MATRIX (offsets)
// ------------------------
if (actualElement.assignedSlot != null) {
const l = offsetTopLeftPolyfill(actualElement, "offsetLeft");
const t = offsetTopLeftPolyfill(actualElement, "offsetTop");
layoutMatrix = new DOMMatrix().translateSelf(l, t).multiplySelf(layoutMatrix);
} else {
if (actualElement instanceof HTMLElement ||
actualElement instanceof (actualElement.ownerDocument.defaultView ?? window).HTMLElement) {
if (lastOffsetParent !== actualElement.offsetParent &&
!(actualElement instanceof HTMLSlotElement)) {
const offsets = getElementOffsetsInContainer(actualElement, actualElement !== node, iframes);
lastOffsetParent = actualElement.offsetParent;
layoutMatrix = new DOMMatrix().translateSelf(offsets.x, offsets.y).multiplySelf(layoutMatrix);
}
} else {
const offsets = getElementOffsetsInContainer(actualElement, actualElement !== node, iframes);
lastOffsetParent = null;
layoutMatrix = new DOMMatrix().translateSelf(offsets.x, offsets.y).multiplySelf(layoutMatrix);
}
}
// ------------------------
// TRANSFORM MATRIX (CSS)
// ------------------------
if (parentElement) {
// NEW - only affects transform pipeline
const parentTransform = getElementCombinedTransform(parentElement, iframes);
transformMatrix = parentTransform.multiply(transformMatrix);
// flattening boundary
const perspectiveParent = getParentElementIncludingSlots(parentElement, iframes);
if (perspectiveParent) {
const s = getCachedComputedStyle(perspectiveParent);
if (s.transformStyle !== "preserve-3d")
projectTo2D(transformMatrix);
}
// ------------------------
// EXIT CONDITION
// ------------------------
if (parentElement === ancestor) {
// NEW - scroll offsets belong to layout
if (parentElement.scrollTop || parentElement.scrollLeft) {
layoutMatrix = new DOMMatrix()
.translate(-parentElement.scrollLeft, -parentElement.scrollTop)
.multiply(layoutMatrix);
}
const result = layoutMatrix.multiply(transformMatrix);
if (transformCache)
transformCache.set(key, result);
return result;
}
}
actualElement = parentElement;
}
const result = layoutMatrix.multiply(transformMatrix);
if (transformCache)
transformCache.set(key, result);
return result;
}
*/
/**
* @param {Node} node
* @param {HTMLIFrameElement[]} iframes
* @returns {Element}
*/
function getParentElementIncludingSlots(node, iframes) {
if ((node instanceof Element || node instanceof (node.ownerDocument.defaultView ?? window).Element) && node.assignedSlot)
return node.assignedSlot;
if (node.parentElement == null) {
if ((node.parentNode instanceof ShadowRoot || node.parentNode instanceof (node.ownerDocument.defaultView ?? window).ShadowRoot)) {
return node.parentNode.host;
}
}
if ((node instanceof HTMLHtmlElement || node instanceof (node.ownerDocument.defaultView ?? window).HTMLHtmlElement)) {
if (iframes) {
for (const f of iframes)
if (f?.contentDocument == node.ownerDocument)
return f;
}
}
return node.parentElement;
}
/**
* @param {Node} element
* @param {HTMLIFrameElement[]=} iframes
*/
export function getElementCombinedTransform(element, iframes) {
if ((element instanceof Text || element instanceof (element.ownerDocument.defaultView ?? window).Text))
return new DOMMatrix;
/** @type {Element} */
// @ts-ignore
const actualElement = element;
//https://www.w3.org/TR/css-transforms-2/#ctm
let s = getCachedComputedStyle(actualElement);
// FIX 10: Check hasTransform first — it's the most common non-identity case.
// Reordering so the most frequent hit is evaluated first.
const hasTransform = s.transform !== 'none' && !!s.transform;
const hasTranslate = s.translate !== 'none' && !!s.translate;
const hasRotate = s.rotate !== 'none' && !!s.rotate;
const hasScale = s.scale !== 'none' && !!s.scale;
const hasOffsetPath = !!s.offsetPath && s.offsetPath !== 'none';
if (!hasTransform && !hasTranslate && !hasRotate && !hasScale && !hasOffsetPath) {
// FIX 1: Check parent perspective right here in the fast-path, so identity
// elements on non-3D pages return a new DOMMatrix() immediately
// without calling getElementPerspectiveTransform at all.
const parent = getParentElementIncludingSlots(actualElement, iframes);
if (!parent) return new DOMMatrix();
const ps = getCachedComputedStyle(parent);
if (!ps.perspective || ps.perspective === 'none') return new DOMMatrix();
// Parent has a perspective — fall through to compute it properly.
//@ts-ignore
const pt = getElementPerspectiveTransform(actualElement, iframes);
return pt != null ? pt : new DOMMatrix();
}
let m = new DOMMatrix();
const origin = s.transformOrigin.split(' ');
const originX = parseFloat(origin[0]);
const originY = parseFloat(origin[1]);
const originZ = origin[2] ? parseFloat(origin[2]) : 0;
// FIX 11: Skip the origin wrap/unwrap entirely when origin is (0,0,0).
// Saves two DOMMatrix allocations and two multiply calls per element.
const hasNonZeroOrigin = (originX !== 0 || originY !== 0 || originZ !== 0);
const mOri = hasNonZeroOrigin ? new DOMMatrix().translateSelf(originX, originY, originZ) : null;
if (hasTranslate) {
let tr = s.translate;
if (tr.includes('%')) {
const v = tr.split(' ');
const r = actualElement.getBoundingClientRect();
if (v[0].endsWith('%'))
v[0] = (parseFloat(v[0]) * r.width / 100) + 'px';
if (v[1]?.endsWith('%'))
v[1] = (parseFloat(v[1]) * r.height / 100) + 'px';
tr = v.join(',');
}
m.multiplySelf(new DOMMatrix('translate(' + tr.replaceAll(' ', ',') + ')'));
}
if (hasRotate) {
m.multiplySelf(new DOMMatrix('rotate(' + s.rotate.replaceAll(' ', ',') + ')'));
}
if (hasScale) {
m.multiplySelf(new DOMMatrix('scale(' + s.scale.replaceAll(' ', ',') + ')'));
}
if (hasOffsetPath) {
m.multiplySelf(computeOffsetTransformMatrix(element));
}
if (hasTransform) {
m.multiplySelf(new DOMMatrix(s.transform));
}
// FIX 11 (continued): Only wrap with origin if non-zero.
if (hasNonZeroOrigin) {
m = mOri.multiply(m.multiplySelf(mOri.inverse()));
}
//@ts-ignore
const pt = getElementPerspectiveTransform(element, iframes);
if (pt != null) {
m = pt.multiplySelf(m);
}
return m;
}
/**
* project a DOM-Matrix to 2D (from firefox matrix.h)
* @param {DOMMatrix} m
*/
function projectTo2D(m) {
m.m31 = 0.0;
m.m32 = 0.0;
m.m13 = 0.0;
m.m23 = 0.0;
m.m33 = 1.0;
m.m43 = 0.0;
m.m34 = 0.0;
// Some matrices, such as those derived from perspective transforms,
// can modify _44 from 1, while leaving the rest of the fourth column
// (_14, _24) at 0. In this case, after resetting the third row and
// third column above, the value of _44 functions only to scale the
// coordinate transform divide by W. The matrix can be converted to
// a true 2D matrix by normalizing out the scaling effect of _44 on
// the remaining components ahead of time.
if (m.m14 == 0.0 && m.m24 == 0.0 && m.m44 != 1.0 && m.m44 != 0.0) {
const scale = 1.0 / m.m44;
m.m11 *= scale;
m.m12 *= scale;
m.m21 *= scale;
m.m22 *= scale;
m.m41 *= scale;
m.m42 *= scale;
m.m44 = 1.0;
}
}
/**
* @param {HTMLElement} element
* @param {HTMLIFrameElement[]} iframes
*/
function getElementPerspectiveTransform(element, iframes) {
/** @type { Element } */
//@ts-ignore
const perspectiveNode = getParentElementIncludingSlots(element, iframes);
if (perspectiveNode) {
//https://drafts.csswg.org/css-transforms-2/#perspective-matrix-computation
let s = getCachedComputedStyle(perspectiveNode);
if (s.perspective !== 'none') {
let m = new DOMMatrix();
let p = parseFloat(s.perspective);
m.m34 = -1.0 / p;
//https://drafts.csswg.org/css-transforms-2/#PerspectiveDefined
if (s.perspectiveOrigin) {
const origin = s.perspectiveOrigin.split(' ');
const originX = parseFloat(origin[0]) - element.offsetLeft;
const originY = parseFloat(origin[1]) - element.offsetTop;
const mOri = new DOMMatrix().translateSelf(originX, originY);
const mOriInv = new DOMMatrix().translateSelf(-originX, -originY);
return mOri.multiplySelf(m.multiplySelf(mOriInv));
}
}
}
return null;
}
function computeOffsetTransformMatrix(elem) {
const cs = getCachedComputedStyle(elem);
const offsetPath = cs.offsetPath; // e.g. "path('M0,0 ...')"
const offsetDistance = cs.offsetDistance; // e.g. "50%"
const offsetRotate = cs.offsetRotate; // e.g. "auto", "45deg", "auto 30deg"
const offsetAnchor = cs.offsetAnchor;
const transformOrigin = cs.transformOrigin;
// Parse offset-distance (px or %)
let distance = parseOffsetDistance(offsetDistance);
// Compute position & tangent on path (in containing block coordinates)
let { x, y, angle } = computeOffsetPathPoint(elem, offsetPath, distance);
// Subtract the element's flow position within its containing block.
// The offset-path positions the element absolutely within the containing block,
// but the walk already adds offsetLeft/offsetTop (flow position). To avoid
// double-counting, make the offset relative to the flow position.
const parent = elem.parentElement;
if (parent instanceof HTMLElement || parent instanceof (parent.ownerDocument.defaultView ?? window).HTMLElement) {
if (elem.offsetParent === parent) {
// Containing block = parent = offsetParent
x -= elem.offsetLeft;
y -= elem.offsetTop;
} else if (elem.offsetParent === parent.offsetParent) {
// Both share the same offsetParent
x -= (elem.offsetLeft - parent.offsetLeft);
y -= (elem.offsetTop - parent.offsetTop);
}
}
// Handle offset-rotate
let rotateFinal = 0;
if (offsetRotate.startsWith("auto")) {
let parts = offsetRotate.split(/\s+/);
let extra = parts.length === 2 ? parseFloat(parts[1]) : 0;
rotateFinal = angle + extra;
} else {
rotateFinal = parseFloat(offsetRotate);
}
const anchor = parseOffsetAnchor(offsetAnchor, transformOrigin, elem);
const anchorMatrix = new DOMMatrix().translateSelf(-anchor.x, -anchor.y);
let m = anchorMatrix.translate(x, y);
m.multiplySelf(anchorMatrix.invertSelf());
m.rotateSelf(rotateFinal);
m.translateSelf(-anchor.x, -anchor.y);
return m;
}
function parseOffsetAnchor(str, transformOrigin, elem) {
const width = elem.offsetWidth;
const height = elem.offsetHeight;
if (!str || str === "auto") {
str = transformOrigin;
}
const parts = str.split(/\s+/);
if (parts.length === 1) {
// 1-value syntax = x only, y = center
const x = parsePosition(parts[0], width);
return { x, y: height / 2 };
}
const x = parsePosition(parts[0], width);
const y = parsePosition(parts[1], height);
return { x, y };
}
function parsePosition(part, size) {
part = part.trim();
if (part.endsWith("%")) {
return parseFloat(part) / 100 * size;
}
if (part.endsWith("px")) {
return parseFloat(part);
}
// keywords
switch (part) {
case "left": return 0;
case "top": return 0;
case "center": return size / 2;
case "right": return size;
case "bottom": return size;
}
return parseFloat(part);
}
function parseOffsetDistance(str) {
str = str.trim();
if (str.endsWith("%")) {
return parseFloat(str) / 100; // normalized (0..1)
}
return parseFloat(str); // px value if pathLength = 1
}
function parseAngle(str) {
if (!str) return 0;
str = str.trim();
if (str.endsWith("deg")) return parseFloat(str);
if (str.endsWith("rad")) return parseFloat(str) * (180 / Math.PI);
if (str.endsWith("grad")) return parseFloat(str) * 0.9;
return parseFloat(str);
}
function computeOffsetPathPoint(elem, offsetPath, distNorm) {
if (!offsetPath || offsetPath === "none") {
return { x: 0, y: 0, angle: 0 };
}
const value = offsetPath.trim();
let m = value.match(/path\(["'](.+)["']\)/);
if (m) return computePathType(m[1], distNorm);
if (value.startsWith("circle(")) return computeCircle(value, distNorm);
if (value.startsWith("ellipse(")) return computeEllipse(value, distNorm);
if (value.startsWith("inset(")) return computeInset(value, elem, distNorm);
if (value.startsWith("rect(")) return computeRect(value, distNorm);
if (value.startsWith("xywh(")) return computeXYWH(value, distNorm);
if (value.startsWith("ray(")) return computeRay(value, distNorm);
if (value.startsWith("polygon(")) return computePolygon(value, distNorm);
console.warn("Unsupported offset-path:", offsetPath);
return { x: 0, y: 0, angle: 0 };
}
function computePathType(pathData, distNorm) {
let svgPath = document.createElementNS("http://www.w3.org/2000/svg", "path");
svgPath.setAttribute("d", pathData);
const total = svgPath.getTotalLength();
const dist = distNorm <= 1 ? distNorm * total : distNorm;
const p1 = svgPath.getPointAtLength(dist);
const p2 = svgPath.getPointAtLength(Math.min(total, dist + 0.01));
let angle = Math.atan2(p2.y - p1.y, p2.x - p1.x) * 180 / Math.PI;
return { x: p1.x, y: p1.y, angle };
}
function computeRay(str, t) {
let m = str.match(/ray\(([^)]+)\)/);
let inside = m[1].trim();
// Split on "at" (optional)
let [beforeAt, atPart] = inside.split("at").map(s => s && s.trim());
// angle
let parts = beforeAt.split(/\s+/);
let angleDeg = parseAngle(parts[0]);
let angleRad = angleDeg * Math.PI / 180;
// point of origin
let ox = 0, oy = 0;
if (atPart) {
const pos = atPart.split(/\s+/);
ox = parseFloat(pos[0]);
oy = parseFloat(pos[1]);
}
// Ray: infinite line; offset-distance is distance along ray
let dist = (t <= 1 ? t : t); // percentage normalized already
let x = ox + Math.cos(angleRad) * dist;
let y = oy + Math.sin(angleRad) * dist;
// tangent is ray direction
return { x, y, angle: angleDeg };
}
function computeCircle(str, t) {
let m = str.match(/circle\(([^)]+)\)/);
let inner = m[1];
let [radiusPart, atPart] = inner.split("at").map(s => s.trim());
let r = parseFloat(radiusPart);
let [cx, cy] = atPart.split(/\s+/).map(parseFloat);
let angleRad = t * 2 * Math.PI;
let x = cx + Math.cos(angleRad) * r;
let y = cy + Math.sin(angleRad) * r;
let tangentAngleDeg = angleRad * 180 / Math.PI + 90;
return { x, y, angle: tangentAngleDeg };
}
function computeEllipse(str, t) {
let m = str.match(/ellipse\(([^)]+)\)/);
let parts = m[1].split("at");
let radii = parts[0].trim().split(/\s+/).map(parseFloat);
let center = parts[1].trim().split(/\s+/).map(parseFloat);
let rx = radii[0];
let ry = radii[1];
let cx = center[0];
let cy = center[1];
let angleRad = t * 2 * Math.PI;
let x = cx + Math.cos(angleRad) * rx;
let y = cy + Math.sin(angleRad) * ry;
// tangent direction derivative
let dx = -Math.sin(angleRad) * rx;
let dy = Math.cos(angleRad) * ry;
let tangentAngleDeg = Math.atan2(dy, dx) * 180 / Math.PI;
return { x, y, angle: tangentAngleDeg };
}
function computeRect(str, t) {
let m = str.match(/rect\(([^)]+)\)/);
let nums = m[1].split(/\s+/).map(s => parseFloat(s));
let top = nums[0], right = nums[1], bottom = nums[2], left = nums[3];
return rectPath(top, left, right, bottom, t);
}
function computeXYWH(str, t) {
let m = str.match(/xywh\(([^)]+)\)/);
let nums = m[1].split(/\s+/).map(parseFloat);
let left = nums[0];
let top = nums[1];
let width = nums[2];
let height = nums[3];
return rectPath(top, left, left + width, top + height, t);
}
function computePolygon(str, t) {
let m = str.match(/polygon\(([^)]+)\)/);
let pairs = m[1].split(",").map(p => p.trim().split(/\s+/).map(parseFloat));
// Build cumulative lengths
let pts = pairs;
let lengths = [0];
for (let i = 1; i < pts.length; i++) {
let dx = pts[i][0] - pts[i - 1][0];
let dy = pts[i][1] - pts[i - 1][1];
lengths.push(Math.hypot(dx, dy) + lengths[i - 1]);
}
// close polygon
let dx = pts[0][0] - pts[pts.length - 1][0];
let dy = pts[0][1] - pts[pts.length - 1][1];
lengths.push(Math.hypot(dx, dy) + lengths[lengths.length - 1]);
let total = lengths[lengths.length - 1];
let target = t * total;
// find segment
let i = lengths.findIndex(len => len >= target);
if (i <= 0) i = 1;
let prevLen = lengths[i - 1];
let nextLen = lengths[i];
let segT = (target - prevLen) / (nextLen - prevLen);
// segment points
let a = pts[(i - 1) % pts.length];
let b = pts[i % pts.length];
let x = a[0] + (b[0] - a[0]) * segT;
let y = a[1] + (b[1] - a[1]) * segT;
let angle = Math.atan2(b[1] - a[1], b[0] - a[0]) * 180 / Math.PI;
return { x, y, angle };
}
function rectPath(top, left, right, bottom, t) {
let w = right - left;
let h = bottom - top;
let perimeter = 2 * (w + h);
let dist = t * perimeter;
// go around edges
if (dist < w) {
// top edge
let x = left + dist;
return { x, y: top, angle: 0 };
}
dist -= w;
if (dist < h) {
// right edge
let y = top + dist;
return { x: right, y, angle: 90 };
}
dist -= h;
if (dist < w) {
// bottom edge
let x = right - dist;
return { x, y: bottom, angle: 180 };
}
dist -= w;
// left edge
let y = bottom - dist;
return { x: left, y, angle: 270 };
}
// normalized inset uses calc
function tokenizeCalc(input) {
let tokens = [];
let i = 0;
while (i < input.length) {
let ch = input[i];
if (/\s/.test(ch)) {
i++;
continue;
}
// operators & parentheses
if ("+-*/()".includes(ch)) {
tokens.push({ type: ch, value: ch });
i++;
continue;
}
// numbers or dimensions or %
if (/[0-9.]/.test(ch)) {
let start = i;
while (/[0-9.]/.test(input[i])) i++;
let num = input.slice(start, i);
if (input[i] === "%") {
i++;
tokens.push({ type: "percentage", value: parseFloat(num) });
continue;
}
// only px supported
if (input.slice(i, i+2) === "px") {
i += 2;
tokens.push({ type: "dimension", value: parseFloat(num), unit: "px" });
continue;
}
// plain number
tokens.push({ type: "number", value: parseFloat(num) });
continue;
}
// function name (calc)
if (/[a-zA-Z]/.test(ch)) {
let start = i;
while (/[a-zA-Z]/.test(input[i])) i++;
let name = input.slice(start, i);
if (name === "calc" && input[i] === "(") {
tokens.push({ type: "func", value: "calc" });
continue;
}
throw new Error("Unsupported function: " + name);
}
throw new Error("Unexpected character in calc(): " + ch);
}
return tokens;
}
// normalized inset uses calc
function parseCalc(tokens) {
let i = 0;
function peek() { return tokens[i]; }
function consume() { return tokens[i++]; }
function parseExpression() {
let node = parseTerm();
while (peek() && (peek().type === "+" || peek().type === "-")) {
let op = consume().type;
let right = parseTerm();
node = { type: "binary", op, left: node, right };
}
return node;
}
function parseTerm() {
let node = parseFactor();
while (peek() && (peek().type === "*" || peek().type === "/")) {
let op = consume().type;
let right = parseFactor();
node = { type: "binary", op, left: node, right };
}
return node;
}
function parseFactor() {
let t = peek();
if (!t) throw "Unexpected end in calc()";
if (t.type === "number") {
consume();
return { type: "number", value: t.value };
}
if (t.type === "dimension") {
consume();
return { type: "dimension", value: t.value, unit: t.unit };
}
if (t.type === "percentage") {
consume();
return { type: "percentage", value: t.value };
}
if (t.type === "func") {
consume(); // "calc"
if (peek().type !== "(") throw "Expected '(' after calc";
consume();
let node = parseExpression();
if (!peek() || peek().type !== ")") throw "Expected ')'";
consume();
return node;
}
if (t.type === "(") {
consume();
let node = parseExpression();
if (!peek() || peek().type !== ")") throw "Expected ')'";
consume();
return node;
}
throw new Error("Unexpected calc token " + JSON.stringify(t));
}
let ast = parseExpression();
if (i !== tokens.length) throw "Extra tokens after calc";
return ast;
}
// normalized inset uses calc
function evalCalc(ast, env) {
switch (ast.type) {
case "number":
return ast.value;
case "dimension":
return ast.value; // px only -> already a number
case "percentage":
return env.percentBase * (ast.value / 100);
case "binary": {
let l = evalCalc(ast.left, env);
let r = evalCalc(ast.right, env);
switch (ast.op) {
case "+": return l + r;
case "-": return l - r;
case "*": return l * r;
case "/": return l / r;
}
}
}
throw "Invalid AST node " + ast.type;
}
function resolveLength(expr, element, useHeight = false) {
expr = expr.trim();
// Fast path: pure px
if (/^[0-9.]+px$/.test(expr))
return parseFloat(expr);
let base = useHeight ? element.offsetHeight : element.offsetWidth;
// Pure %
if (/^[0-9.]+%$/.test(expr)) {
let p = parseFloat(expr);
return base * (p / 100);
}
// calc(...) or mixed values
const ast = parseCalc(tokenizeCalc(expr));
return evalCalc(ast, {
percentBase: base
});
}
function parseInsetArgs(str) {
let inside = str.trim()
.replace(/^inset\s*\(/, "")
.replace(/\)\s*$/, "");
let args = [];
let current = "";
let depth = 0;
for (let i = 0; i < inside.length; i++) {
let ch = inside[i];
if (ch === "(") {
depth++;
current += ch;
} else if (ch === ")") {
depth--;
current += ch;
} else if (/\s/.test(ch) && depth === 0) {
if (current.trim() !== "") {
args.push(current.trim());
current = "";
}
} else {
current += ch;
}
}
if (current.trim() !== "") {
args.push(current.trim());
}
return args;
}
/**
*
* @param {string} str
* @param {HTMLElement} element
* @param {number} progress
* @returns
*/
function computeInset(str, element, progress) {
const args = parseInsetArgs(str);
if (args.length !== 4)
throw new Error("inset() must have 4 arguments");
const topPx = resolveLength(args[0], element, true);
const rightPx = resolveLength(args[1], element, false);
const bottomPx = resolveLength(args[2], element, true);
const leftPx = resolveLength(args[3], element, false);
const w = element.offsetWidth;
const h = element.offsetHeight;
// Actual rectangle coordinates
const x1 = leftPx;
const y1 = topPx;
const x2 = w - rightPx;
const y2 = h - bottomPx;
// Rectangle perimeter
const P = 2 * ((x2 - x1) + (y2 - y1));
let d = P * progress;
// Walk the rectangle clockwise, return point
// Top edge: (x1 -> x2, y1)
let len = x2 - x1;
if (d <= len) return { x: x1 + d, y: y1, angle: 0 };
d -= len;
// Right edge: (x2, y1 -> y2)
len = y2 - y1;
if (d <= len) return { x: x2, y: y1 + d, angle: 90 };
d -= len;
// Bottom edge: (x2 -> x1, y2)
len = x2 - x1;
if (d <= len) return { x: x2 - d, y: y2, angle: 180 };
d -= len;
// Left edge: (x1, y2 -> y1)
return { x: x1, y: y2 - d, angle: 270 };
}
//Code from: https://github.com/floating-ui/floating-ui/blob/master/packages/utils/src/dom.ts
const transformProperties = ['transform', 'translate', 'scale', 'rotate', 'perspective'];
const willChangeValues = ['transform', 'translate', 'scale', 'rotate', 'perspective', 'filter'];
const containValues = ['paint', 'layout', 'strict', 'content'];
function isElement(value) {
const elType = value?.ownerDocument?.defaultView?.Element;
return value instanceof Element || (elType != null && value instanceof elType);
}
/**
*
* @param {CSSStyleDeclaration} css
* @returns {boolean}
*/
function isContainingBlock(css) {
// https://developer.mozilla.org/en-US/docs/Web/CSS/Containing_block#identifying_the_containing_block
// https://drafts.csswg.org/css-transforms-2/#individual-transforms
return (
transformProperties.some((value) => css[value] ? css[value] !== 'none' : false) ||
(css.containerType ? css.containerType !== 'normal' : false) ||
(css.backdropFilter ? css.backdropFilter !== 'none' : false) ||
(css.filter ? css.filter !== 'none' : false) ||
willChangeValues.some((value) => (css.willChange || '').includes(value)) ||
containValues.some((value) => (css.contain || '').includes(value))
);
}
//Code from: https://github.com/jcfranco/composed-offset-position/blob/main/src/index.ts
function flatTreeParent(element) {
if (element.assignedSlot)
return element.assignedSlot;
if (element.parentNode instanceof ShadowRoot)
return element.parentNode.host;
return element.parentNode;
}
function isFlatTreeInclusiveAncestor(ancestor, node) {
for (let current = node; current; current = flatTreeParent(current)) {
if (current === ancestor)
return true;
}
return false;
}
function ancestorTreeScopes(element) {
const scopes = new Set();
let currentScope = element.getRootNode();
const shadowRootCtor = element.ownerDocument?.defaultView?.ShadowRoot ?? ShadowRoot;
while (currentScope) {
scopes.add(currentScope);
if (currentScope instanceof shadowRootCtor) {
currentScope = currentScope.host?.getRootNode() ?? null;
} else {
currentScope = currentScope.parentNode
? currentScope.parentNode.getRootNode()
: null;
}
}
return scopes;
}
function offsetParentPolyfill(element) {
// Do an initial walk to check for display:none ancestors.
for (let ancestor = element; ancestor; ancestor = flatTreeParent(ancestor)) {
if (!(ancestor instanceof Element))
continue;
if (getCachedComputedStyle(ancestor).display === 'none')
return null;
}
for (let ancestor = flatTreeParent(element); ancestor; ancestor = flatTreeParent(ancestor)) {
if (!(ancestor instanceof Element))
continue;
const style = getCachedComputedStyle(ancestor);
if (style.display === 'contents')
continue;
if (style.position !== 'static' || isContainingBlock(style))
return ancestor;
if (ancestor.tagName === 'BODY')
return ancestor;
}
return null;
}
/**
*
* @param {*} element
* @param {'offsetTop' | 'offsetLeft'} offsetTopOrLeft
* @returns
*/
function offsetTopLeftPolyfill(element, offsetTopOrLeft) {
let value = element[offsetTopOrLeft];
let nextOffsetParent = offsetParentPolyfill(element);
const scopes = ancestorTreeScopes(element);
while (nextOffsetParent && !scopes.has(nextOffsetParent.getRootNode())) {
value -= nextOffsetParent[offsetTopOrLeft];
nextOffsetParent = offsetParentPolyfill(nextOffsetParent);
}
return value;
}
/**
* @param {Element} element
* @returns {boolean}
*/
function createsFixedContainingBlock(element) {
const cs = getCachedComputedStyle(element);
if ((cs.transform && cs.transform !== 'none') ||
(cs.perspective && cs.perspective !== 'none') ||
(cs.filter && cs.filter !== 'none') ||
(cs.backdropFilter && cs.backdropFilter !== 'none')) {
return true;
}
const contain = cs.contain || '';
if (contain.includes('paint') || contain.includes('layout') || contain.includes('strict') || contain.includes('content')) {
return true;
}
const willChange = cs.willChange || '';
return /transform|perspective|filter|backdrop-filter/.test(willChange);
}
/**
* @param {HTMLElement} element
* @param {HTMLIFrameElement[]=} iframes
* @returns {Element | null}
*/
function getNearestFixedContainingBlock(element, iframes) {
let parent = getParentElementIncludingSlots(element, iframes);
while (parent) {
if (createsFixedContainingBlock(parent)) return parent;
parent = getParentElementIncludingSlots(parent, iframes);
}
return null;
}
================================================
FILE: packages/web-component-designer/src/elements/helper/w3color.ts
================================================
/* w3color.js ver.1.18 by w3schools.com (Do not remove this line)*/
export class w3color {
red: number = 0;
green: number = 0;
blue: number = 0;
hue: number = 0;
sat: number = 0;
lightness: number = 0;
whiteness: number = 0;
blackness: number = 0;
cyan: number = 0;
magenta: number = 0;
yellow: number = 0;
black: number = 0;
ncol: string = 'R';
opacity: number = 1;
valid: boolean = false;
toRgbString() {
return "rgb(" + this.red + ", " + this.green + ", " + this.blue + ")";
}
toRgbaString() {
return "rgba(" + this.red + ", " + this.green + ", " + this.blue + ", " + this.opacity + ")";
}
toHwbString() {
return "hwb(" + this.hue + ", " + Math.round(this.whiteness * 100) + "%, " + Math.round(this.blackness * 100) + "%)";
}
toHwbStringDecimal() {
return "hwb(" + this.hue + ", " + this.whiteness + ", " + this.blackness + ")";
}
toHwbaString() {
return "hwba(" + this.hue + ", " + Math.round(this.whiteness * 100) + "%, " + Math.round(this.blackness * 100) + "%, " + this.opacity + ")";
}
toHslString() {
return "hsl(" + this.hue + ", " + Math.round(this.sat * 100) + "%, " + Math.round(this.lightness * 100) + "%)";
}
toHslStringDecimal() {
return "hsl(" + this.hue + ", " + this.sat + ", " + this.lightness + ")";
}
toHslaString() {
return "hsla(" + this.hue + ", " + Math.round(this.sat * 100) + "%, " + Math.round(this.lightness * 100) + "%, " + this.opacity + ")";
}
toCmykString() {
return "cmyk(" + Math.round(this.cyan * 100) + "%, " + Math.round(this.magenta * 100) + "%, " + Math.round(this.yellow * 100) + "%, " + Math.round(this.black * 100) + "%)";
}
toCmykStringDecimal() {
return "cmyk(" + this.cyan + ", " + this.magenta + ", " + this.yellow + ", " + this.black + ")";
}
toNcolString() {
return this.ncol + ", " + Math.round(this.whiteness * 100) + "%, " + Math.round(this.blackness * 100) + "%";
}
toNcolStringDecimal() {
return this.ncol + ", " + this.whiteness + ", " + this.blackness;
}
toNcolaString() {
return this.ncol + ", " + Math.round(this.whiteness * 100) + "%, " + Math.round(this.blackness * 100) + "%, " + this.opacity;
}
toName() {
let r, g, b, colorhexs = w3color.getColorArr('hexs');
for (let i = 0; i < colorhexs.length; i++) {
r = parseInt(colorhexs[i].substr(0, 2), 16);
g = parseInt(colorhexs[i].substr(2, 2), 16);
b = parseInt(colorhexs[i].substr(4, 2), 16);
if (this.red == r && this.green == g && this.blue == b) {
return w3color.getColorArr('names')[i];
}
}
return null;
}
toHexString() {
let r = w3color.toHex(this.red);
let g = w3color.toHex(this.green);
let b = w3color.toHex(this.blue);
return "#" + r + g + b;
}
toNameOrHexString() {
let name = this.toName();
if (!name)
name = this.toHexString();
return name;
}
toRgb() {
return { r: this.red, g: this.green, b: this.blue, a: this.opacity };
}
toHsl() {
return { h: this.hue, s: this.sat, l: this.lightness, a: this.opacity };
}
toHwb() {
return { h: this.hue, w: this.whiteness, b: this.blackness, a: this.opacity };
}
toCmyk() {
return { c: this.cyan, m: this.magenta, y: this.yellow, k: this.black, a: this.opacity };
}
toNcol() {
return { ncol: this.ncol, w: this.whiteness, b: this.blackness, a: this.opacity };
}
isDark(n) {
let m = (n || 128);
return (((this.red * 299 + this.green * 587 + this.blue * 114) / 1000) < m);
}
saturate(n) {
let x, rgb, color;
x = (n / 100 || 0.1);
this.sat += x;
if (this.sat > 1) { this.sat = 1; }
rgb = w3color.hslToRgb(this.hue, this.sat, this.lightness);
color = w3color.colorObject(rgb, this.opacity, this.hue, this.sat);
this.attachValues(color);
}
desaturate(n) {
let x, rgb, color;
x = (n / 100 || 0.1);
this.sat -= x;
if (this.sat < 0) { this.sat = 0; }
rgb = w3color.hslToRgb(this.hue, this.sat, this.lightness);
color = w3color.colorObject(rgb, this.opacity, this.hue, this.sat);
this.attachValues(color);
}
lighter(n) {
let x, rgb, color;
x = (n / 100 || 0.1);
this.lightness += x;
if (this.lightness > 1) { this.lightness = 1; }
rgb = w3color.hslToRgb(this.hue, this.sat, this.lightness);
color = w3color.colorObject(rgb, this.opacity, this.hue, this.sat);
this.attachValues(color);
}
darker(n) {
let x, rgb, color;
x = (n / 100 || 0.1);
this.lightness -= x;
if (this.lightness < 0) { this.lightness = 0; }
rgb = w3color.hslToRgb(this.hue, this.sat, this.lightness);
color = w3color.colorObject(rgb, this.opacity, this.hue, this.sat);
this.attachValues(color);
}
attachValues(color) {
this.red = color.red;
this.green = color.green;
this.blue = color.blue;
this.hue = color.hue;
this.sat = color.sat;
this.lightness = color.lightness;
this.whiteness = color.whiteness;
this.blackness = color.blackness;
this.cyan = color.cyan;
this.magenta = color.magenta;
this.yellow = color.yellow;
this.black = color.black;
this.ncol = color.ncol;
this.opacity = color.opacity;
this.valid = color.valid;
}
static toColorObject(c): w3color {
let x, y, typ, arr = [], arrlength, i, opacity, match, a, hue, sat, rgb, colornames = [], colorhexs = [];
c = w3color.w3trim(c.toLowerCase());
x = c.substr(0, 1).toUpperCase();
y = c.substr(1);
a = 1;
if ((x == "R" || x == "Y" || x == "G" || x == "C" || x == "B" || x == "M" || x == "W") && !isNaN(y)) {
if (c.length == 6 && c.indexOf(",") == -1) {
} else {
c = "ncol(" + c + ")";
}
}
if (c.length != 3 && c.length != 6 && !isNaN(c)) { c = "ncol(" + c + ")"; }
if (c.indexOf(",") > 0 && c.indexOf("(") == -1) { c = "ncol(" + c + ")"; }
if (c.substr(0, 3) == "rgb" || c.substr(0, 3) == "hsl" || c.substr(0, 3) == "hwb" || c.substr(0, 4) == "ncol" || c.substr(0, 4) == "cmyk") {
if (c.substr(0, 4) == "ncol") {
if (c.split(",").length == 4 && c.indexOf("ncola") == -1) {
c = c.replace("ncol", "ncola");
}
typ = "ncol";
c = c.substr(4);
} else if (c.substr(0, 4) == "cmyk") {
typ = "cmyk";
c = c.substr(4);
} else {
typ = c.substr(0, 3);
c = c.substr(3);
}
arrlength = 3;
opacity = false;
if (c.substr(0, 1).toLowerCase() == "a") {
arrlength = 4;
opacity = true;
c = c.substr(1);
} else if (typ == "cmyk") {
arrlength = 4;
if (c.split(",").length == 5) {
arrlength = 5;
opacity = true;
}
}
c = c.replace("(", "");
c = c.replace(")", "");
arr = c.split(",");
if (typ == "rgb") {
if (arr.length != arrlength) {
return new w3color();
}
for (i = 0; i < arrlength; i++) {
if (arr[i] == "" || arr[i] == " ") { arr[i] = "0"; }
if (arr[i].indexOf("%") > -1) {
arr[i] = arr[i].replace("%", "");
arr[i] = Number(arr[i] / 100);
if (i < 3) { arr[i] = Math.round(arr[i] * 255); }
}
if (isNaN(arr[i])) { return new w3color(); }
if (parseInt(arr[i]) > 255) { arr[i] = 255; }
if (i < 3) { arr[i] = parseInt(arr[i]); }
if (i == 3 && Number(arr[i]) > 1) { arr[i] = 1; }
}
rgb = { r: arr[0], g: arr[1], b: arr[2] };
if (opacity == true) { a = Number(arr[3]); }
}
if (typ == "hsl" || typ == "hwb" || typ == "ncol") {
while (arr.length < arrlength) { arr.push("0"); }
if (typ == "hsl" || typ == "hwb") {
if (parseInt(arr[0]) >= 360) { arr[0] = 0; }
}
for (i = 1; i < arrlength; i++) {
if (arr[i].indexOf("%") > -1) {
arr[i] = arr[i].replace("%", "");
arr[i] = Number(arr[i]);
if (isNaN(arr[i])) { return new w3color(); }
arr[i] = arr[i] / 100;
} else {
arr[i] = Number(arr[i]);
}
if (Number(arr[i]) > 1) { arr[i] = 1; }
if (Number(arr[i]) < 0) { arr[i] = 0; }
}
if (typ == "hsl") { rgb = w3color.hslToRgb(arr[0], arr[1], arr[2]); hue = Number(arr[0]); sat = Number(arr[1]); }
if (typ == "hwb") { rgb = w3color.hwbToRgb(arr[0], arr[1], arr[2]); }
if (typ == "ncol") { rgb = w3color.ncolToRgb(arr[0], arr[1], arr[2]); }
if (opacity == true) { a = Number(arr[3]); }
}
if (typ == "cmyk") {
while (arr.length < arrlength) { arr.push("0"); }
for (i = 0; i < arrlength; i++) {
if (arr[i].indexOf("%") > -1) {
arr[i] = arr[i].replace("%", "");
arr[i] = Number(arr[i]);
if (isNaN(arr[i])) { return new w3color(); }
arr[i] = arr[i] / 100;
} else {
arr[i] = Number(arr[i]);
}
if (Number(arr[i]) > 1) { arr[i] = 1; }
if (Number(arr[i]) < 0) { arr[i] = 0; }
}
rgb = w3color.cmykToRgb(arr[0], arr[1], arr[2], arr[3]);
if (opacity == true) { a = Number(arr[4]); }
}
} else if (c.substr(0, 3) == "ncs") {
rgb = w3color.ncsToRgb(c);
} else {
match = false;
colornames = w3color.getColorArr('names');
for (i = 0; i < colornames.length; i++) {
if (c.toLowerCase() == colornames[i].toLowerCase()) {
colorhexs = w3color.getColorArr('hexs');
match = true;
rgb = {
r: parseInt(colorhexs[i].substr(0, 2), 16),
g: parseInt(colorhexs[i].substr(2, 2), 16),
b: parseInt(colorhexs[i].substr(4, 2), 16)
};
break;
}
}
if (match == false) {
c = c.replace("#", "");
if (c.length == 3) { c = c.substr(0, 1) + c.substr(0, 1) + c.substr(1, 1) + c.substr(1, 1) + c.substr(2, 1) + c.substr(2, 1); }
for (i = 0; i < c.length; i++) {
if (!w3color.isHex(c.substr(i, 1))) { return new w3color(); }
}
arr[0] = parseInt(c.substr(0, 2), 16);
arr[1] = parseInt(c.substr(2, 2), 16);
arr[2] = parseInt(c.substr(4, 2), 16);
for (i = 0; i < 3; i++) {
if (isNaN(arr[i])) { return new w3color(); }
}
rgb = {
r: arr[0],
g: arr[1],
b: arr[2]
};
}
}
return w3color.colorObject(rgb, a, hue, sat);
}
static colorObject(rgb, a, h, s) {
let hsl, hwb, cmyk, ncol, color, hue, sat;
if (!rgb) { return new w3color(); }
if (a === null) { a = 1; }
hsl = w3color.rgbToHsl(rgb.r, rgb.g, rgb.b);
hwb = w3color.rgbToHwb(rgb.r, rgb.g, rgb.b);
cmyk = w3color.rgbToCmyk(rgb.r, rgb.g, rgb.b);
hue = (h || hsl.h);
sat = (s || hsl.s);
ncol = w3color.hueToNcol(hue);
color = {
red: rgb.r,
green: rgb.g,
blue: rgb.b,
hue: hue,
sat: sat,
lightness: hsl.l,
whiteness: hwb.w,
blackness: hwb.b,
cyan: cmyk.c,
magenta: cmyk.m,
yellow: cmyk.y,
black: cmyk.k,
ncol: ncol,
opacity: a,
valid: true
};
color = w3color.roundDecimals(color);
return Object.assign(new w3color(), color);
}
static getColorArr(x) {
if (x == "names") { return ['AliceBlue', 'AntiqueWhite', 'Aqua', 'Aquamarine', 'Azure', 'Beige', 'Bisque', 'Black', 'BlanchedAlmond', 'Blue', 'BlueViolet', 'Brown', 'BurlyWood', 'CadetBlue', 'Chartreuse', 'Chocolate', 'Coral', 'CornflowerBlue', 'Cornsilk', 'Crimson', 'Cyan', 'DarkBlue', 'DarkCyan', 'DarkGoldenRod', 'DarkGray', 'DarkGrey', 'DarkGreen', 'DarkKhaki', 'DarkMagenta', 'DarkOliveGreen', 'DarkOrange', 'DarkOrchid', 'DarkRed', 'DarkSalmon', 'DarkSeaGreen', 'DarkSlateBlue', 'DarkSlateGray', 'DarkSlateGrey', 'DarkTurquoise', 'DarkViolet', 'DeepPink', 'DeepSkyBlue', 'DimGray', 'DimGrey', 'DodgerBlue', 'FireBrick', 'FloralWhite', 'ForestGreen', 'Fuchsia', 'Gainsboro', 'GhostWhite', 'Gold', 'GoldenRod', 'Gray', 'Grey', 'Green', 'GreenYellow', 'HoneyDew', 'HotPink', 'IndianRed', 'Indigo', 'Ivory', 'Khaki', 'Lavender', 'LavenderBlush', 'LawnGreen', 'LemonChiffon', 'LightBlue', 'LightCoral', 'LightCyan', 'LightGoldenRodYellow', 'LightGray', 'LightGrey', 'LightGreen', 'LightPink', 'LightSalmon', 'LightSeaGreen', 'LightSkyBlue', 'LightSlateGray', 'LightSlateGrey', 'LightSteelBlue', 'LightYellow', 'Lime', 'LimeGreen', 'Linen', 'Magenta', 'Maroon', 'MediumAquaMarine', 'MediumBlue', 'MediumOrchid', 'MediumPurple', 'MediumSeaGreen', 'MediumSlateBlue', 'MediumSpringGreen', 'MediumTurquoise', 'MediumVioletRed', 'MidnightBlue', 'MintCream', 'MistyRose', 'Moccasin', 'NavajoWhite', 'Navy', 'OldLace', 'Olive', 'OliveDrab', 'Orange', 'OrangeRed', 'Orchid', 'PaleGoldenRod', 'PaleGreen', 'PaleTurquoise', 'PaleVioletRed', 'PapayaWhip', 'PeachPuff', 'Peru', 'Pink', 'Plum', 'PowderBlue', 'Purple', 'RebeccaPurple', 'Red', 'RosyBrown', 'RoyalBlue', 'SaddleBrown', 'Salmon', 'SandyBrown', 'SeaGreen', 'SeaShell', 'Sienna', 'Silver', 'SkyBlue', 'SlateBlue', 'SlateGray', 'SlateGrey', 'Snow', 'SpringGreen', 'SteelBlue', 'Tan', 'Teal', 'Thistle', 'Tomato', 'Turquoise', 'Violet', 'Wheat', 'White', 'WhiteSmoke', 'Yellow', 'YellowGreen']; }
if (x == "hexs") { return ['f0f8ff', 'faebd7', '00ffff', '7fffd4', 'f0ffff', 'f5f5dc', 'ffe4c4', '000000', 'ffebcd', '0000ff', '8a2be2', 'a52a2a', 'deb887', '5f9ea0', '7fff00', 'd2691e', 'ff7f50', '6495ed', 'fff8dc', 'dc143c', '00ffff', '00008b', '008b8b', 'b8860b', 'a9a9a9', 'a9a9a9', '006400', 'bdb76b', '8b008b', '556b2f', 'ff8c00', '9932cc', '8b0000', 'e9967a', '8fbc8f', '483d8b', '2f4f4f', '2f4f4f', '00ced1', '9400d3', 'ff1493', '00bfff', '696969', '696969', '1e90ff', 'b22222', 'fffaf0', '228b22', 'ff00ff', 'dcdcdc', 'f8f8ff', 'ffd700', 'daa520', '808080', '808080', '008000', 'adff2f', 'f0fff0', 'ff69b4', 'cd5c5c', '4b0082', 'fffff0', 'f0e68c', 'e6e6fa', 'fff0f5', '7cfc00', 'fffacd', 'add8e6', 'f08080', 'e0ffff', 'fafad2', 'd3d3d3', 'd3d3d3', '90ee90', 'ffb6c1', 'ffa07a', '20b2aa', '87cefa', '778899', '778899', 'b0c4de', 'ffffe0', '00ff00', '32cd32', 'faf0e6', 'ff00ff', '800000', '66cdaa', '0000cd', 'ba55d3', '9370db', '3cb371', '7b68ee', '00fa9a', '48d1cc', 'c71585', '191970', 'f5fffa', 'ffe4e1', 'ffe4b5', 'ffdead', '000080', 'fdf5e6', '808000', '6b8e23', 'ffa500', 'ff4500', 'da70d6', 'eee8aa', '98fb98', 'afeeee', 'db7093', 'ffefd5', 'ffdab9', 'cd853f', 'ffc0cb', 'dda0dd', 'b0e0e6', '800080', '663399', 'ff0000', 'bc8f8f', '4169e1', '8b4513', 'fa8072', 'f4a460', '2e8b57', 'fff5ee', 'a0522d', 'c0c0c0', '87ceeb', '6a5acd', '708090', '708090', 'fffafa', '00ff7f', '4682b4', 'd2b48c', '008080', 'd8bfd8', 'ff6347', '40e0d0', 'ee82ee', 'f5deb3', 'ffffff', 'f5f5f5', 'ffff00', '9acd32']; }
return null;
}
static roundDecimals(c) {
c.red = Number(c.red.toFixed(0));
c.green = Number(c.green.toFixed(0));
c.blue = Number(c.blue.toFixed(0));
c.hue = Number(c.hue.toFixed(0));
c.sat = Number(c.sat.toFixed(2));
c.lightness = Number(c.lightness.toFixed(2));
c.whiteness = Number(c.whiteness.toFixed(2));
c.blackness = Number(c.blackness.toFixed(2));
c.cyan = Number(c.cyan.toFixed(2));
c.magenta = Number(c.magenta.toFixed(2));
c.yellow = Number(c.yellow.toFixed(2));
c.black = Number(c.black.toFixed(2));
c.ncol = c.ncol.substr(0, 1) + Math.round(Number(c.ncol.substr(1)));
c.opacity = Number(c.opacity.toFixed(2));
return c;
}
static hslToRgb(hue, sat, light) {
let t1, t2, r, g, b;
hue = hue / 60;
if (light <= 0.5) {
t2 = light * (sat + 1);
} else {
t2 = light + sat - (light * sat);
}
t1 = light * 2 - t2;
r = w3color.hueToRgb(t1, t2, hue + 2) * 255;
g = w3color.hueToRgb(t1, t2, hue) * 255;
b = w3color.hueToRgb(t1, t2, hue - 2) * 255;
return { r: r, g: g, b: b };
}
static hueToRgb(t1, t2, hue) {
if (hue < 0) hue += 6;
if (hue >= 6) hue -= 6;
if (hue < 1) return (t2 - t1) * hue + t1;
else if (hue < 3) return t2;
else if (hue < 4) return (t2 - t1) * (4 - hue) + t1;
else return t1;
}
static hwbToRgb(hue, white, black) {
let i, rgb, rgbArr = [], tot;
rgb = w3color.hslToRgb(hue, 1, 0.50);
rgbArr[0] = rgb.r / 255;
rgbArr[1] = rgb.g / 255;
rgbArr[2] = rgb.b / 255;
tot = white + black;
if (tot > 1) {
white = Number((white / tot).toFixed(2));
black = Number((black / tot).toFixed(2));
}
for (i = 0; i < 3; i++) {
rgbArr[i] *= (1 - (white) - (black));
rgbArr[i] += (white);
rgbArr[i] = Number(rgbArr[i] * 255);
}
return { r: rgbArr[0], g: rgbArr[1], b: rgbArr[2] };
}
static cmykToRgb(c, m, y, k) {
let r, g, b;
r = 255 - ((Math.min(1, c * (1 - k) + k)) * 255);
g = 255 - ((Math.min(1, m * (1 - k) + k)) * 255);
b = 255 - ((Math.min(1, y * (1 - k) + k)) * 255);
return { r: r, g: g, b: b };
}
static ncolToRgb(ncol, white, black) {
let letter, percent, h;
h = ncol;
if (isNaN(ncol.substr(0, 1))) {
letter = ncol.substr(0, 1).toUpperCase();
percent = ncol.substr(1);
if (percent == "") { percent = 0; }
percent = Number(percent);
if (isNaN(percent)) { return false; }
if (letter == "R") { h = 0 + (percent * 0.6); }
if (letter == "Y") { h = 60 + (percent * 0.6); }
if (letter == "G") { h = 120 + (percent * 0.6); }
if (letter == "C") { h = 180 + (percent * 0.6); }
if (letter == "B") { h = 240 + (percent * 0.6); }
if (letter == "M") { h = 300 + (percent * 0.6); }
if (letter == "W") {
h = 0;
white = 1 - (percent / 100);
black = (percent / 100);
}
}
return w3color.hwbToRgb(h, white, black);
}
static hueToNcol(hue) {
while (hue >= 360) {
hue = hue - 360;
}
if (hue < 60) { return "R" + (hue / 0.6); }
if (hue < 120) { return "Y" + ((hue - 60) / 0.6); }
if (hue < 180) { return "G" + ((hue - 120) / 0.6); }
if (hue < 240) { return "C" + ((hue - 180) / 0.6); }
if (hue < 300) { return "B" + ((hue - 240) / 0.6); }
if (hue < 360) { return "M" + ((hue - 300) / 0.6); }
return null;
}
static ncsToRgb(ncs): { r: number, b: number, g: number } {
let black, chroma, bc, percent, black1, chroma1, factor1, blue1, red1, red2, green1, green2, blue2, max, factor2, grey, r, g, b;
ncs = w3color.w3trim(ncs).toUpperCase();
ncs = ncs.replace("(", "");
ncs = ncs.replace(")", "");
ncs = ncs.replace("NCS", "NCS ");
ncs = ncs.replace(/ /g, " ");
if (ncs.indexOf("NCS") == -1) { ncs = "NCS " + ncs; }
ncs = ncs.match(/^(?:NCS|NCS\sS)\s(\d{2})(\d{2})-(N|[A-Z])(\d{2})?([A-Z])?$/);
if (ncs === null) return null;
black = parseInt(ncs[1], 10);
chroma = parseInt(ncs[2], 10);
bc = ncs[3];
if (bc != "N" && bc != "Y" && bc != "R" && bc != "B" && bc != "G") { return null; }
percent = parseInt(ncs[4], 10) || 0;
if (bc !== 'N') {
black1 = (1.05 * black - 5.25);
chroma1 = chroma;
if (bc === 'Y' && percent <= 60) {
red1 = 1;
} else if ((bc === 'Y' && percent > 60) || (bc === 'R' && percent <= 80)) {
if (bc === 'Y') {
factor1 = percent - 60;
} else {
factor1 = percent + 40;
}
red1 = ((Math.sqrt(14884 - Math.pow(factor1, 2))) - 22) / 100;
} else if ((bc === 'R' && percent > 80) || (bc === 'B')) {
red1 = 0;
} else if (bc === 'G') {
factor1 = (percent - 170);
red1 = ((Math.sqrt(33800 - Math.pow(factor1, 2))) - 70) / 100;
}
if (bc === 'Y' && percent <= 80) {
blue1 = 0;
} else if ((bc === 'Y' && percent > 80) || (bc === 'R' && percent <= 60)) {
if (bc === 'Y') {
factor1 = (percent - 80) + 20.5;
} else {
factor1 = (percent + 20) + 20.5;
}
blue1 = (104 - (Math.sqrt(11236 - Math.pow(factor1, 2)))) / 100;
} else if ((bc === 'R' && percent > 60) || (bc === 'B' && percent <= 80)) {
if (bc === 'R') {
factor1 = (percent - 60) - 60;
} else {
factor1 = (percent + 40) - 60;
}
blue1 = ((Math.sqrt(10000 - Math.pow(factor1, 2))) - 10) / 100;
} else if ((bc === 'B' && percent > 80) || (bc === 'G' && percent <= 40)) {
if (bc === 'B') {
factor1 = (percent - 80) - 131;
} else {
factor1 = (percent + 20) - 131;
}
blue1 = (122 - (Math.sqrt(19881 - Math.pow(factor1, 2)))) / 100;
} else if (bc === 'G' && percent > 40) {
blue1 = 0;
}
if (bc === 'Y') {
green1 = (85 - 17 / 20 * percent) / 100;
} else if (bc === 'R' && percent <= 60) {
green1 = 0;
} else if (bc === 'R' && percent > 60) {
factor1 = (percent - 60) + 35;
green1 = (67.5 - (Math.sqrt(5776 - Math.pow(factor1, 2)))) / 100;
} else if (bc === 'B' && percent <= 60) {
factor1 = (1 * percent - 68.5);
green1 = (6.5 + (Math.sqrt(7044.5 - Math.pow(factor1, 2)))) / 100;
} else if ((bc === 'B' && percent > 60) || (bc === 'G' && percent <= 60)) {
green1 = 0.9;
} else if (bc === 'G' && percent > 60) {
factor1 = (percent - 60);
green1 = (90 - (1 / 8 * factor1)) / 100;
}
factor1 = (red1 + green1 + blue1) / 3;
red2 = ((factor1 - red1) * (100 - chroma1) / 100) + red1;
green2 = ((factor1 - green1) * (100 - chroma1) / 100) + green1;
blue2 = ((factor1 - blue1) * (100 - chroma1) / 100) + blue1;
if (red2 > green2 && red2 > blue2) {
max = red2;
} else if (green2 > red2 && green2 > blue2) {
max = green2;
} else if (blue2 > red2 && blue2 > green2) {
max = blue2;
} else {
max = (red2 + green2 + blue2) / 3;
}
factor2 = 1 / max;
//@ts-ignore
r = parseInt((red2 * factor2 * (100 - black1) / 100) * 255, 10);
//@ts-ignore
g = parseInt((green2 * factor2 * (100 - black1) / 100) * 255, 10);
//@ts-ignore
b = parseInt((blue2 * factor2 * (100 - black1) / 100) * 255, 10);
if (r > 255) { r = 255; }
if (g > 255) { g = 255; }
if (b > 255) { b = 255; }
if (r < 0) { r = 0; }
if (g < 0) { g = 0; }
if (b < 0) { b = 0; }
} else {
//@ts-ignore
grey = parseInt((1 - black / 100) * 255, 10);
if (grey > 255) { grey = 255; }
if (grey < 0) { grey = 0; }
r = grey;
g = grey;
b = grey;
}
return {
r: r,
g: g,
b: b
};
}
static rgbToHsl(r, g, b) {
let min, max, i, l, s, maxcolor, h, rgb = [];
rgb[0] = r / 255;
rgb[1] = g / 255;
rgb[2] = b / 255;
min = rgb[0];
max = rgb[0];
maxcolor = 0;
for (i = 0; i < rgb.length - 1; i++) {
if (rgb[i + 1] <= min) { min = rgb[i + 1]; }
if (rgb[i + 1] >= max) { max = rgb[i + 1]; maxcolor = i + 1; }
}
if (maxcolor == 0) {
h = (rgb[1] - rgb[2]) / (max - min);
}
if (maxcolor == 1) {
h = 2 + (rgb[2] - rgb[0]) / (max - min);
}
if (maxcolor == 2) {
h = 4 + (rgb[0] - rgb[1]) / (max - min);
}
if (isNaN(h)) { h = 0; }
h = h * 60;
if (h < 0) { h = h + 360; }
l = (min + max) / 2;
if (min == max) {
s = 0;
} else {
if (l < 0.5) {
s = (max - min) / (max + min);
} else {
s = (max - min) / (2 - max - min);
}
}
s = s;
return { h: h, s: s, l: l };
}
static rgbToHwb(r, g, b) {
let h, w, bl;
r = r / 255;
g = g / 255;
b = b / 255;
let max = Math.max(r, g, b);
let min = Math.min(r, g, b);
let chroma = max - min;
if (chroma == 0) {
h = 0;
} else if (r == max) {
h = (((g - b) / chroma) % 6) * 360;
} else if (g == max) {
h = ((((b - r) / chroma) + 2) % 6) * 360;
} else {
h = ((((r - g) / chroma) + 4) % 6) * 360;
}
w = min;
bl = 1 - max;
return { h: h, w: w, b: bl };
}
static rgbToCmyk(r, g, b) {
let c, m, y, k;
r = r / 255;
g = g / 255;
b = b / 255;
let max = Math.max(r, g, b);
k = 1 - max;
if (k == 1) {
c = 0;
m = 0;
y = 0;
} else {
c = (1 - r - k) / (1 - k);
m = (1 - g - k) / (1 - k);
y = (1 - b - k) / (1 - k);
}
return { c: c, m: m, y: y, k: k };
}
static toHex(n) {
let hex = n.toString(16);
while (hex.length < 2) { hex = "0" + hex; }
return hex;
}
static w3trim(x) {
return x.replace(/^\s+|\s+$/g, '');
}
static isHex(x) {
return ('0123456789ABCDEFabcdef'.indexOf(x) > -1);
}
}
================================================
FILE: packages/web-component-designer/src/elements/item/BindingMode.ts
================================================
export enum BindingMode {
oneWay = 'oneWay',
twoWay = 'twoWay'
}
================================================
FILE: packages/web-component-designer/src/elements/item/BindingTarget.ts
================================================
export enum BindingTarget {
/** Bindings for example starting with . to explicitly target a property */
explicitProperty = 'explicitProperty',
property = 'property',
attribute = 'attribute',
class = 'class',
css = 'css',
cssvar = 'cssvar',
event = 'event',
content = 'content', //innertext or html... mhmmm,
visible = 'visible'
}
================================================
FILE: packages/web-component-designer/src/elements/item/DesignItem.ts
================================================
import { ServiceContainer } from '../services/ServiceContainer.js';
import { IDesignItem } from './IDesignItem.js';
import { InstanceServiceContainer } from '../services/InstanceServiceContainer.js';
import { CssStyleChangeAction } from '../services/undoService/transactionItems/CssStyleChangeAction.js';
import { ChangeGroup } from '../services/undoService/ChangeGroup.js';
import { NodeType } from './NodeType.js';
import { AttributeChangeAction } from '../services/undoService/transactionItems/AttributeChangeAction.js';
import { ExtensionType } from '../widgets/designerView/extensions/ExtensionType.js';
import { CssAttributeParser } from '../helper/CssAttributeParser.js';
import { ISize } from '../../interfaces/ISize.js';
import { PropertiesHelper } from '../services/propertiesService/services/PropertiesHelper.js';
import { InsertChildAction } from '../services/undoService/transactionItems/InsertChildAction.js';
import { DomConverter } from '../widgets/designerView/DomConverter.js';
import { IStyleRule } from '../services/stylesheetService/IStylesheetService.js';
import { enableStylesheetService } from '../widgets/designerView/extensions/buttons/StylesheetServiceDesignViewConfigButtons.js';
import { TypedEvent } from '@node-projects/base-custom-webcomponent';
import { IPlacementService } from '../services/placementService/IPlacementService.js';
import { TextContentChangeAction } from '../services/undoService/transactionItems/TextContentChangeAction.js';
import { PropertyChangeAction } from '../services/undoService/transactionItems/PropertyChangeAction.js';
import { deepValue } from '../helper/Helper.js';
import { AttributeAndPropertyChangeAction } from '../services/undoService/transactionItems/AttributeAndPropertyChangeAction.js';
export const hideAtDesignTimeAttributeName = 'node-projects-hide-at-design-time';
export const hideAtRunTimeAttributeName = 'node-projects-hide-at-run-time';
export const lockAtDesignTimeAttributeName = 'node-projects-lock-at-design-time';
export const forceHoverAttributeName = 'node-projects-force-hover';
export const forceActiveAttributeName = 'node-projects-force-active';
export const forceVisitedAttributeName = 'node-projects-force-visited';
export const forceFocusAttributeName = 'node-projects-force-focus';
export const forceFocusWithinAttributeName = 'node-projects-force-focus-within';
export const forceFocusVisibleAttributeName = 'node-projects-force-focus-visible';
export class DesignItem implements IDesignItem {
public lastContainerSize: ISize;
parsedNode: any;
node: Node;
view: Node;
serviceContainer: ServiceContainer;
instanceServiceContainer: InstanceServiceContainer;
nodeReplaced = new TypedEvent;
get window() {
if (this.isRootItem && this.node instanceof HTMLIFrameElement)
return this.node.contentDocument.defaultView;
return (this.node.ownerDocument.defaultView ?? window);
}
get document() {
if (this.isRootItem && this.node instanceof HTMLIFrameElement)
return this.node.contentDocument;
return this.node.ownerDocument;
}
get usableContainer() {
if (this.isRootItem && this.element instanceof (this.element.ownerDocument.defaultView ?? window).HTMLIFrameElement)
return this.element.contentWindow.document;
else if (this.isRootItem)
return (this.node).shadowRoot;
return this.element;
}
async clone() {
try {
const html = DomConverter.ConvertToString([this], false);
const parsed = await this.serviceContainer.htmlParserService.parse(html, this.serviceContainer, this.instanceServiceContainer, true);
return parsed[0];
}
catch (err) {
//TODO: clone service for design item, maybe refactor copy&paste to use this also...
console.warn("could not clone design item.", this);
}
return null;
}
*allMatching(selectors: string) {
if (this.hasChildren) {
for (let d of this.children()) {
if (d.nodeType == NodeType.Element && d.element.matches(selectors))
yield d;
yield* d.allMatching(selectors);
}
}
}
public replaceNode(newNode: Node) {
DesignItem._designItemMap.delete(this.node);
DesignItem._designItemMap.set(newNode, this);
if (this.view == this.node)
this.view = newNode;
this.node = newNode;
this.nodeReplaced.emit();
}
public get nodeType(): NodeType {
if (this.node instanceof (this.node.ownerDocument.defaultView ?? window).Comment)
return NodeType.Comment;
if (this.node instanceof (this.node.ownerDocument.defaultView ?? window).Text)
return NodeType.TextNode;
return NodeType.Element;
}
private _attributes: Map
public get hasAttributes() {
return this._attributes.size > 0;
}
public hasAttribute(name: string) {
return this._attributes.has(name);
}
public getAttribute(name: string): string {
return this._attributes.get(name);
}
public *attributes() {
for (let s of this._attributes) {
yield s;
}
}
_withoutUndoSetAttribute(name: string, value: string) {
try {
if (!this.isRootItem)
this.element.setAttribute(name, value);
} catch (e: any) {
if (e?.code !== 5)
console.warn(e)
}
this._attributes.set(name, value);
this.serviceContainer.designItemService.handleSpecialAttributes(name, this);
}
_withoutUndoRemoveAttribute(name: string) {
try {
if (!this.isRootItem)
this.element.removeAttribute(name);
} catch (e: any) {
if (e?.code !== 5)
console.warn(e)
}
this._attributes.delete(name);
this.serviceContainer.designItemService.handleSpecialAttributes(name, this);
}
private _styles: Map
private _stylePriorities: Map
public get hasStyles() {
return this._styles.size > 0;
}
public hasStyle(name: string) {
let nm = name;
if (!nm.startsWith('--'))
nm = PropertiesHelper.camelToDashCase(name);
return this._styles.has(nm);
}
public getStyle(name: string) {
let nm = name;
if (!nm.startsWith('--'))
nm = PropertiesHelper.camelToDashCase(name);
return this._styles.get(nm);
}
public isStyleImportant(name: string) {
let nm = name;
if (!nm.startsWith('--'))
nm = PropertiesHelper.camelToDashCase(name);
return this._stylePriorities.get(nm) === true;
}
public *styles() {
for (let s of this._styles) {
yield s;
}
}
_withoutUndoSetStyle(name: string, value: string, important: boolean = false) {
let nm = name;
if (!nm.startsWith('--'))
nm = PropertiesHelper.camelToDashCase(name);
this._styles.set(nm, value);
if (important)
this._stylePriorities.set(nm, true);
else
this._stylePriorities.delete(nm);
}
_withoutUndoRemoveStyle(name: string) {
let nm = name;
if (!nm.startsWith('--'))
nm = PropertiesHelper.camelToDashCase(name);
this._styles.delete(nm);
this._stylePriorities.delete(nm);
}
/*
// this could maybe usefull to have such function, but naaa atm.
public refreshAttributesAndStylesFromElement() {
if (this.nodeType != NodeType.Element)
return;
const specialAttributeNames = [hideAtDesignTimeAttributeName, hideAtRunTimeAttributeName, lockAtDesignTimeAttributeName];
const specialAttributesToRefresh = new Set();
for (const attributeName of specialAttributeNames) {
if (this._attributes.has(attributeName))
specialAttributesToRefresh.add(attributeName);
}
this._attributes.clear();
for (const attribute of this.element.attributes) {
if (attribute.name === 'style')
continue;
this._attributes.set(attribute.name, attribute.value);
if (specialAttributeNames.includes(attribute.name))
specialAttributesToRefresh.add(attribute.name);
}
this._styles.clear();
if (this.element instanceof (this.node.ownerDocument.defaultView ?? window).HTMLElement || this.element instanceof (this.node.ownerDocument.defaultView ?? window).SVGElement) {
const cssParser = new CssAttributeParser();
const styleText = this.element.getAttribute('style');
if (styleText) {
cssParser.parse(styleText);
for (const entry of cssParser.entries) {
this._styles.set(entry.name, entry.value);
if (entry.important)
this._stylePriorities.set(entry.name, true);
}
}
}
this._stylesCache = null;
for (const attributeName of specialAttributesToRefresh)
this.serviceContainer.designItemService.handleSpecialAttributes(attributeName, this);
}
*/
private static _designItemMap = new WeakMap();
public get element(): Element {
return this.view;
}
public get name() {
return (this.node).localName;
}
public get id(): string {
return this.element.id;
}
public set id(value: string) {
const oldValue = this.element.id;
this.element.id = value;
if (this.id)
this.setAttribute("id", value);
else
this.removeAttribute("id");
if (this.serviceContainer.referencesChangedService)
this.serviceContainer.referencesChangedService.notifyReferencesChanged([{ designItem: this, oldValue, type: 'idChanged' }]);
}
public get isRootItem(): boolean {
return this.instanceServiceContainer.designerCanvas.rootDesignItem === this;
}
*childrenRect(selectors: string) {
if (this.hasChildren) {
for (let d of this.children()) {
if (d.nodeType == NodeType.Element && d.element.matches(selectors))
yield d;
yield* d.allMatching(selectors);
}
}
}
_childArray: IDesignItem[] = [];
public get hasChildren() {
return this._childArray.length > 0;
}
public *children(recursive: boolean = false): IterableIterator {
for (const e of this._childArray) {
yield e;
if (recursive) {
for (const c of e.children(recursive)) {
yield c;
}
}
}
}
public get childCount(): number {
return this._childArray.length;
}
public get firstChild(): IDesignItem {
return this._childArray[0];
}
private _parent: IDesignItem;
public get parent(): IDesignItem {
return this._parent;
}
public indexOf(designItem: IDesignItem): number {
return this._childArray.indexOf(designItem);
}
public insertAdjacentElement(designItem: IDesignItem, where: InsertPosition) {
let action: InsertChildAction;
if (where == 'afterbegin') {
action = new InsertChildAction(designItem, this, 0);
} else if (where == 'beforeend') {
action = new InsertChildAction(designItem, this, this._childArray.length);
} else if (where == 'beforebegin') {
action = new InsertChildAction(designItem, this.parent, this.parent.indexOf(this));
} else if (where == 'afterend') {
action = new InsertChildAction(designItem, this.parent, this.parent.indexOf(this) + 1);
}
this.instanceServiceContainer.undoService.execute(action);
}
public insertChild(designItem: IDesignItem, index?: number) {
const action = new InsertChildAction(designItem, this, index);
this.instanceServiceContainer.undoService.execute(action);
}
public removeChild(designItem: IDesignItem) {
this.serviceContainer.deletionService.removeItems([designItem]);
}
public remove() {
this.serviceContainer.deletionService.removeItems([this]);
}
public clearChildren() {
for (let i = this._childArray.length - 1; i >= 0; i--) {
let di = this._childArray[i];
di.remove();
}
}
//abstract text content to own property. so only change via designer api will use it.
public get hasContent() {
return ((this.nodeType == NodeType.TextNode || this.nodeType == NodeType.Comment) && this.element.textContent != "") || (this._childArray.length === 0);
}
public get content(): string {
if (this.nodeType == NodeType.TextNode || this.nodeType == NodeType.Comment)
return this.node.textContent;
else
return this._childArray.map(x => x.content).join();
}
public set content(value: string) {
const grp = this.openGroup('set content');
this.clearChildren();
let t = document.createTextNode(value);
let di = DesignItem.GetOrCreateDesignItem(t, t, this.serviceContainer, this.instanceServiceContainer);
if (this.nodeType == NodeType.TextNode) {
const idx = this.parent.indexOf(this);
const parent = this.parent;
this.remove()
parent.insertChild(di, idx);
} else if (this.nodeType == NodeType.Comment) {
const action = new TextContentChangeAction(this, value, this.content);
this.instanceServiceContainer.undoService.execute(action);
} else
this.insertChild(di);
grp.commit();
}
public get innerHTML(): string {
const innerHTML = DomConverter.ConvertToString([...this.children()], false);
return innerHTML;
}
public set innerHTML(value: string) {
if (this.nodeType != NodeType.TextNode) {
const grp = this.openGroup('set innerHTML');
this.clearChildren();
const range = document.createRange();
range.selectNode(document.body);
const fragment = range.createContextualFragment(value);
for (const n of [...fragment.childNodes]) {
let di = DesignItem.createDesignItemFromInstance(n, this.serviceContainer, this.instanceServiceContainer)
this.insertChild(di);
}
grp.commit();
}
}
public get isEmptyTextNode(): boolean {
return this.nodeType === NodeType.TextNode && this.content?.trim() == '';
}
public get hideAtDesignTime() {
return this.hasAttribute(hideAtDesignTimeAttributeName);
}
public set hideAtDesignTime(value: boolean) {
if (value)
this.setAttribute(hideAtDesignTimeAttributeName, "");
else
this.removeAttribute(hideAtDesignTimeAttributeName);
}
public get hideAtRunTime() {
return this.hasAttribute(hideAtRunTimeAttributeName);
}
public set hideAtRunTime(value: boolean) {
if (value)
this.setAttribute(hideAtRunTimeAttributeName, "");
else
this.removeAttribute(hideAtRunTimeAttributeName);
}
public get lockAtDesignTime() {
return this.hasAttribute(lockAtDesignTimeAttributeName);
}
public set lockAtDesignTime(value: boolean) {
if (value)
this.setAttribute(lockAtDesignTimeAttributeName, "")
else
this.removeAttribute(lockAtDesignTimeAttributeName);
}
public static createDesignItemFromInstance(node: Node, serviceContainer: ServiceContainer, instanceServiceContainer: InstanceServiceContainer): DesignItem {
node = DesignItem.updateRenderedNode(serviceContainer, node);
let designItem = serviceContainer.designItemService.createDesignItem(node, node, serviceContainer, instanceServiceContainer);
if (node instanceof (node.ownerDocument.defaultView ?? window).HTMLTemplateElement && node.getAttribute('shadowrootmode') == 'open') {
try {
const shadow = (node.parentNode).attachShadow({ mode: 'open' });
const content = node.content.cloneNode(true);
shadow.appendChild(content);
} catch (err) {
console.error("error attaching shadowdom", err)
}
}
if (designItem.nodeType == NodeType.Element) {
for (let a of designItem.element.attributes) {
if (a.name !== 'style') {
designItem._attributes.set(a.name, a.value);
}
}
if (node instanceof (node.ownerDocument.defaultView ?? window).HTMLElement || node instanceof (node.ownerDocument.defaultView ?? window).SVGElement) {
const cssParser = new CssAttributeParser();
const st = node.getAttribute("style");
if (st) {
cssParser.parse(st);
for (let e of cssParser.entries) {
designItem._styles.set(e.name, e.value);
if (e.important)
designItem._stylePriorities.set(e.name, true);
}
}
serviceContainer.designItemService.handleSpecialAttributes(lockAtDesignTimeAttributeName, designItem);
}
(node).draggable = false; //even if it should be true, for better designer exp.
}
designItem._childArray = designItem._internalUpdateChildrenFromNodesChildren();
for (let c of designItem._childArray) {
(c)._parent = designItem;
}
designItem.refreshRenderedDesignItem();
return designItem;
}
static updateRenderedNode(serviceContainer: ServiceContainer, node: Node) {
let renderedNode = node;
for (const service of serviceContainer.renderedDesignItemServices ?? []) {
const nextNode = service.updateRenderedNode(renderedNode) ?? renderedNode;
if (nextNode !== renderedNode && renderedNode.parentNode)
renderedNode.parentNode.replaceChild(nextNode, renderedNode);
renderedNode = nextNode;
}
return renderedNode;
}
querySelectorAll(selectors: string): NodeListOf {
return this.usableContainer.querySelectorAll(selectors);
}
removeDesignerAttributesAndStylesFromChildren() {
const els = this.querySelectorAll('*');
for (let e of els) {
const di = DesignItem.GetDesignItem(e);
if (!di.hasAttribute("draggable"))
e.removeAttribute("draggable");
if (!di.hasStyle("pointer-events"))
e.style.pointerEvents = '';
}
}
updateChildrenFromNodesChildren() {
this._childArray = this._internalUpdateChildrenFromNodesChildren();
for (let c of this._childArray) {
(c)._parent = this;
}
}
_internalUpdateChildrenFromNodesChildren() {
const newChilds = [];
if (this.nodeType == NodeType.Element) {
if (this.element instanceof (this.node.ownerDocument.defaultView ?? window).HTMLTemplateElement) {
for (const c of this.element.content.childNodes) {
const di = DesignItem.createDesignItemFromInstance(c, this.serviceContainer, this.instanceServiceContainer);
newChilds.push(di);
}
} else if (this.isRootItem && this.element instanceof (this.node.ownerDocument.defaultView ?? window).HTMLIFrameElement) {
for (const c of this.element.contentWindow.document.childNodes) {
const di = DesignItem.createDesignItemFromInstance(c, this.serviceContainer, this.instanceServiceContainer);
newChilds.push(di);
}
} else {
for (const c of this.element.childNodes) {
const di = DesignItem.createDesignItemFromInstance(c, this.serviceContainer, this.instanceServiceContainer);
newChilds.push(di);
}
}
}
return newChilds;
}
_backupWhenEditContent;
_inEditContent = false;
editContent() {
this._inEditContent = true;
this._backupWhenEditContent = [...this.element.childNodes];
const nn = this.element.innerHTML
this.element.innerHTML = '';
this.element.innerHTML = nn;
this.element.setAttribute('contenteditable', '');
}
editContentFinish() {
if (this._inEditContent) {
this._inEditContent = false;
this.element.removeAttribute('contenteditable');
this.element.innerHTML = '';
for (let n of this._backupWhenEditContent) {
this.element.appendChild(n);
}
this._backupWhenEditContent = null;
}
}
public constructor(node: Node, parsedNode: any, serviceContainer: ServiceContainer, instanceServiceContainer: InstanceServiceContainer) {
this.node = node;
this.view = node;
this.parsedNode = parsedNode;
this.serviceContainer = serviceContainer;
this.instanceServiceContainer = instanceServiceContainer;
this._attributes = new Map();
this._styles = new Map();
this._stylePriorities = new Map();
DesignItem._designItemMap.set(node, this);
}
public setView(node: Element) {
this.view = node;
DesignItem._designItemMap.set(node, this);
this.refreshRenderedDesignItem();
}
public refreshRenderedDesignItem() {
for (const service of this.serviceContainer.renderedDesignItemServices ?? [])
service.updateRenderedDesignItem(this);
}
public openGroup(title: string): ChangeGroup {
return this.instanceServiceContainer.undoService.openGroup(title);
}
public getOrCreateDesignItem(node: Node) {
return DesignItem.GetOrCreateDesignItem(node, node, this.serviceContainer, this.instanceServiceContainer);
}
static GetOrCreateDesignItem(node: Node, parsedNode: any, serviceContainer: ServiceContainer, instanceServiceContainer: InstanceServiceContainer): IDesignItem {
if (!node)
return null;
let designItem: IDesignItem = DesignItem._designItemMap.get(node);
if (!designItem) {
let dis = serviceContainer.designItemService;
designItem = dis.createDesignItem(node, parsedNode, serviceContainer, instanceServiceContainer);
}
return designItem;
}
static GetDesignItem(node: Node): IDesignItem {
if (!node)
return null;
let designItem: IDesignItem = DesignItem._designItemMap.get(node);
return designItem;
}
public setStyle(name: string, value?: string | null, important?: boolean) {
let nm = name;
if (!nm.startsWith('--'))
nm = PropertiesHelper.camelToDashCase(name);
if (this.isRootItem) {
throw 'not allowed to set style on root item or use async setStyle';
} else {
const action = new CssStyleChangeAction(this, nm, value, this._styles.get(nm), important, this.isStyleImportant(nm));
this.instanceServiceContainer.undoService.execute(action);
}
}
public async setStyleAsync(name: string, value?: string | null, important?: boolean): Promise {
let nm = name;
if (!nm.startsWith('--'))
nm = PropertiesHelper.camelToDashCase(name);
//TODO: remove this special case (I think), should be in CSSSytleChangeAction.
//Maybe we should be able to set a setscope (sheet, local, pseudo selctor, ...)
if (this.isRootItem) {
if (!this.instanceServiceContainer.stylesheetService)
throw 'not allowed to set style on root item';
else {
let decls = this.instanceServiceContainer.stylesheetService.getDeclarationsSortedBySpecificity(this, name);
if (decls !== null && decls.length > 0) {
this.instanceServiceContainer.stylesheetService.updateDeclarationValue(decls[0], value, important);
} else {
let rules = this.instanceServiceContainer.stylesheetService.getRules(':host').filter(x => !x.stylesheet?.readOnly);
if (decls === null || rules.length === 0) {
const cg = this.openGroup('add rule and set style: ' + name);
const sheets = this.instanceServiceContainer.stylesheetService.getStylesheets();
const rule = await this.instanceServiceContainer.stylesheetService.addRule(sheets[0], ':host')
this.instanceServiceContainer.stylesheetService.insertDeclarationIntoRule(rule, name, value, important);
cg.commit();
} else {
this.instanceServiceContainer.stylesheetService.insertDeclarationIntoRule(rules[0], name, value, important);
}
}
}
} else {
const action = new CssStyleChangeAction(this, nm, value, this._styles.get(nm), important, this.isStyleImportant(nm));
this.instanceServiceContainer.undoService.execute(action);
}
}
public removeStyle(name: string) {
let nm = name;
if (!nm.startsWith('--'))
nm = PropertiesHelper.camelToDashCase(name);
const action = new CssStyleChangeAction(this, nm, '', this._styles.get(nm), false, this.isStyleImportant(nm));
this.instanceServiceContainer.undoService.execute(action);
}
public updateStyleInSheetOrLocal(name: string, value?: string | null, important?: boolean, forceSet?: boolean) {
let nm = name;
if (!nm.startsWith('--'))
nm = PropertiesHelper.camelToDashCase(name);
let declarations = this.instanceServiceContainer.stylesheetService?.getDeclarationsSortedBySpecificity(this, nm).filter(x => !x.stylesheet?.readOnly);
if (this.hasStyle(name) || this.instanceServiceContainer.designContext.extensionOptions[enableStylesheetService] === false || !declarations?.length) {
// Set style locally
if (this.getStyle(nm) != value || forceSet) {
this.setStyle(nm, value, important);
} else if (value == null) {
this.removeStyle(nm);
}
} else {
this.instanceServiceContainer.stylesheetService.updateDeclarationValue(declarations[0], value, important);
}
}
public async updateStyleInSheetOrLocalAsync(name: string, value?: string | null, important?: boolean, forceSet?: boolean): Promise {
let nm = name;
if (!nm.startsWith('--'))
nm = PropertiesHelper.camelToDashCase(name);
let declarations = this.instanceServiceContainer.stylesheetService?.getDeclarationsSortedBySpecificity(this, nm).filter(x => !x.stylesheet?.readOnly);
if (this.hasStyle(name) || this.instanceServiceContainer.designContext.extensionOptions[enableStylesheetService] === false || !declarations?.length) {
// Set style locally
if (this.getStyle(nm) != value || forceSet) {
await this.setStyleAsync(nm, value, important);
} else if (value == null) {
this.removeStyle(nm);
}
} else {
this.instanceServiceContainer.stylesheetService.updateDeclarationValue(declarations[0], value, important);
}
}
public getStyleFromSheetOrLocal(name: string, fallback: string = null) {
let nm = name;
if (!nm.startsWith('--'))
nm = PropertiesHelper.camelToDashCase(name);
if (this.hasStyle(name))
// Get style locally
return this.getStyle(nm);
let decls = this.instanceServiceContainer.stylesheetService?.getDeclarationsSortedBySpecificity(this, nm);
if (decls && decls.length > 0)
return decls[0].value;
return null;
}
getStyleFromSheetOrLocalOrComputed(name: string, fallback: string = null) {
let nm = name;
if (!nm.startsWith('--'))
nm = PropertiesHelper.camelToDashCase(name);
let value = this.getStyleFromSheetOrLocal(nm);
if (!value) {
value = getComputedStyle(this.element).getPropertyValue(nm)
}
return value ?? fallback;
}
getComputedStyleProperty(name: string, fallback: string = null) {
let nm = name;
if (!nm.startsWith('--'))
nm = PropertiesHelper.camelToDashCase(name);
let value = this.getStyleFromSheetOrLocal(nm);
if (!value) {
value = getComputedStyle(this.element).getPropertyValue(nm)
}
return value ?? fallback;
}
getComputedStyle() {
if (this.nodeType == NodeType.Element)
return this.window.getComputedStyle(this.element);
return null;
}
_stylesCache: IStyleRule[] = null;
_cacheClearTimer: NodeJS.Timeout;
public getAllStyles(): IStyleRule[] {
let styles = this._stylesCache;
if (styles)
return styles;
if (this.nodeType != NodeType.Element)
return [];
const localStyles = [...this._styles.entries()].map(x => ({ name: x[0], value: x[1], important: this.isStyleImportant(x[0]), parent: null }));
if (this.instanceServiceContainer.stylesheetService) {
try {
const rules = this.instanceServiceContainer.stylesheetService?.getAppliedRules(this);
if (rules) {
return [{ selector: null, declarations: localStyles, specificity: null, stylesheet: null }, ...rules];
}
}
catch (err) {
console.warn('getAppliedRules', err);
}
}
styles = [{ selector: null, declarations: localStyles, specificity: null, stylesheet: null }];
this._stylesCache = styles;
clearTimeout(this._cacheClearTimer);
this._cacheClearTimer = setTimeout(() => this._stylesCache = null, 30);
return styles;
}
public setAttribute(name: string, value?: string | null) {
const action = new AttributeChangeAction(this, name, value, this._attributes.get(name));
this.instanceServiceContainer.undoService.execute(action);
}
public removeAttribute(name: string) {
const action = new AttributeChangeAction(this, name, null, this._attributes.get(name));
this.instanceServiceContainer.undoService.execute(action);
}
public setPropertyAndAttribute(name: string, value?: string | null) {
const attributeName = PropertiesHelper.camelToDashCase(name);
const propertyName = PropertiesHelper.dashToCamelCase(name);
if (this.isRootItem)
throw 'not allowed to set attribute on root item';
const action = new AttributeAndPropertyChangeAction(this, attributeName, propertyName, value, this.element[propertyName]);
this.instanceServiceContainer.undoService.execute(action);
}
public removePropertyAndAttribute(name: string) {
const attributeName = PropertiesHelper.camelToDashCase(name);
const propertyName = PropertiesHelper.dashToCamelCase(name);
const action = new AttributeAndPropertyChangeAction(this, attributeName, propertyName, null, this.element[propertyName]);
this.instanceServiceContainer.undoService.execute(action);
}
public setProperty(name: string, value?: any) { //the prop change action should use the prop service. We need th setPropAndattribute. So undo works!
if (this.isRootItem)
throw 'not allowed to set attribute on root item';
const oldValue = deepValue(this.node, name);
const action = new PropertyChangeAction(this, name, value, oldValue);
this.instanceServiceContainer.undoService.execute(action);
}
// Internal implementations wich don't use undo/redo
public _insertChildInternal(designItem: DesignItem, index?: number) {
this._insertChildsInternal([designItem], index);
}
public _insertChildsInternal(designItems: DesignItem[], index?: number) {
const frag = this.document.createDocumentFragment();
let beforeDs: IDesignItem = null;
for (let designItem of designItems) {
if (designItem.parent && this.instanceServiceContainer.selectionService.primarySelection == designItem) {
designItem.instanceServiceContainer.designerCanvas.extensionManager.removeExtension(designItem.parent, ExtensionType.PrimarySelectionContainer);
designItem.instanceServiceContainer.designerCanvas.extensionManager.removeExtension(designItem.parent, ExtensionType.PrimarySelectionContainerAndCanBeEntered);
}
if (designItem.parent) {
designItem.parent._removeChildInternal(designItem);
}
frag.appendChild(designItem.view);
(designItem)._parent = this;
if (index == null || this._childArray.length == 0 || index >= this._childArray.length) {
this._childArray.push(designItem);
} else {
beforeDs = this._childArray[index];
this._childArray.splice(index, 0, designItem);
index++;
}
}
if (beforeDs == null) {
if (this.isRootItem) {
if (this.usableContainer?.children[0] instanceof this.window.HTMLHtmlElement)
this.usableContainer.children[0].remove();
this.usableContainer.appendChild(frag);
} else if (this.view instanceof (this.node.ownerDocument.defaultView ?? window).HTMLTemplateElement) {
this.view.content.appendChild(frag);
} else
this.view.appendChild(frag);
} else {
if (this.isRootItem) {
if (this.usableContainer?.children[0] instanceof this.window.HTMLHtmlElement)
this.usableContainer.children[0].remove();
this.usableContainer.insertBefore(frag, beforeDs.element);
} else if (this.view instanceof (this.node.ownerDocument.defaultView ?? window).HTMLTemplateElement) {
this.view.content.insertBefore(frag, beforeDs.element)
} else
this.view.insertBefore(frag, beforeDs.element)
}
//TODO: is this still needed???
/*
if (this.instanceServiceContainer.selectionService.primarySelection == designItem) {
designItem.instanceServiceContainer.designerCanvas.extensionManager.applyExtension(designItem.parent, ExtensionType.PrimarySelectionContainer);
if (designItem.getPlacementService().isEnterableContainer(this))
designItem.instanceServiceContainer.designerCanvas.extensionManager.applyExtension(designItem.parent, ExtensionType.PrimarySelectionContainerAndCanBeEntered);
}
*/
this.refreshRenderedDesignItem();
}
public _removeChildInternal(designItem: IDesignItem) {
if (designItem.parent && this.instanceServiceContainer.selectionService.primarySelection == designItem) {
designItem.instanceServiceContainer.designerCanvas.extensionManager.removeExtension(designItem.parent, ExtensionType.PrimarySelectionContainer);
designItem.instanceServiceContainer.designerCanvas.extensionManager.removeExtension(designItem.parent, ExtensionType.PrimarySelectionAndCanBeEntered);
}
designItem.instanceServiceContainer.designerCanvas.extensionManager.removeExtensions([designItem], true);
const index = this._childArray.indexOf(designItem);
if (index > -1) {
this._childArray.splice(index, 1);
designItem.element.remove();
(designItem)._parent = null;
}
this.refreshRenderedDesignItem();
}
getPlacementService(style?: CSSStyleDeclaration): IPlacementService {
if (this.nodeType != NodeType.Element)
return null;
style ??= getComputedStyle(this.element);
return this.serviceContainer.getLastServiceWhere('containerService', x => x.serviceForContainer(this, style));
}
static createDesignItemFromImageBlob(serviceContainer: ServiceContainer, instanceServiceContainer: InstanceServiceContainer, data: Blob): Promise {
return new Promise(resolve => {
let reader = new FileReader();
reader.onloadend = () => {
const img = document.createElement('img');
img.src = reader.result;
const di = DesignItem.createDesignItemFromInstance(img, serviceContainer, instanceServiceContainer);
return resolve(di);
}
reader.readAsDataURL(data);
})
}
get hasForcedCss() {
return this.cssForceHover || this.cssForceActive || this.cssForceVisited || this.cssForceFocus || this.cssForceFocusWithin || this.cssForceFocusVisible;
}
get cssForceHover() {
return this.element.hasAttribute(forceHoverAttributeName);
}
set cssForceHover(value: boolean) {
if (value)
this.element.setAttribute(forceHoverAttributeName, '');
else
this.element.removeAttribute(forceHoverAttributeName);
this.instanceServiceContainer.onContentChanged.emit([{ changeType: 'changed', type: 'attribute', name: forceHoverAttributeName, designItems: [this] }]);
}
get cssForceActive() {
return this.element.hasAttribute(forceActiveAttributeName);
}
set cssForceActive(value: boolean) {
if (value)
this.element.setAttribute(forceActiveAttributeName, '');
else
this.element.removeAttribute(forceActiveAttributeName);
this.instanceServiceContainer.onContentChanged.emit([{ changeType: 'changed', type: 'attribute', name: forceActiveAttributeName, designItems: [this] }]);
}
get cssForceVisited() {
return this.element.hasAttribute(forceVisitedAttributeName);
}
set cssForceVisited(value: boolean) {
if (value)
this.element.setAttribute(forceVisitedAttributeName, '');
else
this.element.removeAttribute(forceVisitedAttributeName);
this.instanceServiceContainer.onContentChanged.emit([{ changeType: 'changed', type: 'attribute', name: forceVisitedAttributeName, designItems: [this] }]);
}
get cssForceFocus() {
return this.element.hasAttribute(forceFocusAttributeName);
}
set cssForceFocus(value: boolean) {
if (value)
this.element.setAttribute(forceFocusAttributeName, '');
else
this.element.removeAttribute(forceFocusAttributeName);
this.instanceServiceContainer.onContentChanged.emit([{ changeType: 'changed', type: 'attribute', name: forceFocusAttributeName, designItems: [this] }]);
}
get cssForceFocusWithin() {
return this.element.hasAttribute(forceFocusWithinAttributeName);
}
set cssForceFocusWithin(value: boolean) {
if (value)
this.element.setAttribute(forceFocusWithinAttributeName, '');
else
this.element.removeAttribute(forceFocusWithinAttributeName);
this.instanceServiceContainer.onContentChanged.emit([{ changeType: 'changed', type: 'attribute', name: forceFocusWithinAttributeName, designItems: [this] }]);
}
get cssForceFocusVisible() {
return this.element.hasAttribute(forceFocusVisibleAttributeName);
}
set cssForceFocusVisible(value: boolean) {
if (value)
this.element.setAttribute(forceFocusVisibleAttributeName, '');
else
this.element.removeAttribute(forceFocusVisibleAttributeName);
this.instanceServiceContainer.onContentChanged.emit([{ changeType: 'changed', type: 'attribute', name: forceFocusVisibleAttributeName, designItems: [this] }]);
}
}
================================================
FILE: packages/web-component-designer/src/elements/item/IBinding.ts
================================================
import { IBindingService } from '../services/bindingsService/IBindingService.js';
import { BindingMode } from './BindingMode.js';
import { BindingTarget } from './BindingTarget.js';
export interface IBinding {
targetName?: string; //Name of Attribute, CSS-Property, Event, ...
target?: BindingTarget;
rawName?: string; //raw attribute name (if it's an attribute)
rawValue?: string; //raw attribute value (or element html)
type?: string //here a name wich the bindings Service recognizes....
expression?: string; //the bindings expression
expressionTwoWay?: string; //a expression wich is used for write back
bindableObjectNames?: string[]; //TODO: deprecate and remove
bindableObjects?: IBindableComplexName[]; //if a name is not enough, use this list
converters?: any;
mode?: BindingMode;
invert?: boolean;
changedEvents?: string[];
nullSafe?: boolean;
service: IBindingService;
}
export interface IBindableComplexName { //e.g. aa:$uservalue.0.name
name?: string; //uservalue.0.name
alias?: string; //aa
modificator?: string; //$
}
================================================
FILE: packages/web-component-designer/src/elements/item/IDesignItem.ts
================================================
import { ServiceContainer } from '../services/ServiceContainer.js';
import { InstanceServiceContainer } from '../services/InstanceServiceContainer.js';
import { ChangeGroup } from '../services/undoService/ChangeGroup.js';
import { NodeType } from './NodeType.js';
import { ISize } from "../../interfaces/ISize.js";
import { IStyleRule } from '../services/stylesheetService/IStylesheetService.js';
import { IPlacementService } from '../services/placementService/IPlacementService.js';
import { TypedEvent } from '@node-projects/base-custom-webcomponent';
export interface IDesignItem {
lastContainerSize: ISize;
readonly window: Window & typeof globalThis;
readonly document: Document;
readonly usableContainer: ShadowRoot | Element | Document;
updateChildrenFromNodesChildren();
refreshRenderedDesignItem();
setView(node: Element);
replaceNode(newNode: Node);
nodeReplaced: TypedEvent;
clone(): Promise
readonly nodeType: NodeType;
readonly name: string;
id: string;
readonly isRootItem: boolean;
readonly hasAttributes: boolean;
readonly hasStyles: boolean;
readonly hasChildren: boolean;
children(recursive?: boolean): IterableIterator
allMatching(selectors: string): IterableIterator
readonly childCount: number;
readonly firstChild: IDesignItem;
readonly parent: IDesignItem;
_insertChildsInternal(designItems: IDesignItem[], index?: number);
_insertChildInternal(designItem: IDesignItem, index?: number);
_removeChildInternal(designItem: IDesignItem);
_withoutUndoSetStyle(name: string, value: string, important?: boolean);
_withoutUndoRemoveStyle(name: string);
_withoutUndoSetAttribute(name: string, value: string);
_withoutUndoRemoveAttribute(name: string);
indexOf(designItem: IDesignItem): number;
insertAdjacentElement(designItem: IDesignItem, where: InsertPosition);
insertChild(designItem: IDesignItem, index?: number);
removeChild(designItem: IDesignItem);
remove();
clearChildren();
removeDesignerAttributesAndStylesFromChildren();
editContent();
editContentFinish();
readonly hasContent: boolean;
content: string;
innerHTML?: string;
readonly isEmptyTextNode: boolean;
/** Could be a special node if another parser is used */
readonly parsedNode: any;
readonly node: Node;
readonly element: Element;
serviceContainer: ServiceContainer;
instanceServiceContainer: InstanceServiceContainer;
getOrCreateDesignItem(node: Node);
openGroup(title: string): ChangeGroup
styles(): Iterable<[name: string, value: string]>;
getStyle(name: string): string
isStyleImportant(name: string): boolean
hasStyle(name: string): boolean
setStyle(name: string, value?: string | null, important?: boolean);
setStyleAsync(name: string, value?: string | null, important?: boolean): Promise;
removeStyle(name: string);
updateStyleInSheetOrLocal(name: string, value?: string | null, important?: boolean, forceSet?: boolean);
updateStyleInSheetOrLocalAsync(name: string, value?: string | null, important?: boolean, forceSet?: boolean): Promise;
getStyleFromSheetOrLocal(name: string, fallback?: string);
getStyleFromSheetOrLocalOrComputed(name: string, fallback?: string)
getAllStyles(): IStyleRule[];
readonly hasForcedCss: boolean;
cssForceHover: boolean;
cssForceActive: boolean;
cssForceVisited: boolean;
cssForceFocus: boolean;
cssForceFocusWithin: boolean;
cssForceFocusVisible: boolean;
attributes(): Iterable<[name: string, value: string]>
getAttribute(name: string): string
hasAttribute(name: string): boolean
setAttribute(name: string, value?: string | null);
removeAttribute(name: string);
setProperty(name: string, value?: any);
setPropertyAndAttribute(name: string, value?: string | null);
removePropertyAndAttribute(name: string);
hideAtDesignTime: boolean;
hideAtRunTime: boolean;
lockAtDesignTime: boolean;
getPlacementService(style?: CSSStyleDeclaration): IPlacementService;
getComputedStyleProperty(name: string, fallback: string): string;
getComputedStyle(): CSSStyleDeclaration;
querySelectorAll(selectors: string): NodeListOf;
}
================================================
FILE: packages/web-component-designer/src/elements/item/NodeType.ts
================================================
export enum NodeType {
Element = 1,
Attribute = 2,
TextNode = 3,
Comment = 8,
Document = 9,
DocumentFragment = 11
}
================================================
FILE: packages/web-component-designer/src/elements/item/info.txt
================================================
Todo -> unfiy Properties & Attributes.
DesignItem should only store Attributes, not Properties.
Maybe setAttribute should set Attribute directly, setProperty should use PropertiesService??
or do we always use properties PropertiesService?
Attributes List in Properties shows all set attributes and you could also add one,
but they show no binding etc information cause they are only the real attributes
how should we serialize bindings?? if they are binding objects, like in tagbinding?
================================================
FILE: packages/web-component-designer/src/elements/services/BaseServiceContainer.ts
================================================
import { TypedEvent } from '@node-projects/base-custom-webcomponent';
import { IService } from './IService.js';
export class BaseServiceContainer {
protected _services: Map = new Map();
public servicesChanged = new TypedEvent<{ serviceName: keyof NameMap }>();
getLastService(service: K): NameMap[K] {
let list: [] = this._services.get(service);
if (list && list.length)
return list[list.length - 1];
return null;
}
getServices(service: K): NameMap[K][] {
return this._services.get(service);
}
register(name: K, service: NameMap[K]) {
if (!this._services.has(name))
this._services.set(name, []);
this._services.get(name).push(service);
this.servicesChanged.emit({ serviceName: name });
}
registerLast(name: K, service: NameMap[K]) {
if (!this._services.has(name))
this._services.set(name, []);
this._services.get(name).unshift(service);
this.servicesChanged.emit({ serviceName: name });
}
registerMultiple(names: K[], service: NameMap[K]) {
for (const name of names) {
if (!this._services.has(name))
this._services.set(name, []);
this._services.get(name).push(service);
this.servicesChanged.emit({ serviceName: name });
}
}
forSomeServicesTillResult(service: K, callback: (service: NameMap[K]) => Y): Y {
let services = this.getServices(service);
if (services == null) {
return null;
}
for (let index = services.length - 1; index >= 0; index--) {
const currentService = services[index];
let result = callback(currentService);
if (result != null)
return result;
}
return null;
}
getLastServiceWhere(service: K, callback: (service: NameMap[K]) => Y): NameMap[K] {
let services = this.getServices(service);
if (services == null) {
return null;
}
for (let index = services.length - 1; index >= 0; index--) {
const currentService = services[index];
let result = callback(currentService);
if (result)
return currentService;
}
return null;
}
getLastServiceResult(service: K, callback: (service: NameMap[K]) => Y): Y {
let services = this.getServices(service);
if (services == null) {
return null;
}
for (let index = services.length - 1; index >= 0; index--) {
const currentService = services[index];
let result = callback(currentService);
if (result)
return result;
}
return null;
}
async getLastServiceWhereAsync(service: K, callback: (service: NameMap[K]) => Promise): Promise {
let services = this.getServices(service);
if (services == null) {
return null;
}
for (let index = services.length - 1; index >= 0; index--) {
const currentService = services[index];
let result = await callback(currentService);
if (result)
return currentService;
}
return null;
}
}
================================================
FILE: packages/web-component-designer/src/elements/services/DefaultServiceBootstrap.ts
================================================
import { ServiceContainer } from './ServiceContainer.js';
import { PolymerPropertiesService } from './propertiesService/services/PolymerPropertiesService.js';
import { LitElementPropertiesService } from './propertiesService/services/LitElementPropertiesService.js';
import { NativeElementsPropertiesService } from './propertiesService/services/NativeElementsPropertiesService.js';
import { SVGElementsPropertiesService } from './propertiesService/services/SVGElementsPropertiesService.js';
import { DefaultInstanceService } from './instanceService/DefaultInstanceService.js';
import { DefaultPropertyEditorTypesService } from './propertiesService/DefaultPropertyEditorTypesService.js';
import { BaseCustomWebComponentPropertiesService } from './propertiesService/services/BaseCustomWebComponentPropertiesService.js';
import { DefaultPlacementService } from './placementService/DefaultPlacementService.js';
import { DefaultHtmlParserService } from './htmlParserService/DefaultHtmlParserService.js';
import { Lit2PropertiesService } from './propertiesService/services/Lit2PropertiesService.js';
import { ExtensionType } from '../widgets/designerView/extensions/ExtensionType.js';
import { ElementDragTitleExtensionProvider } from '../widgets/designerView/extensions/ElementDragTitleExtensionProvider.js';
import { TransformOriginExtensionProvider } from '../widgets/designerView/extensions/transforms/TransformOriginExtensionProvider.js';
import { MarginExtensionProvider } from '../widgets/designerView/extensions/MarginExtensionProvider.js';
import { PositionExtensionProvider } from '../widgets/designerView/extensions/PositionExtensionProvider.js';
import { HighlightElementExtensionProvider } from '../widgets/designerView/extensions/HighlightElementExtensionProvider.js';
import { NamedTools } from '../widgets/designerView/tools/NamedTools.js';
import { PointerTool } from '../widgets/designerView/tools/PointerTool.js';
import { DrawPathTool } from '../widgets/designerView/tools/DrawPathTool.js';
import { SelectionDefaultExtensionProvider } from '../widgets/designerView/extensions/SelectionDefaultExtensionProvider.js';
import { ResizeExtensionProvider } from '../widgets/designerView/extensions/ResizeExtensionProvider.js';
import { RotateExtensionProvider } from '../widgets/designerView/extensions/transforms/RotateExtensionProvider.js';
import { RotateGroupExtensionProvider } from '../widgets/designerView/extensions/transforms/RotateGroupExtensionProvider.js';
import { ZoomTool } from '../widgets/designerView/tools/ZoomTool.js';
import { PanTool } from '../widgets/designerView/tools/PanTool.js';
import { CopyPasteContextMenu } from '../widgets/designerView/extensions/contextMenu/CopyPasteContextMenu.js';
import { PasteFormatContextMenu } from '../widgets/designerView/extensions/contextMenu/PasteFormatContextMenu.js';
import { ToolWindowsContextMenu } from '../widgets/designerView/extensions/contextMenu/ToolWindowsContextMenu.js';
import { ZMoveContextMenu } from '../widgets/designerView/extensions/contextMenu/ZMoveContextMenu.js';
import { MultipleItemsSelectedContextMenu } from '../widgets/designerView/extensions/contextMenu/MultipleItemsSelectedContextMenu.js';
import { RectangleSelectorTool } from '../widgets/designerView/tools/RectangleSelectorTool.js';
import { MagicWandSelectorTool } from '../widgets/designerView/tools/MagicWandSelectorTool.js';
import { PickColorTool } from '../widgets/designerView/tools/PickColorTool.js';
import { TextTool } from '../widgets/designerView/tools/TextTool.js';
import { GrayOutExtensionProvider } from '../widgets/designerView/extensions/GrayOutExtensionProvider.js';
import { AltToEnterContainerExtensionProvider } from '../widgets/designerView/extensions/AltToEnterContainerExtensionProvider.js';
import { InvisibleElementExtensionProvider } from '../widgets/designerView/extensions/InvisibleElementExtensionProvider.js';
import { ItemsBelowContextMenu } from '../widgets/designerView/extensions/contextMenu/ItemsBelowContextMenu.js';
import { GridPlacementService } from './placementService/GridPlacementService.js';
import { ElementAtPointService } from './elementAtPointService/ElementAtPointService.js';
import { FlexBoxPlacementService } from './placementService/FlexBoxPlacementService.js';
import { SnaplinesProviderService } from './placementService/SnaplinesProviderService.js';
import { ExternalDragDropService } from './dragDropService/ExternalDragDropService.js';
import { EditTextExtensionProvider } from '../widgets/designerView/extensions/EditText/EditTextExtensionProvider.js';
import { CopyPasteService } from './copyPasteService/CopyPasteService.js';
import { DefaultModelCommandService } from './modelCommandService/DefaultModelCommandService.js';
import { ButtonSeperatorProvider } from '../widgets/designerView/extensions/buttons/ButtonSeperatorProvider.js';
import { GridExtensionDesignViewConfigButtons } from '../widgets/designerView/extensions/buttons/GridExtensionDesignViewConfigButtons.js';
import { DrawRectTool } from '../widgets/designerView/tools/DrawRectTool.js';
import { DrawEllipsisTool } from '../widgets/designerView/tools/DrawEllipsisTool.js';
import { DrawLineTool } from '../widgets/designerView/tools/DrawLineTool.js';
import { HtmlWriterService } from './htmlWriterService/HtmlWriterService.js';
import { RectContextMenu } from '../widgets/designerView/extensions/contextMenu/RectContextMenu.js';
import { PathContextMenu } from '../widgets/designerView/extensions/contextMenu/PathContextMenu.js';
import { SeperatorContextMenu } from '../widgets/designerView/extensions/contextMenu/SeperatorContextMenu.js';
import { ZoomToElementContextMenu } from '../widgets/designerView/extensions/contextMenu/ZoomToElementContextMenu.js';
import { RotateLeftAndRight } from '../widgets/designerView/extensions/contextMenu/RotateLeftAndRightContextMenu.js';
import { SelectAllChildrenContextMenu } from '../widgets/designerView/extensions/contextMenu/SelectAllChildrenContextMenu.js';
import { PointerToolButtonProvider } from '../widgets/designerView/tools/toolBar/buttons/PointerToolButtonProvider.js';
import { SeperatorToolProvider } from '../widgets/designerView/tools/toolBar/buttons/SeperatorToolProvider.js';
import { ZoomToolButtonProvider } from '../widgets/designerView/tools/toolBar/buttons/ZoomToolButtonProvider.js';
import { DrawToolButtonProvider } from '../widgets/designerView/tools/toolBar/buttons/DrawToolButtonProvider.js';
import { TextToolButtonProvider } from '../widgets/designerView/tools/toolBar/buttons/TextToolButtonProvider.js';
import { SelectorToolButtonProvider } from '../widgets/designerView/tools/toolBar/buttons/SelectorToolButtonProvider.js';
import { GrayOutDragOverContainerExtensionProvider } from '../widgets/designerView/extensions/GrayOutDragOverContainerExtensionProvider.js';
import { PropertyGroupsService } from './propertiesService/PropertyGroupsService.js';
import { PlacementExtensionProvider } from '../widgets/designerView/extensions/PlacementExtensionProvider.js';
import { FlexboxExtensionProvider } from '../widgets/designerView/extensions/flex/FlexboxExtensionProvider.js';
import { FlexboxExtensionDesignViewConfigButtons } from '../widgets/designerView/extensions/buttons/FlexboxExtensionDesignViewConfigButtons.js';
import { InvisibleElementExtensionDesignViewConfigButtons } from '../widgets/designerView/extensions/buttons/InvisibleElementExtensionDesignViewConfigButtons.js';
import { UndoService } from './undoService/UndoService.js';
import { IDesignerCanvas } from '../widgets/designerView/IDesignerCanvas.js';
import { SelectionService } from './selectionService/SelectionService.js';
import { StylesheetServiceDesignViewConfigButtons } from '../widgets/designerView/extensions/buttons/StylesheetServiceDesignViewConfigButtons.js';
import { JumpToElementContextMenu } from '../widgets/designerView/extensions/contextMenu/JumpToElementContextMenu.js';
import { EditGridColumnRowSizesExtensionProvider } from '../widgets/designerView/extensions/grid/EditGridColumnRowSizesExtensionProvider.js';
import { DisplayGridExtensionProvider } from '../widgets/designerView/extensions/grid/DisplayGridExtensionProvider.js';
import { ApplyFirstMachingExtensionProvider } from '../widgets/designerView/extensions/logic/ApplyFirstMachingExtensionProvider.js';
import { DesignItemDocumentPositionService } from './designItemDocumentPositionService/DesignItemDocumentPositionService.js';
import { TransformToolButtonProvider } from '../widgets/designerView/tools/toolBar/buttons/TransformToolButtonProvider.js';
import { MultipleSelectionRectExtensionProvider } from '../widgets/designerView/extensions/MultipleSelectionRectExtensionProvider.js';
import { DragDropService } from './dragDropService/DragDropService.js';
import { EventsService } from './eventsService/EventsService.js';
import { SimpleDemoProviderService } from './demoProviderService/SimpleDemoProviderService.js';
import { DrawElementTool } from '../widgets/designerView/tools/DrawElementTool.js';
import { RoundPixelsDesignViewConfigButton } from '../widgets/designerView/extensions/buttons/RoundPixelsDesignViewConfigButton.js';
import { MathMLElementsPropertiesService } from './propertiesService/services/MathMLElementsPropertiesService.js';
import { UnifiedGeometryExtensionProvider } from '../widgets/designerView/extensions/svg/UnifiedGeometryExtensionProvider.js';
import { SvgPathSourceMapProvider } from './sourceMapService/SvgPathSourceMapProvider.js';
import { ConditionExtensionProvider } from '../widgets/designerView/extensions/logic/ConditionExtensionProvider.js';
import { GridToolbarExtensionProvider } from '../widgets/designerView/extensions/grid/GridToolbarExtensionProvider.js';
import { FlexToolbarExtensionProvider } from '../widgets/designerView/extensions/flex/FlexToolbarExtensionProvider.js';
import { BlockToolbarExtensionProvider } from '../widgets/designerView/extensions/block/BlockToolbarExtensionProvider.js';
import { ChildContextMenu } from '../widgets/designerView/extensions/contextMenu/ChildContextMenu.js';
import { GridChildToolbarExtensionProvider } from '../widgets/designerView/extensions/grid/GridChildToolbarExtensionProvider.js';
import { ToolbarExtensionsDesignViewConfigButtons } from '../widgets/designerView/extensions/buttons/ToolbarExtensionsDesignViewConfigButtons.js';
import { PaddingExtensionProvider } from '../widgets/designerView/extensions/PaddingExtensionProvider.js';
import { GridChildResizeExtensionProvider } from '../widgets/designerView/extensions/grid/GridChildResizeExtensionProvider.js';
import { AlignItemsContextMenu } from '../widgets/designerView/extensions/contextMenu/AlignItemsContextMenu.js';
import { BasicWebcomponentPropertiesService } from './propertiesService/services/BasicWebcomponentPropertiesService.js';
import { PreviousElementSelectExtensionProvider } from '../widgets/designerView/extensions/PreviousElementSelectExtensionProvider.js';
import { ForceCssContextMenu } from '../widgets/designerView/extensions/contextMenu/ForceCssContextMenu.js';
import { OptionsContextMenuButton } from '../widgets/designerView/extensions/buttons/OptionsContextMenuButton.js';
import { ChildrenContextMenu } from '../widgets/designerView/extensions/contextMenu/ChildrenContextMenu.js';
import { MarginTool } from '../widgets/designerView/tools/MarginTool.js';
import { SimpleToolButtonProvider } from '../widgets/designerView/tools/toolBar/buttons/SimpleToolButtonProvider.js';
import { assetsPath } from '../../Constants.js';
import { PaddingTool } from '../widgets/designerView/tools/PaddingTool.js';
import { DesignItemService } from './designItemService/DesignItemService.js';
import { DeletionService } from './deletionService/DeletionService.js';
import { MiniatureViewService } from './miniatureViewService/MiniatureViewService.js';
import { DisplayMediaPngWriterService } from './pngCreatorService/DisplayMediaPngWriterService.js';
import { SearchService } from './searchService/SearchService.js';
import { BasicContextMenu } from '../widgets/designerView/extensions/contextMenu/BasicContextMenu.js';
import { ProjectiveTransformExtension } from '../widgets/designerView/extensions/transforms/ProjectiveTransformExtension.js';
import { ProjectiveTransformExtensionProvider } from '../widgets/designerView/extensions/transforms/ProjectiveTransformExtensionProvider.js';
import { DefaultEditorTypeService } from './propertiesService/DefaultEditorTypeService.js';
import { StyleElementRenderedDesignItemService } from './renderedDesignItemService/StyleElementRenderedDesignItemService.js';
export function createDefaultServiceContainer() {
let serviceContainer = new ServiceContainer();
let defaultPlacementService = new DefaultPlacementService();
serviceContainer.register("containerService", defaultPlacementService);
serviceContainer.register("containerService", new GridPlacementService(defaultPlacementService));
serviceContainer.register("containerService", new FlexBoxPlacementService(defaultPlacementService));
serviceContainer.register("propertyService", new BasicWebcomponentPropertiesService());
serviceContainer.register("propertyService", new PolymerPropertiesService());
serviceContainer.register("propertyService", new LitElementPropertiesService());
serviceContainer.register("propertyService", new NativeElementsPropertiesService());
serviceContainer.register("propertyService", new SVGElementsPropertiesService());
serviceContainer.register("propertyService", new MathMLElementsPropertiesService());
serviceContainer.register("propertyService", new Lit2PropertiesService());
serviceContainer.register("propertyService", new BaseCustomWebComponentPropertiesService());
serviceContainer.register("propertyGroupsService", new PropertyGroupsService());
serviceContainer.register("instanceService", new DefaultInstanceService());
serviceContainer.register("propertyEditorTypesService", new DefaultPropertyEditorTypesService());
serviceContainer.register("editorTypeService", new DefaultEditorTypeService());
serviceContainer.register("htmlWriterService", new HtmlWriterService());
serviceContainer.register("snaplinesProviderService", new SnaplinesProviderService());
serviceContainer.register("htmlParserService", new DefaultHtmlParserService());
serviceContainer.register("renderedDesignItemService", new StyleElementRenderedDesignItemService());
serviceContainer.register("elementAtPointService", new ElementAtPointService());
serviceContainer.register("externalDragDropService", new ExternalDragDropService());
serviceContainer.register("dragDropService", new DragDropService());
serviceContainer.register("copyPasteService", new CopyPasteService());
serviceContainer.register("modelCommandService", new DefaultModelCommandService());
serviceContainer.register("demoProviderService", new SimpleDemoProviderService());
serviceContainer.register("eventsService", new EventsService());
serviceContainer.register("designItemService", new DesignItemService());
serviceContainer.register("deletionService", new DeletionService());
serviceContainer.register("miniatureViewService", new MiniatureViewService());
serviceContainer.register("pngCreatorService", new DisplayMediaPngWriterService());
serviceContainer.register("searchService", new SearchService());
serviceContainer.register("undoService", (designerCanvas: IDesignerCanvas) => new UndoService(designerCanvas));
serviceContainer.register("selectionService", (designerCanvas: IDesignerCanvas) => new SelectionService(designerCanvas, false));
serviceContainer.register("designItemDocumentPositionService", (designerCanvas: IDesignerCanvas) => new DesignItemDocumentPositionService(designerCanvas));
serviceContainer.sourceMapProviders.push(new SvgPathSourceMapProvider());
serviceContainer.designerExtensions.set(ExtensionType.Permanent, [
new InvisibleElementExtensionProvider(),
]);
serviceContainer.designerExtensions.set(ExtensionType.PrimarySelection, [
new ConditionExtensionProvider(new MultipleSelectionRectExtensionProvider(), item => !(item.node instanceof item.window.SVGElement) || item.node instanceof item.window.SVGSVGElement),
]);
serviceContainer.designerExtensions.set(ExtensionType.Selection, [
new ConditionExtensionProvider(new SelectionDefaultExtensionProvider(), item => !(item.node instanceof item.window.SVGElement) || item.node instanceof item.window.SVGSVGElement),
]);
serviceContainer.designerExtensions.set(ExtensionType.OnlyOneItemSelected, [
new ConditionExtensionProvider(new ElementDragTitleExtensionProvider(), item => !(item.node instanceof item.window.SVGElement) || item.node instanceof item.window.SVGSVGElement),
new ConditionExtensionProvider(new PreviousElementSelectExtensionProvider(), item => !(item.node instanceof item.window.SVGElement) || item.node instanceof item.window.SVGSVGElement),
new ConditionExtensionProvider(new MarginExtensionProvider, (_, c) => c.activeTool instanceof MarginTool || c.activeTool instanceof PaddingTool, true),
new ConditionExtensionProvider(new PaddingExtensionProvider, (_, c) => c.activeTool instanceof MarginTool || c.activeTool instanceof PaddingTool, true),
new PositionExtensionProvider(),
new UnifiedGeometryExtensionProvider(),
new ApplyFirstMachingExtensionProvider(new GridChildResizeExtensionProvider(), new ResizeExtensionProvider(true)),
new TransformOriginExtensionProvider(true),
new RotateExtensionProvider(),
]);
serviceContainer.designerExtensions.set(ExtensionType.MultipleItemsSelected, [
new RotateGroupExtensionProvider(),
]);
serviceContainer.designerExtensions.set(ExtensionType.PrimarySelectionRefreshed, [
new GridChildToolbarExtensionProvider(),
new GridToolbarExtensionProvider(),
new FlexToolbarExtensionProvider(),
new BlockToolbarExtensionProvider(),
]);
serviceContainer.designerExtensions.set(ExtensionType.PrimarySelectionAndCanBeEntered, [
new DisplayGridExtensionProvider(),
new EditGridColumnRowSizesExtensionProvider(),
new FlexboxExtensionProvider(),
]);
serviceContainer.designerExtensions.set(ExtensionType.PrimarySelectionContainerAndCanBeEntered, [
new DisplayGridExtensionProvider('lightgray', '#8080802b'),
new FlexboxExtensionProvider()
]);
serviceContainer.designerExtensions.set(ExtensionType.MouseOver, [
new HighlightElementExtensionProvider(),
//new ConditionExtensionProvider(new ElementDragTitleExtensionProvider(), item => item.instanceServiceContainer.selectionService.primarySelection !== item && !(item.node instanceof item.window.SVGElement) || item.node instanceof item.window.SVGSVGElement),
//new ConditionExtensionProvider(new PreviousElementSelectExtensionProvider(), item => item.instanceServiceContainer.selectionService.primarySelection !== item && !(item.node instanceof item.window.SVGElement) || item.node instanceof item.window.SVGSVGElement),
]);
serviceContainer.designerExtensions.set(ExtensionType.Placement, [
new PlacementExtensionProvider()
]);
serviceContainer.designerExtensions.set(ExtensionType.ContainerDrag, [
new GrayOutExtensionProvider()
]);
serviceContainer.designerExtensions.set(ExtensionType.ContainerDragOverAndCanBeEntered, [
new ApplyFirstMachingExtensionProvider(
new DisplayGridExtensionProvider(),
new GrayOutDragOverContainerExtensionProvider(),
),
new AltToEnterContainerExtensionProvider()
]);
serviceContainer.designerExtensions.set(ExtensionType.ContainerExternalDragOverAndCanBeEntered, [
new ApplyFirstMachingExtensionProvider(
new DisplayGridExtensionProvider(),
new GrayOutDragOverContainerExtensionProvider(),
),
]);
serviceContainer.designerExtensions.set(ExtensionType.Doubleclick, [
new EditTextExtensionProvider()
]);
serviceContainer.designerExtensions.set(ExtensionType.ManualApplied, [
new ProjectiveTransformExtensionProvider()
]);
serviceContainer.designerPointerExtensions.push(
//new CursorLinePointerExtensionProvider()
);
serviceContainer.designerTools.set(NamedTools.Pointer, new PointerTool());
serviceContainer.designerTools.set(NamedTools.DrawSelection, new RectangleSelectorTool());
serviceContainer.designerTools.set(NamedTools.DrawPath, new DrawPathTool({ interpolatePoints: true }));
serviceContainer.designerTools.set(NamedTools.DrawRect, new DrawRectTool());
serviceContainer.designerTools.set(NamedTools.DrawEllipsis, new DrawEllipsisTool());
serviceContainer.designerTools.set(NamedTools.DrawLine, new DrawLineTool());
serviceContainer.designerTools.set(NamedTools.Zoom, new ZoomTool());
serviceContainer.designerTools.set(NamedTools.Pan, new PanTool());
serviceContainer.designerTools.set(NamedTools.RectangleSelector, new RectangleSelectorTool());
serviceContainer.designerTools.set(NamedTools.MagicWandSelector, new MagicWandSelectorTool());
serviceContainer.designerTools.set(NamedTools.PickColor, new PickColorTool());
serviceContainer.designerTools.set(NamedTools.Text, new TextTool());
serviceContainer.designerTools.set(NamedTools.DrawElementTool, DrawElementTool);
serviceContainer.designerTools.set(NamedTools.Margin, new MarginTool());
serviceContainer.designerTools.set(NamedTools.Padding, new PaddingTool());
serviceContainer.designViewConfigButtons.push(
new ButtonSeperatorProvider(20),
new GridExtensionDesignViewConfigButtons(),
new FlexboxExtensionDesignViewConfigButtons(),
new ButtonSeperatorProvider(10),
new InvisibleElementExtensionDesignViewConfigButtons(),
new ButtonSeperatorProvider(10),
new StylesheetServiceDesignViewConfigButtons(),
new ButtonSeperatorProvider(10),
new ToolbarExtensionsDesignViewConfigButtons(),
new ButtonSeperatorProvider(30),
new RoundPixelsDesignViewConfigButton(),
new ButtonSeperatorProvider(30),
new OptionsContextMenuButton()
);
serviceContainer.designViewToolbarButtons.push(
new PointerToolButtonProvider(),
new SeperatorToolProvider(22),
new SelectorToolButtonProvider(),
new SeperatorToolProvider(22),
new SimpleToolButtonProvider("Margin", assetsPath + 'images/tools/Margin.svg'),
new SeperatorToolProvider(22),
new SimpleToolButtonProvider("Padding", assetsPath + 'images/tools/Padding.svg'),
new SeperatorToolProvider(22),
new ZoomToolButtonProvider(),
new SeperatorToolProvider(22),
new DrawToolButtonProvider(),
new SeperatorToolProvider(22),
new TextToolButtonProvider(),
new SeperatorToolProvider(22),
new TransformToolButtonProvider()
);
serviceContainer.designerContextMenuExtensions = [
new ChildContextMenu('edit', new CopyPasteContextMenu(), new SeperatorContextMenu(), new PasteFormatContextMenu()),
new SeperatorContextMenu(),
new ToolWindowsContextMenu(),
new SeperatorContextMenu(),
new ChildContextMenu('modify',
new RotateLeftAndRight(),
new SeperatorContextMenu(),
new ZMoveContextMenu(),
new SeperatorContextMenu(),
new AlignItemsContextMenu(),
new SeperatorContextMenu(),
new BasicContextMenu({ title: '3D transform', action: (e, designerCanvas, designItem) => {
designerCanvas.extensionManager.removeExtensions([designItem], false, ExtensionType.PrimarySelection);
designerCanvas.extensionManager.removeExtensions([designItem], false, ExtensionType.OnlyOneItemSelected);
designerCanvas.extensionManager.applyExtensionInstance(designItem, new ProjectiveTransformExtension(designerCanvas.extensionManager, designerCanvas, designItem), ExtensionType.OnlyOneItemSelected); } })),
new SeperatorContextMenu(),
new ChildContextMenu('view', new JumpToElementContextMenu(), new ZoomToElementContextMenu()),
new SeperatorContextMenu(),
new ChildContextMenu('force', new ForceCssContextMenu()),
new SeperatorContextMenu(),
new MultipleItemsSelectedContextMenu(),
new PathContextMenu(),
new RectContextMenu(),
new SeperatorContextMenu(),
new SelectAllChildrenContextMenu(),
new SeperatorContextMenu(),
new ItemsBelowContextMenu(),
new ChildrenContextMenu(),
];
return serviceContainer;
}
export default createDefaultServiceContainer;
================================================
FILE: packages/web-component-designer/src/elements/services/GlobalContext.ts
================================================
//Service container should not be something with changeing information, so global context is for tool and color (and maybe more)
import { PropertyChangedArgs, TypedEvent } from "@node-projects/base-custom-webcomponent";
import { ITool } from '../widgets/designerView/tools/ITool.js';
import { ServiceContainer } from './ServiceContainer.js';
import { IDesignItem } from "../item/IDesignItem.js";
export class GlobalContext {
private _serviceContainer: ServiceContainer
private _tool: ITool;
private _strokeColor: string = 'black';
private _strokeThickness: string = '3';
private _fillBrush: string = 'none';
constructor(serviceContainer: ServiceContainer) {
this._serviceContainer = serviceContainer;
}
public set tool(tool: ITool) {
if (this._tool !== tool) {
const oldTool = this._tool;
if (oldTool) {
oldTool.dispose();
}
this._tool = tool;
let toolName = null;
for (let t of this._serviceContainer.designerTools) {
if (t[1] == tool)
toolName = t[0];
}
this.onToolChanged.emit(new PropertyChangedArgs<{ name: string, tool: ITool }>({ name: toolName, tool: tool }, { name: null, tool: oldTool }));
if (this._tool)
this._tool.activated(this._serviceContainer);
}
}
public get tool(): ITool {
return this._tool;
}
readonly onToolChanged = new TypedEvent>();
finishedWithTool: (tool: ITool) => void = () => this.tool = null;
public set strokeColor(strokeColor: string) {
if (this._strokeColor !== strokeColor) {
const oldStrokeColor = this._strokeColor;
this._strokeColor = strokeColor;
this.onStrokeColorChanged.emit(new PropertyChangedArgs(strokeColor, oldStrokeColor));
}
}
public get strokeColor(): string {
return this._strokeColor;
}
readonly onStrokeColorChanged = new TypedEvent>();
public set strokeThickness(strokeThickness: string) {
if (this._strokeThickness !== strokeThickness) {
const oldStrokeThickness = this._strokeThickness;
this._strokeThickness = strokeThickness;
this.onStrokeThicknessChanged.emit(new PropertyChangedArgs(strokeThickness, oldStrokeThickness));
}
}
public get strokeThickness(): string {
return this._strokeThickness;
}
readonly onStrokeThicknessChanged = new TypedEvent>();
public set fillBrush(fillBrush: string) {
this._fillBrush = fillBrush;
if (this._fillBrush !== fillBrush) {
const oldFillBrush = this._fillBrush;
this._fillBrush = fillBrush;
this.onFillBrushChanged.emit(new PropertyChangedArgs(fillBrush, oldFillBrush));
}
}
public get fillBrush(): string {
return this._fillBrush;
}
readonly onFillBrushChanged = new TypedEvent>();
readonly showConfigClicked = new TypedEvent<{configUi:Element, designItem: IDesignItem}>();
}
================================================
FILE: packages/web-component-designer/src/elements/services/IService.ts
================================================
export interface IService {
}
================================================
FILE: packages/web-component-designer/src/elements/services/IServiceContainer.ts
================================================
export interface IServiceContainer {
register(name: string, service: any);
getLastService(service: string): any;
getServices(service: string): any[];
}
================================================
FILE: packages/web-component-designer/src/elements/services/InstanceServiceContainer.ts
================================================
import { ISelectionService } from './selectionService/ISelectionService.js';
import { IUndoService } from './undoService/IUndoService.js';
import { BaseServiceContainer } from './BaseServiceContainer.js';
import { DesignContext } from '../widgets/designerView/DesignContext.js';
import { IDesignContext } from '../widgets/designerView/IDesignContext.js';
import { IDesignerCanvas } from '../widgets/designerView/IDesignerCanvas.js';
import { IStylesheetService } from './stylesheetService/IStylesheetService.js';
import { IDesignItemDocumentPositionService } from './designItemDocumentPositionService/IDesignItemDocumentPositionService.js';
import { DocumentContainer } from '../documentContainer.js';
import { ICollaborationService } from './collaborationService/ICollaborationService.js';
import { TypedEvent } from '@node-projects/base-custom-webcomponent';
import { IDesignItem } from '../item/IDesignItem.js';
interface InstanceServiceNameMap {
"undoService": IUndoService;
"selectionService": ISelectionService;
"stylesheetService": IStylesheetService;
"designItemDocumentPositionService": IDesignItemDocumentPositionService;
}
interface IContentChangedParsed {
changeType: 'parsed';
}
interface IContentChangedWithDesignItems {
changeType: "added" | "removed" | "moved";
designItems: IDesignItem[];
}
interface IContentChangedChangeWithDesignItems {
changeType: "changed";
designItems: IDesignItem[];
type: "attribute" | "css" | "property";
name: string;
oldValue?: any;
newValue?: any;
}
export type IContentChanged = IContentChangedParsed | IContentChangedWithDesignItems | IContentChangedChangeWithDesignItems ;
export class InstanceServiceContainer extends BaseServiceContainer {
public designContext: IDesignContext = new DesignContext();
public readonly designerCanvas: IDesignerCanvas;
public collaborationService?: ICollaborationService;
public designer: any; //usable to assign designer from outside
public documentContainer: DocumentContainer; //usable to assign designer from outside
/** Event fired when the content of the designer changes, but raised from UndoService, so it should not be used to modify the elements again */
public readonly onContentChanged = new TypedEvent();
constructor(designerCanvas: IDesignerCanvas) {
super();
this.designerCanvas = designerCanvas;
}
get rootDesignItem(): IDesignItem {
return this.designerCanvas.rootDesignItem;
}
get undoService(): IUndoService {
return this.getLastService('undoService');
}
get selectionService(): ISelectionService {
return this.getLastService('selectionService');
}
get stylesheetService(): IStylesheetService {
return this.getLastService('stylesheetService');
}
get designItemDocumentPositionService(): IDesignItemDocumentPositionService {
return this.getLastService('designItemDocumentPositionService');
}
}
================================================
FILE: packages/web-component-designer/src/elements/services/ServiceContainer.ts
================================================
import { IPropertiesService } from './propertiesService/IPropertiesService.js';
import { IPlacementService } from './placementService/IPlacementService.js';
import { IElementsService } from './elementsService/IElementsService.js';
import { IInstanceService } from './instanceService/IInstanceService.js';
import { IPropertyEditorTypesService } from './propertiesService/IPropertyEditorTypesService.js';
import { BaseServiceContainer } from './BaseServiceContainer.js';
import { IHtmlWriterService } from './htmlWriterService/IHtmlWriterService.js';
import { ICodeView } from '../widgets/codeView/ICodeView.js';
import { IHtmlParserService } from './htmlParserService/IHtmlParserService.js';
import { IIntializationService } from './initializationService/IIntializationService.js';
import { IDemoView } from '../widgets/demoView/IDemoView.js';
import { DemoView } from '../widgets/demoView/demoView.js';
import { ITool } from '../widgets/designerView/tools/ITool.js';
import { ExtensionType } from '../widgets/designerView/extensions/ExtensionType.js';
import { IDesignerExtensionProvider } from '../widgets/designerView/extensions/IDesignerExtensionProvider.js';
import { NamedTools } from '../widgets/designerView/tools/NamedTools.js';
import { IContextMenuExtension } from '../widgets/designerView/extensions/contextMenu/IContextMenuExtension.js';
import { GlobalContext } from './GlobalContext.js';
import { IBindingService } from './bindingsService/IBindingService.js';
import { IElementAtPointService } from './elementAtPointService/IElementAtPointService.js';
import { ISnaplinesProviderService } from "./placementService/ISnaplinesProviderService.js";
import { IExternalDragDropService } from './dragDropService/IExternalDragDropService.js';
import { ICopyPasteService } from "./copyPasteService/ICopyPasteService.js";
import { IDesignerPointerExtensionProvider } from "../widgets/designerView/extensions/pointerExtensions/IDesignerPointerExtensionProvider.js";
import { IModelCommandService } from "./modelCommandService/IModelCommandService.js";
import { IDesignViewConfigButtonsProvider } from "../widgets/designerView/extensions/buttons/IDesignViewConfigButtonsProvider.js";
import { IDemoProviderService } from "./demoProviderService/IDemoProviderService.js";
import { IBindableObjectsService } from "./bindableObjectsService/IBindableObjectsService.js";
import { IBindableObjectDragDropService } from "./bindableObjectsService/IBindableObjectDragDropService.js";
import { IDesignViewToolbarButtonProvider } from "../widgets/designerView/tools/toolBar/IDesignViewToolbarButtonProvider.js";
import { IElementInteractionService } from './elementInteractionService/IElementInteractionService.js';
import { IProperty } from "./propertiesService/IProperty.js";
import { IDesignItem } from "../item/IDesignItem.js";
import { IBinding } from '../item/IBinding.js';
import { BindingTarget } from '../item/BindingTarget.js';
import { IPropertyGroupsService } from './propertiesService/IPropertyGroupsService.js';
import { CodeViewSimple } from '../widgets/codeView/code-view-simple.js';
import { IUndoService } from './undoService/IUndoService.js';
import { ISelectionService } from './selectionService/ISelectionService.js';
import { IStylesheetService } from './stylesheetService/IStylesheetService.js';
import { IDesignerCanvas } from '../widgets/designerView/IDesignerCanvas.js';
import { IDesignItemDocumentPositionService } from './designItemDocumentPositionService/IDesignItemDocumentPositionService.js';
import { IDragDropService } from './dragDropService/IDragDropService.js';
import { IDesignItemService } from './designItemService/IDesignItemService.js';
import { IEventsService } from './eventsService/IEventsService.js';
import { IPropertyGridDragDropService } from './dragDropService/IPropertyGridDragDropService.js';
import { IConfigUiService } from './configUiService/IConfigUiService.js';
import { IRefactorService } from './refactorService/IRefactorService.js';
import { InstanceServiceContainer } from './InstanceServiceContainer.js';
import { IDeletionService } from './deletionService/IDeletionService.js';
import { IReferencesChangedService } from './referencesChangedService/IReferencesChangedService.js';
import { IMiniatureViewService } from './miniatureViewService/IMiniatureViewService.js';
import { IPngCreatorService } from './pngCreatorService/IPngCreatorService.js';
import { ISearchService } from './searchService/ISearchService.js';
import { ICollaborationService } from './collaborationService/ICollaborationService.js';
import { IEditorTypeService } from './propertiesService/IEditorTypeService.js';
import { ISourceMapProvider } from './sourceMapService/ISourceMapProvider.js';
import { IRenderedDesignItemService } from './renderedDesignItemService/IRenderedDesignItemService.js';
interface ServiceNameMap {
"propertyService": IPropertiesService;
"attachedPropertyService": IPropertiesService;
"containerService": IPlacementService;
"snaplinesProviderService": ISnaplinesProviderService;
"elementsService": IElementsService;
"instanceService": IInstanceService;
"propertyEditorTypesService": IPropertyEditorTypesService;
"htmlWriterService": IHtmlWriterService;
"htmlParserService": IHtmlParserService;
"intializationService": IIntializationService;
"bindingService": IBindingService;
"bindableObjectsService": IBindableObjectsService;
"bindableObjectDragDropService": IBindableObjectDragDropService;
"elementAtPointService": IElementAtPointService;
"externalDragDropService": IExternalDragDropService;
"copyPasteService": ICopyPasteService;
"modelCommandService": IModelCommandService
"demoProviderService": IDemoProviderService;
"elementInteractionService": IElementInteractionService;
"propertyGroupsService": IPropertyGroupsService;
"dragDropService": IDragDropService;
"designItemService": IDesignItemService;
"eventsService": IEventsService;
"propertyGridDragDropService": IPropertyGridDragDropService;
"configUiService": IConfigUiService;
"refactorService": IRefactorService;
"deletionService": IDeletionService;
"referencesChangedService": IReferencesChangedService;
"miniatureViewService": IMiniatureViewService;
"pngCreatorService": IPngCreatorService;
"searchService": ISearchService;
"editorTypeService": IEditorTypeService;
"renderedDesignItemService": IRenderedDesignItemService;
//Factories for Instance Service Containers
"undoService": (designerCanvas: IDesignerCanvas) => IUndoService;
"selectionService": (designerCanvas: IDesignerCanvas) => ISelectionService;
"stylesheetService": (designerCanvas: IDesignerCanvas) => IStylesheetService;
"collaborationService": (designerCanvas: IDesignerCanvas) => ICollaborationService;
"designItemDocumentPositionService": (designerCanvas: IDesignerCanvas) => IDesignItemDocumentPositionService;
}
const isTouchUi = navigator.maxTouchPoints > 0;
export class ServiceContainer extends BaseServiceContainer {
readonly config: {
codeViewWidget: new (...args: any[]) => ICodeView & HTMLElement;
demoViewWidget: new (...args: any[]) => IDemoView & HTMLElement;
openBindingsEditor?: (property: IProperty, designItems: IDesignItem[], binding: IBinding, bindingTarget: BindingTarget) => Promise
} = {
codeViewWidget: CodeViewSimple,
demoViewWidget: DemoView
};
public readonly designerExtensions: Map<(ExtensionType | string), IDesignerExtensionProvider[]> = new Map();
public readonly sourceMapProviders: ISourceMapProvider[] = [];
removeDesignerExtensionOfType(container: (ExtensionType | string), lambda: new (...args: any[]) => IDesignerExtensionProvider): void {
const extContainer = this.designerExtensions.get(container);
for (let i = 0; i < extContainer.length; i++) {
if (extContainer[i].constructor === lambda) {
extContainer.splice(i, 1);
}
}
}
public readonly instanceServiceContainerCreatedCallbacks: ((instanceServiceContainer: InstanceServiceContainer) => void)[] = [];
public readonly designViewConfigButtons: IDesignViewConfigButtonsProvider[] = [];
public readonly designViewToolbarButtons: IDesignViewToolbarButtonProvider[] = [];
public readonly designerPointerExtensions: IDesignerPointerExtensionProvider[] = [];
public designerContextMenuExtensions: IContextMenuExtension[];
public readonly overlayLayerViewAdditionalStyles: CSSStyleSheet[] = [];
public readonly globalContext: GlobalContext = new GlobalContext(this);
public readonly options = {
zoomDesignerBackground: true,
roundPixelsToDecimalPlaces: 0,
resizerPixelSize: isTouchUi ? 8 : 3
};
public readonly designerTools: Map ITool)> = new Map();
get bindingService(): IBindingService {
return this.getLastService('bindingService');
}
get bindableObjectsServices(): IBindableObjectsService[] {
return this.getServices('bindableObjectsService');
}
get bindableObjectDragDropService(): IBindableObjectDragDropService {
return this.getLastService('bindableObjectDragDropService');
}
get propertyGridDragDropService(): IPropertyGridDragDropService {
return this.getLastService('propertyGridDragDropService');
}
get dragDropService(): IDragDropService {
return this.getLastService('dragDropService');
}
get elementInteractionServices(): IElementInteractionService[] {
return this.getServices('elementInteractionService');
}
get propertiesServices(): IPropertiesService[] {
return this.getServices('propertyService');
}
get attachedPropertyServices(): IPropertiesService[] {
return this.getServices('attachedPropertyService');
}
get propertyGroupService(): IPropertyGroupsService {
return this.getLastService('propertyGroupsService');
}
get containerServices(): IPlacementService[] {
return this.getServices('containerService');
}
get snaplinesProviderService(): ISnaplinesProviderService {
return this.getLastService('snaplinesProviderService');
}
get elementsServices(): IElementsService[] {
return this.getServices('elementsService');
}
get eventsService(): IEventsService[] {
return this.getServices('eventsService');
}
get instanceServices(): IInstanceService[] {
return this.getServices('instanceService');
}
get propertyEditorTypesServices(): IPropertyEditorTypesService[] {
return this.getServices('propertyEditorTypesService');
}
get htmlWriterService(): IHtmlWriterService {
return this.getLastService('htmlWriterService');
}
get htmlParserService(): IHtmlParserService {
return this.getLastService('htmlParserService');
}
get renderedDesignItemServices(): IRenderedDesignItemService[] {
return this.getServices('renderedDesignItemService');
}
get intializationService(): IIntializationService {
return this.getLastService('intializationService');
}
get elementAtPointService(): IElementAtPointService {
return this.getLastService('elementAtPointService');
}
get externalDragDropService(): IExternalDragDropService {
return this.getLastService('externalDragDropService');
}
get copyPasteService(): ICopyPasteService {
return this.getLastService('copyPasteService');
}
get modelCommandService(): IModelCommandService {
return this.getLastService('modelCommandService');
}
get demoProviderService(): IDemoProviderService {
return this.getLastService('demoProviderService');
}
get designItemService(): IDesignItemService {
return this.getLastService('designItemService');
}
get configUiServices(): IConfigUiService[] {
return this.getServices('configUiService');
}
get refactorServices(): IRefactorService[] {
return this.getServices('refactorService');
}
get deletionService(): IDeletionService {
return this.getLastService('deletionService');
}
get referencesChangedService(): IReferencesChangedService {
return this.getLastService('referencesChangedService');
}
get miniatureViewService(): IMiniatureViewService {
return this.getLastService('miniatureViewService');
}
get pngCreatorService(): IPngCreatorService {
return this.getLastService('pngCreatorService');
}
get searchService(): ISearchService {
return this.getLastService('searchService');
}
get editorTypesServices(): IEditorTypeService[] {
return this.getServices('editorTypeService');
}
}
================================================
FILE: packages/web-component-designer/src/elements/services/bindableObjectsService/BindableObjectType.ts
================================================
export enum BindableObjectType {
undefined = 'undefined',
folder = 'folder',
boolean = 'boolean',
number = 'number',
string = 'string',
date = 'date',
color = 'color',
object = 'object',
}
================================================
FILE: packages/web-component-designer/src/elements/services/bindableObjectsService/BindableObjectsTarget.ts
================================================
export type BindableObjectsTarget = 'itemsView' | 'binding' | 'script' | 'property';
================================================
FILE: packages/web-component-designer/src/elements/services/bindableObjectsService/IBindableObject.ts
================================================
import { BindableObjectType } from './BindableObjectType.js';
export interface IBindableObject {
readonly bindabletype?: 'signal' | 'property' | 'context'
readonly specialType?: string //e.g. signalProperty
readonly type: BindableObjectType
readonly name: string;
readonly fullName: string;
readonly children?: false | IBindableObject[];
readonly originalObject?: T;
readonly description?: string;
}
================================================
FILE: packages/web-component-designer/src/elements/services/bindableObjectsService/IBindableObjectDragDropService.ts
================================================
import { IDesignItem } from "../../item/IDesignItem.js";
import { IDesignerCanvas } from "../../widgets/designerView/IDesignerCanvas.js";
import { IProperty } from "../propertiesService/IProperty.js";
import { IBindableObject } from "./IBindableObject.js";
export interface IBindableObjectDragDropService {
dragEnter(designerCanvas: IDesignerCanvas, event: DragEvent, element: Element): void;
dragLeave(designerCanvas: IDesignerCanvas, event: DragEvent, element: Element): void;
dragOver(designerCanvas: IDesignerCanvas, event: DragEvent, element: Element): 'none' | 'copy' | 'link' | 'move';
drop(designerCanvas: IDesignerCanvas, event: DragEvent, bindableObject: IBindableObject, element: Element): void;
dragOverOnProperty?(event: DragEvent, property: IProperty, designItems: IDesignItem[]): 'none' | 'copy' | 'link' | 'move';
dropOnProperty?(event: DragEvent, property: IProperty, bindableObject: IBindableObject, designItems: IDesignItem[]): void;
}
================================================
FILE: packages/web-component-designer/src/elements/services/bindableObjectsService/IBindableObjectsService.ts
================================================
import { InstanceServiceContainer } from '../InstanceServiceContainer.js';
import { BindableObjectsTarget } from './BindableObjectsTarget.js';
import { IBindableObject } from './IBindableObject.js';
export interface IBindableObjectsService {
readonly name: string;
hasObjectsForInstanceServiceContainer(instanceServiceContainer: InstanceServiceContainer, source: BindableObjectsTarget);
getBindableObject(fullName: string, instanceServiceContainer?: InstanceServiceContainer): Promise>;
getBindableObjects(parent?: IBindableObject, instanceServiceContainer?: InstanceServiceContainer): Promise[]>;
}
================================================
FILE: packages/web-component-designer/src/elements/services/bindingsService/BaseCustomWebcomponentBindingsService.ts
================================================
import { IDesignItem } from '../../item/IDesignItem.js';
import { IBinding } from '../../item/IBinding.js';
import { IBindingService } from './IBindingService.js';
import { BindingMode } from '../../item/BindingMode.js';
import { BindingTarget } from "../../item/BindingTarget.js";
import { PropertiesHelper } from '../propertiesService/services/PropertiesHelper.js';
export class BaseCustomWebcomponentBindingsService implements IBindingService {
public static type = 'base-custom-webcomponent-binding';
getBindings(designItem: IDesignItem): IBinding[] {
let bindings: IBinding[] = null;
for (let a of designItem.attributes()) {
const name = a[0];
const value = a[1];
if ((value.startsWith('[[') || value.startsWith('{{')) && (value.endsWith('}}') || value.endsWith(']]'))) {
if (!bindings)
bindings = [];
let bnd: IBinding = { rawName: name, rawValue: value, service: this };
if (a[0] === 'bcw:visible') {
bnd.targetName = 'visibility';
bnd.target = BindingTarget.css;
bnd.expression = value.substring(2, value.length - 2);
} else if (a[0].startsWith('css:')) {
bnd.targetName = name.substring(4);
bnd.target = BindingTarget.css;
bnd.expression = value.substring(2, value.length - 2);
} else if (a[0].startsWith('class:')) {
bnd.targetName = name.substring(4);
bnd.target = BindingTarget.class;
bnd.expression = value.substring(2, value.length - 2);
} else if (a[0].startsWith('$')) {
bnd.targetName = name.substring(1);
bnd.target = BindingTarget.attribute;
bnd.expression = value.substring(2, value.length - 2);
} else if (a[0].startsWith('@')) {
bnd.targetName = name.substring(1);
bnd.target = BindingTarget.event;
bnd.expression = value.substring(2, value.length - 2);
} else if (a[0].startsWith('.')) {
bnd.targetName = PropertiesHelper.dashToCamelCase(name.substring(1));
bnd.target = BindingTarget.explicitProperty;
bnd.expression = value.substring(2, value.length - 2);
} else {
bnd.targetName = PropertiesHelper.dashToCamelCase(name);
bnd.target = BindingTarget.property;
bnd.expression = value.substring(2, value.length - 2);
}
bnd.type = BaseCustomWebcomponentBindingsService.type;
bnd.targetName = bnd.targetName;
bnd.bindableObjectNames = [value.substring(2, value.length - 2)];
bindings.push(bnd);
}
}
return bindings;
}
setBinding(designItem: IDesignItem, binding: IBinding): boolean {
if (binding.type !== BaseCustomWebcomponentBindingsService.type)
return false;
let nm = '';
switch (binding.target) {
case BindingTarget.explicitProperty:
nm += '.';
break;
case BindingTarget.css:
nm += 'css:';
break;
case BindingTarget.class:
nm += 'class';
break;
case BindingTarget.attribute:
nm += '$';
break;
case BindingTarget.event:
nm += '@';
break;
}
nm += binding.targetName;
let value = (binding.mode == BindingMode.oneWay ? '[[' : '{{') + binding.expression + (binding.mode == BindingMode.oneWay ? ']]' : '}}')
designItem.setAttribute(nm, value);
return true;
}
clearBinding(designItem: IDesignItem, propertyName: string, propertyTarget: BindingTarget): boolean {
return true;
}
}
================================================
FILE: packages/web-component-designer/src/elements/services/bindingsService/IBindingService.ts
================================================
import { IDesignItem } from '../../item/IDesignItem.js';
import { IBinding } from '../../item/IBinding.js';
import { BindingTarget } from '../../item/BindingTarget.js';
/**
* Can be used to parse bindings wich are done via special HTML Attributes or special Elements
* If your Bindings are to special, or HTML is not valid with them, maybe you need to parse the Bindings already in the
* htmlParserService
*/
export interface IBindingService {
getBindings(designItem: IDesignItem): IBinding[];
setBinding(designItem: IDesignItem, binding: IBinding): boolean;
clearBinding(designItem: IDesignItem, propertyName: string, propertyTarget: BindingTarget): boolean;
}
================================================
FILE: packages/web-component-designer/src/elements/services/bindingsService/SpecialTagsBindingService.ts
================================================
import { IDesignItem } from '../../item/IDesignItem.js';
import { IBinding } from '../../item/IBinding.js';
import { IBindingService } from './IBindingService.js';
import { BindingTarget } from "../../item/BindingTarget.js";
import { BindingMode } from "../../item/BindingMode.js";
/* Service wich read bindings from special HTML elements -> like tag-binding */
//TODO: refactor so we could use it
export class SpecialTagsBindingService implements IBindingService {
public static type = 'visu-tagbinding-binding'
_bindingTagName: string = "visu-tagbinding";
_elementIdAttribute: string = "elemnt-id";
_propertyNameAttribute: string = "property";
_isStyleNameAttribute: string = "is-style";
constructor() {
}
getBindings(designItem: IDesignItem): IBinding[] {
const bindings = [];
const directBindings = designItem.element.querySelectorAll(':scope > ' + this._bindingTagName);
for (let b of directBindings) {
bindings.push(this._parseBindingElement(b));
}
if (designItem.id) {
const nameBindings = designItem.instanceServiceContainer.designerCanvas.rootDesignItem.element.querySelectorAll(this._bindingTagName + "[" + this._elementIdAttribute + "='" + designItem.id + "]");
for (let b of nameBindings) {
const bnd = this._parseBindingElement(b);
(bnd).targetId = designItem.id
bindings.push(bnd);
}
}
return null;
}
private _parseBindingElement(b: Element): IBinding {
let bnd: IBinding = { targetName: b.getAttribute(this._propertyNameAttribute), service: this }
bnd.target = b.hasAttribute(this._isStyleNameAttribute) ? BindingTarget.css : BindingTarget.property;
bnd.invert = b.hasAttribute('negative-logic');
bnd.rawValue = b.outerHTML;
bnd.type = SpecialTagsBindingService.type;
bnd.mode = b.hasAttribute('two-way') ? BindingMode.twoWay : BindingMode.oneWay;
return bnd;
}
setBinding(designItem: IDesignItem, binding: IBinding): boolean {
return true;
}
clearBinding(designItem: IDesignItem, propertyName: string, propertyTarget: BindingTarget): boolean {
return true;
}
}
================================================
FILE: packages/web-component-designer/src/elements/services/bindingsService/VueBindingsService.ts
================================================
//read vue bindings:
//v-bind:class
//v-bind:style
//v-if
//v-else
//v-show
================================================
FILE: packages/web-component-designer/src/elements/services/collaborationService/CollaborationNodeIndex.ts
================================================
import { IDesignItem } from '../../item/IDesignItem.js';
import { NodeType } from '../../item/NodeType.js';
type CollaborationNodeIndexCache = {
orderedItems: IDesignItem[];
byItem: WeakMap;
};
const collaborationNodeIndexCacheKey = Symbol('collaborationNodeIndexCache');
function buildCache(rootDesignItem: IDesignItem): CollaborationNodeIndexCache {
const orderedItems: IDesignItem[] = [];
const byItem = new WeakMap();
let index = 0;
const visit = (designItem: IDesignItem) => {
if (!designItem)
return;
if (!designItem.isRootItem && designItem.nodeType === NodeType.Element) {
orderedItems.push(designItem);
byItem.set(designItem, index++);
}
if (designItem.hasChildren) {
for (const child of designItem.children())
visit(child);
}
};
visit(rootDesignItem);
return { orderedItems, byItem };
}
function getCache(rootDesignItem: IDesignItem, cache?: Record): CollaborationNodeIndexCache {
if (cache?.[collaborationNodeIndexCacheKey])
return cache[collaborationNodeIndexCacheKey];
const nodeIndexCache = buildCache(rootDesignItem);
if (cache)
cache[collaborationNodeIndexCacheKey] = nodeIndexCache;
return nodeIndexCache;
}
export function getCollaborationNodeIndex(designItem: IDesignItem, cache?: Record): number {
if (!designItem || designItem.isRootItem)
return null;
const rootDesignItem = designItem.instanceServiceContainer.designerCanvas.rootDesignItem;
return getCache(rootDesignItem, cache).byItem.get(designItem) ?? null;
}
export function getCollaborationNodeIndexes(designItems: IDesignItem[], cache?: Record): number[] {
if (!designItems?.length)
return [];
return designItems.map(x => getCollaborationNodeIndex(x, cache)).filter(x => x != null);
}
export function getDesignItemByCollaborationNodeIndex(rootDesignItem: IDesignItem, nodeIndex: number, cache?: Record): IDesignItem {
if (nodeIndex == null || nodeIndex < 0)
return null;
return getCache(rootDesignItem, cache).orderedItems[nodeIndex] ?? null;
}
================================================
FILE: packages/web-component-designer/src/elements/services/collaborationService/ICollaborationService.ts
================================================
import { TypedEvent } from '@node-projects/base-custom-webcomponent';
import { IStylesheet } from '../stylesheetService/IStylesheetService.js';
import { IUndoChangeEvent, UndoChangeKind, UndoChangeSource } from '../undoService/IUndoChangeEvent.js';
import { IService } from '../IService.js';
export type CollaborationConnectionState = 'disconnected' | 'connecting' | 'connected';
export interface ICollaborationPeerPresence {
peerId: string;
displayName?: string;
color?: string;
activeDesignItemId?: string;
selectedDesignItemIds?: string[];
activeNodeIndex?: number;
selectedNodeIndexes?: number[];
cursorPosition?: { x: number, y: number };
updatedAt: number;
}
export interface ICollaborationComment {
id: string;
targetDesignItemId?: string;
targetNodeIndex?: number;
text: string;
authorPeerId: string;
createdAt: number;
updatedAt: number;
resolved?: boolean;
}
export interface ICollaborationSession {
sessionId: string;
peerId: string;
displayName?: string;
}
export interface ICollaborationStateChangedEvent {
state: CollaborationConnectionState;
session?: ICollaborationSession;
}
export interface ICollaborationSelectionEvent {
source: UndoChangeSource;
peerId: string;
selectedNodeIndexes: number[];
primaryNodeIndex?: number;
selectedDesignItemIds: string[];
primaryDesignItemId?: string;
}
export interface ICollaborationDocumentSnapshot {
html: string;
stylesheets: IStylesheet[];
updatedAt: number;
}
export interface ICollaborationRemoteChange {
kind: UndoChangeKind;
title?: string;
}
export interface ICollaborationPeersChangedEvent {
source: UndoChangeSource;
peer?: ICollaborationPeerPresence;
peerId?: string;
peers: readonly ICollaborationPeerPresence[];
}
export interface ICollaborationCommentsChangedEvent {
source: UndoChangeSource;
comment?: ICollaborationComment;
commentId?: string;
comments: readonly ICollaborationComment[];
}
export interface ICollaborationTransport {
attach(service: ICollaborationService): void | Promise;
detach(): void | Promise;
connect(session: ICollaborationSession): void | Promise;
disconnect(): void | Promise;
sendChange(change: ICollaborationRemoteChange, snapshot: ICollaborationDocumentSnapshot): void | Promise;
sendSelection(selection: ICollaborationSelectionEvent): void | Promise;
sendPresence(peer: ICollaborationPeerPresence): void | Promise;
sendComment(change: ICollaborationCommentsChangedEvent): void | Promise;
}
export interface ICollaborationService extends IService {
readonly state: CollaborationConnectionState;
readonly session: ICollaborationSession;
readonly peers: readonly ICollaborationPeerPresence[];
readonly comments: readonly ICollaborationComment[];
readonly transport: ICollaborationTransport;
readonly isApplyingRemoteChanges: boolean;
attachTransport(transport: ICollaborationTransport): void;
detachTransport(): void;
connect(sessionId: string, peerId: string, displayName?: string): void;
disconnect(): void;
createSnapshot(): ICollaborationDocumentSnapshot;
applyRemoteSnapshot(snapshot: ICollaborationDocumentSnapshot): Promise;
applyRemoteChange(change: ICollaborationRemoteChange, snapshot?: ICollaborationDocumentSnapshot): Promise;
updateRemoteSelection(peerId: string, selectedNodeIndexes: number[], primaryNodeIndex?: number): void;
updatePeerPresence(peer: ICollaborationPeerPresence, source?: UndoChangeSource): void;
removePeer(peerId: string, source?: UndoChangeSource): void;
upsertComment(comment: ICollaborationComment, source?: UndoChangeSource): void;
removeComment(commentId: string, source?: UndoChangeSource): void;
readonly onStateChanged: TypedEvent;
readonly onChange: TypedEvent;
readonly onSelectionChanged: TypedEvent;
readonly onPeersChanged: TypedEvent;
readonly onCommentsChanged: TypedEvent;
}
================================================
FILE: packages/web-component-designer/src/elements/services/configUiService/IConfigUiService.ts
================================================
import { IDesignItem } from "../../item/IDesignItem.js";
export interface IConfigUiService {
hasConfigUi(designItem: IDesignItem): Promise
getConfigUi(designItem: IDesignItem): Promise