@elementor/editor-canvas 4.3.0-1051 → 4.3.0-1052

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.mts CHANGED
@@ -239,7 +239,7 @@ type CreateTemplatedElementTypeOptions = {
239
239
  renderer: DomRenderer;
240
240
  element: TemplatedElementConfig;
241
241
  };
242
- type TemplatedElementConfig = Required<Pick<V1ElementConfig, 'twig_templates' | 'twig_main_template' | 'atomic_props_schema' | 'base_styles_dictionary'>>;
242
+ type TemplatedElementConfig = Required<Pick<V1ElementConfig, 'twig_templates' | 'twig_main_template' | 'atomic_props_schema' | 'base_styles_dictionary'>> & Pick<V1ElementConfig, 'default_html_tag' | 'html_tag_follows_link'>;
243
243
  declare function createTemplatedElementView({ type, renderer, element, }: CreateTemplatedElementTypeOptions): typeof TemplatedElementView;
244
244
 
245
245
  type NestedTemplatedElementConfig = TemplatedElementConfig & {
package/dist/index.d.ts CHANGED
@@ -239,7 +239,7 @@ type CreateTemplatedElementTypeOptions = {
239
239
  renderer: DomRenderer;
240
240
  element: TemplatedElementConfig;
241
241
  };
242
- type TemplatedElementConfig = Required<Pick<V1ElementConfig, 'twig_templates' | 'twig_main_template' | 'atomic_props_schema' | 'base_styles_dictionary'>>;
242
+ type TemplatedElementConfig = Required<Pick<V1ElementConfig, 'twig_templates' | 'twig_main_template' | 'atomic_props_schema' | 'base_styles_dictionary'>> & Pick<V1ElementConfig, 'default_html_tag' | 'html_tag_follows_link'>;
243
243
  declare function createTemplatedElementView({ type, renderer, element, }: CreateTemplatedElementTypeOptions): typeof TemplatedElementView;
244
244
 
245
245
  type NestedTemplatedElementConfig = TemplatedElementConfig & {
package/dist/index.js CHANGED
@@ -2713,6 +2713,52 @@ function createElementViewClassDeclaration() {
2713
2713
  // src/legacy/create-nested-templated-element-type.ts
2714
2714
  var import_editor_elements6 = require("@elementor/editor-elements");
2715
2715
 
2716
+ // src/renderers/compute-html-tag.ts
2717
+ var DEFAULT_LINK_TAG = "a";
2718
+ function computeHtmlTag(settings, defaultTag, options = {}) {
2719
+ const followLink = options.followLink ?? true;
2720
+ if (followLink && settingsHaveActiveLink(settings)) {
2721
+ const link = settings.link;
2722
+ return extractLinkHtmlTag(isRecord(link) ? link : {});
2723
+ }
2724
+ const settingsTag = extractHtmlTagValue(settings.tag);
2725
+ if (null !== settingsTag && "" !== settingsTag) {
2726
+ return settingsTag;
2727
+ }
2728
+ return defaultTag;
2729
+ }
2730
+ function settingsHaveActiveLink(settings) {
2731
+ const link = settings.link;
2732
+ if (!isRecord(link)) {
2733
+ return false;
2734
+ }
2735
+ const href = extractHtmlTagValue(link.href);
2736
+ if (null !== href && "" !== href) {
2737
+ return true;
2738
+ }
2739
+ const attributes = link.attributes;
2740
+ return typeof attributes === "string" && "" !== attributes;
2741
+ }
2742
+ function extractLinkHtmlTag(link) {
2743
+ const tag = extractHtmlTagValue(link.tag);
2744
+ if (null !== tag && "" !== tag) {
2745
+ return tag;
2746
+ }
2747
+ return DEFAULT_LINK_TAG;
2748
+ }
2749
+ function extractHtmlTagValue(value) {
2750
+ if (isRecord(value) && typeof value.value === "string") {
2751
+ return value.value;
2752
+ }
2753
+ if (typeof value === "string") {
2754
+ return value;
2755
+ }
2756
+ return null;
2757
+ }
2758
+ function isRecord(value) {
2759
+ return typeof value === "object" && null !== value && !Array.isArray(value);
2760
+ }
2761
+
2716
2762
  // src/legacy/create-pending-element.ts
2717
2763
  var import_editor_elements5 = require("@elementor/editor-elements");
2718
2764
  function createPendingElement(wrapperView, data, options = {}) {
@@ -2773,7 +2819,13 @@ function setupTwigRenderer({ renderer, element }) {
2773
2819
  transformers: settingsTransformersRegistry,
2774
2820
  schema: element.atomic_props_schema
2775
2821
  });
2776
- return { templateKey, baseStylesDictionary, resolveProps };
2822
+ return {
2823
+ templateKey,
2824
+ baseStylesDictionary,
2825
+ resolveProps,
2826
+ defaultHtmlTag: element.default_html_tag ?? "div",
2827
+ htmlTagFollowsLink: element.html_tag_follows_link ?? true
2828
+ };
2777
2829
  }
2778
2830
  function createBeforeRender(view) {
2779
2831
  view._ensureViewIsIntact();
@@ -2813,7 +2865,7 @@ function createTemplatedElementView({
2813
2865
  element
2814
2866
  }) {
2815
2867
  const BaseView = createElementViewClassDeclaration();
2816
- const { templateKey, baseStylesDictionary, resolveProps } = setupTwigRenderer({
2868
+ const { templateKey, baseStylesDictionary, resolveProps, defaultHtmlTag, htmlTagFollowsLink } = setupTwigRenderer({
2817
2869
  type,
2818
2870
  renderer,
2819
2871
  element
@@ -2883,6 +2935,7 @@ function createTemplatedElementView({
2883
2935
  interaction_id: this.getInteractionId(),
2884
2936
  type,
2885
2937
  settings,
2938
+ tag: computeHtmlTag(settings, defaultHtmlTag, { followLink: htmlTagFollowsLink }),
2886
2939
  base_styles: baseStylesDictionary,
2887
2940
  ...this.getResolverRenderContext?.() ?? {}
2888
2941
  };
@@ -2972,7 +3025,7 @@ function createNestedTemplatedElementView({
2972
3025
  element
2973
3026
  }) {
2974
3027
  const legacyWindow = window;
2975
- const { templateKey, baseStylesDictionary, resolveProps } = setupTwigRenderer({
3028
+ const { templateKey, baseStylesDictionary, resolveProps, defaultHtmlTag, htmlTagFollowsLink } = setupTwigRenderer({
2976
3029
  type,
2977
3030
  renderer,
2978
3031
  element
@@ -3047,6 +3100,7 @@ function createNestedTemplatedElementView({
3047
3100
  interaction_id: this.getInteractionId(),
3048
3101
  type,
3049
3102
  settings: resolvedSettings,
3103
+ tag: computeHtmlTag(resolvedSettings, defaultHtmlTag, { followLink: htmlTagFollowsLink }),
3050
3104
  base_styles: baseStylesDictionary,
3051
3105
  editor_attributes: buildEditorAttributes(model),
3052
3106
  editor_classes: buildEditorClasses(model),
package/dist/index.mjs CHANGED
@@ -2665,6 +2665,52 @@ function createElementViewClassDeclaration() {
2665
2665
  // src/legacy/create-nested-templated-element-type.ts
2666
2666
  import { ELEMENT_STYLE_CHANGE_EVENT as ELEMENT_STYLE_CHANGE_EVENT2 } from "@elementor/editor-elements";
2667
2667
 
2668
+ // src/renderers/compute-html-tag.ts
2669
+ var DEFAULT_LINK_TAG = "a";
2670
+ function computeHtmlTag(settings, defaultTag, options = {}) {
2671
+ const followLink = options.followLink ?? true;
2672
+ if (followLink && settingsHaveActiveLink(settings)) {
2673
+ const link = settings.link;
2674
+ return extractLinkHtmlTag(isRecord(link) ? link : {});
2675
+ }
2676
+ const settingsTag = extractHtmlTagValue(settings.tag);
2677
+ if (null !== settingsTag && "" !== settingsTag) {
2678
+ return settingsTag;
2679
+ }
2680
+ return defaultTag;
2681
+ }
2682
+ function settingsHaveActiveLink(settings) {
2683
+ const link = settings.link;
2684
+ if (!isRecord(link)) {
2685
+ return false;
2686
+ }
2687
+ const href = extractHtmlTagValue(link.href);
2688
+ if (null !== href && "" !== href) {
2689
+ return true;
2690
+ }
2691
+ const attributes = link.attributes;
2692
+ return typeof attributes === "string" && "" !== attributes;
2693
+ }
2694
+ function extractLinkHtmlTag(link) {
2695
+ const tag = extractHtmlTagValue(link.tag);
2696
+ if (null !== tag && "" !== tag) {
2697
+ return tag;
2698
+ }
2699
+ return DEFAULT_LINK_TAG;
2700
+ }
2701
+ function extractHtmlTagValue(value) {
2702
+ if (isRecord(value) && typeof value.value === "string") {
2703
+ return value.value;
2704
+ }
2705
+ if (typeof value === "string") {
2706
+ return value;
2707
+ }
2708
+ return null;
2709
+ }
2710
+ function isRecord(value) {
2711
+ return typeof value === "object" && null !== value && !Array.isArray(value);
2712
+ }
2713
+
2668
2714
  // src/legacy/create-pending-element.ts
2669
2715
  import {
2670
2716
  addModelToParent,
@@ -2730,7 +2776,13 @@ function setupTwigRenderer({ renderer, element }) {
2730
2776
  transformers: settingsTransformersRegistry,
2731
2777
  schema: element.atomic_props_schema
2732
2778
  });
2733
- return { templateKey, baseStylesDictionary, resolveProps };
2779
+ return {
2780
+ templateKey,
2781
+ baseStylesDictionary,
2782
+ resolveProps,
2783
+ defaultHtmlTag: element.default_html_tag ?? "div",
2784
+ htmlTagFollowsLink: element.html_tag_follows_link ?? true
2785
+ };
2734
2786
  }
2735
2787
  function createBeforeRender(view) {
2736
2788
  view._ensureViewIsIntact();
@@ -2770,7 +2822,7 @@ function createTemplatedElementView({
2770
2822
  element
2771
2823
  }) {
2772
2824
  const BaseView = createElementViewClassDeclaration();
2773
- const { templateKey, baseStylesDictionary, resolveProps } = setupTwigRenderer({
2825
+ const { templateKey, baseStylesDictionary, resolveProps, defaultHtmlTag, htmlTagFollowsLink } = setupTwigRenderer({
2774
2826
  type,
2775
2827
  renderer,
2776
2828
  element
@@ -2840,6 +2892,7 @@ function createTemplatedElementView({
2840
2892
  interaction_id: this.getInteractionId(),
2841
2893
  type,
2842
2894
  settings,
2895
+ tag: computeHtmlTag(settings, defaultHtmlTag, { followLink: htmlTagFollowsLink }),
2843
2896
  base_styles: baseStylesDictionary,
2844
2897
  ...this.getResolverRenderContext?.() ?? {}
2845
2898
  };
@@ -2929,7 +2982,7 @@ function createNestedTemplatedElementView({
2929
2982
  element
2930
2983
  }) {
2931
2984
  const legacyWindow = window;
2932
- const { templateKey, baseStylesDictionary, resolveProps } = setupTwigRenderer({
2985
+ const { templateKey, baseStylesDictionary, resolveProps, defaultHtmlTag, htmlTagFollowsLink } = setupTwigRenderer({
2933
2986
  type,
2934
2987
  renderer,
2935
2988
  element
@@ -3004,6 +3057,7 @@ function createNestedTemplatedElementView({
3004
3057
  interaction_id: this.getInteractionId(),
3005
3058
  type,
3006
3059
  settings: resolvedSettings,
3060
+ tag: computeHtmlTag(resolvedSettings, defaultHtmlTag, { followLink: htmlTagFollowsLink }),
3007
3061
  base_styles: baseStylesDictionary,
3008
3062
  editor_attributes: buildEditorAttributes(model),
3009
3063
  editor_classes: buildEditorClasses(model),
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@elementor/editor-canvas",
3
3
  "description": "Elementor Editor Canvas",
4
- "version": "4.3.0-1051",
4
+ "version": "4.3.0-1052",
5
5
  "private": false,
6
6
  "author": "Elementor Team",
7
7
  "homepage": "https://elementor.com/",
@@ -37,26 +37,26 @@
37
37
  "react-dom": "^18.3.1"
38
38
  },
39
39
  "dependencies": {
40
- "@elementor/editor": "4.3.0-1051",
41
- "@elementor/editor-controls": "4.3.0-1051",
42
- "@elementor/editor-documents": "4.3.0-1051",
43
- "@elementor/editor-elements": "4.3.0-1051",
44
- "@elementor/editor-interactions": "4.3.0-1051",
45
- "@elementor/editor-mcp": "4.3.0-1051",
46
- "@elementor/editor-notifications": "4.3.0-1051",
47
- "@elementor/editor-props": "4.3.0-1051",
48
- "@elementor/editor-responsive": "4.3.0-1051",
49
- "@elementor/editor-styles": "4.3.0-1051",
50
- "@elementor/editor-styles-repository": "4.3.0-1051",
51
- "@elementor/editor-ui": "4.3.0-1051",
52
- "@elementor/editor-v1-adapters": "4.3.0-1051",
53
- "@elementor/events": "4.3.0-1051",
54
- "@elementor/http-client": "4.3.0-1051",
55
- "@elementor/schema": "4.3.0-1051",
56
- "@elementor/twing": "4.3.0-1051",
40
+ "@elementor/editor": "4.3.0-1052",
41
+ "@elementor/editor-controls": "4.3.0-1052",
42
+ "@elementor/editor-documents": "4.3.0-1052",
43
+ "@elementor/editor-elements": "4.3.0-1052",
44
+ "@elementor/editor-interactions": "4.3.0-1052",
45
+ "@elementor/editor-mcp": "4.3.0-1052",
46
+ "@elementor/editor-notifications": "4.3.0-1052",
47
+ "@elementor/editor-props": "4.3.0-1052",
48
+ "@elementor/editor-responsive": "4.3.0-1052",
49
+ "@elementor/editor-styles": "4.3.0-1052",
50
+ "@elementor/editor-styles-repository": "4.3.0-1052",
51
+ "@elementor/editor-ui": "4.3.0-1052",
52
+ "@elementor/editor-v1-adapters": "4.3.0-1052",
53
+ "@elementor/events": "4.3.0-1052",
54
+ "@elementor/http-client": "4.3.0-1052",
55
+ "@elementor/schema": "4.3.0-1052",
56
+ "@elementor/twing": "4.3.0-1052",
57
57
  "@elementor/ui": "1.37.5",
58
- "@elementor/utils": "4.3.0-1051",
59
- "@elementor/wp-media": "4.3.0-1051",
58
+ "@elementor/utils": "4.3.0-1052",
59
+ "@elementor/wp-media": "4.3.0-1052",
60
60
  "@floating-ui/react": "^0.27.5",
61
61
  "@wordpress/i18n": "^5.13.0",
62
62
  "dompurify": "^3.2.6"
@@ -5,6 +5,7 @@ import {
5
5
  createTemplatedElementType,
6
6
  createTemplatedElementView,
7
7
  } from '../create-templated-element-type';
8
+ import { type ElementView } from '../types';
8
9
 
9
10
  const MOCK_ELEMENT_TYPE = 'test-element';
10
11
  const MOCK_HTML = '<div>Element</div>';
@@ -19,6 +20,8 @@ const createMockElementConfig = () => ( {
19
20
  twig_main_template: 'main',
20
21
  atomic_props_schema: {},
21
22
  base_styles_dictionary: {},
23
+ default_html_tag: 'div',
24
+ html_tag_follows_link: true,
22
25
  } );
23
26
 
24
27
  describe( 'createTemplatedElementType', () => {
@@ -116,6 +119,53 @@ describe( 'createTemplatedElementView', () => {
116
119
  expect( utils.register ).toHaveBeenCalledWith( 'template2', '<div>Template 2</div>' );
117
120
  } );
118
121
  } );
122
+
123
+ describe( 'render context', () => {
124
+ it( 'should inject computed tag into the twig render context', async () => {
125
+ // Arrange
126
+ const utils = createMockRenderer();
127
+ const ViewClass = createTemplatedElementView( {
128
+ type: MOCK_ELEMENT_TYPE,
129
+ renderer: utils,
130
+ element: {
131
+ ...createMockElementConfig(),
132
+ twig_templates: {
133
+ main: '<{{ tag | e("html_tag") }}></{{ tag | e("html_tag") }}>',
134
+ },
135
+ },
136
+ } );
137
+
138
+ const view = new ViewClass() as unknown as ElementView & {
139
+ _abortController: AbortController;
140
+ };
141
+ view.model = {
142
+ get: ( key: string ) => {
143
+ if ( key === 'id' ) {
144
+ return 'test-id';
145
+ }
146
+
147
+ if ( key === 'settings' ) {
148
+ return { toJSON: () => ( {} ) };
149
+ }
150
+
151
+ return undefined;
152
+ },
153
+ } as unknown as ElementView[ 'model' ];
154
+ view.isRendered = false;
155
+ view._abortController = new AbortController();
156
+ view.triggerMethod = jest.fn();
157
+ view.bindUIElements = jest.fn();
158
+ view.$el = { html: jest.fn() } as unknown as ElementView[ '$el' ];
159
+ view._ensureViewIsIntact = jest.fn();
160
+ view.resetChildViewContainer = jest.fn();
161
+
162
+ // Act
163
+ await view._renderTemplate();
164
+
165
+ // Assert
166
+ expect( utils.render ).toHaveBeenCalledWith( 'main', expect.objectContaining( { tag: 'div' } ) );
167
+ } );
168
+ } );
119
169
  } );
120
170
 
121
171
  describe( 'canBeTemplated', () => {
@@ -1,5 +1,6 @@
1
1
  import { ELEMENT_STYLE_CHANGE_EVENT, type V1ElementModelProps } from '@elementor/editor-elements';
2
2
 
3
+ import { computeHtmlTag } from '../renderers/compute-html-tag';
3
4
  import { type DomRenderer } from '../renderers/create-dom-renderer';
4
5
  import { signalizedProcess } from '../utils/signalized-process';
5
6
  import { createPendingElement } from './create-pending-element';
@@ -93,7 +94,7 @@ export function createNestedTemplatedElementView( {
93
94
  }: CreateNestedTemplatedElementViewOptions ): NestedTemplatedElementViewClass {
94
95
  const legacyWindow = window as unknown as LegacyWindow;
95
96
 
96
- const { templateKey, baseStylesDictionary, resolveProps } = setupTwigRenderer( {
97
+ const { templateKey, baseStylesDictionary, resolveProps, defaultHtmlTag, htmlTagFollowsLink } = setupTwigRenderer( {
97
98
  type,
98
99
  renderer,
99
100
  element,
@@ -198,6 +199,7 @@ export function createNestedTemplatedElementView( {
198
199
  interaction_id: this.getInteractionId(),
199
200
  type,
200
201
  settings: resolvedSettings,
202
+ tag: computeHtmlTag( resolvedSettings, defaultHtmlTag, { followLink: htmlTagFollowsLink } ),
201
203
  base_styles: baseStylesDictionary,
202
204
  editor_attributes: buildEditorAttributes( model ),
203
205
  editor_classes: buildEditorClasses( model ),
@@ -1,5 +1,6 @@
1
1
  import { type V1ElementConfig } from '@elementor/editor-elements';
2
2
 
3
+ import { computeHtmlTag } from '../renderers/compute-html-tag';
3
4
  import { type DomRenderer } from '../renderers/create-dom-renderer';
4
5
  import { signalizedProcess } from '../utils/signalized-process';
5
6
  import { createElementViewClassDeclaration } from './create-element-type';
@@ -27,7 +28,8 @@ export type CreateTemplatedElementTypeOptions = {
27
28
 
28
29
  export type TemplatedElementConfig = Required<
29
30
  Pick< V1ElementConfig, 'twig_templates' | 'twig_main_template' | 'atomic_props_schema' | 'base_styles_dictionary' >
30
- >;
31
+ > &
32
+ Pick< V1ElementConfig, 'default_html_tag' | 'html_tag_follows_link' >;
31
33
 
32
34
  export function createTemplatedElementType( {
33
35
  type,
@@ -69,7 +71,7 @@ export function createTemplatedElementView( {
69
71
  }: CreateTemplatedElementTypeOptions ): typeof TemplatedElementView {
70
72
  const BaseView = createElementViewClassDeclaration();
71
73
 
72
- const { templateKey, baseStylesDictionary, resolveProps } = setupTwigRenderer( {
74
+ const { templateKey, baseStylesDictionary, resolveProps, defaultHtmlTag, htmlTagFollowsLink } = setupTwigRenderer( {
73
75
  type,
74
76
  renderer,
75
77
  element,
@@ -165,6 +167,7 @@ export function createTemplatedElementView( {
165
167
  interaction_id: this.getInteractionId(),
166
168
  type,
167
169
  settings,
170
+ tag: computeHtmlTag( settings, defaultHtmlTag, { followLink: htmlTagFollowsLink } ),
168
171
  base_styles: baseStylesDictionary,
169
172
  ...( this.getResolverRenderContext?.() ?? {} ),
170
173
  };
@@ -7,7 +7,8 @@ import { type ElementView, type RenderContext } from './types';
7
7
 
8
8
  export type TwigElementConfig = Required<
9
9
  Pick< V1ElementConfig, 'twig_templates' | 'twig_main_template' | 'atomic_props_schema' | 'base_styles_dictionary' >
10
- >;
10
+ > &
11
+ Pick< V1ElementConfig, 'default_html_tag' | 'html_tag_follows_link' >;
11
12
 
12
13
  export type SetupTwigRendererOptions = {
13
14
  type: string;
@@ -19,6 +20,8 @@ export type SetupTwigRendererResult = {
19
20
  templateKey: string;
20
21
  baseStylesDictionary: Record< string, unknown >;
21
22
  resolveProps: PropsResolver;
23
+ defaultHtmlTag: string;
24
+ htmlTagFollowsLink: boolean;
22
25
  };
23
26
 
24
27
  export function setupTwigRenderer( { renderer, element }: SetupTwigRendererOptions ): SetupTwigRendererResult {
@@ -34,7 +37,13 @@ export function setupTwigRenderer( { renderer, element }: SetupTwigRendererOptio
34
37
  schema: element.atomic_props_schema,
35
38
  } );
36
39
 
37
- return { templateKey, baseStylesDictionary, resolveProps };
40
+ return {
41
+ templateKey,
42
+ baseStylesDictionary,
43
+ resolveProps,
44
+ defaultHtmlTag: element.default_html_tag ?? 'div',
45
+ htmlTagFollowsLink: element.html_tag_follows_link ?? true,
46
+ };
38
47
  }
39
48
 
40
49
  export interface TwigViewInterface extends Omit< ElementView, 'getResolverRenderContext' > {
@@ -0,0 +1,55 @@
1
+ import { readFileSync } from 'fs';
2
+ import { join } from 'path';
3
+
4
+ import { computeHtmlTag } from '../compute-html-tag';
5
+
6
+ type HtmlTagComputerCase = {
7
+ settings: Record< string, unknown >;
8
+ default: string;
9
+ followLink: boolean;
10
+ expected: string;
11
+ };
12
+
13
+ const contractCases: HtmlTagComputerCase[] = JSON.parse(
14
+ readFileSync( join( __dirname, 'fixtures/html-tag-computer-cases.json' ), 'utf8' )
15
+ );
16
+
17
+ describe( 'computeHtmlTag', () => {
18
+ it.each( contractCases )(
19
+ 'contract case: default=$default followLink=$followLink expected=$expected',
20
+ ( { settings, default: defaultTag, followLink, expected } ) => {
21
+ expect( computeHtmlTag( settings, defaultTag, { followLink } ) ).toBe( expected );
22
+ }
23
+ );
24
+
25
+ it( 'uses link tag when href is present', () => {
26
+ expect(
27
+ computeHtmlTag(
28
+ {
29
+ tag: 'div',
30
+ link: {
31
+ href: 'https://example.com',
32
+ tag: 'a',
33
+ },
34
+ },
35
+ 'div'
36
+ )
37
+ ).toBe( 'a' );
38
+ } );
39
+
40
+ it( 'ignores link when followLink is false', () => {
41
+ expect(
42
+ computeHtmlTag(
43
+ {
44
+ tag: 'h4',
45
+ link: {
46
+ href: 'https://example.com',
47
+ tag: 'a',
48
+ },
49
+ },
50
+ 'h2',
51
+ { followLink: false }
52
+ )
53
+ ).toBe( 'h4' );
54
+ } );
55
+ } );
@@ -0,0 +1,87 @@
1
+ [
2
+ {
3
+ "settings": {},
4
+ "default": "div",
5
+ "followLink": true,
6
+ "expected": "div"
7
+ },
8
+ {
9
+ "settings": { "tag": "header" },
10
+ "default": "div",
11
+ "followLink": true,
12
+ "expected": "header"
13
+ },
14
+ {
15
+ "settings": { "tag": "" },
16
+ "default": "div",
17
+ "followLink": true,
18
+ "expected": "div"
19
+ },
20
+ {
21
+ "settings": {
22
+ "link": {
23
+ "href": "https://example.com",
24
+ "tag": "a"
25
+ }
26
+ },
27
+ "default": "div",
28
+ "followLink": true,
29
+ "expected": "a"
30
+ },
31
+ {
32
+ "settings": {
33
+ "link": {
34
+ "tag": "a"
35
+ }
36
+ },
37
+ "default": "button",
38
+ "followLink": true,
39
+ "expected": "button"
40
+ },
41
+ {
42
+ "settings": {
43
+ "link": {
44
+ "tag": "a",
45
+ "attributes": "href=\"https://example.com\""
46
+ }
47
+ },
48
+ "default": "div",
49
+ "followLink": true,
50
+ "expected": "a"
51
+ },
52
+ {
53
+ "settings": {
54
+ "tag": "div",
55
+ "link": {
56
+ "href": "https://example.com",
57
+ "tag": "a"
58
+ }
59
+ },
60
+ "default": "div",
61
+ "followLink": true,
62
+ "expected": "a"
63
+ },
64
+ {
65
+ "settings": {
66
+ "tag": "div",
67
+ "link": {
68
+ "href": "https://example.com",
69
+ "tag": "a"
70
+ }
71
+ },
72
+ "default": "div",
73
+ "followLink": false,
74
+ "expected": "div"
75
+ },
76
+ {
77
+ "settings": {
78
+ "tag": {
79
+ "$$type": "string",
80
+ "value": "h3"
81
+ }
82
+ },
83
+ "default": "h2",
84
+ "followLink": true,
85
+ "expected": "h3"
86
+ }
87
+ ]
@@ -0,0 +1,73 @@
1
+ export type ComputeHtmlTagOptions = {
2
+ followLink?: boolean;
3
+ };
4
+
5
+ export const DEFAULT_LINK_TAG = 'a';
6
+
7
+ type LinkSettings = Record< string, unknown >;
8
+
9
+ export function computeHtmlTag(
10
+ settings: Record< string, unknown >,
11
+ defaultTag: string,
12
+ options: ComputeHtmlTagOptions = {}
13
+ ): string {
14
+ const followLink = options.followLink ?? true;
15
+
16
+ if ( followLink && settingsHaveActiveLink( settings ) ) {
17
+ const link = settings.link;
18
+
19
+ return extractLinkHtmlTag( isRecord( link ) ? link : {} );
20
+ }
21
+
22
+ const settingsTag = extractHtmlTagValue( settings.tag );
23
+
24
+ if ( null !== settingsTag && '' !== settingsTag ) {
25
+ return settingsTag;
26
+ }
27
+
28
+ return defaultTag;
29
+ }
30
+
31
+ function settingsHaveActiveLink( settings: Record< string, unknown > ): boolean {
32
+ const link = settings.link;
33
+
34
+ if ( ! isRecord( link ) ) {
35
+ return false;
36
+ }
37
+
38
+ const href = extractHtmlTagValue( link.href );
39
+
40
+ if ( null !== href && '' !== href ) {
41
+ return true;
42
+ }
43
+
44
+ const attributes = link.attributes;
45
+
46
+ return typeof attributes === 'string' && '' !== attributes;
47
+ }
48
+
49
+ function extractLinkHtmlTag( link: LinkSettings ): string {
50
+ const tag = extractHtmlTagValue( link.tag );
51
+
52
+ if ( null !== tag && '' !== tag ) {
53
+ return tag;
54
+ }
55
+
56
+ return DEFAULT_LINK_TAG;
57
+ }
58
+
59
+ function extractHtmlTagValue( value: unknown ): string | null {
60
+ if ( isRecord( value ) && typeof value.value === 'string' ) {
61
+ return value.value;
62
+ }
63
+
64
+ if ( typeof value === 'string' ) {
65
+ return value;
66
+ }
67
+
68
+ return null;
69
+ }
70
+
71
+ function isRecord( value: unknown ): value is Record< string, unknown > {
72
+ return typeof value === 'object' && null !== value && ! Array.isArray( value );
73
+ }