@rulecms/source-components-angular 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +9 -0
- package/README.md +117 -0
- package/THIRD_PARTY_NOTICES +50 -0
- package/fesm2022/rulecms-source-components-angular-icon-registry.mjs +144 -0
- package/fesm2022/rulecms-source-components-angular-icon-registry.mjs.map +1 -0
- package/fesm2022/rulecms-source-components-angular.mjs +3979 -0
- package/fesm2022/rulecms-source-components-angular.mjs.map +1 -0
- package/icon-registry/icon-aliases.d.ts +10 -0
- package/icon-registry/icon-aliases.d.ts.map +1 -0
- package/icon-registry/index.d.ts +6 -0
- package/icon-registry/index.d.ts.map +1 -0
- package/icon-registry/resolve-icon-name.d.ts +6 -0
- package/icon-registry/resolve-icon-name.d.ts.map +1 -0
- package/icon-registry/resolve-icon-node.d.ts +25 -0
- package/icon-registry/resolve-icon-node.d.ts.map +1 -0
- package/icon-registry/rulecms-source-components-angular-icon-registry.d.ts.map +1 -0
- package/icon-registry/search-icon-names.d.ts +11 -0
- package/icon-registry/search-icon-names.d.ts.map +1 -0
- package/index.d.ts +6 -0
- package/lib/base-components/display/card-entries/card-entries.d.ts +3 -0
- package/lib/base-components/display/card-entries/card-entries.d.ts.map +1 -0
- package/lib/base-components/display/card-groups.d.ts +3 -0
- package/lib/base-components/display/card-groups.d.ts.map +1 -0
- package/lib/base-components/display/component-attributes/attribute-list-for-component-types.d.ts +20 -0
- package/lib/base-components/display/component-attributes/attribute-list-for-component-types.d.ts.map +1 -0
- package/lib/base-components/display/component-attributes/component-name-to-modify-attribute.d.ts +12 -0
- package/lib/base-components/display/component-attributes/component-name-to-modify-attribute.d.ts.map +1 -0
- package/lib/base-components/display/components/cloudinary-advanced-image/attribute-edit-mode-configs.d.ts +3 -0
- package/lib/base-components/display/components/cloudinary-advanced-image/attribute-edit-mode-configs.d.ts.map +1 -0
- package/lib/base-components/display/components/cloudinary-advanced-image/build-cloudinary-image-urls.d.ts +7 -0
- package/lib/base-components/display/components/cloudinary-advanced-image/build-cloudinary-image-urls.d.ts.map +1 -0
- package/lib/base-components/display/components/cloudinary-advanced-image/cloudinary-advanced-image.component.d.ts +28 -0
- package/lib/base-components/display/components/cloudinary-advanced-image/cloudinary-advanced-image.component.d.ts.map +1 -0
- package/lib/base-components/display/components/cloudinary-advanced-image/default-attribute-values.d.ts +3 -0
- package/lib/base-components/display/components/cloudinary-advanced-image/default-attribute-values.d.ts.map +1 -0
- package/lib/base-components/display/components/cloudinary-advanced-image/display-card-entry.d.ts +3 -0
- package/lib/base-components/display/components/cloudinary-advanced-image/display-card-entry.d.ts.map +1 -0
- package/lib/base-components/display/components/cloudinary-advanced-image/resolve-no-container.d.ts +8 -0
- package/lib/base-components/display/components/cloudinary-advanced-image/resolve-no-container.d.ts.map +1 -0
- package/lib/base-components/display/components/cloudinary-advanced-image/resolve-renders-own-element.d.ts +6 -0
- package/lib/base-components/display/components/cloudinary-advanced-image/resolve-renders-own-element.d.ts.map +1 -0
- package/lib/base-components/display/components/r-accordion/accordion-attributes.d.ts +109 -0
- package/lib/base-components/display/components/r-accordion/accordion-attributes.d.ts.map +1 -0
- package/lib/base-components/display/components/r-accordion/attribute-edit-mode-configs.d.ts +16 -0
- package/lib/base-components/display/components/r-accordion/attribute-edit-mode-configs.d.ts.map +1 -0
- package/lib/base-components/display/components/r-accordion/default-attribute-values.d.ts +3 -0
- package/lib/base-components/display/components/r-accordion/default-attribute-values.d.ts.map +1 -0
- package/lib/base-components/display/components/r-accordion/display-card-entry.d.ts +3 -0
- package/lib/base-components/display/components/r-accordion/display-card-entry.d.ts.map +1 -0
- package/lib/base-components/display/components/r-accordion/r-accordion.component.d.ts +27 -0
- package/lib/base-components/display/components/r-accordion/r-accordion.component.d.ts.map +1 -0
- package/lib/base-components/display/components/r-accordion/resolve-no-container.d.ts +8 -0
- package/lib/base-components/display/components/r-accordion/resolve-no-container.d.ts.map +1 -0
- package/lib/base-components/display/components/r-accordion/resolve-renders-own-element.d.ts +6 -0
- package/lib/base-components/display/components/r-accordion/resolve-renders-own-element.d.ts.map +1 -0
- package/lib/base-components/display/components/r-button/attribute-edit-mode-configs.d.ts +12 -0
- package/lib/base-components/display/components/r-button/attribute-edit-mode-configs.d.ts.map +1 -0
- package/lib/base-components/display/components/r-button/button-attributes.d.ts +51 -0
- package/lib/base-components/display/components/r-button/button-attributes.d.ts.map +1 -0
- package/lib/base-components/display/components/r-button/button-css-variables.d.ts +28 -0
- package/lib/base-components/display/components/r-button/button-css-variables.d.ts.map +1 -0
- package/lib/base-components/display/components/r-button/button-instance-props.d.ts +23 -0
- package/lib/base-components/display/components/r-button/button-instance-props.d.ts.map +1 -0
- package/lib/base-components/display/components/r-button/button-preset-styles.d.ts +13 -0
- package/lib/base-components/display/components/r-button/button-preset-styles.d.ts.map +1 -0
- package/lib/base-components/display/components/r-button/button-presets.d.ts +76 -0
- package/lib/base-components/display/components/r-button/button-presets.d.ts.map +1 -0
- package/lib/base-components/display/components/r-button/default-attribute-values.d.ts +14 -0
- package/lib/base-components/display/components/r-button/default-attribute-values.d.ts.map +1 -0
- package/lib/base-components/display/components/r-button/display-card-entry.d.ts +3 -0
- package/lib/base-components/display/components/r-button/display-card-entry.d.ts.map +1 -0
- package/lib/base-components/display/components/r-button/r-button.component.d.ts +26 -0
- package/lib/base-components/display/components/r-button/r-button.component.d.ts.map +1 -0
- package/lib/base-components/display/components/r-button/resolve-button-preset.d.ts +11 -0
- package/lib/base-components/display/components/r-button/resolve-button-preset.d.ts.map +1 -0
- package/lib/base-components/display/components/r-button/resolve-no-container.d.ts +8 -0
- package/lib/base-components/display/components/r-button/resolve-no-container.d.ts.map +1 -0
- package/lib/base-components/display/components/r-button/resolve-renders-own-element.d.ts +6 -0
- package/lib/base-components/display/components/r-button/resolve-renders-own-element.d.ts.map +1 -0
- package/lib/base-components/display/components/r-divider/attribute-edit-mode-configs.d.ts +10 -0
- package/lib/base-components/display/components/r-divider/attribute-edit-mode-configs.d.ts.map +1 -0
- package/lib/base-components/display/components/r-divider/default-attribute-values.d.ts +11 -0
- package/lib/base-components/display/components/r-divider/default-attribute-values.d.ts.map +1 -0
- package/lib/base-components/display/components/r-divider/display-card-entry.d.ts +3 -0
- package/lib/base-components/display/components/r-divider/display-card-entry.d.ts.map +1 -0
- package/lib/base-components/display/components/r-divider/divider-attributes.d.ts +41 -0
- package/lib/base-components/display/components/r-divider/divider-attributes.d.ts.map +1 -0
- package/lib/base-components/display/components/r-divider/divider-styles.d.ts +14 -0
- package/lib/base-components/display/components/r-divider/divider-styles.d.ts.map +1 -0
- package/lib/base-components/display/components/r-divider/r-divider.component.d.ts +16 -0
- package/lib/base-components/display/components/r-divider/r-divider.component.d.ts.map +1 -0
- package/lib/base-components/display/components/r-divider/resolve-no-container.d.ts +7 -0
- package/lib/base-components/display/components/r-divider/resolve-no-container.d.ts.map +1 -0
- package/lib/base-components/display/components/r-divider/resolve-renders-own-element.d.ts +6 -0
- package/lib/base-components/display/components/r-divider/resolve-renders-own-element.d.ts.map +1 -0
- package/lib/base-components/display/components/r-embed/attribute-edit-mode-configs.d.ts +8 -0
- package/lib/base-components/display/components/r-embed/attribute-edit-mode-configs.d.ts.map +1 -0
- package/lib/base-components/display/components/r-embed/default-attribute-values.d.ts +14 -0
- package/lib/base-components/display/components/r-embed/default-attribute-values.d.ts.map +1 -0
- package/lib/base-components/display/components/r-embed/display-card-entry.d.ts +3 -0
- package/lib/base-components/display/components/r-embed/display-card-entry.d.ts.map +1 -0
- package/lib/base-components/display/components/r-embed/embed-attributes.d.ts +71 -0
- package/lib/base-components/display/components/r-embed/embed-attributes.d.ts.map +1 -0
- package/lib/base-components/display/components/r-embed/embed-providers.d.ts +78 -0
- package/lib/base-components/display/components/r-embed/embed-providers.d.ts.map +1 -0
- package/lib/base-components/display/components/r-embed/r-embed.component.d.ts +21 -0
- package/lib/base-components/display/components/r-embed/r-embed.component.d.ts.map +1 -0
- package/lib/base-components/display/components/r-embed/resolve-embed.d.ts +41 -0
- package/lib/base-components/display/components/r-embed/resolve-embed.d.ts.map +1 -0
- package/lib/base-components/display/components/r-embed/resolve-no-container.d.ts +13 -0
- package/lib/base-components/display/components/r-embed/resolve-no-container.d.ts.map +1 -0
- package/lib/base-components/display/components/r-embed/resolve-renders-own-element.d.ts +9 -0
- package/lib/base-components/display/components/r-embed/resolve-renders-own-element.d.ts.map +1 -0
- package/lib/base-components/display/components/r-icon/attribute-edit-mode-configs.d.ts +3 -0
- package/lib/base-components/display/components/r-icon/attribute-edit-mode-configs.d.ts.map +1 -0
- package/lib/base-components/display/components/r-icon/default-attribute-values.d.ts +3 -0
- package/lib/base-components/display/components/r-icon/default-attribute-values.d.ts.map +1 -0
- package/lib/base-components/display/components/r-icon/display-card-entry.d.ts +3 -0
- package/lib/base-components/display/components/r-icon/display-card-entry.d.ts.map +1 -0
- package/lib/base-components/display/components/r-icon/icon-attribute-value.d.ts +26 -0
- package/lib/base-components/display/components/r-icon/icon-attribute-value.d.ts.map +1 -0
- package/lib/base-components/display/components/r-icon/r-icon.component.d.ts +16 -0
- package/lib/base-components/display/components/r-icon/r-icon.component.d.ts.map +1 -0
- package/lib/base-components/display/components/r-icon/resolve-no-container.d.ts +7 -0
- package/lib/base-components/display/components/r-icon/resolve-no-container.d.ts.map +1 -0
- package/lib/base-components/display/components/r-icon/resolve-renders-own-element.d.ts +7 -0
- package/lib/base-components/display/components/r-icon/resolve-renders-own-element.d.ts.map +1 -0
- package/lib/base-components/display/components/r-list/attribute-edit-mode-configs.d.ts +10 -0
- package/lib/base-components/display/components/r-list/attribute-edit-mode-configs.d.ts.map +1 -0
- package/lib/base-components/display/components/r-list/default-attribute-values.d.ts +3 -0
- package/lib/base-components/display/components/r-list/default-attribute-values.d.ts.map +1 -0
- package/lib/base-components/display/components/r-list/display-card-entry.d.ts +3 -0
- package/lib/base-components/display/components/r-list/display-card-entry.d.ts.map +1 -0
- package/lib/base-components/display/components/r-list/list-attributes.d.ts +72 -0
- package/lib/base-components/display/components/r-list/list-attributes.d.ts.map +1 -0
- package/lib/base-components/display/components/r-list/r-list.component.d.ts +22 -0
- package/lib/base-components/display/components/r-list/r-list.component.d.ts.map +1 -0
- package/lib/base-components/display/components/r-list/resolve-no-container.d.ts +8 -0
- package/lib/base-components/display/components/r-list/resolve-no-container.d.ts.map +1 -0
- package/lib/base-components/display/components/r-list/resolve-renders-own-element.d.ts +7 -0
- package/lib/base-components/display/components/r-list/resolve-renders-own-element.d.ts.map +1 -0
- package/lib/base-components/display/components/r-text/attribute-edit-mode-configs.d.ts +3 -0
- package/lib/base-components/display/components/r-text/attribute-edit-mode-configs.d.ts.map +1 -0
- package/lib/base-components/display/components/r-text/default-attribute-values.d.ts +4 -0
- package/lib/base-components/display/components/r-text/default-attribute-values.d.ts.map +1 -0
- package/lib/base-components/display/components/r-text/display-card-entry.d.ts +3 -0
- package/lib/base-components/display/components/r-text/display-card-entry.d.ts.map +1 -0
- package/lib/base-components/display/components/r-text/enclosing-tags.d.ts +48 -0
- package/lib/base-components/display/components/r-text/enclosing-tags.d.ts.map +1 -0
- package/lib/base-components/display/components/r-text/r-text.component.d.ts +16 -0
- package/lib/base-components/display/components/r-text/r-text.component.d.ts.map +1 -0
- package/lib/base-components/display/components/r-text/resolve-enclosing-tag.d.ts +18 -0
- package/lib/base-components/display/components/r-text/resolve-enclosing-tag.d.ts.map +1 -0
- package/lib/base-components/display/components/r-text/resolve-no-container.d.ts +13 -0
- package/lib/base-components/display/components/r-text/resolve-no-container.d.ts.map +1 -0
- package/lib/base-components/display/components/r-text/resolve-renders-own-element.d.ts +11 -0
- package/lib/base-components/display/components/r-text/resolve-renders-own-element.d.ts.map +1 -0
- package/lib/base-components/display/components/r-video/attribute-edit-mode-configs.d.ts +3 -0
- package/lib/base-components/display/components/r-video/attribute-edit-mode-configs.d.ts.map +1 -0
- package/lib/base-components/display/components/r-video/build-video-delivery-urls.d.ts +7 -0
- package/lib/base-components/display/components/r-video/build-video-delivery-urls.d.ts.map +1 -0
- package/lib/base-components/display/components/r-video/default-attribute-values.d.ts +3 -0
- package/lib/base-components/display/components/r-video/default-attribute-values.d.ts.map +1 -0
- package/lib/base-components/display/components/r-video/display-card-entry.d.ts +3 -0
- package/lib/base-components/display/components/r-video/display-card-entry.d.ts.map +1 -0
- package/lib/base-components/display/components/r-video/r-video.component.d.ts +26 -0
- package/lib/base-components/display/components/r-video/r-video.component.d.ts.map +1 -0
- package/lib/base-components/display/components/r-video/resolve-no-container.d.ts +8 -0
- package/lib/base-components/display/components/r-video/resolve-no-container.d.ts.map +1 -0
- package/lib/base-components/display/components/r-video/resolve-renders-own-element.d.ts +6 -0
- package/lib/base-components/display/components/r-video/resolve-renders-own-element.d.ts.map +1 -0
- package/lib/base-components/editor-preview/card-entries/card-entries.d.ts +3 -0
- package/lib/base-components/editor-preview/card-entries/card-entries.d.ts.map +1 -0
- package/lib/base-components/editor-preview/card-groups.d.ts +3 -0
- package/lib/base-components/editor-preview/card-groups.d.ts.map +1 -0
- package/lib/component-identifiers.d.ts +4 -0
- package/lib/component-identifiers.d.ts.map +1 -0
- package/lib/component-library-module.d.ts +25 -0
- package/lib/component-library-module.d.ts.map +1 -0
- package/lib/get-component-metadata-for-editing.d.ts +3 -0
- package/lib/get-component-metadata-for-editing.d.ts.map +1 -0
- package/lib/get-preview-components.d.ts +12 -0
- package/lib/get-preview-components.d.ts.map +1 -0
- package/lib/get-source-components-plugin.d.ts +9 -0
- package/lib/get-source-components-plugin.d.ts.map +1 -0
- package/lib/shared/html-tag.component.d.ts +22 -0
- package/lib/shared/html-tag.component.d.ts.map +1 -0
- package/lib/shared/icons/append-icon-children.d.ts +5 -0
- package/lib/shared/icons/append-icon-children.d.ts.map +1 -0
- package/lib/shared/icons/svg-icon.component.d.ts +21 -0
- package/lib/shared/icons/svg-icon.component.d.ts.map +1 -0
- package/lib/shared/inline-markup/index.d.ts +5 -0
- package/lib/shared/inline-markup/index.d.ts.map +1 -0
- package/lib/shared/inline-markup/inline-markup.component.d.ts +16 -0
- package/lib/shared/inline-markup/inline-markup.component.d.ts.map +1 -0
- package/lib/shared/inline-markup/parse-inline-markers.d.ts +45 -0
- package/lib/shared/inline-markup/parse-inline-markers.d.ts.map +1 -0
- package/lib/shared/inline-markup/serialize-inline-markers.d.ts +14 -0
- package/lib/shared/inline-markup/serialize-inline-markers.d.ts.map +1 -0
- package/lib/shared/links/index.d.ts +8 -0
- package/lib/shared/links/index.d.ts.map +1 -0
- package/lib/shared/links/is-safe-href.d.ts +20 -0
- package/lib/shared/links/is-safe-href.d.ts.map +1 -0
- package/lib/shared/links/link-item.d.ts +39 -0
- package/lib/shared/links/link-item.d.ts.map +1 -0
- package/lib/shared/links/parse-inline-link-markers.d.ts +25 -0
- package/lib/shared/links/parse-inline-link-markers.d.ts.map +1 -0
- package/lib/shared/links/resolve-link-attributes.d.ts +13 -0
- package/lib/shared/links/resolve-link-attributes.d.ts.map +1 -0
- package/lib/types.d.ts +113 -0
- package/lib/types.d.ts.map +1 -0
- package/package.json +56 -0
- package/public-api.d.ts +30 -0
- package/public-api.d.ts.map +1 -0
- package/rulecms-source-components-angular.d.ts.map +1 -0
|
@@ -0,0 +1,3979 @@
|
|
|
1
|
+
import * as i0 from '@angular/core';
|
|
2
|
+
import { Input, Component, ViewChild } from '@angular/core';
|
|
3
|
+
import { NgStyle } from '@angular/common';
|
|
4
|
+
import * as i1 from '@angular/platform-browser';
|
|
5
|
+
|
|
6
|
+
const DEFAULT_CONTENT_TEXT = 'This is a text block. Click to edit and add your own content.';
|
|
7
|
+
const defaultAttributeValues$8 = [
|
|
8
|
+
{
|
|
9
|
+
name: 'content-text-3-resolutions',
|
|
10
|
+
defaultValues: {
|
|
11
|
+
all: DEFAULT_CONTENT_TEXT,
|
|
12
|
+
},
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
name: 'enclosing-tag-3-resolutions',
|
|
16
|
+
defaultValues: {},
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
name: 'links-3-resolutions',
|
|
20
|
+
defaultValues: {},
|
|
21
|
+
},
|
|
22
|
+
];
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* HTML tags the RText component is allowed to enclose its text content in,
|
|
26
|
+
* ordered from most common to least common.
|
|
27
|
+
*
|
|
28
|
+
* Every entry is a standard HTML element that can directly contain text
|
|
29
|
+
* without changing how that text behaves. Deliberately excluded:
|
|
30
|
+
* - Interactive / behavior-changing tags: a, button, label, summary, option,
|
|
31
|
+
* textarea, select, details, dialog
|
|
32
|
+
* - Void elements that cannot enclose text: br, wbr, hr, img, input
|
|
33
|
+
* - Tags only valid inside a specific parent: li, dt, dd, td, th, caption,
|
|
34
|
+
* legend, figcaption, rt, rp, optgroup
|
|
35
|
+
* - Raw-text / metadata tags: script, style, title, template
|
|
36
|
+
* - Live-region / form-output tags that alter assistive-technology behavior:
|
|
37
|
+
* output
|
|
38
|
+
*/
|
|
39
|
+
const ENCLOSING_TAGS = [
|
|
40
|
+
// Common block-level text containers
|
|
41
|
+
'p',
|
|
42
|
+
'div',
|
|
43
|
+
// Generic inline container
|
|
44
|
+
'span',
|
|
45
|
+
// Headings
|
|
46
|
+
'h1',
|
|
47
|
+
'h2',
|
|
48
|
+
'h3',
|
|
49
|
+
'h4',
|
|
50
|
+
'h5',
|
|
51
|
+
'h6',
|
|
52
|
+
// Common inline emphasis and importance
|
|
53
|
+
'strong',
|
|
54
|
+
'em',
|
|
55
|
+
'b',
|
|
56
|
+
'i',
|
|
57
|
+
'u',
|
|
58
|
+
// Common text presentation
|
|
59
|
+
'small',
|
|
60
|
+
'mark',
|
|
61
|
+
// Preformatted and code text
|
|
62
|
+
'pre',
|
|
63
|
+
'code',
|
|
64
|
+
// Quotations
|
|
65
|
+
'blockquote',
|
|
66
|
+
'q',
|
|
67
|
+
// Subscript and superscript
|
|
68
|
+
'sub',
|
|
69
|
+
'sup',
|
|
70
|
+
// Edits and strikethrough
|
|
71
|
+
's',
|
|
72
|
+
'del',
|
|
73
|
+
'ins',
|
|
74
|
+
// Citations, definitions and abbreviations
|
|
75
|
+
'cite',
|
|
76
|
+
'abbr',
|
|
77
|
+
'dfn',
|
|
78
|
+
// Computer I/O text
|
|
79
|
+
'kbd',
|
|
80
|
+
'samp',
|
|
81
|
+
'var',
|
|
82
|
+
// Machine-readable text
|
|
83
|
+
'time',
|
|
84
|
+
'data',
|
|
85
|
+
// Contact information
|
|
86
|
+
'address',
|
|
87
|
+
// Bidirectional text
|
|
88
|
+
'bdi',
|
|
89
|
+
'bdo',
|
|
90
|
+
// Sectioning and grouping containers
|
|
91
|
+
'figure',
|
|
92
|
+
'article',
|
|
93
|
+
'section',
|
|
94
|
+
'aside',
|
|
95
|
+
'header',
|
|
96
|
+
'footer',
|
|
97
|
+
'main',
|
|
98
|
+
'nav',
|
|
99
|
+
];
|
|
100
|
+
/**
|
|
101
|
+
* The tags above whose default CSS display is block-level.
|
|
102
|
+
*
|
|
103
|
+
* Box styles — margin, padding, width, border — behave as authored on these
|
|
104
|
+
* elements, so RText can carry the column's styles on the tag itself and
|
|
105
|
+
* needs no wrapper div (see `resolve-no-container.ts`).
|
|
106
|
+
*/
|
|
107
|
+
const BLOCK_ENCLOSING_TAGS = [
|
|
108
|
+
'p',
|
|
109
|
+
'div',
|
|
110
|
+
'h1',
|
|
111
|
+
'h2',
|
|
112
|
+
'h3',
|
|
113
|
+
'h4',
|
|
114
|
+
'h5',
|
|
115
|
+
'h6',
|
|
116
|
+
'pre',
|
|
117
|
+
'blockquote',
|
|
118
|
+
'address',
|
|
119
|
+
'figure',
|
|
120
|
+
'article',
|
|
121
|
+
'section',
|
|
122
|
+
'aside',
|
|
123
|
+
'header',
|
|
124
|
+
'footer',
|
|
125
|
+
'main',
|
|
126
|
+
'nav',
|
|
127
|
+
];
|
|
128
|
+
/**
|
|
129
|
+
* The tags above that default to `display: inline`, where vertical
|
|
130
|
+
* margin/padding and width/height are ignored by the layout engine. Column
|
|
131
|
+
* box styles must go on a wrapper element instead of on the tag.
|
|
132
|
+
*
|
|
133
|
+
* Kept as an explicit list rather than derived from BLOCK_ENCLOSING_TAGS so
|
|
134
|
+
* adding a tag to ENCLOSING_TAGS forces a deliberate block-or-inline call;
|
|
135
|
+
* the coverage test in `__tests__/enclosing-tags.test.ts` fails otherwise.
|
|
136
|
+
*/
|
|
137
|
+
const INLINE_ENCLOSING_TAGS = [
|
|
138
|
+
'span',
|
|
139
|
+
'strong',
|
|
140
|
+
'em',
|
|
141
|
+
'b',
|
|
142
|
+
'i',
|
|
143
|
+
'u',
|
|
144
|
+
'small',
|
|
145
|
+
'mark',
|
|
146
|
+
'code',
|
|
147
|
+
'q',
|
|
148
|
+
'sub',
|
|
149
|
+
'sup',
|
|
150
|
+
's',
|
|
151
|
+
'del',
|
|
152
|
+
'ins',
|
|
153
|
+
'cite',
|
|
154
|
+
'abbr',
|
|
155
|
+
'dfn',
|
|
156
|
+
'kbd',
|
|
157
|
+
'samp',
|
|
158
|
+
'var',
|
|
159
|
+
'time',
|
|
160
|
+
'data',
|
|
161
|
+
'bdi',
|
|
162
|
+
'bdo',
|
|
163
|
+
];
|
|
164
|
+
/**
|
|
165
|
+
* Runtime guard for values arriving through componentAttributes (untyped
|
|
166
|
+
* editor-authored data). Unknown values are rejected so a bad persisted value
|
|
167
|
+
* can never render an unexpected element.
|
|
168
|
+
*/
|
|
169
|
+
function isEnclosingTag(value) {
|
|
170
|
+
return (typeof value === 'string' &&
|
|
171
|
+
ENCLOSING_TAGS.includes(value));
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* True when the tag renders as a block-level box. Unknown values count as
|
|
175
|
+
* non-block: the safe answer, because it routes box styles to a wrapper
|
|
176
|
+
* element where they are guaranteed to apply.
|
|
177
|
+
*/
|
|
178
|
+
function isBlockEnclosingTag(value) {
|
|
179
|
+
return (isEnclosingTag(value) &&
|
|
180
|
+
BLOCK_ENCLOSING_TAGS.includes(value));
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/** Editor-authored attribute holding the tag, flattened per device resolution. */
|
|
184
|
+
const ENCLOSING_TAG_ATTRIBUTE = 'enclosing-tag-3-resolutions';
|
|
185
|
+
/**
|
|
186
|
+
* The single source of truth for which tag RText will render.
|
|
187
|
+
*
|
|
188
|
+
* Both the component and `resolveNoContainer` go through here: if they
|
|
189
|
+
* disagreed, widget-react would wrap a block element in a redundant div or
|
|
190
|
+
* hand box styles to an inline element that ignores them.
|
|
191
|
+
*
|
|
192
|
+
* The editor-authored attribute wins over the caller's prop. Attribute values
|
|
193
|
+
* are untyped, so an unrecognised value falls back to the prop, then to no
|
|
194
|
+
* enclosing tag at all.
|
|
195
|
+
*/
|
|
196
|
+
function resolveEnclosingTag(componentAttributes, fallback) {
|
|
197
|
+
const userAssignedTag = componentAttributes?.[ENCLOSING_TAG_ATTRIBUTE];
|
|
198
|
+
return isEnclosingTag(userAssignedTag) ? userAssignedTag : fallback;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Runtime guard for entries arriving through componentAttributes (untyped,
|
|
203
|
+
* editor-authored data).
|
|
204
|
+
*
|
|
205
|
+
* Only object shape is checked. Field types are not: a single bad field must
|
|
206
|
+
* cost that field, not the whole entry, so per-field validation belongs to
|
|
207
|
+
* `resolveLinkAttributes` where each value is turned into an attribute.
|
|
208
|
+
*/
|
|
209
|
+
function isLinkItem(value) {
|
|
210
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* True for a cleared slot. Clearing a link stores `{}` rather than removing
|
|
214
|
+
* the array element, because marker indexes are 1-based positions: dropping an
|
|
215
|
+
* element would silently repoint every later marker.
|
|
216
|
+
*/
|
|
217
|
+
function isEmptyLinkItem(item) {
|
|
218
|
+
if (!isLinkItem(item)) {
|
|
219
|
+
return true;
|
|
220
|
+
}
|
|
221
|
+
return Object.values(item).every((value) => value === undefined ||
|
|
222
|
+
value === null ||
|
|
223
|
+
(typeof value === 'string' && value.trim() === ''));
|
|
224
|
+
}
|
|
225
|
+
/**
|
|
226
|
+
* The entry a `{n|…}` marker points at, by 1-based position. Tolerant of bad
|
|
227
|
+
* data on every axis — a non-array list, a fractional or out-of-range index,
|
|
228
|
+
* a hole in the array — because none of it may throw during a render.
|
|
229
|
+
*/
|
|
230
|
+
function getLinkItemAt(links, oneBasedIndex) {
|
|
231
|
+
if (!Array.isArray(links)) {
|
|
232
|
+
return undefined;
|
|
233
|
+
}
|
|
234
|
+
if (!Number.isInteger(oneBasedIndex) || oneBasedIndex < 1) {
|
|
235
|
+
return undefined;
|
|
236
|
+
}
|
|
237
|
+
const entry = links[oneBasedIndex - 1];
|
|
238
|
+
return isLinkItem(entry) ? entry : undefined;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Schemes that can execute or smuggle a payload. Mirrors the denylist in
|
|
243
|
+
* RuleCMS `text-safety/validate-text-content.ts`; the two must stay in step,
|
|
244
|
+
* because that one warns at save and this one is the guarantee at render.
|
|
245
|
+
*/
|
|
246
|
+
const REJECTED_HREF_SCHEMES = [
|
|
247
|
+
'javascript',
|
|
248
|
+
// Netscape-era aliases for javascript:. No browser in use today runs them,
|
|
249
|
+
// but RuleCMS refuses them in prose, and an href accepting what the text
|
|
250
|
+
// field rejects is the kind of gap that outlives the reason for it.
|
|
251
|
+
'livescript',
|
|
252
|
+
'mocha',
|
|
253
|
+
'vbscript',
|
|
254
|
+
'data',
|
|
255
|
+
'file',
|
|
256
|
+
'blob',
|
|
257
|
+
];
|
|
258
|
+
const SCHEME = /^([a-z][a-z0-9+.-]*):/;
|
|
259
|
+
/**
|
|
260
|
+
* Characters a browser ignores while reading a scheme. Stripped before the
|
|
261
|
+
* scheme is matched so `java\tscript:` and a leading NUL cannot hide one.
|
|
262
|
+
*/
|
|
263
|
+
const isIgnoredWhenReadingAScheme = (charCode) => charCode <= 0x20 || charCode === 0x7f;
|
|
264
|
+
const schemeProbe = (value) => {
|
|
265
|
+
let probe = '';
|
|
266
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
267
|
+
if (!isIgnoredWhenReadingAScheme(value.charCodeAt(index))) {
|
|
268
|
+
probe += value[index];
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
return probe.toLowerCase();
|
|
272
|
+
};
|
|
273
|
+
/**
|
|
274
|
+
* Whether a value is safe to put in an `href`.
|
|
275
|
+
*
|
|
276
|
+
* A denylist, not an allowlist. Customers link with git and ssh URLs, mobile
|
|
277
|
+
* deep links, `mailto:`, `tel:`, `sms:`, custom app schemes, protocol-relative
|
|
278
|
+
* `//host/path`, and bare `/path`, `#anchor`, `?q=1`, `./rel` — all of which
|
|
279
|
+
* are accepted. A host is not required and neither is http/https. Only the
|
|
280
|
+
* schemes that turn a link into code or an opaque payload are turned away.
|
|
281
|
+
*
|
|
282
|
+
* Rejection happens at render, never at save: the composer may use this for a
|
|
283
|
+
* non-blocking warning, but stored content is not trusted content.
|
|
284
|
+
*/
|
|
285
|
+
function isSafeHref(value) {
|
|
286
|
+
if (typeof value !== 'string' || value.trim() === '') {
|
|
287
|
+
return false;
|
|
288
|
+
}
|
|
289
|
+
const match = SCHEME.exec(schemeProbe(value));
|
|
290
|
+
// No scheme at all: relative, fragment, query, or protocol-relative.
|
|
291
|
+
if (!match) {
|
|
292
|
+
return true;
|
|
293
|
+
}
|
|
294
|
+
return !REJECTED_HREF_SCHEMES.includes(match[1]);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
const BLANK_TARGET = '_blank';
|
|
298
|
+
const IMPLIED_REL_FOR_BLANK_TARGET = ['noopener', 'noreferrer'];
|
|
299
|
+
const cleanString = (value) => {
|
|
300
|
+
if (typeof value !== 'string') {
|
|
301
|
+
return undefined;
|
|
302
|
+
}
|
|
303
|
+
const trimmed = value.trim();
|
|
304
|
+
return trimmed === '' ? undefined : trimmed;
|
|
305
|
+
};
|
|
306
|
+
const resolveRel = (authorRel, target) => {
|
|
307
|
+
const tokens = authorRel ? authorRel.split(/\s+/) : [];
|
|
308
|
+
if (target?.toLowerCase() !== BLANK_TARGET) {
|
|
309
|
+
return tokens.length > 0 ? tokens.join(' ') : undefined;
|
|
310
|
+
}
|
|
311
|
+
const present = new Set(tokens.map((token) => token.toLowerCase()));
|
|
312
|
+
const merged = [...tokens];
|
|
313
|
+
IMPLIED_REL_FOR_BLANK_TARGET.forEach((token) => {
|
|
314
|
+
if (!present.has(token)) {
|
|
315
|
+
merged.push(token);
|
|
316
|
+
}
|
|
317
|
+
});
|
|
318
|
+
return merged.join(' ');
|
|
319
|
+
};
|
|
320
|
+
const resolveDownload = (download) => {
|
|
321
|
+
if (download === true) {
|
|
322
|
+
return true;
|
|
323
|
+
}
|
|
324
|
+
return cleanString(download);
|
|
325
|
+
};
|
|
326
|
+
function resolveLinkAttributes(item) {
|
|
327
|
+
if (!isLinkItem(item)) {
|
|
328
|
+
return undefined;
|
|
329
|
+
}
|
|
330
|
+
const href = cleanString(item.href);
|
|
331
|
+
if (!isSafeHref(href)) {
|
|
332
|
+
return undefined;
|
|
333
|
+
}
|
|
334
|
+
const target = cleanString(item.target);
|
|
335
|
+
const rel = resolveRel(cleanString(item.rel), target);
|
|
336
|
+
const title = cleanString(item.title);
|
|
337
|
+
const ariaLabel = cleanString(item.ariaLabel);
|
|
338
|
+
const download = resolveDownload(item.download);
|
|
339
|
+
const hrefLang = cleanString(item.hreflang);
|
|
340
|
+
const referrerPolicy = cleanString(item.referrerPolicy);
|
|
341
|
+
return {
|
|
342
|
+
href: href,
|
|
343
|
+
...(target !== undefined ? { target } : {}),
|
|
344
|
+
...(rel !== undefined ? { rel } : {}),
|
|
345
|
+
...(title !== undefined ? { title } : {}),
|
|
346
|
+
...(ariaLabel !== undefined ? { 'aria-label': ariaLabel } : {}),
|
|
347
|
+
...(download !== undefined ? { download } : {}),
|
|
348
|
+
...(hrefLang !== undefined ? { hrefLang } : {}),
|
|
349
|
+
...(referrerPolicy !== undefined ? { referrerPolicy } : {}),
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* `{` + optional mark letters + optional index + `|` + text + `}`.
|
|
355
|
+
*
|
|
356
|
+
* Sticky, so it can be tried at one position without slicing the string. The
|
|
357
|
+
* text is everything up to the first `}`, which is what makes markers
|
|
358
|
+
* non-nesting.
|
|
359
|
+
*/
|
|
360
|
+
const MARKER = /\{([a-z]*)(\d*)\|([^}]*)\}/y;
|
|
361
|
+
/**
|
|
362
|
+
* One letter per mark. Order is not significant (`{bi1|x}` and `{ib1|x}` are
|
|
363
|
+
* the same span) and repeats are harmless, because the letters resolve to a
|
|
364
|
+
* set.
|
|
365
|
+
*
|
|
366
|
+
* **`r` must never become a mark letter.** `br` is the line-break token below,
|
|
367
|
+
* and it is only unambiguous while `r` cannot appear in a mark sequence.
|
|
368
|
+
*/
|
|
369
|
+
const MARK_LETTERS$1 = {
|
|
370
|
+
b: 'bold',
|
|
371
|
+
i: 'italic',
|
|
372
|
+
s: 'strike',
|
|
373
|
+
};
|
|
374
|
+
/** The one marker that carries no text: `{br|}`. */
|
|
375
|
+
const BREAK_TOKEN = 'br';
|
|
376
|
+
const NO_MARKS = { bold: false, italic: false, strike: false };
|
|
377
|
+
/**
|
|
378
|
+
* Letters to a mark set, or `undefined` when any letter is not a mark. An
|
|
379
|
+
* unknown letter makes the whole marker literal text rather than a span with
|
|
380
|
+
* the bad letter ignored: an author who mistypes should see it.
|
|
381
|
+
*/
|
|
382
|
+
const resolveMarks = (letters) => {
|
|
383
|
+
if (letters === '') {
|
|
384
|
+
return { ...NO_MARKS };
|
|
385
|
+
}
|
|
386
|
+
const marks = { ...NO_MARKS };
|
|
387
|
+
for (const letter of letters) {
|
|
388
|
+
const mark = MARK_LETTERS$1[letter];
|
|
389
|
+
if (mark === undefined) {
|
|
390
|
+
return undefined;
|
|
391
|
+
}
|
|
392
|
+
marks[mark] = true;
|
|
393
|
+
}
|
|
394
|
+
return marks;
|
|
395
|
+
};
|
|
396
|
+
/**
|
|
397
|
+
* Splits authored text into plain runs, marked/linked spans, and line breaks.
|
|
398
|
+
*
|
|
399
|
+
* The grammar, and everything it deliberately leaves as literal text, is
|
|
400
|
+
* specified in `rulecms/__docs__/__ARCHITECTURE__/INLINE_MARKUP_ARCHITECTURE.md`.
|
|
401
|
+
* In short: a marker is `{`, then lowercase mark letters, then a link index,
|
|
402
|
+
* then `|`, then its text, then `}`. At least one of the letters and the index
|
|
403
|
+
* must be present, and everything that does not match that shape stays
|
|
404
|
+
* verbatim — `{name}`, `{99}`, a lone `{`, JSON fragments, an unterminated
|
|
405
|
+
* marker, an unknown mark letter, and empty span text.
|
|
406
|
+
*
|
|
407
|
+
* Indexes are 1-based and normalize through a base-10 parse, so `{01|x}` and
|
|
408
|
+
* `{1|x}` point at the same entry. Resolving an index to an entry is the
|
|
409
|
+
* caller's job; this stays pure and free of React.
|
|
410
|
+
*/
|
|
411
|
+
function parseInlineMarkers(value) {
|
|
412
|
+
if (typeof value !== 'string' || value === '') {
|
|
413
|
+
return [];
|
|
414
|
+
}
|
|
415
|
+
const runs = [];
|
|
416
|
+
let pendingText = '';
|
|
417
|
+
let cursor = 0;
|
|
418
|
+
const flushPendingText = () => {
|
|
419
|
+
if (pendingText !== '') {
|
|
420
|
+
runs.push({ kind: 'text', text: pendingText });
|
|
421
|
+
pendingText = '';
|
|
422
|
+
}
|
|
423
|
+
};
|
|
424
|
+
while (cursor < value.length) {
|
|
425
|
+
const open = value.indexOf('{', cursor);
|
|
426
|
+
if (open === -1) {
|
|
427
|
+
pendingText += value.slice(cursor);
|
|
428
|
+
break;
|
|
429
|
+
}
|
|
430
|
+
MARKER.lastIndex = open;
|
|
431
|
+
const match = MARKER.exec(value);
|
|
432
|
+
const run = match ? resolveRun(match[1], match[2], match[3]) : undefined;
|
|
433
|
+
if (run === undefined) {
|
|
434
|
+
// Not a marker: keep the brace as text and look for the next one.
|
|
435
|
+
pendingText += value.slice(cursor, open + 1);
|
|
436
|
+
cursor = open + 1;
|
|
437
|
+
continue;
|
|
438
|
+
}
|
|
439
|
+
pendingText += value.slice(cursor, open);
|
|
440
|
+
flushPendingText();
|
|
441
|
+
runs.push(run);
|
|
442
|
+
cursor = open + match[0].length;
|
|
443
|
+
}
|
|
444
|
+
flushPendingText();
|
|
445
|
+
return runs;
|
|
446
|
+
}
|
|
447
|
+
/**
|
|
448
|
+
* One matched `{…|…}` to the run it means, or `undefined` when the match is
|
|
449
|
+
* not a marker after all and its opening brace is literal text.
|
|
450
|
+
*/
|
|
451
|
+
const resolveRun = (letters, digits, text) => {
|
|
452
|
+
if (letters === BREAK_TOKEN) {
|
|
453
|
+
// A break carries nothing. `{br|x}` is a mistake, not a break with text.
|
|
454
|
+
return digits === '' && text === '' ? { kind: 'break' } : undefined;
|
|
455
|
+
}
|
|
456
|
+
if (letters === '' && digits === '') {
|
|
457
|
+
return undefined;
|
|
458
|
+
}
|
|
459
|
+
// Empty text has no words to mark or link, and staying literal is what shows
|
|
460
|
+
// the author the mistake instead of swallowing the marker.
|
|
461
|
+
if (text === '') {
|
|
462
|
+
return undefined;
|
|
463
|
+
}
|
|
464
|
+
const marks = resolveMarks(letters);
|
|
465
|
+
if (marks === undefined) {
|
|
466
|
+
return undefined;
|
|
467
|
+
}
|
|
468
|
+
return {
|
|
469
|
+
kind: 'span',
|
|
470
|
+
text,
|
|
471
|
+
...(digits === '' ? {} : { index: Number.parseInt(digits, 10) }),
|
|
472
|
+
marks,
|
|
473
|
+
};
|
|
474
|
+
};
|
|
475
|
+
|
|
476
|
+
const MARK_TAGS = [
|
|
477
|
+
['bold', 'strong'],
|
|
478
|
+
['italic', 'em'],
|
|
479
|
+
['strike', 's'],
|
|
480
|
+
];
|
|
481
|
+
const hasMark = (marks) => marks.bold || marks.italic || marks.strike;
|
|
482
|
+
const plainTextOf = (run) => (run.kind === 'break' ? '' : run.text);
|
|
483
|
+
class InlineMarkupComponent {
|
|
484
|
+
constructor(host, renderer) {
|
|
485
|
+
this.host = host;
|
|
486
|
+
this.renderer = renderer;
|
|
487
|
+
this.text = '';
|
|
488
|
+
}
|
|
489
|
+
ngOnChanges() {
|
|
490
|
+
this.clear();
|
|
491
|
+
const runs = parseInlineMarkers(this.text);
|
|
492
|
+
const anchors = runs.map((run) => run.kind === 'span' && run.index !== undefined
|
|
493
|
+
? resolveLinkAttributes(getLinkItemAt(this.links, run.index))
|
|
494
|
+
: undefined);
|
|
495
|
+
const needsElement = runs.some((run, position) => run.kind === 'break' ||
|
|
496
|
+
(run.kind === 'span' && (anchors[position] !== undefined || hasMark(run.marks))));
|
|
497
|
+
if (!needsElement) {
|
|
498
|
+
this.renderer.appendChild(this.host.nativeElement, this.renderer.createText(runs.map(plainTextOf).join('')));
|
|
499
|
+
return;
|
|
500
|
+
}
|
|
501
|
+
runs.forEach((run, position) => {
|
|
502
|
+
if (run.kind === 'text') {
|
|
503
|
+
this.renderer.appendChild(this.host.nativeElement, this.renderer.createText(run.text));
|
|
504
|
+
return;
|
|
505
|
+
}
|
|
506
|
+
if (run.kind === 'break') {
|
|
507
|
+
this.renderer.appendChild(this.host.nativeElement, this.renderer.createElement('br'));
|
|
508
|
+
return;
|
|
509
|
+
}
|
|
510
|
+
this.renderer.appendChild(this.host.nativeElement, this.renderSpan(run, anchors[position]));
|
|
511
|
+
});
|
|
512
|
+
}
|
|
513
|
+
renderSpan(run, anchor) {
|
|
514
|
+
let node = anchor === undefined
|
|
515
|
+
? this.renderer.createText(run.text)
|
|
516
|
+
: this.createAnchor(anchor, run.text);
|
|
517
|
+
for (let position = MARK_TAGS.length - 1; position >= 0; position -= 1) {
|
|
518
|
+
const [mark, tag] = MARK_TAGS[position];
|
|
519
|
+
if (run.marks[mark]) {
|
|
520
|
+
const wrapper = this.renderer.createElement(tag);
|
|
521
|
+
this.renderer.appendChild(wrapper, node);
|
|
522
|
+
node = wrapper;
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
return node;
|
|
526
|
+
}
|
|
527
|
+
createAnchor(anchor, text) {
|
|
528
|
+
const el = this.renderer.createElement('a');
|
|
529
|
+
this.renderer.setAttribute(el, 'href', anchor.href);
|
|
530
|
+
if (anchor.target)
|
|
531
|
+
this.renderer.setAttribute(el, 'target', anchor.target);
|
|
532
|
+
if (anchor.rel)
|
|
533
|
+
this.renderer.setAttribute(el, 'rel', anchor.rel);
|
|
534
|
+
if (anchor.title)
|
|
535
|
+
this.renderer.setAttribute(el, 'title', anchor.title);
|
|
536
|
+
if (anchor['aria-label'])
|
|
537
|
+
this.renderer.setAttribute(el, 'aria-label', anchor['aria-label']);
|
|
538
|
+
if (anchor.download === true)
|
|
539
|
+
this.renderer.setAttribute(el, 'download', '');
|
|
540
|
+
else if (typeof anchor.download === 'string')
|
|
541
|
+
this.renderer.setAttribute(el, 'download', anchor.download);
|
|
542
|
+
if (anchor.hrefLang)
|
|
543
|
+
this.renderer.setAttribute(el, 'hreflang', anchor.hrefLang);
|
|
544
|
+
if (anchor.referrerPolicy)
|
|
545
|
+
this.renderer.setAttribute(el, 'referrerpolicy', anchor.referrerPolicy);
|
|
546
|
+
this.renderer.appendChild(el, this.renderer.createText(text));
|
|
547
|
+
return el;
|
|
548
|
+
}
|
|
549
|
+
clear() {
|
|
550
|
+
const el = this.host.nativeElement;
|
|
551
|
+
while (el.firstChild) {
|
|
552
|
+
this.renderer.removeChild(el, el.firstChild);
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.25", ngImport: i0, type: InlineMarkupComponent, deps: [{ token: i0.ElementRef }, { token: i0.Renderer2 }], target: i0.ɵɵFactoryTarget.Component }); }
|
|
556
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.25", type: InlineMarkupComponent, isStandalone: true, selector: "rulecms-inline-markup", inputs: { text: "text", links: "links" }, host: { styleAttribute: "display: contents" }, usesOnChanges: true, ngImport: i0, template: '', isInline: true }); }
|
|
557
|
+
}
|
|
558
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.25", ngImport: i0, type: InlineMarkupComponent, decorators: [{
|
|
559
|
+
type: Component,
|
|
560
|
+
args: [{
|
|
561
|
+
selector: 'rulecms-inline-markup',
|
|
562
|
+
standalone: true,
|
|
563
|
+
template: '',
|
|
564
|
+
host: { style: 'display: contents' },
|
|
565
|
+
}]
|
|
566
|
+
}], ctorParameters: () => [{ type: i0.ElementRef }, { type: i0.Renderer2 }], propDecorators: { text: [{
|
|
567
|
+
type: Input
|
|
568
|
+
}], links: [{
|
|
569
|
+
type: Input
|
|
570
|
+
}] } });
|
|
571
|
+
|
|
572
|
+
class HtmlTagComponent {
|
|
573
|
+
constructor(host, renderer, vcr) {
|
|
574
|
+
this.host = host;
|
|
575
|
+
this.renderer = renderer;
|
|
576
|
+
this.vcr = vcr;
|
|
577
|
+
this.created = null;
|
|
578
|
+
this.viewReady = false;
|
|
579
|
+
}
|
|
580
|
+
ngAfterViewInit() {
|
|
581
|
+
this.viewReady = true;
|
|
582
|
+
this.render();
|
|
583
|
+
}
|
|
584
|
+
ngOnChanges() {
|
|
585
|
+
if (this.viewReady) {
|
|
586
|
+
this.render();
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
render() {
|
|
590
|
+
if (this.created) {
|
|
591
|
+
this.renderer.removeChild(this.host.nativeElement, this.created);
|
|
592
|
+
this.created = null;
|
|
593
|
+
}
|
|
594
|
+
const el = this.renderer.createElement(this.tag);
|
|
595
|
+
if (this.className !== undefined) {
|
|
596
|
+
this.renderer.setAttribute(el, 'class', this.className);
|
|
597
|
+
}
|
|
598
|
+
if (this.styleProps) {
|
|
599
|
+
Object.entries(this.styleProps).forEach(([key, value]) => {
|
|
600
|
+
if (value !== undefined && value !== null && value !== '') {
|
|
601
|
+
this.renderer.setStyle(el, key, String(value));
|
|
602
|
+
}
|
|
603
|
+
});
|
|
604
|
+
}
|
|
605
|
+
const view = this.vcr.createEmbeddedView(this.projected);
|
|
606
|
+
view.rootNodes.forEach((node) => this.renderer.appendChild(el, node));
|
|
607
|
+
this.renderer.appendChild(this.host.nativeElement, el);
|
|
608
|
+
this.created = el;
|
|
609
|
+
}
|
|
610
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.25", ngImport: i0, type: HtmlTagComponent, deps: [{ token: i0.ElementRef }, { token: i0.Renderer2 }, { token: i0.ViewContainerRef }], target: i0.ɵɵFactoryTarget.Component }); }
|
|
611
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.25", type: HtmlTagComponent, isStandalone: true, selector: "rulecms-html-tag", inputs: { tag: "tag", styleProps: "styleProps", className: "className" }, host: { styleAttribute: "display: contents" }, viewQueries: [{ propertyName: "projected", first: true, predicate: ["projected"], descendants: true, static: true }], usesOnChanges: true, ngImport: i0, template: `
|
|
612
|
+
<ng-template #projected>
|
|
613
|
+
<ng-content />
|
|
614
|
+
</ng-template>
|
|
615
|
+
`, isInline: true }); }
|
|
616
|
+
}
|
|
617
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.25", ngImport: i0, type: HtmlTagComponent, decorators: [{
|
|
618
|
+
type: Component,
|
|
619
|
+
args: [{
|
|
620
|
+
selector: 'rulecms-html-tag',
|
|
621
|
+
standalone: true,
|
|
622
|
+
template: `
|
|
623
|
+
<ng-template #projected>
|
|
624
|
+
<ng-content />
|
|
625
|
+
</ng-template>
|
|
626
|
+
`,
|
|
627
|
+
host: { style: 'display: contents' },
|
|
628
|
+
}]
|
|
629
|
+
}], ctorParameters: () => [{ type: i0.ElementRef }, { type: i0.Renderer2 }, { type: i0.ViewContainerRef }], propDecorators: { tag: [{
|
|
630
|
+
type: Input,
|
|
631
|
+
args: [{ required: true }]
|
|
632
|
+
}], styleProps: [{
|
|
633
|
+
type: Input
|
|
634
|
+
}], className: [{
|
|
635
|
+
type: Input
|
|
636
|
+
}], projected: [{
|
|
637
|
+
type: ViewChild,
|
|
638
|
+
args: ['projected', { static: true }]
|
|
639
|
+
}] } });
|
|
640
|
+
|
|
641
|
+
class RTextComponent {
|
|
642
|
+
get content() {
|
|
643
|
+
const contentVal = this.componentAttributes?.['content-text-3-resolutions'] ?? DEFAULT_CONTENT_TEXT;
|
|
644
|
+
return typeof contentVal === 'string' ? contentVal : String(contentVal ?? '');
|
|
645
|
+
}
|
|
646
|
+
get links() {
|
|
647
|
+
return this.componentAttributes?.['links-3-resolutions'];
|
|
648
|
+
}
|
|
649
|
+
get tag() {
|
|
650
|
+
return resolveEnclosingTag(this.componentAttributes);
|
|
651
|
+
}
|
|
652
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.25", ngImport: i0, type: RTextComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
653
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.2.25", type: RTextComponent, isStandalone: true, selector: "rulecms-r-text", inputs: { componentAttributes: "componentAttributes", styleProps: "styleProps", className: "className" }, host: { styleAttribute: "display: contents" }, ngImport: i0, template: `
|
|
654
|
+
@if (tag) {
|
|
655
|
+
<rulecms-html-tag [tag]="tag" [styleProps]="styleProps" [className]="className">
|
|
656
|
+
<rulecms-inline-markup [text]="content" [links]="links" />
|
|
657
|
+
</rulecms-html-tag>
|
|
658
|
+
} @else {
|
|
659
|
+
<rulecms-inline-markup [text]="content" [links]="links" />
|
|
660
|
+
}
|
|
661
|
+
`, isInline: true, dependencies: [{ kind: "component", type: InlineMarkupComponent, selector: "rulecms-inline-markup", inputs: ["text", "links"] }, { kind: "component", type: HtmlTagComponent, selector: "rulecms-html-tag", inputs: ["tag", "styleProps", "className"] }] }); }
|
|
662
|
+
}
|
|
663
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.25", ngImport: i0, type: RTextComponent, decorators: [{
|
|
664
|
+
type: Component,
|
|
665
|
+
args: [{
|
|
666
|
+
selector: 'rulecms-r-text',
|
|
667
|
+
standalone: true,
|
|
668
|
+
imports: [InlineMarkupComponent, HtmlTagComponent],
|
|
669
|
+
template: `
|
|
670
|
+
@if (tag) {
|
|
671
|
+
<rulecms-html-tag [tag]="tag" [styleProps]="styleProps" [className]="className">
|
|
672
|
+
<rulecms-inline-markup [text]="content" [links]="links" />
|
|
673
|
+
</rulecms-html-tag>
|
|
674
|
+
} @else {
|
|
675
|
+
<rulecms-inline-markup [text]="content" [links]="links" />
|
|
676
|
+
}
|
|
677
|
+
`,
|
|
678
|
+
host: { style: 'display: contents' },
|
|
679
|
+
}]
|
|
680
|
+
}], propDecorators: { componentAttributes: [{
|
|
681
|
+
type: Input
|
|
682
|
+
}], styleProps: [{
|
|
683
|
+
type: Input
|
|
684
|
+
}], className: [{
|
|
685
|
+
type: Input
|
|
686
|
+
}] } });
|
|
687
|
+
|
|
688
|
+
/**
|
|
689
|
+
* RText needs a wrapper div for the column's box styles unless it is about to
|
|
690
|
+
* render a block-level element that can hold those styles itself.
|
|
691
|
+
*
|
|
692
|
+
* - No enclosing tag: bare text with no element to style → wrapper needed.
|
|
693
|
+
* - Inline tag (`span`, `em`, `code`, …): vertical margin/padding and width
|
|
694
|
+
* are ignored on inline boxes → wrapper needed.
|
|
695
|
+
* - Block tag (`p`, `div`, `h1`, `section`, …): RText puts styleProps on the
|
|
696
|
+
* tag itself → no wrapper, one less element in the DOM.
|
|
697
|
+
*/
|
|
698
|
+
const resolveNoContainer$8 = ({ componentAttributes, }) => !isBlockEnclosingTag(resolveEnclosingTag(componentAttributes));
|
|
699
|
+
|
|
700
|
+
/**
|
|
701
|
+
* True when RText is about to render an element that can carry a className —
|
|
702
|
+
* any enclosing tag, block or inline. Bare text has no element, so false.
|
|
703
|
+
*
|
|
704
|
+
* Shares `resolveEnclosingTag` with RText and `resolveNoContainer`: if this
|
|
705
|
+
* disagreed with what the component renders, a class would either land on
|
|
706
|
+
* nothing or force a redundant wrapper.
|
|
707
|
+
*/
|
|
708
|
+
const resolveRendersOwnElement$8 = ({ componentAttributes, }) => !!resolveEnclosingTag(componentAttributes);
|
|
709
|
+
|
|
710
|
+
const attributeEditModeConfigs$8 = [
|
|
711
|
+
{
|
|
712
|
+
name: 'content-text-3-resolutions',
|
|
713
|
+
defaultValues: {},
|
|
714
|
+
},
|
|
715
|
+
{
|
|
716
|
+
name: 'enclosing-tag-3-resolutions',
|
|
717
|
+
heading: 'Enclosing Tag',
|
|
718
|
+
subHeading: 'HTML tag to enclose the text in (e.g. p, h1, span); see ENCLOSING_TAGS for the allowed values, for 3 different device resolutions',
|
|
719
|
+
defaultValues: {},
|
|
720
|
+
},
|
|
721
|
+
{
|
|
722
|
+
name: 'links-3-resolutions',
|
|
723
|
+
heading: 'Links',
|
|
724
|
+
subHeading: 'Links this text can point at. To link part of the text, wrap those words in {1|linked text}, where the number is the position of the entry in this list counting from 1; clearing an entry leaves its position in place so the other numbers keep pointing where they did, for 3 different device resolutions',
|
|
725
|
+
defaultValues: {},
|
|
726
|
+
},
|
|
727
|
+
];
|
|
728
|
+
|
|
729
|
+
const ComponentIdentifiers = {
|
|
730
|
+
rText: 'r-text',
|
|
731
|
+
somethingElse: 'something-else',
|
|
732
|
+
cloudinaryAdvancedImage: 'cloudinary-advanced-image',
|
|
733
|
+
rVideo: 'r-video',
|
|
734
|
+
rIcon: 'r-icon',
|
|
735
|
+
rButton: 'r-button',
|
|
736
|
+
rDivider: 'r-divider',
|
|
737
|
+
rEmbed: 'r-embed',
|
|
738
|
+
rList: 'r-list',
|
|
739
|
+
rAccordion: 'r-accordion',
|
|
740
|
+
};
|
|
741
|
+
|
|
742
|
+
const displayCardEntry$8 = {
|
|
743
|
+
id: ComponentIdentifiers.rText,
|
|
744
|
+
resolveNoContainer: resolveNoContainer$8,
|
|
745
|
+
resolveRendersOwnElement: resolveRendersOwnElement$8,
|
|
746
|
+
card: RTextComponent,
|
|
747
|
+
defaultAttributeValues: defaultAttributeValues$8,
|
|
748
|
+
attributeEditModeConfigs: attributeEditModeConfigs$8,
|
|
749
|
+
};
|
|
750
|
+
|
|
751
|
+
// Builds Cloudinary delivery URLs directly so the component renders a complete
|
|
752
|
+
// <img src/srcSet> during server rendering (no browser-only Cloudinary SDK).
|
|
753
|
+
// `f_auto,q_auto` lets Cloudinary pick format/quality; `c_limit` scales down
|
|
754
|
+
// but never upscales beyond the original asset.
|
|
755
|
+
// Covers phone through large desktop displays, including 2x DPR on common
|
|
756
|
+
// viewport widths. Upper bound keeps multi-thousand-pixel originals in check.
|
|
757
|
+
const cloudinarySrcSetWidths = [480, 768, 1080, 1600, 2400];
|
|
758
|
+
// Mid-size fallback for browsers/tools that ignore srcset.
|
|
759
|
+
const fallbackSrcWidth = 1080;
|
|
760
|
+
const defaultTransformations = {
|
|
761
|
+
f: 'auto',
|
|
762
|
+
q: 'auto',
|
|
763
|
+
c: 'limit',
|
|
764
|
+
};
|
|
765
|
+
function serializeTransformations$1(transformations) {
|
|
766
|
+
return Object.entries(transformations)
|
|
767
|
+
.map(([key, value]) => `${key}_${value}`)
|
|
768
|
+
.join(',');
|
|
769
|
+
}
|
|
770
|
+
// Accepts the raw Cloudinary string form ("c_fill,g_auto,ar_4:3") and parses
|
|
771
|
+
// it into an object so it merges over the defaults the same way object input
|
|
772
|
+
// does (e.g. a user c_fill cleanly replaces the default c_limit)
|
|
773
|
+
function parseTransformationString$1(value) {
|
|
774
|
+
const result = {};
|
|
775
|
+
value.split(',').forEach((entry) => {
|
|
776
|
+
const trimmed = entry.trim();
|
|
777
|
+
if (!trimmed) {
|
|
778
|
+
return;
|
|
779
|
+
}
|
|
780
|
+
const separatorIndex = trimmed.indexOf('_');
|
|
781
|
+
if (separatorIndex <= 0) {
|
|
782
|
+
return;
|
|
783
|
+
}
|
|
784
|
+
result[trimmed.slice(0, separatorIndex)] = trimmed.slice(separatorIndex + 1);
|
|
785
|
+
});
|
|
786
|
+
return result;
|
|
787
|
+
}
|
|
788
|
+
function buildCloudinaryImageUrl(cloudName, publicId, width, transformations) {
|
|
789
|
+
const userTransformations = typeof transformations === 'string'
|
|
790
|
+
? parseTransformationString$1(transformations)
|
|
791
|
+
: transformations;
|
|
792
|
+
// User keys override defaults (object spread = "split the object onto the
|
|
793
|
+
// configuration"). `w` is always component-managed so srcset width variants
|
|
794
|
+
// stay correct; combined with user ar/c/g values Cloudinary produces
|
|
795
|
+
// consistent crops at every width.
|
|
796
|
+
const merged = {
|
|
797
|
+
...defaultTransformations,
|
|
798
|
+
...userTransformations,
|
|
799
|
+
w: width,
|
|
800
|
+
};
|
|
801
|
+
// encodeURI preserves the slashes that are part of Cloudinary public IDs
|
|
802
|
+
// (e.g. "images/<uuid>/<id>") while escaping unsafe characters
|
|
803
|
+
return encodeURI(`https://res.cloudinary.com/${cloudName}/image/upload/${serializeTransformations$1(merged)}/${publicId}`);
|
|
804
|
+
}
|
|
805
|
+
function buildCloudinarySrc(cloudName, publicId, transformations) {
|
|
806
|
+
return buildCloudinaryImageUrl(cloudName, publicId, fallbackSrcWidth, transformations);
|
|
807
|
+
}
|
|
808
|
+
function buildCloudinarySrcSet(cloudName, publicId, transformations) {
|
|
809
|
+
return cloudinarySrcSetWidths
|
|
810
|
+
.map((width) => `${buildCloudinaryImageUrl(cloudName, publicId, width, transformations)} ${width}w`)
|
|
811
|
+
.join(', ');
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
class CloudinaryAdvancedImageComponent {
|
|
815
|
+
constructor() {
|
|
816
|
+
this.cloudName = 'rulecms';
|
|
817
|
+
this.publicId = 'main-sample';
|
|
818
|
+
this.alt = 'RuleCMS Component';
|
|
819
|
+
}
|
|
820
|
+
get finalCloudName() {
|
|
821
|
+
return this.componentAttributes?.['cloudinary-cloud-name'] || this.cloudName;
|
|
822
|
+
}
|
|
823
|
+
get finalPublicId() {
|
|
824
|
+
return (this.componentAttributes?.['media-selector-3-resolutions'] ||
|
|
825
|
+
this.componentAttributes?.['cloudinary-public-id'] ||
|
|
826
|
+
this.publicId);
|
|
827
|
+
}
|
|
828
|
+
get finalTransformations() {
|
|
829
|
+
return this.componentAttributes?.['cloudinary-transformations-3-resolutions'] || this.transformations;
|
|
830
|
+
}
|
|
831
|
+
get src() {
|
|
832
|
+
return buildCloudinarySrc(this.finalCloudName, this.finalPublicId, this.finalTransformations);
|
|
833
|
+
}
|
|
834
|
+
get srcSet() {
|
|
835
|
+
return buildCloudinarySrcSet(this.finalCloudName, this.finalPublicId, this.finalTransformations);
|
|
836
|
+
}
|
|
837
|
+
get imageAttributes() {
|
|
838
|
+
const imageAttributes = {
|
|
839
|
+
alt: this.alt,
|
|
840
|
+
decoding: 'async',
|
|
841
|
+
sizes: '100vw',
|
|
842
|
+
};
|
|
843
|
+
const imageStyles = { height: 'auto' };
|
|
844
|
+
if (this.componentAttributes) {
|
|
845
|
+
Object.keys(this.componentAttributes).forEach((keyWithDimensionSuffix) => {
|
|
846
|
+
const value = this.componentAttributes[keyWithDimensionSuffix];
|
|
847
|
+
if (value === undefined || value === null) {
|
|
848
|
+
return;
|
|
849
|
+
}
|
|
850
|
+
const key = keyWithDimensionSuffix.endsWith('-3-resolutions')
|
|
851
|
+
? keyWithDimensionSuffix.slice(0, -'-3-resolutions'.length)
|
|
852
|
+
: keyWithDimensionSuffix;
|
|
853
|
+
if (key.startsWith('pass-through-image-attributes-style-')) {
|
|
854
|
+
imageStyles[key.slice('pass-through-image-attributes-style-'.length)] = value;
|
|
855
|
+
}
|
|
856
|
+
else if (key.startsWith('pass-through-image-attributes-')) {
|
|
857
|
+
imageAttributes[key.slice('pass-through-image-attributes-'.length)] = value;
|
|
858
|
+
}
|
|
859
|
+
});
|
|
860
|
+
}
|
|
861
|
+
imageAttributes.__styles = imageStyles;
|
|
862
|
+
return imageAttributes;
|
|
863
|
+
}
|
|
864
|
+
get mergedClassName() {
|
|
865
|
+
const { className: passThroughClassName } = this.imageAttributes;
|
|
866
|
+
return [passThroughClassName, this.className].filter(Boolean).join(' ') || undefined;
|
|
867
|
+
}
|
|
868
|
+
get mergedStyles() {
|
|
869
|
+
return { ...this.imageAttributes.__styles, ...this.styleProps };
|
|
870
|
+
}
|
|
871
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.25", ngImport: i0, type: CloudinaryAdvancedImageComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
872
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.25", type: CloudinaryAdvancedImageComponent, isStandalone: true, selector: "rulecms-cloudinary-image", inputs: { cloudName: "cloudName", publicId: "publicId", alt: "alt", transformations: "transformations", componentAttributes: "componentAttributes", styleProps: "styleProps", className: "className" }, host: { styleAttribute: "display: contents" }, ngImport: i0, template: `
|
|
873
|
+
<img
|
|
874
|
+
[src]="src"
|
|
875
|
+
[srcset]="srcSet"
|
|
876
|
+
[attr.alt]="imageAttributes['alt']"
|
|
877
|
+
[attr.decoding]="imageAttributes['decoding']"
|
|
878
|
+
[attr.sizes]="imageAttributes['sizes']"
|
|
879
|
+
[attr.loading]="imageAttributes['loading']"
|
|
880
|
+
[attr.fetchpriority]="imageAttributes['fetchpriority']"
|
|
881
|
+
[attr.width]="imageAttributes['width']"
|
|
882
|
+
[attr.height]="imageAttributes['height']"
|
|
883
|
+
[class]="mergedClassName"
|
|
884
|
+
[ngStyle]="mergedStyles"
|
|
885
|
+
/>
|
|
886
|
+
`, isInline: true, dependencies: [{ kind: "directive", type: NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }] }); }
|
|
887
|
+
}
|
|
888
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.25", ngImport: i0, type: CloudinaryAdvancedImageComponent, decorators: [{
|
|
889
|
+
type: Component,
|
|
890
|
+
args: [{
|
|
891
|
+
selector: 'rulecms-cloudinary-image',
|
|
892
|
+
standalone: true,
|
|
893
|
+
imports: [NgStyle],
|
|
894
|
+
template: `
|
|
895
|
+
<img
|
|
896
|
+
[src]="src"
|
|
897
|
+
[srcset]="srcSet"
|
|
898
|
+
[attr.alt]="imageAttributes['alt']"
|
|
899
|
+
[attr.decoding]="imageAttributes['decoding']"
|
|
900
|
+
[attr.sizes]="imageAttributes['sizes']"
|
|
901
|
+
[attr.loading]="imageAttributes['loading']"
|
|
902
|
+
[attr.fetchpriority]="imageAttributes['fetchpriority']"
|
|
903
|
+
[attr.width]="imageAttributes['width']"
|
|
904
|
+
[attr.height]="imageAttributes['height']"
|
|
905
|
+
[class]="mergedClassName"
|
|
906
|
+
[ngStyle]="mergedStyles"
|
|
907
|
+
/>
|
|
908
|
+
`,
|
|
909
|
+
host: { style: 'display: contents' },
|
|
910
|
+
}]
|
|
911
|
+
}], propDecorators: { cloudName: [{
|
|
912
|
+
type: Input
|
|
913
|
+
}], publicId: [{
|
|
914
|
+
type: Input
|
|
915
|
+
}], alt: [{
|
|
916
|
+
type: Input
|
|
917
|
+
}], transformations: [{
|
|
918
|
+
type: Input
|
|
919
|
+
}], componentAttributes: [{
|
|
920
|
+
type: Input
|
|
921
|
+
}], styleProps: [{
|
|
922
|
+
type: Input
|
|
923
|
+
}], className: [{
|
|
924
|
+
type: Input
|
|
925
|
+
}] } });
|
|
926
|
+
|
|
927
|
+
/**
|
|
928
|
+
* The component spreads styleProps onto its `<img>`, a replaced element that
|
|
929
|
+
* honors min/max sizing, margin and padding directly — a wrapper div would
|
|
930
|
+
* only add a redundant element. There is no configuration that changes this,
|
|
931
|
+
* so the answer is the same for every render.
|
|
932
|
+
*/
|
|
933
|
+
const resolveNoContainer$7 = () => false;
|
|
934
|
+
|
|
935
|
+
/**
|
|
936
|
+
* Always renders an `<img>`, which can carry a className. There is no
|
|
937
|
+
* configuration that changes this.
|
|
938
|
+
*/
|
|
939
|
+
const resolveRendersOwnElement$7 = () => true;
|
|
940
|
+
|
|
941
|
+
const defaultAttributeValues$7 = [
|
|
942
|
+
{
|
|
943
|
+
name: 'cloudinary-cloud-name',
|
|
944
|
+
defaultValues: {},
|
|
945
|
+
},
|
|
946
|
+
{
|
|
947
|
+
name: 'cloudinary-public-id',
|
|
948
|
+
defaultValues: {},
|
|
949
|
+
},
|
|
950
|
+
{
|
|
951
|
+
name: 'media-selector-3-resolutions',
|
|
952
|
+
defaultValues: {
|
|
953
|
+
all: 'main-sample',
|
|
954
|
+
},
|
|
955
|
+
},
|
|
956
|
+
{
|
|
957
|
+
name: 'pass-through-image-attributes-alt-3-resolutions',
|
|
958
|
+
defaultValues: {},
|
|
959
|
+
},
|
|
960
|
+
{
|
|
961
|
+
name: 'pass-through-image-attributes-width-3-resolutions',
|
|
962
|
+
defaultValues: {},
|
|
963
|
+
},
|
|
964
|
+
{
|
|
965
|
+
name: 'pass-through-image-attributes-height-3-resolutions',
|
|
966
|
+
defaultValues: {},
|
|
967
|
+
},
|
|
968
|
+
{
|
|
969
|
+
name: 'pass-through-image-attributes-sizes-3-resolutions',
|
|
970
|
+
defaultValues: {},
|
|
971
|
+
},
|
|
972
|
+
{
|
|
973
|
+
name: 'pass-through-image-attributes-loading-3-resolutions',
|
|
974
|
+
defaultValues: {},
|
|
975
|
+
},
|
|
976
|
+
{
|
|
977
|
+
name: 'pass-through-image-attributes-fetchpriority-3-resolutions',
|
|
978
|
+
defaultValues: {},
|
|
979
|
+
},
|
|
980
|
+
{
|
|
981
|
+
name: 'cloudinary-transformations-3-resolutions',
|
|
982
|
+
defaultValues: {},
|
|
983
|
+
},
|
|
984
|
+
];
|
|
985
|
+
|
|
986
|
+
const attributeEditModeConfigs$7 = [
|
|
987
|
+
{
|
|
988
|
+
name: 'cloudinary-cloud-name',
|
|
989
|
+
heading: 'Cloudinary Cloud Name',
|
|
990
|
+
subHeading: 'Set the Cloudinary cloud name for 3 different device resolutions',
|
|
991
|
+
defaultValues: {},
|
|
992
|
+
// Filled by Media Selector / team defaults — not something marketers edit by hand.
|
|
993
|
+
hiddenToUser: true,
|
|
994
|
+
},
|
|
995
|
+
{
|
|
996
|
+
name: 'cloudinary-public-id',
|
|
997
|
+
heading: 'Image Public ID',
|
|
998
|
+
subHeading: 'Set the Cloudinary image public ID for 3 different device resolutions',
|
|
999
|
+
defaultValues: {},
|
|
1000
|
+
// Filled by Media Selector — not something marketers edit by hand.
|
|
1001
|
+
hiddenToUser: true,
|
|
1002
|
+
},
|
|
1003
|
+
{
|
|
1004
|
+
name: 'media-selector-3-resolutions',
|
|
1005
|
+
heading: 'Media Selector',
|
|
1006
|
+
subHeading: 'Select media for 3 different device resolutions',
|
|
1007
|
+
defaultValues: {},
|
|
1008
|
+
},
|
|
1009
|
+
{
|
|
1010
|
+
name: 'pass-through-image-attributes-alt-3-resolutions',
|
|
1011
|
+
heading: 'Image Alt Text',
|
|
1012
|
+
subHeading: 'Set the image alt text for 3 different device resolutions',
|
|
1013
|
+
defaultValues: {},
|
|
1014
|
+
},
|
|
1015
|
+
{
|
|
1016
|
+
name: 'pass-through-image-attributes-width-3-resolutions',
|
|
1017
|
+
heading: 'Image Intrinsic Width',
|
|
1018
|
+
subHeading: 'Set the intrinsic pixel width (with height, reserves layout space before the image loads) for 3 different device resolutions',
|
|
1019
|
+
defaultValues: {},
|
|
1020
|
+
},
|
|
1021
|
+
{
|
|
1022
|
+
name: 'pass-through-image-attributes-height-3-resolutions',
|
|
1023
|
+
heading: 'Image Intrinsic Height',
|
|
1024
|
+
subHeading: 'Set the intrinsic pixel height (with width, reserves layout space before the image loads) for 3 different device resolutions',
|
|
1025
|
+
defaultValues: {},
|
|
1026
|
+
},
|
|
1027
|
+
{
|
|
1028
|
+
name: 'pass-through-image-attributes-sizes-3-resolutions',
|
|
1029
|
+
heading: 'Image Sizes',
|
|
1030
|
+
subHeading: 'Set the sizes attribute (e.g. "(max-width: 600px) 100vw, 600px") so the browser downloads the right srcset variant, for 3 different device resolutions',
|
|
1031
|
+
defaultValues: {},
|
|
1032
|
+
},
|
|
1033
|
+
{
|
|
1034
|
+
name: 'pass-through-image-attributes-loading-3-resolutions',
|
|
1035
|
+
heading: 'Image Loading',
|
|
1036
|
+
subHeading: 'Set to "lazy" for below-the-fold images (never for the main hero image), for 3 different device resolutions',
|
|
1037
|
+
defaultValues: {},
|
|
1038
|
+
},
|
|
1039
|
+
{
|
|
1040
|
+
name: 'pass-through-image-attributes-fetchpriority-3-resolutions',
|
|
1041
|
+
heading: 'Image Fetch Priority',
|
|
1042
|
+
subHeading: 'Set to "high" for the most important image on the page (e.g. the hero), for 3 different device resolutions',
|
|
1043
|
+
defaultValues: {},
|
|
1044
|
+
},
|
|
1045
|
+
{
|
|
1046
|
+
name: 'cloudinary-transformations-3-resolutions',
|
|
1047
|
+
heading: 'Cloudinary Transformations',
|
|
1048
|
+
subHeading: 'Set extra Cloudinary transformations (e.g. "c_fill,g_auto,ar_4:3"); merged over the defaults f_auto,q_auto,c_limit, for 3 different device resolutions',
|
|
1049
|
+
defaultValues: {},
|
|
1050
|
+
},
|
|
1051
|
+
];
|
|
1052
|
+
|
|
1053
|
+
const displayCardEntry$7 = {
|
|
1054
|
+
id: ComponentIdentifiers.cloudinaryAdvancedImage,
|
|
1055
|
+
resolveNoContainer: resolveNoContainer$7,
|
|
1056
|
+
resolveRendersOwnElement: resolveRendersOwnElement$7,
|
|
1057
|
+
card: CloudinaryAdvancedImageComponent,
|
|
1058
|
+
defaultAttributeValues: defaultAttributeValues$7,
|
|
1059
|
+
attributeEditModeConfigs: attributeEditModeConfigs$7,
|
|
1060
|
+
};
|
|
1061
|
+
|
|
1062
|
+
// Builds media-host delivery URLs directly so the component renders a complete
|
|
1063
|
+
// <video src/poster> during server rendering (no browser-only SDK).
|
|
1064
|
+
//
|
|
1065
|
+
// Delivery choices for web video:
|
|
1066
|
+
// - `.mp4` extension: every uploaded source format (webm, mov, …) is
|
|
1067
|
+
// transcoded to progressive MP4, which plays in every browser.
|
|
1068
|
+
// - `vc_auto,q_auto`: the host picks the best codec and quality for the
|
|
1069
|
+
// requested container.
|
|
1070
|
+
// - `c_limit,w_1920`: scales oversized originals down to a web-appropriate
|
|
1071
|
+
// width but never upscales.
|
|
1072
|
+
const defaultVideoTransformations = {
|
|
1073
|
+
q: 'auto',
|
|
1074
|
+
vc: 'auto',
|
|
1075
|
+
c: 'limit',
|
|
1076
|
+
w: 1920,
|
|
1077
|
+
};
|
|
1078
|
+
// Poster is a single still frame: take the first frame (so_0), same
|
|
1079
|
+
// downscale cap as the video itself. Format comes from the .jpg extension.
|
|
1080
|
+
const defaultPosterTransformations = {
|
|
1081
|
+
so: 0,
|
|
1082
|
+
q: 'auto',
|
|
1083
|
+
c: 'limit',
|
|
1084
|
+
w: 1920,
|
|
1085
|
+
};
|
|
1086
|
+
// Custom posters are uploaded images, so they get the image pipeline's
|
|
1087
|
+
// automatic format/quality.
|
|
1088
|
+
const defaultCustomPosterTransformations = {
|
|
1089
|
+
f: 'auto',
|
|
1090
|
+
q: 'auto',
|
|
1091
|
+
c: 'limit',
|
|
1092
|
+
w: 1920,
|
|
1093
|
+
};
|
|
1094
|
+
function serializeTransformations(transformations) {
|
|
1095
|
+
return Object.entries(transformations)
|
|
1096
|
+
.map(([key, value]) => (value === '' ? key : `${key}_${value}`))
|
|
1097
|
+
.join(',');
|
|
1098
|
+
}
|
|
1099
|
+
// Accepts the raw string form ("c_fill,ar_16:9") and parses it into an object
|
|
1100
|
+
// so it merges over the defaults the same way object input does.
|
|
1101
|
+
function parseTransformationString(value) {
|
|
1102
|
+
const result = {};
|
|
1103
|
+
value.split(',').forEach((entry) => {
|
|
1104
|
+
const trimmed = entry.trim();
|
|
1105
|
+
if (!trimmed) {
|
|
1106
|
+
return;
|
|
1107
|
+
}
|
|
1108
|
+
const separatorIndex = trimmed.indexOf('_');
|
|
1109
|
+
if (separatorIndex <= 0) {
|
|
1110
|
+
// Flag-style component with no value (e.g. "so_0" handled above; keep
|
|
1111
|
+
// unknown flags as bare keys so they serialize unchanged).
|
|
1112
|
+
result[trimmed] = '';
|
|
1113
|
+
return;
|
|
1114
|
+
}
|
|
1115
|
+
result[trimmed.slice(0, separatorIndex)] = trimmed.slice(separatorIndex + 1);
|
|
1116
|
+
});
|
|
1117
|
+
return result;
|
|
1118
|
+
}
|
|
1119
|
+
function mergeTransformations(defaults, transformations) {
|
|
1120
|
+
const userTransformations = typeof transformations === 'string'
|
|
1121
|
+
? parseTransformationString(transformations)
|
|
1122
|
+
: transformations;
|
|
1123
|
+
return { ...defaults, ...userTransformations };
|
|
1124
|
+
}
|
|
1125
|
+
function buildVideoSrc(cloudName, publicId, transformations) {
|
|
1126
|
+
const merged = mergeTransformations(defaultVideoTransformations, transformations);
|
|
1127
|
+
// encodeURI preserves the slashes that are part of public IDs
|
|
1128
|
+
// (e.g. "videos/<uuid>/<id>") while escaping unsafe characters
|
|
1129
|
+
return encodeURI(`https://res.cloudinary.com/${cloudName}/video/upload/${serializeTransformations(merged)}/${publicId}.mp4`);
|
|
1130
|
+
}
|
|
1131
|
+
// First frame of the video itself — the zero-configuration poster.
|
|
1132
|
+
function buildVideoPosterSrc(cloudName, publicId) {
|
|
1133
|
+
return encodeURI(`https://res.cloudinary.com/${cloudName}/video/upload/${serializeTransformations(defaultPosterTransformations)}/${publicId}.jpg`);
|
|
1134
|
+
}
|
|
1135
|
+
// A separately uploaded image chosen as the poster.
|
|
1136
|
+
function buildCustomPosterSrc(cloudName, posterPublicId) {
|
|
1137
|
+
return encodeURI(`https://res.cloudinary.com/${cloudName}/image/upload/${serializeTransformations(defaultCustomPosterTransformations)}/${posterPublicId}`);
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1140
|
+
const BOOLEAN_VIDEO_ATTRIBUTES = new Set(['controls', 'autoplay', 'loop', 'muted', 'playsinline']);
|
|
1141
|
+
const coerceBoolean = (value) => value === true || value === 'true';
|
|
1142
|
+
class RVideoComponent {
|
|
1143
|
+
constructor() {
|
|
1144
|
+
this.cloudName = 'rulecms';
|
|
1145
|
+
this.publicId = 'main-sample-video';
|
|
1146
|
+
}
|
|
1147
|
+
get finalCloudName() {
|
|
1148
|
+
return this.componentAttributes?.['video-host-cloud-name'] || this.cloudName;
|
|
1149
|
+
}
|
|
1150
|
+
get finalPublicId() {
|
|
1151
|
+
return (this.componentAttributes?.['video-selector-3-resolutions'] ||
|
|
1152
|
+
this.componentAttributes?.['video-public-id'] ||
|
|
1153
|
+
this.publicId);
|
|
1154
|
+
}
|
|
1155
|
+
get finalTransformations() {
|
|
1156
|
+
return this.componentAttributes?.['video-transformations-3-resolutions'] || this.transformations;
|
|
1157
|
+
}
|
|
1158
|
+
get src() {
|
|
1159
|
+
return buildVideoSrc(this.finalCloudName, this.finalPublicId, this.finalTransformations);
|
|
1160
|
+
}
|
|
1161
|
+
get videoAttributes() {
|
|
1162
|
+
const posterPublicId = this.componentAttributes?.['video-poster-selector-3-resolutions'];
|
|
1163
|
+
const videoAttributes = {
|
|
1164
|
+
controls: true,
|
|
1165
|
+
playsinline: true,
|
|
1166
|
+
preload: 'metadata',
|
|
1167
|
+
poster: posterPublicId
|
|
1168
|
+
? buildCustomPosterSrc(this.finalCloudName, posterPublicId)
|
|
1169
|
+
: buildVideoPosterSrc(this.finalCloudName, this.finalPublicId),
|
|
1170
|
+
};
|
|
1171
|
+
const videoStyles = {
|
|
1172
|
+
maxWidth: '100%',
|
|
1173
|
+
height: 'auto',
|
|
1174
|
+
};
|
|
1175
|
+
if (this.componentAttributes) {
|
|
1176
|
+
Object.keys(this.componentAttributes).forEach((keyWithDimensionSuffix) => {
|
|
1177
|
+
const value = this.componentAttributes[keyWithDimensionSuffix];
|
|
1178
|
+
if (value === undefined || value === null) {
|
|
1179
|
+
return;
|
|
1180
|
+
}
|
|
1181
|
+
const key = keyWithDimensionSuffix.endsWith('-3-resolutions')
|
|
1182
|
+
? keyWithDimensionSuffix.slice(0, -'-3-resolutions'.length)
|
|
1183
|
+
: keyWithDimensionSuffix;
|
|
1184
|
+
if (key.startsWith('pass-through-video-attributes-style-')) {
|
|
1185
|
+
videoStyles[key.slice('pass-through-video-attributes-style-'.length)] = value;
|
|
1186
|
+
}
|
|
1187
|
+
else if (key.startsWith('pass-through-video-attributes-')) {
|
|
1188
|
+
const attributeName = key.slice('pass-through-video-attributes-'.length);
|
|
1189
|
+
if (BOOLEAN_VIDEO_ATTRIBUTES.has(attributeName)) {
|
|
1190
|
+
if (coerceBoolean(value)) {
|
|
1191
|
+
videoAttributes[attributeName] = true;
|
|
1192
|
+
}
|
|
1193
|
+
else {
|
|
1194
|
+
delete videoAttributes[attributeName];
|
|
1195
|
+
}
|
|
1196
|
+
return;
|
|
1197
|
+
}
|
|
1198
|
+
videoAttributes[attributeName] = value;
|
|
1199
|
+
}
|
|
1200
|
+
});
|
|
1201
|
+
}
|
|
1202
|
+
videoAttributes.__styles = videoStyles;
|
|
1203
|
+
return videoAttributes;
|
|
1204
|
+
}
|
|
1205
|
+
get mergedClassName() {
|
|
1206
|
+
const { className: passThroughClassName } = this.videoAttributes;
|
|
1207
|
+
return [passThroughClassName, this.className].filter(Boolean).join(' ') || undefined;
|
|
1208
|
+
}
|
|
1209
|
+
get mergedStyles() {
|
|
1210
|
+
return { ...this.videoAttributes.__styles, ...this.styleProps };
|
|
1211
|
+
}
|
|
1212
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.25", ngImport: i0, type: RVideoComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
1213
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.25", type: RVideoComponent, isStandalone: true, selector: "rulecms-r-video", inputs: { cloudName: "cloudName", publicId: "publicId", transformations: "transformations", componentAttributes: "componentAttributes", styleProps: "styleProps", className: "className" }, host: { styleAttribute: "display: contents" }, ngImport: i0, template: `
|
|
1214
|
+
<video
|
|
1215
|
+
[src]="src"
|
|
1216
|
+
[attr.poster]="videoAttributes['poster']"
|
|
1217
|
+
[attr.controls]="videoAttributes['controls'] ? true : null"
|
|
1218
|
+
[attr.autoplay]="videoAttributes['autoplay'] ? true : null"
|
|
1219
|
+
[attr.loop]="videoAttributes['loop'] ? true : null"
|
|
1220
|
+
[attr.muted]="videoAttributes['muted'] ? true : null"
|
|
1221
|
+
[attr.playsinline]="videoAttributes['playsinline'] ? true : null"
|
|
1222
|
+
[attr.preload]="videoAttributes['preload']"
|
|
1223
|
+
[attr.width]="videoAttributes['width']"
|
|
1224
|
+
[attr.height]="videoAttributes['height']"
|
|
1225
|
+
[class]="mergedClassName"
|
|
1226
|
+
[ngStyle]="mergedStyles"
|
|
1227
|
+
></video>
|
|
1228
|
+
`, isInline: true, dependencies: [{ kind: "directive", type: NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }] }); }
|
|
1229
|
+
}
|
|
1230
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.25", ngImport: i0, type: RVideoComponent, decorators: [{
|
|
1231
|
+
type: Component,
|
|
1232
|
+
args: [{
|
|
1233
|
+
selector: 'rulecms-r-video',
|
|
1234
|
+
standalone: true,
|
|
1235
|
+
imports: [NgStyle],
|
|
1236
|
+
template: `
|
|
1237
|
+
<video
|
|
1238
|
+
[src]="src"
|
|
1239
|
+
[attr.poster]="videoAttributes['poster']"
|
|
1240
|
+
[attr.controls]="videoAttributes['controls'] ? true : null"
|
|
1241
|
+
[attr.autoplay]="videoAttributes['autoplay'] ? true : null"
|
|
1242
|
+
[attr.loop]="videoAttributes['loop'] ? true : null"
|
|
1243
|
+
[attr.muted]="videoAttributes['muted'] ? true : null"
|
|
1244
|
+
[attr.playsinline]="videoAttributes['playsinline'] ? true : null"
|
|
1245
|
+
[attr.preload]="videoAttributes['preload']"
|
|
1246
|
+
[attr.width]="videoAttributes['width']"
|
|
1247
|
+
[attr.height]="videoAttributes['height']"
|
|
1248
|
+
[class]="mergedClassName"
|
|
1249
|
+
[ngStyle]="mergedStyles"
|
|
1250
|
+
></video>
|
|
1251
|
+
`,
|
|
1252
|
+
host: { style: 'display: contents' },
|
|
1253
|
+
}]
|
|
1254
|
+
}], propDecorators: { cloudName: [{
|
|
1255
|
+
type: Input
|
|
1256
|
+
}], publicId: [{
|
|
1257
|
+
type: Input
|
|
1258
|
+
}], transformations: [{
|
|
1259
|
+
type: Input
|
|
1260
|
+
}], componentAttributes: [{
|
|
1261
|
+
type: Input
|
|
1262
|
+
}], styleProps: [{
|
|
1263
|
+
type: Input
|
|
1264
|
+
}], className: [{
|
|
1265
|
+
type: Input
|
|
1266
|
+
}] } });
|
|
1267
|
+
|
|
1268
|
+
/**
|
|
1269
|
+
* The component spreads styleProps onto its `<video>`, a replaced element that
|
|
1270
|
+
* honors min/max sizing, margin and padding directly — a wrapper div would
|
|
1271
|
+
* only add a redundant element. There is no configuration that changes this,
|
|
1272
|
+
* so the answer is the same for every render.
|
|
1273
|
+
*/
|
|
1274
|
+
const resolveNoContainer$6 = () => false;
|
|
1275
|
+
|
|
1276
|
+
/**
|
|
1277
|
+
* Always renders a `<video>`, which can carry a className. There is no
|
|
1278
|
+
* configuration that changes this.
|
|
1279
|
+
*/
|
|
1280
|
+
const resolveRendersOwnElement$6 = () => true;
|
|
1281
|
+
|
|
1282
|
+
const defaultAttributeValues$6 = [
|
|
1283
|
+
{
|
|
1284
|
+
name: 'video-host-cloud-name',
|
|
1285
|
+
defaultValues: {},
|
|
1286
|
+
},
|
|
1287
|
+
{
|
|
1288
|
+
name: 'video-public-id',
|
|
1289
|
+
defaultValues: {},
|
|
1290
|
+
},
|
|
1291
|
+
{
|
|
1292
|
+
name: 'video-selector-3-resolutions',
|
|
1293
|
+
defaultValues: {
|
|
1294
|
+
all: 'main-sample-video',
|
|
1295
|
+
},
|
|
1296
|
+
},
|
|
1297
|
+
{
|
|
1298
|
+
name: 'video-poster-selector-3-resolutions',
|
|
1299
|
+
defaultValues: {},
|
|
1300
|
+
},
|
|
1301
|
+
{
|
|
1302
|
+
name: 'pass-through-video-attributes-controls-3-resolutions',
|
|
1303
|
+
defaultValues: {},
|
|
1304
|
+
},
|
|
1305
|
+
{
|
|
1306
|
+
name: 'pass-through-video-attributes-autoplay-3-resolutions',
|
|
1307
|
+
defaultValues: {},
|
|
1308
|
+
},
|
|
1309
|
+
{
|
|
1310
|
+
name: 'pass-through-video-attributes-loop-3-resolutions',
|
|
1311
|
+
defaultValues: {},
|
|
1312
|
+
},
|
|
1313
|
+
{
|
|
1314
|
+
name: 'pass-through-video-attributes-muted-3-resolutions',
|
|
1315
|
+
defaultValues: {},
|
|
1316
|
+
},
|
|
1317
|
+
{
|
|
1318
|
+
name: 'pass-through-video-attributes-playsinline-3-resolutions',
|
|
1319
|
+
defaultValues: {},
|
|
1320
|
+
},
|
|
1321
|
+
{
|
|
1322
|
+
name: 'pass-through-video-attributes-preload-3-resolutions',
|
|
1323
|
+
defaultValues: {},
|
|
1324
|
+
},
|
|
1325
|
+
{
|
|
1326
|
+
name: 'pass-through-video-attributes-width-3-resolutions',
|
|
1327
|
+
defaultValues: {},
|
|
1328
|
+
},
|
|
1329
|
+
{
|
|
1330
|
+
name: 'pass-through-video-attributes-height-3-resolutions',
|
|
1331
|
+
defaultValues: {},
|
|
1332
|
+
},
|
|
1333
|
+
{
|
|
1334
|
+
name: 'video-transformations-3-resolutions',
|
|
1335
|
+
defaultValues: {},
|
|
1336
|
+
},
|
|
1337
|
+
];
|
|
1338
|
+
|
|
1339
|
+
const attributeEditModeConfigs$6 = [
|
|
1340
|
+
{
|
|
1341
|
+
name: 'video-host-cloud-name',
|
|
1342
|
+
heading: 'Video Host Cloud Name',
|
|
1343
|
+
subHeading: 'Set the media host cloud name for 3 different device resolutions',
|
|
1344
|
+
defaultValues: {},
|
|
1345
|
+
// Filled by Video Selector / team defaults — not something marketers edit by hand.
|
|
1346
|
+
hiddenToUser: true,
|
|
1347
|
+
},
|
|
1348
|
+
{
|
|
1349
|
+
name: 'video-public-id',
|
|
1350
|
+
heading: 'Video Public ID',
|
|
1351
|
+
subHeading: 'Set the video public ID for 3 different device resolutions',
|
|
1352
|
+
defaultValues: {},
|
|
1353
|
+
// Filled by Video Selector — not something marketers edit by hand.
|
|
1354
|
+
hiddenToUser: true,
|
|
1355
|
+
},
|
|
1356
|
+
{
|
|
1357
|
+
name: 'video-selector-3-resolutions',
|
|
1358
|
+
heading: 'Video Selector',
|
|
1359
|
+
subHeading: 'Select a video for 3 different device resolutions',
|
|
1360
|
+
defaultValues: {},
|
|
1361
|
+
},
|
|
1362
|
+
{
|
|
1363
|
+
name: 'video-poster-selector-3-resolutions',
|
|
1364
|
+
heading: 'Poster Image',
|
|
1365
|
+
subHeading: 'Select an uploaded image shown before playback; defaults to the video\'s first frame, for 3 different device resolutions',
|
|
1366
|
+
defaultValues: {},
|
|
1367
|
+
},
|
|
1368
|
+
{
|
|
1369
|
+
name: 'pass-through-video-attributes-controls-3-resolutions',
|
|
1370
|
+
heading: 'Video Controls',
|
|
1371
|
+
subHeading: 'Show the browser playback controls (default "true"; set "false" for background/ambient video), for 3 different device resolutions',
|
|
1372
|
+
defaultValues: {},
|
|
1373
|
+
},
|
|
1374
|
+
{
|
|
1375
|
+
name: 'pass-through-video-attributes-autoplay-3-resolutions',
|
|
1376
|
+
heading: 'Video Autoplay',
|
|
1377
|
+
subHeading: 'Start playback automatically (browsers require muted "true" for autoplay to work), for 3 different device resolutions',
|
|
1378
|
+
defaultValues: {},
|
|
1379
|
+
},
|
|
1380
|
+
{
|
|
1381
|
+
name: 'pass-through-video-attributes-loop-3-resolutions',
|
|
1382
|
+
heading: 'Video Loop',
|
|
1383
|
+
subHeading: 'Restart the video automatically when it ends, for 3 different device resolutions',
|
|
1384
|
+
defaultValues: {},
|
|
1385
|
+
},
|
|
1386
|
+
{
|
|
1387
|
+
name: 'pass-through-video-attributes-muted-3-resolutions',
|
|
1388
|
+
heading: 'Video Muted',
|
|
1389
|
+
subHeading: 'Start with the audio muted (required by browsers for autoplay), for 3 different device resolutions',
|
|
1390
|
+
defaultValues: {},
|
|
1391
|
+
},
|
|
1392
|
+
{
|
|
1393
|
+
name: 'pass-through-video-attributes-playsinline-3-resolutions',
|
|
1394
|
+
heading: 'Video Plays Inline',
|
|
1395
|
+
subHeading: 'Play within the page on mobile instead of forcing fullscreen (default "true"), for 3 different device resolutions',
|
|
1396
|
+
defaultValues: {},
|
|
1397
|
+
},
|
|
1398
|
+
{
|
|
1399
|
+
name: 'pass-through-video-attributes-preload-3-resolutions',
|
|
1400
|
+
heading: 'Video Preload',
|
|
1401
|
+
subHeading: 'How much to download before playback: "none", "metadata" (default) or "auto", for 3 different device resolutions',
|
|
1402
|
+
defaultValues: {},
|
|
1403
|
+
},
|
|
1404
|
+
{
|
|
1405
|
+
name: 'pass-through-video-attributes-width-3-resolutions',
|
|
1406
|
+
heading: 'Video Intrinsic Width',
|
|
1407
|
+
subHeading: 'Set the intrinsic pixel width (with height, reserves layout space before the video loads) for 3 different device resolutions',
|
|
1408
|
+
defaultValues: {},
|
|
1409
|
+
},
|
|
1410
|
+
{
|
|
1411
|
+
name: 'pass-through-video-attributes-height-3-resolutions',
|
|
1412
|
+
heading: 'Video Intrinsic Height',
|
|
1413
|
+
subHeading: 'Set the intrinsic pixel height (with width, reserves layout space before the video loads) for 3 different device resolutions',
|
|
1414
|
+
defaultValues: {},
|
|
1415
|
+
},
|
|
1416
|
+
{
|
|
1417
|
+
name: 'video-transformations-3-resolutions',
|
|
1418
|
+
heading: 'Video Transformations',
|
|
1419
|
+
subHeading: 'Set extra delivery transformations (e.g. "c_fill,ar_16:9"); merged over the defaults q_auto,vc_auto,c_limit,w_1920, for 3 different device resolutions',
|
|
1420
|
+
defaultValues: {},
|
|
1421
|
+
},
|
|
1422
|
+
];
|
|
1423
|
+
|
|
1424
|
+
const displayCardEntry$6 = {
|
|
1425
|
+
id: ComponentIdentifiers.rVideo,
|
|
1426
|
+
resolveNoContainer: resolveNoContainer$6,
|
|
1427
|
+
resolveRendersOwnElement: resolveRendersOwnElement$6,
|
|
1428
|
+
card: RVideoComponent,
|
|
1429
|
+
defaultAttributeValues: defaultAttributeValues$6,
|
|
1430
|
+
attributeEditModeConfigs: attributeEditModeConfigs$6,
|
|
1431
|
+
};
|
|
1432
|
+
|
|
1433
|
+
/**
|
|
1434
|
+
* The placeholder a freshly dropped icon shows before the author picks one:
|
|
1435
|
+
* Lucide's `shapes`, inlined as a literal.
|
|
1436
|
+
*
|
|
1437
|
+
* It is a literal rather than a registry lookup on purpose. Nothing on the
|
|
1438
|
+
* render path may import `lucide-static` — the default library is registered
|
|
1439
|
+
* eagerly by every consumer, so its bundle reaches every customer page.
|
|
1440
|
+
* `__tests__/default-attribute-values.test.ts` checks this literal still
|
|
1441
|
+
* matches the registry, so a drift shows up as a test failure rather than a
|
|
1442
|
+
* placeholder that quietly stops matching the real icon.
|
|
1443
|
+
*/
|
|
1444
|
+
const PLACEHOLDER_ICON_NAME = 'shapes';
|
|
1445
|
+
const PLACEHOLDER_ICON_NODE = [
|
|
1446
|
+
[
|
|
1447
|
+
'path',
|
|
1448
|
+
{ d: 'M8.3 10a.7.7 0 0 1-.626-1.079L11.4 3a.7.7 0 0 1 1.198-.043L16.3 8.9a.7.7 0 0 1-.572 1.1Z' },
|
|
1449
|
+
],
|
|
1450
|
+
['rect', { x: '3', y: '14', width: '7', height: '7', rx: '1' }],
|
|
1451
|
+
['circle', { cx: '17.5', cy: '17.5', r: '3.5' }],
|
|
1452
|
+
];
|
|
1453
|
+
const PLACEHOLDER_ICON = {
|
|
1454
|
+
name: PLACEHOLDER_ICON_NAME,
|
|
1455
|
+
iconNode: PLACEHOLDER_ICON_NODE,
|
|
1456
|
+
};
|
|
1457
|
+
const isIconNode = (value) => Array.isArray(value) &&
|
|
1458
|
+
value.length > 0 &&
|
|
1459
|
+
value.every((entry) => Array.isArray(entry) &&
|
|
1460
|
+
entry.length >= 2 &&
|
|
1461
|
+
typeof entry[0] === 'string' &&
|
|
1462
|
+
entry[1] != null &&
|
|
1463
|
+
typeof entry[1] === 'object' &&
|
|
1464
|
+
!Array.isArray(entry[1]));
|
|
1465
|
+
/**
|
|
1466
|
+
* Read geometry out of a persisted icon attribute.
|
|
1467
|
+
*
|
|
1468
|
+
* The composer writes `{ name, iconNode }` when the author picks an icon, so
|
|
1469
|
+
* every surface renders from the value alone and never needs the registry.
|
|
1470
|
+
* Anything else — a bare name string from a hand-edited config, a half-written
|
|
1471
|
+
* value — yields null and renders nothing, which is the specified behaviour
|
|
1472
|
+
* for an unresolvable icon on a published page.
|
|
1473
|
+
*/
|
|
1474
|
+
const resolveIconAttributeValue = (value) => {
|
|
1475
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
1476
|
+
return null;
|
|
1477
|
+
}
|
|
1478
|
+
const { iconNode } = value;
|
|
1479
|
+
return isIconNode(iconNode) ? iconNode : null;
|
|
1480
|
+
};
|
|
1481
|
+
|
|
1482
|
+
/** Turns persisted Lucide geometry into SVG children on `svg`. */
|
|
1483
|
+
function appendIconChildren(renderer, svg, iconNode) {
|
|
1484
|
+
iconNode.forEach(([tag, attributes]) => {
|
|
1485
|
+
const child = renderer.createElement(tag, 'svg');
|
|
1486
|
+
Object.entries(attributes).forEach(([name, value]) => {
|
|
1487
|
+
if (name !== 'key') {
|
|
1488
|
+
renderer.setAttribute(child, name, value);
|
|
1489
|
+
}
|
|
1490
|
+
});
|
|
1491
|
+
renderer.appendChild(svg, child);
|
|
1492
|
+
});
|
|
1493
|
+
}
|
|
1494
|
+
|
|
1495
|
+
class SvgIconComponent {
|
|
1496
|
+
constructor(host, renderer) {
|
|
1497
|
+
this.host = host;
|
|
1498
|
+
this.renderer = renderer;
|
|
1499
|
+
this.width = 24;
|
|
1500
|
+
this.height = 24;
|
|
1501
|
+
this.ariaHidden = true;
|
|
1502
|
+
this.a11yProps = {};
|
|
1503
|
+
}
|
|
1504
|
+
ngOnChanges() {
|
|
1505
|
+
const el = this.host.nativeElement;
|
|
1506
|
+
while (el.firstChild) {
|
|
1507
|
+
this.renderer.removeChild(el, el.firstChild);
|
|
1508
|
+
}
|
|
1509
|
+
const svg = this.renderer.createElement('svg', 'svg');
|
|
1510
|
+
this.renderer.setAttribute(svg, 'xmlns', 'http://www.w3.org/2000/svg');
|
|
1511
|
+
this.renderer.setAttribute(svg, 'width', String(this.width));
|
|
1512
|
+
this.renderer.setAttribute(svg, 'height', String(this.height));
|
|
1513
|
+
this.renderer.setAttribute(svg, 'viewBox', '0 0 24 24');
|
|
1514
|
+
this.renderer.setAttribute(svg, 'fill', 'none');
|
|
1515
|
+
this.renderer.setAttribute(svg, 'stroke', 'currentColor');
|
|
1516
|
+
this.renderer.setAttribute(svg, 'stroke-width', '2');
|
|
1517
|
+
this.renderer.setAttribute(svg, 'stroke-linecap', 'round');
|
|
1518
|
+
this.renderer.setAttribute(svg, 'stroke-linejoin', 'round');
|
|
1519
|
+
if (this.className !== undefined) {
|
|
1520
|
+
this.renderer.setAttribute(svg, 'class', this.className);
|
|
1521
|
+
}
|
|
1522
|
+
if (this.styleProps) {
|
|
1523
|
+
Object.entries(this.styleProps).forEach(([key, value]) => {
|
|
1524
|
+
if (value !== undefined && value !== null && value !== '') {
|
|
1525
|
+
this.renderer.setStyle(svg, key, String(value));
|
|
1526
|
+
}
|
|
1527
|
+
});
|
|
1528
|
+
}
|
|
1529
|
+
Object.entries(this.a11yProps).forEach(([key, value]) => {
|
|
1530
|
+
this.renderer.setAttribute(svg, key, value);
|
|
1531
|
+
});
|
|
1532
|
+
if (this.ariaHidden && Object.keys(this.a11yProps).length === 0) {
|
|
1533
|
+
this.renderer.setAttribute(svg, 'aria-hidden', 'true');
|
|
1534
|
+
}
|
|
1535
|
+
appendIconChildren(this.renderer, svg, this.iconNode);
|
|
1536
|
+
this.renderer.appendChild(el, svg);
|
|
1537
|
+
}
|
|
1538
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.25", ngImport: i0, type: SvgIconComponent, deps: [{ token: i0.ElementRef }, { token: i0.Renderer2 }], target: i0.ɵɵFactoryTarget.Component }); }
|
|
1539
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.25", type: SvgIconComponent, isStandalone: true, selector: "rulecms-svg-icon", inputs: { iconNode: "iconNode", width: "width", height: "height", className: "className", styleProps: "styleProps", ariaHidden: "ariaHidden", a11yProps: "a11yProps" }, host: { styleAttribute: "display: contents" }, usesOnChanges: true, ngImport: i0, template: '', isInline: true }); }
|
|
1540
|
+
}
|
|
1541
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.25", ngImport: i0, type: SvgIconComponent, decorators: [{
|
|
1542
|
+
type: Component,
|
|
1543
|
+
args: [{
|
|
1544
|
+
selector: 'rulecms-svg-icon',
|
|
1545
|
+
standalone: true,
|
|
1546
|
+
template: '',
|
|
1547
|
+
host: { style: 'display: contents' },
|
|
1548
|
+
}]
|
|
1549
|
+
}], ctorParameters: () => [{ type: i0.ElementRef }, { type: i0.Renderer2 }], propDecorators: { iconNode: [{
|
|
1550
|
+
type: Input,
|
|
1551
|
+
args: [{ required: true }]
|
|
1552
|
+
}], width: [{
|
|
1553
|
+
type: Input
|
|
1554
|
+
}], height: [{
|
|
1555
|
+
type: Input
|
|
1556
|
+
}], className: [{
|
|
1557
|
+
type: Input
|
|
1558
|
+
}], styleProps: [{
|
|
1559
|
+
type: Input
|
|
1560
|
+
}], ariaHidden: [{
|
|
1561
|
+
type: Input
|
|
1562
|
+
}], a11yProps: [{
|
|
1563
|
+
type: Input
|
|
1564
|
+
}] } });
|
|
1565
|
+
|
|
1566
|
+
const A11Y_ATTR_KEYS = [
|
|
1567
|
+
'aria-label',
|
|
1568
|
+
'aria-labelledby',
|
|
1569
|
+
'aria-describedby',
|
|
1570
|
+
'role',
|
|
1571
|
+
'title',
|
|
1572
|
+
];
|
|
1573
|
+
class RIconComponent {
|
|
1574
|
+
get iconNode() {
|
|
1575
|
+
const value = this.componentAttributes?.['icon-name-3-resolutions'];
|
|
1576
|
+
return value == null ? PLACEHOLDER_ICON_NODE : resolveIconAttributeValue(value);
|
|
1577
|
+
}
|
|
1578
|
+
get a11yProps() {
|
|
1579
|
+
if (!this.componentAttributes)
|
|
1580
|
+
return {};
|
|
1581
|
+
const a11yProps = {};
|
|
1582
|
+
for (const key of A11Y_ATTR_KEYS) {
|
|
1583
|
+
const value = this.componentAttributes[key];
|
|
1584
|
+
if (typeof value === 'string' && value.length > 0) {
|
|
1585
|
+
a11yProps[key] = value;
|
|
1586
|
+
}
|
|
1587
|
+
}
|
|
1588
|
+
return a11yProps;
|
|
1589
|
+
}
|
|
1590
|
+
get hasA11yProp() {
|
|
1591
|
+
return Object.keys(this.a11yProps).length > 0;
|
|
1592
|
+
}
|
|
1593
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.25", ngImport: i0, type: RIconComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
1594
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.2.25", type: RIconComponent, isStandalone: true, selector: "rulecms-r-icon", inputs: { componentAttributes: "componentAttributes", styleProps: "styleProps", className: "className" }, host: { styleAttribute: "display: contents" }, ngImport: i0, template: `
|
|
1595
|
+
@if (iconNode) {
|
|
1596
|
+
<rulecms-svg-icon
|
|
1597
|
+
[iconNode]="iconNode"
|
|
1598
|
+
[className]="className"
|
|
1599
|
+
[styleProps]="styleProps"
|
|
1600
|
+
[a11yProps]="a11yProps"
|
|
1601
|
+
[ariaHidden]="!hasA11yProp"
|
|
1602
|
+
/>
|
|
1603
|
+
}
|
|
1604
|
+
`, isInline: true, dependencies: [{ kind: "component", type: SvgIconComponent, selector: "rulecms-svg-icon", inputs: ["iconNode", "width", "height", "className", "styleProps", "ariaHidden", "a11yProps"] }] }); }
|
|
1605
|
+
}
|
|
1606
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.25", ngImport: i0, type: RIconComponent, decorators: [{
|
|
1607
|
+
type: Component,
|
|
1608
|
+
args: [{
|
|
1609
|
+
selector: 'rulecms-r-icon',
|
|
1610
|
+
standalone: true,
|
|
1611
|
+
imports: [SvgIconComponent],
|
|
1612
|
+
template: `
|
|
1613
|
+
@if (iconNode) {
|
|
1614
|
+
<rulecms-svg-icon
|
|
1615
|
+
[iconNode]="iconNode"
|
|
1616
|
+
[className]="className"
|
|
1617
|
+
[styleProps]="styleProps"
|
|
1618
|
+
[a11yProps]="a11yProps"
|
|
1619
|
+
[ariaHidden]="!hasA11yProp"
|
|
1620
|
+
/>
|
|
1621
|
+
}
|
|
1622
|
+
`,
|
|
1623
|
+
host: { style: 'display: contents' },
|
|
1624
|
+
}]
|
|
1625
|
+
}], propDecorators: { componentAttributes: [{
|
|
1626
|
+
type: Input
|
|
1627
|
+
}], styleProps: [{
|
|
1628
|
+
type: Input
|
|
1629
|
+
}], className: [{
|
|
1630
|
+
type: Input
|
|
1631
|
+
}] } });
|
|
1632
|
+
|
|
1633
|
+
/**
|
|
1634
|
+
* Always renders an `<svg>`, a replaced element that honours sizing, margin
|
|
1635
|
+
* and colour (via currentColor) directly, so a wrapper div would only add a
|
|
1636
|
+
* redundant element. No configuration changes that.
|
|
1637
|
+
*/
|
|
1638
|
+
const resolveNoContainer$5 = () => false;
|
|
1639
|
+
|
|
1640
|
+
/**
|
|
1641
|
+
* The `<svg>` carries the className. An unresolvable attribute renders no
|
|
1642
|
+
* element at all, but that is a data error rather than a different root type,
|
|
1643
|
+
* so the configured happy path is what this answers for.
|
|
1644
|
+
*/
|
|
1645
|
+
const resolveRendersOwnElement$5 = () => true;
|
|
1646
|
+
|
|
1647
|
+
const defaultAttributeValues$5 = [
|
|
1648
|
+
{
|
|
1649
|
+
// Holds `{ name, iconNode }`, not a bare name: the composer stamps the
|
|
1650
|
+
// geometry when the author picks the icon so the render path never needs
|
|
1651
|
+
// the icon registry.
|
|
1652
|
+
name: 'icon-name-3-resolutions',
|
|
1653
|
+
defaultValues: {
|
|
1654
|
+
all: PLACEHOLDER_ICON,
|
|
1655
|
+
},
|
|
1656
|
+
},
|
|
1657
|
+
];
|
|
1658
|
+
|
|
1659
|
+
const attributeEditModeConfigs$5 = [
|
|
1660
|
+
{
|
|
1661
|
+
name: 'icon-name-3-resolutions',
|
|
1662
|
+
heading: 'Icon',
|
|
1663
|
+
subHeading: 'Search by name or by what the icon depicts (e.g. "launch" finds the rocket), for 3 different device resolutions',
|
|
1664
|
+
defaultValues: {
|
|
1665
|
+
all: PLACEHOLDER_ICON,
|
|
1666
|
+
},
|
|
1667
|
+
},
|
|
1668
|
+
];
|
|
1669
|
+
|
|
1670
|
+
const displayCardEntry$5 = {
|
|
1671
|
+
id: ComponentIdentifiers.rIcon,
|
|
1672
|
+
resolveNoContainer: resolveNoContainer$5,
|
|
1673
|
+
resolveRendersOwnElement: resolveRendersOwnElement$5,
|
|
1674
|
+
card: RIconComponent,
|
|
1675
|
+
defaultAttributeValues: defaultAttributeValues$5,
|
|
1676
|
+
attributeEditModeConfigs: attributeEditModeConfigs$5,
|
|
1677
|
+
};
|
|
1678
|
+
|
|
1679
|
+
/**
|
|
1680
|
+
* @deprecated Use `parseInlineMarkers` from `shared/inline-markup`.
|
|
1681
|
+
*
|
|
1682
|
+
* The link-only view of the grammar, kept because it is public API. Markers now
|
|
1683
|
+
* also carry marks (`{b|word}`) and line breaks (`{br|}`), which this shape
|
|
1684
|
+
* cannot express, so anything that is not a linked span is flattened into text:
|
|
1685
|
+
* marked spans contribute their words without the marks, and a break
|
|
1686
|
+
* contributes a newline. That is exactly what a caller stripping markers out of
|
|
1687
|
+
* a string wants — `resolveAccordionSummary` is the one in this package — and
|
|
1688
|
+
* it is wrong for anything that means to render them.
|
|
1689
|
+
*
|
|
1690
|
+
* Adjacent text is merged, so text with no linked spans is still one run.
|
|
1691
|
+
*/
|
|
1692
|
+
function parseInlineLinkMarkers(value) {
|
|
1693
|
+
const runs = [];
|
|
1694
|
+
let pendingText = '';
|
|
1695
|
+
const flushPendingText = () => {
|
|
1696
|
+
if (pendingText !== '') {
|
|
1697
|
+
runs.push({ kind: 'text', text: pendingText });
|
|
1698
|
+
pendingText = '';
|
|
1699
|
+
}
|
|
1700
|
+
};
|
|
1701
|
+
for (const run of parseInlineMarkers(value)) {
|
|
1702
|
+
if (run.kind === 'span' && run.index !== undefined) {
|
|
1703
|
+
flushPendingText();
|
|
1704
|
+
runs.push({ kind: 'link', index: run.index, text: run.text });
|
|
1705
|
+
continue;
|
|
1706
|
+
}
|
|
1707
|
+
pendingText += run.kind === 'break' ? '\n' : run.text;
|
|
1708
|
+
}
|
|
1709
|
+
flushPendingText();
|
|
1710
|
+
return runs;
|
|
1711
|
+
}
|
|
1712
|
+
|
|
1713
|
+
/**
|
|
1714
|
+
* The preset vocabulary for `r-button`.
|
|
1715
|
+
*
|
|
1716
|
+
* Three independent axes rather than one flat list of named buttons: five
|
|
1717
|
+
* variants times four tones times five sizes is a hundred looks from three
|
|
1718
|
+
* short tables, and an author picks each axis without us having to have
|
|
1719
|
+
* anticipated the combination.
|
|
1720
|
+
*
|
|
1721
|
+
* Every preset name here is persisted customer data, exactly like an icon
|
|
1722
|
+
* name. A name that has shipped is never deleted or repurposed — add a new
|
|
1723
|
+
* one and alias the old one, or published pages change appearance on upgrade.
|
|
1724
|
+
*/
|
|
1725
|
+
/** How the button is filled and bordered. */
|
|
1726
|
+
const BUTTON_VARIANTS = ['solid', 'soft', 'outline', 'ghost', 'link'];
|
|
1727
|
+
/** Which colour family the variant draws from. */
|
|
1728
|
+
const BUTTON_TONES = ['brand', 'neutral', 'danger', 'success'];
|
|
1729
|
+
/** Type scale, padding and control height, smallest to largest. */
|
|
1730
|
+
const BUTTON_SIZES = ['xs', 'sm', 'md', 'lg', 'xl'];
|
|
1731
|
+
/**
|
|
1732
|
+
* What an unconfigured button looks like. Dropping the component on a canvas
|
|
1733
|
+
* and touching nothing must already produce something shippable.
|
|
1734
|
+
*/
|
|
1735
|
+
const DEFAULT_BUTTON_PRESET = {
|
|
1736
|
+
variant: 'solid',
|
|
1737
|
+
tone: 'brand',
|
|
1738
|
+
size: 'md',
|
|
1739
|
+
};
|
|
1740
|
+
const isButtonVariant = (value) => BUTTON_VARIANTS.includes(value);
|
|
1741
|
+
const isButtonTone = (value) => BUTTON_TONES.includes(value);
|
|
1742
|
+
const isButtonSize = (value) => BUTTON_SIZES.includes(value);
|
|
1743
|
+
/**
|
|
1744
|
+
* Defaults only. Every one of these reaches the page as the fallback half of a
|
|
1745
|
+
* `var(--rulecms-button-…, default)`, so a customer rebrands every button on
|
|
1746
|
+
* their site by defining the custom property once, with no per-button edits
|
|
1747
|
+
* and no composer access. See `button-css-variables.ts`.
|
|
1748
|
+
*/
|
|
1749
|
+
const TONE_PALETTES = {
|
|
1750
|
+
brand: {
|
|
1751
|
+
base: '#2563eb',
|
|
1752
|
+
hover: '#1d4ed8',
|
|
1753
|
+
active: '#1e40af',
|
|
1754
|
+
contrast: '#ffffff',
|
|
1755
|
+
border: '#2563eb',
|
|
1756
|
+
soft: '#eff6ff',
|
|
1757
|
+
softHover: '#dbeafe',
|
|
1758
|
+
softContrast: '#1d4ed8',
|
|
1759
|
+
},
|
|
1760
|
+
neutral: {
|
|
1761
|
+
base: '#334155',
|
|
1762
|
+
hover: '#1e293b',
|
|
1763
|
+
active: '#0f172a',
|
|
1764
|
+
contrast: '#ffffff',
|
|
1765
|
+
// Deliberately lighter than `base`: a secondary button whose border matches
|
|
1766
|
+
// its dark label text reads as heavy as a primary one.
|
|
1767
|
+
border: '#cbd5e1',
|
|
1768
|
+
soft: '#f1f5f9',
|
|
1769
|
+
softHover: '#e2e8f0',
|
|
1770
|
+
softContrast: '#334155',
|
|
1771
|
+
},
|
|
1772
|
+
danger: {
|
|
1773
|
+
base: '#dc2626',
|
|
1774
|
+
hover: '#b91c1c',
|
|
1775
|
+
active: '#991b1b',
|
|
1776
|
+
contrast: '#ffffff',
|
|
1777
|
+
border: '#dc2626',
|
|
1778
|
+
soft: '#fef2f2',
|
|
1779
|
+
softHover: '#fee2e2',
|
|
1780
|
+
softContrast: '#b91c1c',
|
|
1781
|
+
},
|
|
1782
|
+
success: {
|
|
1783
|
+
base: '#16a34a',
|
|
1784
|
+
hover: '#15803d',
|
|
1785
|
+
active: '#166534',
|
|
1786
|
+
contrast: '#ffffff',
|
|
1787
|
+
border: '#16a34a',
|
|
1788
|
+
soft: '#f0fdf4',
|
|
1789
|
+
softHover: '#dcfce7',
|
|
1790
|
+
softContrast: '#15803d',
|
|
1791
|
+
},
|
|
1792
|
+
};
|
|
1793
|
+
const SIZE_SCALES = {
|
|
1794
|
+
xs: {
|
|
1795
|
+
fontSize: '12px',
|
|
1796
|
+
minHeight: '28px',
|
|
1797
|
+
paddingInline: '10px',
|
|
1798
|
+
paddingBlock: '4px',
|
|
1799
|
+
borderRadius: '4px',
|
|
1800
|
+
gap: '4px',
|
|
1801
|
+
},
|
|
1802
|
+
sm: {
|
|
1803
|
+
fontSize: '14px',
|
|
1804
|
+
minHeight: '34px',
|
|
1805
|
+
paddingInline: '12px',
|
|
1806
|
+
paddingBlock: '6px',
|
|
1807
|
+
borderRadius: '6px',
|
|
1808
|
+
gap: '6px',
|
|
1809
|
+
},
|
|
1810
|
+
md: {
|
|
1811
|
+
fontSize: '16px',
|
|
1812
|
+
minHeight: '40px',
|
|
1813
|
+
paddingInline: '16px',
|
|
1814
|
+
paddingBlock: '8px',
|
|
1815
|
+
borderRadius: '8px',
|
|
1816
|
+
gap: '8px',
|
|
1817
|
+
},
|
|
1818
|
+
lg: {
|
|
1819
|
+
fontSize: '18px',
|
|
1820
|
+
minHeight: '48px',
|
|
1821
|
+
paddingInline: '20px',
|
|
1822
|
+
paddingBlock: '10px',
|
|
1823
|
+
borderRadius: '10px',
|
|
1824
|
+
gap: '8px',
|
|
1825
|
+
},
|
|
1826
|
+
xl: {
|
|
1827
|
+
fontSize: '20px',
|
|
1828
|
+
minHeight: '56px',
|
|
1829
|
+
paddingInline: '24px',
|
|
1830
|
+
paddingBlock: '12px',
|
|
1831
|
+
borderRadius: '12px',
|
|
1832
|
+
gap: '10px',
|
|
1833
|
+
},
|
|
1834
|
+
};
|
|
1835
|
+
|
|
1836
|
+
/**
|
|
1837
|
+
* Every colour a preset emits goes out as `var(--rulecms-button-…, default)`.
|
|
1838
|
+
*
|
|
1839
|
+
* This is the whole white-label story for buttons. A customer who sets
|
|
1840
|
+
*
|
|
1841
|
+
* :root { --rulecms-button-brand: #c8102e; }
|
|
1842
|
+
*
|
|
1843
|
+
* once in their own stylesheet rebrands every RuleCMS button on the site,
|
|
1844
|
+
* without touching a widget, without composer access, and without us shipping
|
|
1845
|
+
* a theme provider or a design system to their page. Authors who want one
|
|
1846
|
+
* button to differ still override it per column, as before.
|
|
1847
|
+
*/
|
|
1848
|
+
const BUTTON_CSS_VARIABLE_PREFIX = '--rulecms-button';
|
|
1849
|
+
const toneCssVariable = (tone, slot = '') => slot === ''
|
|
1850
|
+
? `${BUTTON_CSS_VARIABLE_PREFIX}-${tone}`
|
|
1851
|
+
: `${BUTTON_CSS_VARIABLE_PREFIX}-${tone}-${slot}`;
|
|
1852
|
+
/**
|
|
1853
|
+
* A `var()` reference carrying our default as the fallback, so the button is
|
|
1854
|
+
* fully styled whether or not the host defines anything.
|
|
1855
|
+
*/
|
|
1856
|
+
const cssVarWithFallback = (variableName, fallback) => `var(${variableName}, ${fallback})`;
|
|
1857
|
+
/** Shorthand for the common case: one tone slot with its palette default. */
|
|
1858
|
+
const toneColor = (tone, slot, fallback) => cssVarWithFallback(toneCssVariable(tone, slot), fallback);
|
|
1859
|
+
|
|
1860
|
+
/**
|
|
1861
|
+
* The rules that stop a native `<button>` looking like a native `<button>`.
|
|
1862
|
+
*
|
|
1863
|
+
* Two of these do most of the work. `appearance: none` removes the platform
|
|
1864
|
+
* chrome iOS Safari still paints on form controls, and `fontFamily: inherit`
|
|
1865
|
+
* opts out of the browser's refusal to inherit typography into form controls —
|
|
1866
|
+
* which is the single biggest reason an unstyled button looks foreign on a
|
|
1867
|
+
* designed page. Inheriting means the button picks up the customer's brand
|
|
1868
|
+
* typeface with no configuration at all.
|
|
1869
|
+
*/
|
|
1870
|
+
const structuralStyles = {
|
|
1871
|
+
appearance: 'none',
|
|
1872
|
+
WebkitAppearance: 'none',
|
|
1873
|
+
boxSizing: 'border-box',
|
|
1874
|
+
display: 'inline-flex',
|
|
1875
|
+
alignItems: 'center',
|
|
1876
|
+
justifyContent: 'center',
|
|
1877
|
+
textAlign: 'center',
|
|
1878
|
+
textDecoration: 'none',
|
|
1879
|
+
cursor: 'pointer',
|
|
1880
|
+
userSelect: 'none',
|
|
1881
|
+
WebkitUserSelect: 'none',
|
|
1882
|
+
verticalAlign: 'middle',
|
|
1883
|
+
borderStyle: 'solid',
|
|
1884
|
+
fontFamily: 'inherit',
|
|
1885
|
+
fontWeight: 600,
|
|
1886
|
+
lineHeight: 1.25,
|
|
1887
|
+
// Inert until something changes state. Kept because the host's own `:hover`
|
|
1888
|
+
// rule — the only hover mechanism available to a widget today — then eases
|
|
1889
|
+
// for free instead of snapping.
|
|
1890
|
+
transition: 'background-color 150ms ease, border-color 150ms ease, color 150ms ease',
|
|
1891
|
+
};
|
|
1892
|
+
const sizeStyles = (preset) => {
|
|
1893
|
+
const scale = SIZE_SCALES[preset.size];
|
|
1894
|
+
return {
|
|
1895
|
+
fontSize: scale.fontSize,
|
|
1896
|
+
minHeight: scale.minHeight,
|
|
1897
|
+
// Logical properties so a right-to-left page mirrors correctly.
|
|
1898
|
+
paddingInline: scale.paddingInline,
|
|
1899
|
+
paddingBlock: scale.paddingBlock,
|
|
1900
|
+
borderRadius: scale.borderRadius,
|
|
1901
|
+
gap: scale.gap,
|
|
1902
|
+
};
|
|
1903
|
+
};
|
|
1904
|
+
/**
|
|
1905
|
+
* Every variant keeps a 1px border, transparent where it is not wanted, so
|
|
1906
|
+
* switching preset never changes the button's box and never nudges the layout
|
|
1907
|
+
* around it.
|
|
1908
|
+
*/
|
|
1909
|
+
const variantStyles = (preset) => {
|
|
1910
|
+
const palette = TONE_PALETTES[preset.tone];
|
|
1911
|
+
const tone = preset.tone;
|
|
1912
|
+
switch (preset.variant) {
|
|
1913
|
+
case 'solid':
|
|
1914
|
+
return {
|
|
1915
|
+
borderWidth: '1px',
|
|
1916
|
+
backgroundColor: toneColor(tone, '', palette.base),
|
|
1917
|
+
borderColor: toneColor(tone, '', palette.base),
|
|
1918
|
+
color: toneColor(tone, 'contrast', palette.contrast),
|
|
1919
|
+
};
|
|
1920
|
+
case 'soft':
|
|
1921
|
+
return {
|
|
1922
|
+
borderWidth: '1px',
|
|
1923
|
+
backgroundColor: toneColor(tone, 'soft', palette.soft),
|
|
1924
|
+
borderColor: 'transparent',
|
|
1925
|
+
color: toneColor(tone, 'soft-contrast', palette.softContrast),
|
|
1926
|
+
};
|
|
1927
|
+
case 'outline':
|
|
1928
|
+
return {
|
|
1929
|
+
borderWidth: '1px',
|
|
1930
|
+
backgroundColor: 'transparent',
|
|
1931
|
+
borderColor: toneColor(tone, 'border', palette.border),
|
|
1932
|
+
color: toneColor(tone, '', palette.base),
|
|
1933
|
+
};
|
|
1934
|
+
case 'ghost':
|
|
1935
|
+
return {
|
|
1936
|
+
borderWidth: '1px',
|
|
1937
|
+
backgroundColor: 'transparent',
|
|
1938
|
+
borderColor: 'transparent',
|
|
1939
|
+
color: toneColor(tone, '', palette.base),
|
|
1940
|
+
};
|
|
1941
|
+
case 'link':
|
|
1942
|
+
default:
|
|
1943
|
+
// Reads as prose, not as a control: the size scale's box is stripped
|
|
1944
|
+
// back off so it can sit in a sentence without a padded hit area.
|
|
1945
|
+
return {
|
|
1946
|
+
borderWidth: 0,
|
|
1947
|
+
backgroundColor: 'transparent',
|
|
1948
|
+
borderColor: 'transparent',
|
|
1949
|
+
color: toneColor(tone, '', palette.base),
|
|
1950
|
+
minHeight: 0,
|
|
1951
|
+
paddingInline: 0,
|
|
1952
|
+
paddingBlock: 0,
|
|
1953
|
+
borderRadius: 0,
|
|
1954
|
+
textDecoration: 'underline',
|
|
1955
|
+
textUnderlineOffset: '0.2em',
|
|
1956
|
+
};
|
|
1957
|
+
}
|
|
1958
|
+
};
|
|
1959
|
+
/**
|
|
1960
|
+
* The preset's contribution to the button's inline style.
|
|
1961
|
+
*
|
|
1962
|
+
* Callers spread column `styleProps` *after* this, which is what makes the
|
|
1963
|
+
* preset a set of defaults rather than a set of rules: anything the author
|
|
1964
|
+
* touched in Column Styles lands later in the object and wins.
|
|
1965
|
+
*/
|
|
1966
|
+
function buildButtonPresetStyles(preset) {
|
|
1967
|
+
return {
|
|
1968
|
+
...structuralStyles,
|
|
1969
|
+
...sizeStyles(preset),
|
|
1970
|
+
...variantStyles(preset),
|
|
1971
|
+
};
|
|
1972
|
+
}
|
|
1973
|
+
/** Applied on top when the button is disabled, before column styles. */
|
|
1974
|
+
const disabledStyles = {
|
|
1975
|
+
opacity: 0.5,
|
|
1976
|
+
cursor: 'not-allowed',
|
|
1977
|
+
};
|
|
1978
|
+
|
|
1979
|
+
/** The only key `r-button` reads out of the host's per-instance props. */
|
|
1980
|
+
const BUTTON_CLICK_HANDLER_PROP = 'onClick';
|
|
1981
|
+
/**
|
|
1982
|
+
* The host's click handler for this instance, or `undefined` when there is
|
|
1983
|
+
* nothing to attach.
|
|
1984
|
+
*
|
|
1985
|
+
* Two reasons to answer `undefined` even when the host supplied something:
|
|
1986
|
+
*
|
|
1987
|
+
* - **Disabled wins.** A disabled plain `<button>` is inert natively, but a
|
|
1988
|
+
* disabled *linked* button renders `<a role="link" aria-disabled="true">`,
|
|
1989
|
+
* which has no native inertness at all — a handler left on it would fire on
|
|
1990
|
+
* every click and make "disabled" a lie.
|
|
1991
|
+
* - **The bag is untyped.** It crosses a package boundary as
|
|
1992
|
+
* `{ [key: string]: any }`, so a host that passes a string here should get a
|
|
1993
|
+
* button that does nothing rather than a React crash on the customer's page.
|
|
1994
|
+
*/
|
|
1995
|
+
const resolveButtonClickHandler = ({ widgetInstanceProps, disabled, }) => {
|
|
1996
|
+
if (disabled) {
|
|
1997
|
+
return undefined;
|
|
1998
|
+
}
|
|
1999
|
+
const handler = widgetInstanceProps?.[BUTTON_CLICK_HANDLER_PROP];
|
|
2000
|
+
return typeof handler === 'function' ? handler : undefined;
|
|
2001
|
+
};
|
|
2002
|
+
|
|
2003
|
+
/**
|
|
2004
|
+
* Turns whatever the composer stored into a complete preset.
|
|
2005
|
+
*
|
|
2006
|
+
* Tolerant on every axis independently: a value saved by a newer composer, a
|
|
2007
|
+
* half-written object, or an outright wrong type costs that one axis and
|
|
2008
|
+
* nothing else. A published page must never fail to render a button because
|
|
2009
|
+
* one field went stale.
|
|
2010
|
+
*/
|
|
2011
|
+
function resolveButtonPreset(value) {
|
|
2012
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
|
|
2013
|
+
return DEFAULT_BUTTON_PRESET;
|
|
2014
|
+
}
|
|
2015
|
+
const candidate = value;
|
|
2016
|
+
return {
|
|
2017
|
+
variant: isButtonVariant(candidate.variant)
|
|
2018
|
+
? candidate.variant
|
|
2019
|
+
: DEFAULT_BUTTON_PRESET.variant,
|
|
2020
|
+
tone: isButtonTone(candidate.tone) ? candidate.tone : DEFAULT_BUTTON_PRESET.tone,
|
|
2021
|
+
size: isButtonSize(candidate.size) ? candidate.size : DEFAULT_BUTTON_PRESET.size,
|
|
2022
|
+
};
|
|
2023
|
+
}
|
|
2024
|
+
|
|
2025
|
+
/**
|
|
2026
|
+
* The attribute names `r-button` reads.
|
|
2027
|
+
*
|
|
2028
|
+
* Every one is button-specific, including the label and the icon, which are
|
|
2029
|
+
* conceptually the same thing `r-text` and `r-icon` already offer. Sharing
|
|
2030
|
+
* their attribute names would have saved two lines of editor wiring and cost
|
|
2031
|
+
* the documentation contract: RuleCMS publishes one page per component-scope
|
|
2032
|
+
* attribute and requires exactly one owning component, so a shared name has
|
|
2033
|
+
* no single page to link to from the drawer.
|
|
2034
|
+
*
|
|
2035
|
+
* Reuse happens a level down instead. The composer resolves an attribute to
|
|
2036
|
+
* an *editor* by name, and several names already share one editor, so these
|
|
2037
|
+
* point at the existing text field and the existing Lucide picker without
|
|
2038
|
+
* either component having to give up ownership of its own settings.
|
|
2039
|
+
*
|
|
2040
|
+
* Nothing here duplicates a column style attribute. Colour, border, radius,
|
|
2041
|
+
* spacing and width already have editors that write into the same bag, and a
|
|
2042
|
+
* second way to set the same CSS property would only create a question about
|
|
2043
|
+
* which one wins.
|
|
2044
|
+
*/
|
|
2045
|
+
const BUTTON_PRESET_ATTRIBUTE = 'button-preset-3-resolutions';
|
|
2046
|
+
const BUTTON_LABEL_ATTRIBUTE = 'button-label-3-resolutions';
|
|
2047
|
+
const BUTTON_LINK_ATTRIBUTE = 'button-link-3-resolutions';
|
|
2048
|
+
const BUTTON_DISABLED_ATTRIBUTE = 'button-disabled-3-resolutions';
|
|
2049
|
+
const BUTTON_ICON_ATTRIBUTE = 'button-icon-3-resolutions';
|
|
2050
|
+
const BUTTON_ICON_POSITION_ATTRIBUTE = 'button-icon-position-3-resolutions';
|
|
2051
|
+
const BUTTON_ARIA_LABEL_ATTRIBUTE = 'aria-label';
|
|
2052
|
+
/** What a freshly dropped button says before the author types anything. */
|
|
2053
|
+
const DEFAULT_BUTTON_LABEL = 'Button';
|
|
2054
|
+
const ICON_POSITIONS = ['leading', 'trailing'];
|
|
2055
|
+
const DEFAULT_ICON_POSITION = 'leading';
|
|
2056
|
+
/**
|
|
2057
|
+
* An unset label falls back to the placeholder, but a label the author
|
|
2058
|
+
* deliberately cleared stays cleared — that is how an icon-only button is
|
|
2059
|
+
* made, so an empty string cannot be treated as "nothing set".
|
|
2060
|
+
*/
|
|
2061
|
+
const resolveButtonLabel = (componentAttributes) => {
|
|
2062
|
+
const value = componentAttributes?.[BUTTON_LABEL_ATTRIBUTE];
|
|
2063
|
+
if (value === undefined || value === null) {
|
|
2064
|
+
return DEFAULT_BUTTON_LABEL;
|
|
2065
|
+
}
|
|
2066
|
+
return typeof value === 'string' ? value : String(value);
|
|
2067
|
+
};
|
|
2068
|
+
/**
|
|
2069
|
+
* Select editors in the composer store `'true'` / `'false'` as strings, while
|
|
2070
|
+
* a hand-written or migrated config may carry a real boolean. Both mean the
|
|
2071
|
+
* same thing here.
|
|
2072
|
+
*/
|
|
2073
|
+
const resolveButtonDisabled = (componentAttributes) => {
|
|
2074
|
+
const value = componentAttributes?.[BUTTON_DISABLED_ATTRIBUTE];
|
|
2075
|
+
if (typeof value === 'boolean') {
|
|
2076
|
+
return value;
|
|
2077
|
+
}
|
|
2078
|
+
return typeof value === 'string' && value.trim().toLowerCase() === 'true';
|
|
2079
|
+
};
|
|
2080
|
+
const resolveIconPosition = (componentAttributes) => {
|
|
2081
|
+
const value = componentAttributes?.[BUTTON_ICON_POSITION_ATTRIBUTE];
|
|
2082
|
+
return value === 'trailing' ? 'trailing' : DEFAULT_ICON_POSITION;
|
|
2083
|
+
};
|
|
2084
|
+
const resolveButtonAriaLabel = (componentAttributes) => {
|
|
2085
|
+
const value = componentAttributes?.[BUTTON_ARIA_LABEL_ATTRIBUTE];
|
|
2086
|
+
if (typeof value !== 'string') {
|
|
2087
|
+
return undefined;
|
|
2088
|
+
}
|
|
2089
|
+
const trimmed = value.trim();
|
|
2090
|
+
return trimmed === '' ? undefined : trimmed;
|
|
2091
|
+
};
|
|
2092
|
+
|
|
2093
|
+
class RButtonComponent {
|
|
2094
|
+
get preset() {
|
|
2095
|
+
return resolveButtonPreset(this.componentAttributes?.[BUTTON_PRESET_ATTRIBUTE]);
|
|
2096
|
+
}
|
|
2097
|
+
get disabled() {
|
|
2098
|
+
return resolveButtonDisabled(this.componentAttributes);
|
|
2099
|
+
}
|
|
2100
|
+
get style() {
|
|
2101
|
+
return {
|
|
2102
|
+
...buildButtonPresetStyles(this.preset),
|
|
2103
|
+
...(this.disabled ? disabledStyles : {}),
|
|
2104
|
+
...this.styleProps,
|
|
2105
|
+
};
|
|
2106
|
+
}
|
|
2107
|
+
get label() {
|
|
2108
|
+
return resolveButtonLabel(this.componentAttributes);
|
|
2109
|
+
}
|
|
2110
|
+
get ariaLabel() {
|
|
2111
|
+
return resolveButtonAriaLabel(this.componentAttributes);
|
|
2112
|
+
}
|
|
2113
|
+
get link() {
|
|
2114
|
+
return resolveLinkAttributes(this.componentAttributes?.[BUTTON_LINK_ATTRIBUTE]);
|
|
2115
|
+
}
|
|
2116
|
+
get iconNode() {
|
|
2117
|
+
return resolveIconAttributeValue(this.componentAttributes?.[BUTTON_ICON_ATTRIBUTE]);
|
|
2118
|
+
}
|
|
2119
|
+
get iconPosition() {
|
|
2120
|
+
return resolveIconPosition(this.componentAttributes);
|
|
2121
|
+
}
|
|
2122
|
+
handleClick(event) {
|
|
2123
|
+
const onClick = resolveButtonClickHandler({
|
|
2124
|
+
widgetInstanceProps: this.widgetInstanceProps,
|
|
2125
|
+
disabled: this.disabled,
|
|
2126
|
+
});
|
|
2127
|
+
onClick?.(event);
|
|
2128
|
+
}
|
|
2129
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.25", ngImport: i0, type: RButtonComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
2130
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.2.25", type: RButtonComponent, isStandalone: true, selector: "rulecms-r-button", inputs: { componentAttributes: "componentAttributes", styleProps: "styleProps", className: "className", widgetInstanceProps: "widgetInstanceProps" }, host: { styleAttribute: "display: contents" }, ngImport: i0, template: `
|
|
2131
|
+
@if (link && disabled) {
|
|
2132
|
+
<a [ngStyle]="style" [class]="className || undefined" role="link" aria-disabled="true" [attr.aria-label]="ariaLabel">
|
|
2133
|
+
@if (iconNode && iconPosition !== 'trailing') {
|
|
2134
|
+
<rulecms-svg-icon [iconNode]="iconNode" width="1em" height="1em" [styleProps]="{ flexShrink: 0 }" />
|
|
2135
|
+
}
|
|
2136
|
+
{{ label }}
|
|
2137
|
+
@if (iconNode && iconPosition === 'trailing') {
|
|
2138
|
+
<rulecms-svg-icon [iconNode]="iconNode" width="1em" height="1em" [styleProps]="{ flexShrink: 0 }" />
|
|
2139
|
+
}
|
|
2140
|
+
</a>
|
|
2141
|
+
} @else if (link) {
|
|
2142
|
+
<a
|
|
2143
|
+
[attr.href]="link.href"
|
|
2144
|
+
[attr.target]="link.target"
|
|
2145
|
+
[attr.rel]="link.rel"
|
|
2146
|
+
[attr.title]="link.title"
|
|
2147
|
+
[attr.aria-label]="link['aria-label'] ?? ariaLabel"
|
|
2148
|
+
[attr.download]="link.download === true ? '' : link.download"
|
|
2149
|
+
[attr.hreflang]="link.hrefLang"
|
|
2150
|
+
[attr.referrerpolicy]="link.referrerPolicy"
|
|
2151
|
+
[ngStyle]="style"
|
|
2152
|
+
[class]="className || undefined"
|
|
2153
|
+
(click)="handleClick($event)"
|
|
2154
|
+
>
|
|
2155
|
+
@if (iconNode && iconPosition !== 'trailing') {
|
|
2156
|
+
<rulecms-svg-icon [iconNode]="iconNode" width="1em" height="1em" [styleProps]="{ flexShrink: 0 }" />
|
|
2157
|
+
}
|
|
2158
|
+
{{ label }}
|
|
2159
|
+
@if (iconNode && iconPosition === 'trailing') {
|
|
2160
|
+
<rulecms-svg-icon [iconNode]="iconNode" width="1em" height="1em" [styleProps]="{ flexShrink: 0 }" />
|
|
2161
|
+
}
|
|
2162
|
+
</a>
|
|
2163
|
+
} @else {
|
|
2164
|
+
<button
|
|
2165
|
+
type="button"
|
|
2166
|
+
[disabled]="disabled"
|
|
2167
|
+
[ngStyle]="style"
|
|
2168
|
+
[class]="className || undefined"
|
|
2169
|
+
[attr.aria-label]="ariaLabel"
|
|
2170
|
+
(click)="handleClick($event)"
|
|
2171
|
+
>
|
|
2172
|
+
@if (iconNode && iconPosition !== 'trailing') {
|
|
2173
|
+
<rulecms-svg-icon [iconNode]="iconNode" width="1em" height="1em" [styleProps]="{ flexShrink: 0 }" />
|
|
2174
|
+
}
|
|
2175
|
+
{{ label }}
|
|
2176
|
+
@if (iconNode && iconPosition === 'trailing') {
|
|
2177
|
+
<rulecms-svg-icon [iconNode]="iconNode" width="1em" height="1em" [styleProps]="{ flexShrink: 0 }" />
|
|
2178
|
+
}
|
|
2179
|
+
</button>
|
|
2180
|
+
}
|
|
2181
|
+
`, isInline: true, dependencies: [{ kind: "directive", type: NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }, { kind: "component", type: SvgIconComponent, selector: "rulecms-svg-icon", inputs: ["iconNode", "width", "height", "className", "styleProps", "ariaHidden", "a11yProps"] }] }); }
|
|
2182
|
+
}
|
|
2183
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.25", ngImport: i0, type: RButtonComponent, decorators: [{
|
|
2184
|
+
type: Component,
|
|
2185
|
+
args: [{
|
|
2186
|
+
selector: 'rulecms-r-button',
|
|
2187
|
+
standalone: true,
|
|
2188
|
+
imports: [NgStyle, SvgIconComponent],
|
|
2189
|
+
template: `
|
|
2190
|
+
@if (link && disabled) {
|
|
2191
|
+
<a [ngStyle]="style" [class]="className || undefined" role="link" aria-disabled="true" [attr.aria-label]="ariaLabel">
|
|
2192
|
+
@if (iconNode && iconPosition !== 'trailing') {
|
|
2193
|
+
<rulecms-svg-icon [iconNode]="iconNode" width="1em" height="1em" [styleProps]="{ flexShrink: 0 }" />
|
|
2194
|
+
}
|
|
2195
|
+
{{ label }}
|
|
2196
|
+
@if (iconNode && iconPosition === 'trailing') {
|
|
2197
|
+
<rulecms-svg-icon [iconNode]="iconNode" width="1em" height="1em" [styleProps]="{ flexShrink: 0 }" />
|
|
2198
|
+
}
|
|
2199
|
+
</a>
|
|
2200
|
+
} @else if (link) {
|
|
2201
|
+
<a
|
|
2202
|
+
[attr.href]="link.href"
|
|
2203
|
+
[attr.target]="link.target"
|
|
2204
|
+
[attr.rel]="link.rel"
|
|
2205
|
+
[attr.title]="link.title"
|
|
2206
|
+
[attr.aria-label]="link['aria-label'] ?? ariaLabel"
|
|
2207
|
+
[attr.download]="link.download === true ? '' : link.download"
|
|
2208
|
+
[attr.hreflang]="link.hrefLang"
|
|
2209
|
+
[attr.referrerpolicy]="link.referrerPolicy"
|
|
2210
|
+
[ngStyle]="style"
|
|
2211
|
+
[class]="className || undefined"
|
|
2212
|
+
(click)="handleClick($event)"
|
|
2213
|
+
>
|
|
2214
|
+
@if (iconNode && iconPosition !== 'trailing') {
|
|
2215
|
+
<rulecms-svg-icon [iconNode]="iconNode" width="1em" height="1em" [styleProps]="{ flexShrink: 0 }" />
|
|
2216
|
+
}
|
|
2217
|
+
{{ label }}
|
|
2218
|
+
@if (iconNode && iconPosition === 'trailing') {
|
|
2219
|
+
<rulecms-svg-icon [iconNode]="iconNode" width="1em" height="1em" [styleProps]="{ flexShrink: 0 }" />
|
|
2220
|
+
}
|
|
2221
|
+
</a>
|
|
2222
|
+
} @else {
|
|
2223
|
+
<button
|
|
2224
|
+
type="button"
|
|
2225
|
+
[disabled]="disabled"
|
|
2226
|
+
[ngStyle]="style"
|
|
2227
|
+
[class]="className || undefined"
|
|
2228
|
+
[attr.aria-label]="ariaLabel"
|
|
2229
|
+
(click)="handleClick($event)"
|
|
2230
|
+
>
|
|
2231
|
+
@if (iconNode && iconPosition !== 'trailing') {
|
|
2232
|
+
<rulecms-svg-icon [iconNode]="iconNode" width="1em" height="1em" [styleProps]="{ flexShrink: 0 }" />
|
|
2233
|
+
}
|
|
2234
|
+
{{ label }}
|
|
2235
|
+
@if (iconNode && iconPosition === 'trailing') {
|
|
2236
|
+
<rulecms-svg-icon [iconNode]="iconNode" width="1em" height="1em" [styleProps]="{ flexShrink: 0 }" />
|
|
2237
|
+
}
|
|
2238
|
+
</button>
|
|
2239
|
+
}
|
|
2240
|
+
`,
|
|
2241
|
+
host: { style: 'display: contents' },
|
|
2242
|
+
}]
|
|
2243
|
+
}], propDecorators: { componentAttributes: [{
|
|
2244
|
+
type: Input
|
|
2245
|
+
}], styleProps: [{
|
|
2246
|
+
type: Input
|
|
2247
|
+
}], className: [{
|
|
2248
|
+
type: Input
|
|
2249
|
+
}], widgetInstanceProps: [{
|
|
2250
|
+
type: Input
|
|
2251
|
+
}] } });
|
|
2252
|
+
|
|
2253
|
+
/**
|
|
2254
|
+
* Always renders a `<button>` or an `<a>`, both of which take padding, border,
|
|
2255
|
+
* background and sizing directly. A wrapper div would add an element that does
|
|
2256
|
+
* nothing, and would put the column's background behind the button rather than
|
|
2257
|
+
* on it. No configuration changes that.
|
|
2258
|
+
*/
|
|
2259
|
+
const resolveNoContainer$4 = () => false;
|
|
2260
|
+
|
|
2261
|
+
/**
|
|
2262
|
+
* There is always a root element to carry the class: the button is the
|
|
2263
|
+
* component, so no configuration leaves it without one.
|
|
2264
|
+
*/
|
|
2265
|
+
const resolveRendersOwnElement$4 = () => true;
|
|
2266
|
+
|
|
2267
|
+
/**
|
|
2268
|
+
* Seeds the button's own semantics only.
|
|
2269
|
+
*
|
|
2270
|
+
* No style attribute may be seeded here, and the omission is load-bearing
|
|
2271
|
+
* rather than an oversight. Defaults are merged into the column's attribute
|
|
2272
|
+
* bag before styles are computed, so a seeded `border-radius-3-resolutions`
|
|
2273
|
+
* would be present on every button and would override the preset's radius
|
|
2274
|
+
* forever — the preset would appear to have no effect on anything a default
|
|
2275
|
+
* happened to cover. Leaving the bag empty is what lets "unset means preset"
|
|
2276
|
+
* be true.
|
|
2277
|
+
*/
|
|
2278
|
+
const defaultAttributeValues$4 = [
|
|
2279
|
+
{
|
|
2280
|
+
name: BUTTON_PRESET_ATTRIBUTE,
|
|
2281
|
+
defaultValues: {
|
|
2282
|
+
all: DEFAULT_BUTTON_PRESET,
|
|
2283
|
+
},
|
|
2284
|
+
},
|
|
2285
|
+
{
|
|
2286
|
+
name: BUTTON_LABEL_ATTRIBUTE,
|
|
2287
|
+
defaultValues: {
|
|
2288
|
+
all: DEFAULT_BUTTON_LABEL,
|
|
2289
|
+
},
|
|
2290
|
+
},
|
|
2291
|
+
];
|
|
2292
|
+
|
|
2293
|
+
/**
|
|
2294
|
+
* Every setting the button offers, in the order the Modify drawer shows them:
|
|
2295
|
+
* how it looks, what it says, where it goes, then the two refinements most
|
|
2296
|
+
* buttons never need.
|
|
2297
|
+
*
|
|
2298
|
+
* Order is not cosmetic. The drawer renders these top to bottom and the
|
|
2299
|
+
* documentation registry asserts its pages match that sequence, so moving an
|
|
2300
|
+
* entry here moves it in both places at once.
|
|
2301
|
+
*/
|
|
2302
|
+
const attributeEditModeConfigs$4 = [
|
|
2303
|
+
{
|
|
2304
|
+
name: BUTTON_PRESET_ATTRIBUTE,
|
|
2305
|
+
heading: 'Button Style',
|
|
2306
|
+
subHeading: 'Variant, tone and size. Sets how the button looks when Column Styles leave it alone — anything set there wins, for 3 different device resolutions',
|
|
2307
|
+
defaultValues: { all: DEFAULT_BUTTON_PRESET },
|
|
2308
|
+
},
|
|
2309
|
+
{
|
|
2310
|
+
name: BUTTON_LABEL_ATTRIBUTE,
|
|
2311
|
+
heading: 'Label',
|
|
2312
|
+
subHeading: 'Text shown on the button. Clear it for an icon-only button',
|
|
2313
|
+
defaultValues: {},
|
|
2314
|
+
},
|
|
2315
|
+
{
|
|
2316
|
+
name: BUTTON_LINK_ATTRIBUTE,
|
|
2317
|
+
heading: 'Link',
|
|
2318
|
+
subHeading: 'Where the button goes. Setting it renders an anchor styled as a button; leaving it empty renders a plain button, for 3 different device resolutions',
|
|
2319
|
+
defaultValues: {},
|
|
2320
|
+
},
|
|
2321
|
+
{
|
|
2322
|
+
name: BUTTON_ICON_ATTRIBUTE,
|
|
2323
|
+
heading: 'Icon',
|
|
2324
|
+
subHeading: 'Optional icon shown beside the label. Search by name or by what the icon depicts (e.g. "launch" finds the rocket), for 3 different device resolutions',
|
|
2325
|
+
defaultValues: {},
|
|
2326
|
+
},
|
|
2327
|
+
{
|
|
2328
|
+
name: BUTTON_ICON_POSITION_ATTRIBUTE,
|
|
2329
|
+
heading: 'Icon Position',
|
|
2330
|
+
subHeading: 'Whether the icon sits before or after the label',
|
|
2331
|
+
defaultValues: { all: DEFAULT_ICON_POSITION },
|
|
2332
|
+
},
|
|
2333
|
+
{
|
|
2334
|
+
name: BUTTON_DISABLED_ATTRIBUTE,
|
|
2335
|
+
heading: 'Disabled',
|
|
2336
|
+
subHeading: 'Renders the button dimmed and non-interactive',
|
|
2337
|
+
defaultValues: {},
|
|
2338
|
+
},
|
|
2339
|
+
];
|
|
2340
|
+
|
|
2341
|
+
const displayCardEntry$4 = {
|
|
2342
|
+
id: ComponentIdentifiers.rButton,
|
|
2343
|
+
resolveNoContainer: resolveNoContainer$4,
|
|
2344
|
+
resolveRendersOwnElement: resolveRendersOwnElement$4,
|
|
2345
|
+
card: RButtonComponent,
|
|
2346
|
+
defaultAttributeValues: defaultAttributeValues$4,
|
|
2347
|
+
attributeEditModeConfigs: attributeEditModeConfigs$4,
|
|
2348
|
+
};
|
|
2349
|
+
|
|
2350
|
+
/**
|
|
2351
|
+
* The attribute names `r-divider` reads.
|
|
2352
|
+
*
|
|
2353
|
+
* There are two, and the list is short on purpose. A divider is a line, and a
|
|
2354
|
+
* line has four things about it worth setting: how thick it is, whether it is
|
|
2355
|
+
* broken up, what colour it is, and how far across the column it runs. Only
|
|
2356
|
+
* the first two are here.
|
|
2357
|
+
*
|
|
2358
|
+
* Colour and width are missing because they already exist. The line is drawn
|
|
2359
|
+
* in `currentColor`, so Text Color on the column sets it the same way it sets
|
|
2360
|
+
* the colour of an icon, and the element is the column's own box, so Width on
|
|
2361
|
+
* the column shortens it. Offering a second control for either would only
|
|
2362
|
+
* raise the question of which one wins — the same reasoning that keeps colour
|
|
2363
|
+
* and border off `r-button`.
|
|
2364
|
+
*
|
|
2365
|
+
* Both remaining attributes are closed sets rather than free text, which is
|
|
2366
|
+
* what lets them go through the composer's shared select and lets the render
|
|
2367
|
+
* path treat anything unrecognised as unset. A stored value reaches a CSS
|
|
2368
|
+
* property here, so an allowlist is the boundary that keeps it from being
|
|
2369
|
+
* whatever a request happened to write.
|
|
2370
|
+
*/
|
|
2371
|
+
const DIVIDER_STYLE_ATTRIBUTE = 'divider-style-3-resolutions';
|
|
2372
|
+
const DIVIDER_THICKNESS_ATTRIBUTE = 'divider-thickness-3-resolutions';
|
|
2373
|
+
const DIVIDER_STYLES = ['solid', 'dashed', 'dotted'];
|
|
2374
|
+
const DEFAULT_DIVIDER_STYLE = 'solid';
|
|
2375
|
+
/**
|
|
2376
|
+
* Weights rather than a free measurement. Anything under a pixel disappears on
|
|
2377
|
+
* a low-density screen and anything much over eight reads as a band rather
|
|
2378
|
+
* than a rule, so the useful range is small enough to offer whole.
|
|
2379
|
+
*/
|
|
2380
|
+
const DIVIDER_THICKNESSES = ['1px', '2px', '3px', '4px', '8px'];
|
|
2381
|
+
const DEFAULT_DIVIDER_THICKNESS = '1px';
|
|
2382
|
+
const resolveDividerStyle = (componentAttributes) => {
|
|
2383
|
+
const value = componentAttributes?.[DIVIDER_STYLE_ATTRIBUTE];
|
|
2384
|
+
return DIVIDER_STYLES.includes(value) ? value : DEFAULT_DIVIDER_STYLE;
|
|
2385
|
+
};
|
|
2386
|
+
const resolveDividerThickness = (componentAttributes) => {
|
|
2387
|
+
const value = componentAttributes?.[DIVIDER_THICKNESS_ATTRIBUTE];
|
|
2388
|
+
return DIVIDER_THICKNESSES.includes(value) ? value : DEFAULT_DIVIDER_THICKNESS;
|
|
2389
|
+
};
|
|
2390
|
+
|
|
2391
|
+
/**
|
|
2392
|
+
* The rules that stop a host page's CSS reset having an opinion about the line.
|
|
2393
|
+
*
|
|
2394
|
+
* Every popular reset has already decided what an `<hr>` looks like, and they
|
|
2395
|
+
* disagree: Bootstrap draws one at a quarter opacity, older versions paint it
|
|
2396
|
+
* as a background bar with no border at all, browsers with no reset at all
|
|
2397
|
+
* still paint the inset 3D groove from the days of Netscape. A widget is
|
|
2398
|
+
* dropped into a page whose stylesheet it has never seen, so the only line
|
|
2399
|
+
* that renders the same everywhere is one whose box is stated outright.
|
|
2400
|
+
*
|
|
2401
|
+
* These are inline styles, so they win against anything the host declares —
|
|
2402
|
+
* and column styles are spread over them afterwards, so they lose to anything
|
|
2403
|
+
* the author sets.
|
|
2404
|
+
*/
|
|
2405
|
+
const hostResetStyles = {
|
|
2406
|
+
boxSizing: 'border-box',
|
|
2407
|
+
// The line is the top border. The element itself has no content, and the
|
|
2408
|
+
// other three sides are what the browser's own inset groove is made of.
|
|
2409
|
+
height: 0,
|
|
2410
|
+
borderRightStyle: 'none',
|
|
2411
|
+
borderBottomStyle: 'none',
|
|
2412
|
+
borderLeftStyle: 'none',
|
|
2413
|
+
backgroundColor: 'transparent',
|
|
2414
|
+
opacity: 1,
|
|
2415
|
+
// Spacing around a divider belongs to the row's gap and the column's margin,
|
|
2416
|
+
// the same as it does for every other component. Zero here is a decision,
|
|
2417
|
+
// not an omission: the alternative is spacing that depends on which reset
|
|
2418
|
+
// the host page happens to load.
|
|
2419
|
+
marginBlock: 0,
|
|
2420
|
+
// Only visible once the author narrows the column: an inset rule centres
|
|
2421
|
+
// rather than hugging the left edge, which is what a shortened divider is
|
|
2422
|
+
// almost always for.
|
|
2423
|
+
marginInline: 'auto',
|
|
2424
|
+
};
|
|
2425
|
+
/**
|
|
2426
|
+
* The divider's contribution to its own inline style.
|
|
2427
|
+
*
|
|
2428
|
+
* Written entirely in per-side longhands. Column styles arrive as the
|
|
2429
|
+
* all-sides `borderWidth` / `borderStyle` / `borderColor` — different keys,
|
|
2430
|
+
* which is what matters: spread later they are appended after these rather
|
|
2431
|
+
* than replacing them in place, so an author who sets Border on the column
|
|
2432
|
+
* gets a border and not a silent no-op.
|
|
2433
|
+
*/
|
|
2434
|
+
function buildDividerStyles(componentAttributes) {
|
|
2435
|
+
return {
|
|
2436
|
+
...hostResetStyles,
|
|
2437
|
+
borderTopStyle: resolveDividerStyle(componentAttributes),
|
|
2438
|
+
borderTopWidth: resolveDividerThickness(componentAttributes),
|
|
2439
|
+
// Inherited, so Text Color on the column colours the line — and a column
|
|
2440
|
+
// that says nothing gets a rule in the same ink as the words around it.
|
|
2441
|
+
borderTopColor: 'currentColor',
|
|
2442
|
+
};
|
|
2443
|
+
}
|
|
2444
|
+
|
|
2445
|
+
class RDividerComponent {
|
|
2446
|
+
get style() {
|
|
2447
|
+
return {
|
|
2448
|
+
...buildDividerStyles(this.componentAttributes),
|
|
2449
|
+
...this.styleProps,
|
|
2450
|
+
};
|
|
2451
|
+
}
|
|
2452
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.25", ngImport: i0, type: RDividerComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
2453
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.25", type: RDividerComponent, isStandalone: true, selector: "rulecms-r-divider", inputs: { componentAttributes: "componentAttributes", styleProps: "styleProps", className: "className" }, host: { styleAttribute: "display: contents" }, ngImport: i0, template: `<hr [ngStyle]="style" [class]="className || undefined" />`, isInline: true, dependencies: [{ kind: "directive", type: NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }] }); }
|
|
2454
|
+
}
|
|
2455
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.25", ngImport: i0, type: RDividerComponent, decorators: [{
|
|
2456
|
+
type: Component,
|
|
2457
|
+
args: [{
|
|
2458
|
+
selector: 'rulecms-r-divider',
|
|
2459
|
+
standalone: true,
|
|
2460
|
+
imports: [NgStyle],
|
|
2461
|
+
template: `<hr [ngStyle]="style" [class]="className || undefined" />`,
|
|
2462
|
+
host: { style: 'display: contents' },
|
|
2463
|
+
}]
|
|
2464
|
+
}], propDecorators: { componentAttributes: [{
|
|
2465
|
+
type: Input
|
|
2466
|
+
}], styleProps: [{
|
|
2467
|
+
type: Input
|
|
2468
|
+
}], className: [{
|
|
2469
|
+
type: Input
|
|
2470
|
+
}] } });
|
|
2471
|
+
|
|
2472
|
+
/**
|
|
2473
|
+
* Always renders an `<hr>`, a block-level element that holds margin, padding,
|
|
2474
|
+
* width and border on itself, so a wrapper div would only add a redundant
|
|
2475
|
+
* element. No configuration changes that.
|
|
2476
|
+
*/
|
|
2477
|
+
const resolveNoContainer$3 = () => false;
|
|
2478
|
+
|
|
2479
|
+
/**
|
|
2480
|
+
* The `<hr>` carries the className. There is always exactly one element and it
|
|
2481
|
+
* is always the root, so there is nothing for this to depend on.
|
|
2482
|
+
*/
|
|
2483
|
+
const resolveRendersOwnElement$3 = () => true;
|
|
2484
|
+
|
|
2485
|
+
/**
|
|
2486
|
+
* Seeds the divider's own two settings so the drawer opens on the values a
|
|
2487
|
+
* fresh divider is already drawn with, rather than on two empty pickers.
|
|
2488
|
+
*
|
|
2489
|
+
* Seeding them is safe here in a way it would not be for `r-button`: these
|
|
2490
|
+
* names are the divider's own, so nothing in Column Styles writes to them and
|
|
2491
|
+
* a seeded value can never shadow a style the author set.
|
|
2492
|
+
*/
|
|
2493
|
+
const defaultAttributeValues$3 = [
|
|
2494
|
+
{
|
|
2495
|
+
name: DIVIDER_STYLE_ATTRIBUTE,
|
|
2496
|
+
defaultValues: {
|
|
2497
|
+
all: DEFAULT_DIVIDER_STYLE,
|
|
2498
|
+
},
|
|
2499
|
+
},
|
|
2500
|
+
{
|
|
2501
|
+
name: DIVIDER_THICKNESS_ATTRIBUTE,
|
|
2502
|
+
defaultValues: {
|
|
2503
|
+
all: DEFAULT_DIVIDER_THICKNESS,
|
|
2504
|
+
},
|
|
2505
|
+
},
|
|
2506
|
+
];
|
|
2507
|
+
|
|
2508
|
+
/**
|
|
2509
|
+
* Both settings the divider offers, in the order the Modify drawer shows them.
|
|
2510
|
+
*
|
|
2511
|
+
* Order is not cosmetic. The drawer renders these top to bottom and the
|
|
2512
|
+
* documentation registry asserts its pages match that sequence, so moving an
|
|
2513
|
+
* entry here moves it in both places at once.
|
|
2514
|
+
*/
|
|
2515
|
+
const attributeEditModeConfigs$3 = [
|
|
2516
|
+
{
|
|
2517
|
+
name: DIVIDER_STYLE_ATTRIBUTE,
|
|
2518
|
+
heading: 'Line Style',
|
|
2519
|
+
subHeading: 'Whether the rule is unbroken or broken up. Colour comes from Text Color on the column, for 3 different device resolutions',
|
|
2520
|
+
defaultValues: { all: DEFAULT_DIVIDER_STYLE },
|
|
2521
|
+
},
|
|
2522
|
+
{
|
|
2523
|
+
name: DIVIDER_THICKNESS_ATTRIBUTE,
|
|
2524
|
+
heading: 'Thickness',
|
|
2525
|
+
subHeading: 'How heavy the rule is, from a hairline to a band',
|
|
2526
|
+
defaultValues: { all: DEFAULT_DIVIDER_THICKNESS },
|
|
2527
|
+
},
|
|
2528
|
+
];
|
|
2529
|
+
|
|
2530
|
+
const displayCardEntry$3 = {
|
|
2531
|
+
id: ComponentIdentifiers.rDivider,
|
|
2532
|
+
resolveNoContainer: resolveNoContainer$3,
|
|
2533
|
+
resolveRendersOwnElement: resolveRendersOwnElement$3,
|
|
2534
|
+
card: RDividerComponent,
|
|
2535
|
+
defaultAttributeValues: defaultAttributeValues$3,
|
|
2536
|
+
attributeEditModeConfigs: attributeEditModeConfigs$3,
|
|
2537
|
+
};
|
|
2538
|
+
|
|
2539
|
+
/**
|
|
2540
|
+
* The attribute names `r-embed` reads.
|
|
2541
|
+
*
|
|
2542
|
+
* Four, and the shortest list that still produces a usable embed: what to
|
|
2543
|
+
* frame, what to call it, what shape it is, and whether it loads before the
|
|
2544
|
+
* visitor scrolls to it.
|
|
2545
|
+
*
|
|
2546
|
+
* There is no provider attribute. The provider is derived from the URL, so
|
|
2547
|
+
* pasting is the whole interaction and an author cannot pick YouTube and then
|
|
2548
|
+
* paste a Vimeo link. There is no width, colour, border or radius either —
|
|
2549
|
+
* those are column settings, and a second route to the same CSS property only
|
|
2550
|
+
* raises the question of which one is in force.
|
|
2551
|
+
*
|
|
2552
|
+
* Nothing here can influence the frame's privileges. `sandbox`, `allow` and
|
|
2553
|
+
* `referrerpolicy` are fixed per provider in `embed-providers.ts` and are not
|
|
2554
|
+
* attributes at all, which is also why this component deliberately has no
|
|
2555
|
+
* `pass-through-embed-attributes-*` convention.
|
|
2556
|
+
*/
|
|
2557
|
+
const EMBED_URL_ATTRIBUTE = 'embed-url-3-resolutions';
|
|
2558
|
+
const EMBED_TITLE_ATTRIBUTE = 'embed-title-3-resolutions';
|
|
2559
|
+
const EMBED_ASPECT_RATIO_ATTRIBUTE = 'embed-aspect-ratio-3-resolutions';
|
|
2560
|
+
const EMBED_LOADING_ATTRIBUTE = 'embed-loading-3-resolutions';
|
|
2561
|
+
/**
|
|
2562
|
+
* Shapes rather than dimensions. An iframe has no intrinsic size to preserve,
|
|
2563
|
+
* so something has to state the proportion or the box collapses to the
|
|
2564
|
+
* browser's 300×150 default — and a ratio does it without the author having to
|
|
2565
|
+
* work out a height for every screen width.
|
|
2566
|
+
*
|
|
2567
|
+
* `none` is the escape hatch for content that has no natural shape, a booking
|
|
2568
|
+
* form being the obvious one. It is the only value that hands sizing back to
|
|
2569
|
+
* the column's Height setting.
|
|
2570
|
+
*/
|
|
2571
|
+
const EMBED_ASPECT_RATIOS = ['16:9', '4:3', '1:1', '9:16', '21:9', 'none'];
|
|
2572
|
+
/**
|
|
2573
|
+
* What an unshaped embed is tall by default. Applied by the component rather
|
|
2574
|
+
* than seeded onto the column, so the column's Height still overrides it and
|
|
2575
|
+
* clearing that returns the embed to a height that works — the same
|
|
2576
|
+
* "unset means the component's default" arrangement `r-button` uses for its
|
|
2577
|
+
* preset. Sized for a Calendly month view, which is the content that needs it.
|
|
2578
|
+
*/
|
|
2579
|
+
const EMBED_UNSHAPED_HEIGHT = '700px';
|
|
2580
|
+
const EMBED_LOADING_VALUES = ['lazy', 'eager'];
|
|
2581
|
+
/**
|
|
2582
|
+
* Lazy by default. An embed is a whole third-party document — for YouTube,
|
|
2583
|
+
* comfortably more than the rest of the page around it — and most of them sit
|
|
2584
|
+
* below the fold. `eager` exists for the ones that do not.
|
|
2585
|
+
*/
|
|
2586
|
+
const DEFAULT_EMBED_LOADING = 'lazy';
|
|
2587
|
+
/**
|
|
2588
|
+
* The aspect ratio, or `undefined` when the author has not chosen one and the
|
|
2589
|
+
* provider's own default should apply. Anything unrecognised is treated as
|
|
2590
|
+
* unset rather than passed through: this value reaches a CSS property.
|
|
2591
|
+
*/
|
|
2592
|
+
const resolveEmbedAspectRatio = (componentAttributes) => {
|
|
2593
|
+
const value = componentAttributes?.[EMBED_ASPECT_RATIO_ATTRIBUTE];
|
|
2594
|
+
return EMBED_ASPECT_RATIOS.includes(value) ? value : undefined;
|
|
2595
|
+
};
|
|
2596
|
+
const resolveEmbedLoading = (componentAttributes) => {
|
|
2597
|
+
const value = componentAttributes?.[EMBED_LOADING_ATTRIBUTE];
|
|
2598
|
+
return value === 'eager' ? 'eager' : DEFAULT_EMBED_LOADING;
|
|
2599
|
+
};
|
|
2600
|
+
/**
|
|
2601
|
+
* An iframe's accessible name. A screen reader reaching an untitled frame
|
|
2602
|
+
* announces it as "frame" and nothing else, so this never returns empty — an
|
|
2603
|
+
* author who clears the field gets the provider's name back rather than a
|
|
2604
|
+
* silent frame.
|
|
2605
|
+
*/
|
|
2606
|
+
const resolveEmbedTitle = (componentAttributes, fallback) => {
|
|
2607
|
+
const value = componentAttributes?.[EMBED_TITLE_ATTRIBUTE];
|
|
2608
|
+
if (typeof value !== 'string') {
|
|
2609
|
+
return fallback;
|
|
2610
|
+
}
|
|
2611
|
+
const trimmed = value.trim();
|
|
2612
|
+
return trimmed === '' ? fallback : trimmed;
|
|
2613
|
+
};
|
|
2614
|
+
/** CSS `aspect-ratio` wants `16 / 9`; the attribute stores the familiar `16:9`. */
|
|
2615
|
+
const toCssAspectRatio = (ratio) => ratio === 'none' ? undefined : ratio.replace(':', ' / ');
|
|
2616
|
+
|
|
2617
|
+
/**
|
|
2618
|
+
* `allow-scripts` with `allow-same-origin` looks like it cancels the sandbox,
|
|
2619
|
+
* and for a same-origin frame it would. Here the framed document is
|
|
2620
|
+
* cross-origin, so "same origin" means youtube.com's own origin, not ours: the
|
|
2621
|
+
* frame gets its own storage and cookies and still cannot reach into the
|
|
2622
|
+
* embedding page. Dropping `allow-same-origin` does not harden anything — it
|
|
2623
|
+
* gives the frame an opaque origin, which breaks every one of these providers
|
|
2624
|
+
* outright.
|
|
2625
|
+
*/
|
|
2626
|
+
const FRAMED_APP_SANDBOX = ['allow-scripts', 'allow-same-origin'];
|
|
2627
|
+
const YOUTUBE_ID = /^[A-Za-z0-9_-]{11}$/;
|
|
2628
|
+
const VIMEO_ID = /^[0-9]+$/;
|
|
2629
|
+
const VIMEO_PRIVACY_HASH = /^[A-Za-z0-9]+$/;
|
|
2630
|
+
const CALENDLY_SEGMENT = /^[A-Za-z0-9_-]+$/;
|
|
2631
|
+
const GOOGLE_MAPS_PB = /^[A-Za-z0-9!*'().~_-]+$/;
|
|
2632
|
+
/**
|
|
2633
|
+
* Calendly paths that are the scheduling app rather than someone's booking
|
|
2634
|
+
* page. Framing one gets a login screen, so they fail closed here instead.
|
|
2635
|
+
*/
|
|
2636
|
+
const CALENDLY_RESERVED_SEGMENTS = new Set([
|
|
2637
|
+
'app',
|
|
2638
|
+
'event_types',
|
|
2639
|
+
'login',
|
|
2640
|
+
'signup',
|
|
2641
|
+
'pages',
|
|
2642
|
+
'api',
|
|
2643
|
+
'help',
|
|
2644
|
+
'blog',
|
|
2645
|
+
'pricing',
|
|
2646
|
+
]);
|
|
2647
|
+
/**
|
|
2648
|
+
* Calendly's own appearance and prefill options, which authors legitimately
|
|
2649
|
+
* copy along with the booking URL. Anything else in the query is dropped
|
|
2650
|
+
* rather than forwarded — the point of rebuilding the URL is that only things
|
|
2651
|
+
* on a list get through.
|
|
2652
|
+
*/
|
|
2653
|
+
const CALENDLY_ALLOWED_PARAMS = [
|
|
2654
|
+
'hide_gdpr_banner',
|
|
2655
|
+
'hide_event_type_details',
|
|
2656
|
+
'hide_landing_page_details',
|
|
2657
|
+
'primary_color',
|
|
2658
|
+
'background_color',
|
|
2659
|
+
'text_color',
|
|
2660
|
+
'month',
|
|
2661
|
+
'date',
|
|
2662
|
+
];
|
|
2663
|
+
/** Lowercase and drop a leading `www.`, so host matching is not a trick question. */
|
|
2664
|
+
const normaliseHost = (hostname) => hostname.toLowerCase().replace(/^www\./, '');
|
|
2665
|
+
const pathSegments = (url) => url.pathname.split('/').filter((segment) => segment !== '');
|
|
2666
|
+
/**
|
|
2667
|
+
* Every YouTube URL an author is likely to have in the clipboard — the watch
|
|
2668
|
+
* page, the share link, a Short, a live stream, and an embed URL someone
|
|
2669
|
+
* copied out of another site's markup — reduced to the video id.
|
|
2670
|
+
*/
|
|
2671
|
+
const youTubeVideoId = (url) => {
|
|
2672
|
+
const segments = pathSegments(url);
|
|
2673
|
+
const host = normaliseHost(url.hostname);
|
|
2674
|
+
if (host === 'youtu.be') {
|
|
2675
|
+
return segments[0] ?? null;
|
|
2676
|
+
}
|
|
2677
|
+
if (segments[0] === 'watch') {
|
|
2678
|
+
return url.searchParams.get('v');
|
|
2679
|
+
}
|
|
2680
|
+
if (segments.length === 2 && ['embed', 'shorts', 'live', 'v'].includes(segments[0])) {
|
|
2681
|
+
return segments[1];
|
|
2682
|
+
}
|
|
2683
|
+
return null;
|
|
2684
|
+
};
|
|
2685
|
+
const EMBED_PROVIDERS = [
|
|
2686
|
+
{
|
|
2687
|
+
id: 'youtube',
|
|
2688
|
+
label: 'YouTube',
|
|
2689
|
+
inputHosts: ['youtube.com', 'm.youtube.com', 'youtu.be', 'youtube-nocookie.com'],
|
|
2690
|
+
toEmbedUrl: (url) => {
|
|
2691
|
+
const videoId = youTubeVideoId(url);
|
|
2692
|
+
if (!videoId || !YOUTUBE_ID.test(videoId)) {
|
|
2693
|
+
return null;
|
|
2694
|
+
}
|
|
2695
|
+
// youtube-nocookie.com is the same player without the tracking cookies
|
|
2696
|
+
// it would otherwise set on a visitor who never pressed play. Nothing
|
|
2697
|
+
// about the embed needs them, and a widget that quietly adds third-party
|
|
2698
|
+
// cookies to a customer's site is a consent problem we would be handing
|
|
2699
|
+
// them.
|
|
2700
|
+
return `https://www.youtube-nocookie.com/embed/${videoId}`;
|
|
2701
|
+
},
|
|
2702
|
+
sandbox: [...FRAMED_APP_SANDBOX, 'allow-presentation', 'allow-popups', 'allow-popups-to-escape-sandbox'],
|
|
2703
|
+
allow: ['accelerometer', 'clipboard-write', 'encrypted-media', 'gyroscope', 'picture-in-picture'],
|
|
2704
|
+
allowFullScreen: true,
|
|
2705
|
+
defaultAspectRatio: '16:9',
|
|
2706
|
+
defaultTitle: 'YouTube video player',
|
|
2707
|
+
},
|
|
2708
|
+
{
|
|
2709
|
+
id: 'vimeo',
|
|
2710
|
+
label: 'Vimeo',
|
|
2711
|
+
inputHosts: ['vimeo.com', 'player.vimeo.com'],
|
|
2712
|
+
toEmbedUrl: (url) => {
|
|
2713
|
+
const segments = pathSegments(url);
|
|
2714
|
+
// player.vimeo.com/video/123, vimeo.com/123, vimeo.com/channels/staffpicks/123
|
|
2715
|
+
const withoutPrefix = segments[0] === 'video' ? segments.slice(1) : segments;
|
|
2716
|
+
const numeric = withoutPrefix.filter((segment) => VIMEO_ID.test(segment));
|
|
2717
|
+
const videoId = numeric[0];
|
|
2718
|
+
if (!videoId) {
|
|
2719
|
+
return null;
|
|
2720
|
+
}
|
|
2721
|
+
// An unlisted video carries a hash, either as a trailing path segment
|
|
2722
|
+
// (vimeo.com/123/abc123) or as ?h=. Without it the player refuses to
|
|
2723
|
+
// load, so it is the one piece of the input besides the id that is
|
|
2724
|
+
// carried across.
|
|
2725
|
+
const trailing = withoutPrefix[withoutPrefix.indexOf(videoId) + 1];
|
|
2726
|
+
const hash = url.searchParams.get('h') ?? trailing;
|
|
2727
|
+
const privacyHash = hash && VIMEO_PRIVACY_HASH.test(hash) ? hash : null;
|
|
2728
|
+
return privacyHash
|
|
2729
|
+
? `https://player.vimeo.com/video/${videoId}?h=${privacyHash}`
|
|
2730
|
+
: `https://player.vimeo.com/video/${videoId}`;
|
|
2731
|
+
},
|
|
2732
|
+
sandbox: [...FRAMED_APP_SANDBOX, 'allow-presentation', 'allow-popups', 'allow-popups-to-escape-sandbox'],
|
|
2733
|
+
allow: ['autoplay', 'clipboard-write', 'encrypted-media', 'picture-in-picture'],
|
|
2734
|
+
allowFullScreen: true,
|
|
2735
|
+
defaultAspectRatio: '16:9',
|
|
2736
|
+
defaultTitle: 'Vimeo video player',
|
|
2737
|
+
},
|
|
2738
|
+
{
|
|
2739
|
+
id: 'google-maps',
|
|
2740
|
+
label: 'Google Maps',
|
|
2741
|
+
inputHosts: ['google.com', 'maps.google.com'],
|
|
2742
|
+
toEmbedUrl: (url) => {
|
|
2743
|
+
// Only the URL from "Share → Embed a map" is accepted, because it is the
|
|
2744
|
+
// only one that can be framed at all: Google serves an ordinary maps
|
|
2745
|
+
// link with X-Frame-Options and the visitor gets a blank box. Failing
|
|
2746
|
+
// here means the author is told to copy the other URL, rather than
|
|
2747
|
+
// shipping a page with a hole in it.
|
|
2748
|
+
if (pathSegments(url).join('/') !== 'maps/embed') {
|
|
2749
|
+
return null;
|
|
2750
|
+
}
|
|
2751
|
+
const pb = url.searchParams.get('pb');
|
|
2752
|
+
// `pb` is Google's own opaque camera-and-place blob. It is carried
|
|
2753
|
+
// across verbatim rather than re-encoded, because it is the one input
|
|
2754
|
+
// this file cannot regenerate from an id — so instead it is held to a
|
|
2755
|
+
// charset with no way out of a query string. Everything that could end
|
|
2756
|
+
// one, or start an attribute, is absent from this list.
|
|
2757
|
+
if (!pb || !GOOGLE_MAPS_PB.test(pb)) {
|
|
2758
|
+
return null;
|
|
2759
|
+
}
|
|
2760
|
+
return `https://www.google.com/maps/embed?pb=${pb}`;
|
|
2761
|
+
},
|
|
2762
|
+
sandbox: [...FRAMED_APP_SANDBOX, 'allow-popups', 'allow-popups-to-escape-sandbox'],
|
|
2763
|
+
allow: [],
|
|
2764
|
+
allowFullScreen: true,
|
|
2765
|
+
defaultAspectRatio: '4:3',
|
|
2766
|
+
defaultTitle: 'Map',
|
|
2767
|
+
},
|
|
2768
|
+
{
|
|
2769
|
+
id: 'calendly',
|
|
2770
|
+
label: 'Calendly',
|
|
2771
|
+
inputHosts: ['calendly.com'],
|
|
2772
|
+
toEmbedUrl: (url) => {
|
|
2773
|
+
const segments = pathSegments(url);
|
|
2774
|
+
if (segments.length === 0 || segments.length > 2) {
|
|
2775
|
+
return null;
|
|
2776
|
+
}
|
|
2777
|
+
if (!segments.every((segment) => CALENDLY_SEGMENT.test(segment))) {
|
|
2778
|
+
return null;
|
|
2779
|
+
}
|
|
2780
|
+
if (CALENDLY_RESERVED_SEGMENTS.has(segments[0].toLowerCase())) {
|
|
2781
|
+
return null;
|
|
2782
|
+
}
|
|
2783
|
+
const options = new URLSearchParams();
|
|
2784
|
+
for (const parameter of CALENDLY_ALLOWED_PARAMS) {
|
|
2785
|
+
const value = url.searchParams.get(parameter);
|
|
2786
|
+
if (value !== null) {
|
|
2787
|
+
options.set(parameter, value);
|
|
2788
|
+
}
|
|
2789
|
+
}
|
|
2790
|
+
const query = options.toString();
|
|
2791
|
+
const path = segments.join('/');
|
|
2792
|
+
return query ? `https://calendly.com/${path}?${query}` : `https://calendly.com/${path}`;
|
|
2793
|
+
},
|
|
2794
|
+
sandbox: [...FRAMED_APP_SANDBOX, 'allow-forms', 'allow-popups', 'allow-popups-to-escape-sandbox'],
|
|
2795
|
+
allow: [],
|
|
2796
|
+
allowFullScreen: false,
|
|
2797
|
+
// A booking widget is a form of unpredictable length, not a picture with a
|
|
2798
|
+
// shape. It gets a height instead of a ratio; see `embed-attributes.ts`.
|
|
2799
|
+
defaultAspectRatio: 'none',
|
|
2800
|
+
defaultTitle: 'Booking calendar',
|
|
2801
|
+
},
|
|
2802
|
+
];
|
|
2803
|
+
/**
|
|
2804
|
+
* The single place an embed URL is checked against the allowlist.
|
|
2805
|
+
*
|
|
2806
|
+
* Every caller — the component, both container resolvers, and the composer's
|
|
2807
|
+
* URL editor through the package's public exports — comes through here rather
|
|
2808
|
+
* than reading `EMBED_PROVIDERS` and matching for itself. That is what keeps
|
|
2809
|
+
* a per-app allowlist a change to one function's argument later on, instead of
|
|
2810
|
+
* a hunt for call sites: see
|
|
2811
|
+
* `rulecms/__docs__/__TODO__/__COMPONENTS__/TODO_PLATFORM_EMBED_ALLOWLIST_PER_APP.md`.
|
|
2812
|
+
*
|
|
2813
|
+
* Returns `null` for anything it cannot vouch for, which is the fail-closed
|
|
2814
|
+
* behaviour the component depends on. It never throws: a malformed URL in a
|
|
2815
|
+
* stored widget must not take down the page it was published to.
|
|
2816
|
+
*/
|
|
2817
|
+
function resolveEmbedUrl(value, providers = EMBED_PROVIDERS) {
|
|
2818
|
+
if (typeof value !== 'string' || value.trim() === '') {
|
|
2819
|
+
return null;
|
|
2820
|
+
}
|
|
2821
|
+
let url;
|
|
2822
|
+
try {
|
|
2823
|
+
url = new URL(value.trim());
|
|
2824
|
+
}
|
|
2825
|
+
catch {
|
|
2826
|
+
return null;
|
|
2827
|
+
}
|
|
2828
|
+
// https only. Every provider here serves it, and requiring it disposes of
|
|
2829
|
+
// javascript:, data:, blob: and file: as a class rather than one at a time.
|
|
2830
|
+
if (url.protocol !== 'https:') {
|
|
2831
|
+
return null;
|
|
2832
|
+
}
|
|
2833
|
+
// `https://youtube.com@evil.example/` parses with `evil.example` as the host
|
|
2834
|
+
// and the rest as credentials. The host check below is not fooled by it, but
|
|
2835
|
+
// a URL carrying credentials at all is not one an author pasted from a
|
|
2836
|
+
// provider, so it is refused outright.
|
|
2837
|
+
if (url.username !== '' || url.password !== '') {
|
|
2838
|
+
return null;
|
|
2839
|
+
}
|
|
2840
|
+
const host = normaliseHost(url.hostname);
|
|
2841
|
+
const provider = providers.find((candidate) => candidate.inputHosts.includes(host));
|
|
2842
|
+
if (!provider) {
|
|
2843
|
+
return null;
|
|
2844
|
+
}
|
|
2845
|
+
const src = provider.toEmbedUrl(url);
|
|
2846
|
+
return src ? { provider, src } : null;
|
|
2847
|
+
}
|
|
2848
|
+
/** Whether a URL would render. The composer's editor uses this for its hint. */
|
|
2849
|
+
const isAllowedEmbedUrl = (value, providers = EMBED_PROVIDERS) => resolveEmbedUrl(value, providers) !== null;
|
|
2850
|
+
|
|
2851
|
+
/**
|
|
2852
|
+
* The single resolution path, shared by `REmbed` and by both container
|
|
2853
|
+
* resolvers, in the way `r-text` shares `resolveEnclosingTag`.
|
|
2854
|
+
*
|
|
2855
|
+
* Sharing it is not tidiness. `resolveNoContainer` and
|
|
2856
|
+
* `resolveRendersOwnElement` both answer questions about an element that only
|
|
2857
|
+
* exists when a URL passed the allowlist, so if they decided that
|
|
2858
|
+
* independently of the component, a rejected URL would have the renderer
|
|
2859
|
+
* either handing column styles to an element that was never rendered or
|
|
2860
|
+
* wrapping nothing in a styled div. Neither would look wrong in the drawer, in
|
|
2861
|
+
* Redux, or in the stored JSON.
|
|
2862
|
+
*
|
|
2863
|
+
* `null` means nothing will be rendered: no URL, or one the allowlist refused.
|
|
2864
|
+
*/
|
|
2865
|
+
function resolveEmbed(componentAttributes) {
|
|
2866
|
+
const resolved = resolveEmbedUrl(componentAttributes?.[EMBED_URL_ATTRIBUTE]);
|
|
2867
|
+
if (!resolved) {
|
|
2868
|
+
return null;
|
|
2869
|
+
}
|
|
2870
|
+
const { provider, src } = resolved;
|
|
2871
|
+
return {
|
|
2872
|
+
provider,
|
|
2873
|
+
src,
|
|
2874
|
+
title: resolveEmbedTitle(componentAttributes, provider.defaultTitle),
|
|
2875
|
+
// An author's choice wins; otherwise the provider's own shape, which is
|
|
2876
|
+
// what makes a pasted map 4:3 and a pasted video 16:9 without anyone
|
|
2877
|
+
// having to know that is what they wanted.
|
|
2878
|
+
aspectRatio: resolveEmbedAspectRatio(componentAttributes) ?? provider.defaultAspectRatio,
|
|
2879
|
+
loading: resolveEmbedLoading(componentAttributes),
|
|
2880
|
+
};
|
|
2881
|
+
}
|
|
2882
|
+
/**
|
|
2883
|
+
* The frame's own appearance, before column styles are laid over it.
|
|
2884
|
+
*
|
|
2885
|
+
* `display: block` removes the descender gap an inline replaced element leaves
|
|
2886
|
+
* under itself, and the zero border removes the 2px inset ridge every browser
|
|
2887
|
+
* still draws around an iframe by default.
|
|
2888
|
+
*/
|
|
2889
|
+
const buildEmbedStyles = (aspectRatio) => ({
|
|
2890
|
+
display: 'block',
|
|
2891
|
+
width: '100%',
|
|
2892
|
+
border: 0,
|
|
2893
|
+
...(aspectRatio === 'none'
|
|
2894
|
+
? { height: EMBED_UNSHAPED_HEIGHT }
|
|
2895
|
+
: { aspectRatio: toCssAspectRatio(aspectRatio) }),
|
|
2896
|
+
});
|
|
2897
|
+
|
|
2898
|
+
class REmbedComponent {
|
|
2899
|
+
constructor(sanitizer) {
|
|
2900
|
+
this.sanitizer = sanitizer;
|
|
2901
|
+
}
|
|
2902
|
+
get embed() {
|
|
2903
|
+
return resolveEmbed(this.componentAttributes);
|
|
2904
|
+
}
|
|
2905
|
+
get safeSrc() {
|
|
2906
|
+
const embed = this.embed;
|
|
2907
|
+
return embed ? this.sanitizer.bypassSecurityTrustResourceUrl(embed.src) : null;
|
|
2908
|
+
}
|
|
2909
|
+
get style() {
|
|
2910
|
+
const embed = this.embed;
|
|
2911
|
+
if (!embed) {
|
|
2912
|
+
return this.styleProps;
|
|
2913
|
+
}
|
|
2914
|
+
return { ...buildEmbedStyles(embed.aspectRatio), ...this.styleProps };
|
|
2915
|
+
}
|
|
2916
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.25", ngImport: i0, type: REmbedComponent, deps: [{ token: i1.DomSanitizer }], target: i0.ɵɵFactoryTarget.Component }); }
|
|
2917
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.2.25", type: REmbedComponent, isStandalone: true, selector: "rulecms-r-embed", inputs: { componentAttributes: "componentAttributes", styleProps: "styleProps", className: "className" }, host: { styleAttribute: "display: contents" }, ngImport: i0, template: `
|
|
2918
|
+
@if (embed) {
|
|
2919
|
+
<iframe
|
|
2920
|
+
[src]="safeSrc"
|
|
2921
|
+
[title]="embed.title"
|
|
2922
|
+
[ngStyle]="style"
|
|
2923
|
+
[attr.sandbox]="embed.provider.sandbox.join(' ')"
|
|
2924
|
+
[attr.allow]="embed.provider.allow.length > 0 ? embed.provider.allow.join('; ') : null"
|
|
2925
|
+
[attr.allowfullscreen]="embed.provider.allowFullScreen ? true : null"
|
|
2926
|
+
referrerpolicy="strict-origin-when-cross-origin"
|
|
2927
|
+
[attr.loading]="embed.loading"
|
|
2928
|
+
[class]="className || undefined"
|
|
2929
|
+
></iframe>
|
|
2930
|
+
}
|
|
2931
|
+
`, isInline: true, dependencies: [{ kind: "directive", type: NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }] }); }
|
|
2932
|
+
}
|
|
2933
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.25", ngImport: i0, type: REmbedComponent, decorators: [{
|
|
2934
|
+
type: Component,
|
|
2935
|
+
args: [{
|
|
2936
|
+
selector: 'rulecms-r-embed',
|
|
2937
|
+
standalone: true,
|
|
2938
|
+
imports: [NgStyle],
|
|
2939
|
+
template: `
|
|
2940
|
+
@if (embed) {
|
|
2941
|
+
<iframe
|
|
2942
|
+
[src]="safeSrc"
|
|
2943
|
+
[title]="embed.title"
|
|
2944
|
+
[ngStyle]="style"
|
|
2945
|
+
[attr.sandbox]="embed.provider.sandbox.join(' ')"
|
|
2946
|
+
[attr.allow]="embed.provider.allow.length > 0 ? embed.provider.allow.join('; ') : null"
|
|
2947
|
+
[attr.allowfullscreen]="embed.provider.allowFullScreen ? true : null"
|
|
2948
|
+
referrerpolicy="strict-origin-when-cross-origin"
|
|
2949
|
+
[attr.loading]="embed.loading"
|
|
2950
|
+
[class]="className || undefined"
|
|
2951
|
+
></iframe>
|
|
2952
|
+
}
|
|
2953
|
+
`,
|
|
2954
|
+
host: { style: 'display: contents' },
|
|
2955
|
+
}]
|
|
2956
|
+
}], ctorParameters: () => [{ type: i1.DomSanitizer }], propDecorators: { componentAttributes: [{
|
|
2957
|
+
type: Input
|
|
2958
|
+
}], styleProps: [{
|
|
2959
|
+
type: Input
|
|
2960
|
+
}], className: [{
|
|
2961
|
+
type: Input
|
|
2962
|
+
}] } });
|
|
2963
|
+
|
|
2964
|
+
/**
|
|
2965
|
+
* `false` whenever an iframe is rendered: it is a replaced element that holds
|
|
2966
|
+
* margin, padding, width and height on itself, so a wrapper div would only add
|
|
2967
|
+
* an element that does nothing.
|
|
2968
|
+
*
|
|
2969
|
+
* `true` when the URL is missing or the allowlist refused it, because then
|
|
2970
|
+
* there is no element at all — the same answer `r-text` gives for bare text.
|
|
2971
|
+
* Deciding this from `resolveEmbed`, rather than from the fact that this
|
|
2972
|
+
* component "is an iframe component", is what keeps the two in step.
|
|
2973
|
+
*/
|
|
2974
|
+
const resolveNoContainer$2 = ({ componentAttributes }) => resolveEmbed(componentAttributes) === null;
|
|
2975
|
+
|
|
2976
|
+
/**
|
|
2977
|
+
* The iframe carries the className — when there is one. A rejected or unset
|
|
2978
|
+
* URL renders nothing, and a class handed to a component that renders nothing
|
|
2979
|
+
* disappears without trace, so this answers from the same resolution the
|
|
2980
|
+
* component uses.
|
|
2981
|
+
*/
|
|
2982
|
+
const resolveRendersOwnElement$2 = ({ componentAttributes, }) => resolveEmbed(componentAttributes) !== null;
|
|
2983
|
+
|
|
2984
|
+
/**
|
|
2985
|
+
* Nothing is seeded, including the URL.
|
|
2986
|
+
*
|
|
2987
|
+
* A freshly dropped embed renders nothing until an author pastes something,
|
|
2988
|
+
* which is the honest result: the alternative is shipping a placeholder video
|
|
2989
|
+
* that a customer's page would publish if anyone forgot to replace it. The
|
|
2990
|
+
* palette card and the empty drawer say what to do instead.
|
|
2991
|
+
*
|
|
2992
|
+
* Aspect ratio is left unset on purpose too, so it resolves from the provider
|
|
2993
|
+
* — a map is 4:3 and a video is 16:9 without either being written down here.
|
|
2994
|
+
*/
|
|
2995
|
+
const defaultAttributeValues$2 = [
|
|
2996
|
+
{
|
|
2997
|
+
name: 'embed-url-3-resolutions',
|
|
2998
|
+
defaultValues: {},
|
|
2999
|
+
},
|
|
3000
|
+
{
|
|
3001
|
+
name: 'embed-title-3-resolutions',
|
|
3002
|
+
defaultValues: {},
|
|
3003
|
+
},
|
|
3004
|
+
{
|
|
3005
|
+
name: 'embed-aspect-ratio-3-resolutions',
|
|
3006
|
+
defaultValues: {},
|
|
3007
|
+
},
|
|
3008
|
+
{
|
|
3009
|
+
name: 'embed-loading-3-resolutions',
|
|
3010
|
+
defaultValues: {},
|
|
3011
|
+
},
|
|
3012
|
+
];
|
|
3013
|
+
|
|
3014
|
+
/**
|
|
3015
|
+
* Drawer order, which is also the order the documentation pages are listed in:
|
|
3016
|
+
* the URL first because nothing renders without it, then the three settings
|
|
3017
|
+
* that only matter once something does.
|
|
3018
|
+
*/
|
|
3019
|
+
const attributeEditModeConfigs$2 = [
|
|
3020
|
+
{
|
|
3021
|
+
name: 'embed-url-3-resolutions',
|
|
3022
|
+
heading: 'Embed URL',
|
|
3023
|
+
subHeading: 'Paste the address of a YouTube or Vimeo video, a Google Maps "Embed a map" link, or a Calendly booking page, for 3 different device resolutions',
|
|
3024
|
+
defaultValues: {},
|
|
3025
|
+
},
|
|
3026
|
+
{
|
|
3027
|
+
name: 'embed-title-3-resolutions',
|
|
3028
|
+
heading: 'Embed Title',
|
|
3029
|
+
subHeading: 'Describe what is being embedded, for screen readers; defaults to the provider name, for 3 different device resolutions',
|
|
3030
|
+
defaultValues: {},
|
|
3031
|
+
},
|
|
3032
|
+
{
|
|
3033
|
+
name: 'embed-aspect-ratio-3-resolutions',
|
|
3034
|
+
heading: 'Embed Aspect Ratio',
|
|
3035
|
+
subHeading: 'The shape of the embed — "16:9" for video, "4:3" for maps, or "none" to set the height on the column instead, for 3 different device resolutions',
|
|
3036
|
+
defaultValues: {},
|
|
3037
|
+
},
|
|
3038
|
+
{
|
|
3039
|
+
name: 'embed-loading-3-resolutions',
|
|
3040
|
+
heading: 'Embed Loading',
|
|
3041
|
+
subHeading: 'Whether the embed loads with the page ("eager") or waits until the visitor scrolls near it ("lazy", the default), for 3 different device resolutions',
|
|
3042
|
+
defaultValues: {},
|
|
3043
|
+
},
|
|
3044
|
+
];
|
|
3045
|
+
|
|
3046
|
+
const displayCardEntry$2 = {
|
|
3047
|
+
id: ComponentIdentifiers.rEmbed,
|
|
3048
|
+
resolveNoContainer: resolveNoContainer$2,
|
|
3049
|
+
resolveRendersOwnElement: resolveRendersOwnElement$2,
|
|
3050
|
+
card: REmbedComponent,
|
|
3051
|
+
defaultAttributeValues: defaultAttributeValues$2,
|
|
3052
|
+
attributeEditModeConfigs: attributeEditModeConfigs$2,
|
|
3053
|
+
};
|
|
3054
|
+
|
|
3055
|
+
/**
|
|
3056
|
+
* The attribute names `r-list` reads.
|
|
3057
|
+
*
|
|
3058
|
+
* Every one is list-specific, including the links, which are conceptually the
|
|
3059
|
+
* same numbered list `r-text` already offers. Sharing that attribute name
|
|
3060
|
+
* would have saved one line of editor wiring and cost the documentation
|
|
3061
|
+
* contract: RuleCMS publishes one page per component-scope attribute and
|
|
3062
|
+
* requires exactly one owning component, so a shared name has no single page
|
|
3063
|
+
* to link to from the drawer. Reuse happens a level down instead — the
|
|
3064
|
+
* composer resolves an attribute to an *editor* by name, and `list-links`
|
|
3065
|
+
* points at the editor `links-3-resolutions` already resolves to.
|
|
3066
|
+
*
|
|
3067
|
+
* Nothing here duplicates a column style attribute. Spacing, colour and
|
|
3068
|
+
* typography come from Column Styles on the root element and inherit into
|
|
3069
|
+
* the items; the attributes below are only what is specific to a list —
|
|
3070
|
+
* which element it is, what the markers look like, and where numbering
|
|
3071
|
+
* starts. Kind and marker are closed sets rather than free text, which is
|
|
3072
|
+
* what lets them go through the composer's shared select and lets the render
|
|
3073
|
+
* path treat anything unrecognised as unset.
|
|
3074
|
+
*/
|
|
3075
|
+
const LIST_ITEMS_ATTRIBUTE = 'list-items-3-resolutions';
|
|
3076
|
+
const LIST_KIND_ATTRIBUTE = 'list-kind-3-resolutions';
|
|
3077
|
+
const LIST_MARKER_ATTRIBUTE = 'list-marker-3-resolutions';
|
|
3078
|
+
const LIST_START_ATTRIBUTE = 'list-start-3-resolutions';
|
|
3079
|
+
const LIST_REVERSED_ATTRIBUTE = 'list-reversed-3-resolutions';
|
|
3080
|
+
const LIST_LINKS_ATTRIBUTE = 'list-links-3-resolutions';
|
|
3081
|
+
/** What a freshly dropped list shows before the author types anything. */
|
|
3082
|
+
const DEFAULT_LIST_ITEMS = 'First item\nSecond item\nThird item';
|
|
3083
|
+
const LIST_KINDS = ['unordered', 'ordered'];
|
|
3084
|
+
const DEFAULT_LIST_KIND = 'unordered';
|
|
3085
|
+
/**
|
|
3086
|
+
* `list-style-type` values worth offering. Every entry is valid CSS on both
|
|
3087
|
+
* `<ul>` and `<ol>`, so the marker survives the author switching kind rather
|
|
3088
|
+
* than resetting. Unset means the browser default — disc for unordered,
|
|
3089
|
+
* decimal for ordered — which is why there is no default constant here: not
|
|
3090
|
+
* setting the property at all is the default.
|
|
3091
|
+
*/
|
|
3092
|
+
const LIST_MARKERS = [
|
|
3093
|
+
'disc',
|
|
3094
|
+
'circle',
|
|
3095
|
+
'square',
|
|
3096
|
+
'decimal',
|
|
3097
|
+
'lower-alpha',
|
|
3098
|
+
'upper-alpha',
|
|
3099
|
+
'lower-roman',
|
|
3100
|
+
'upper-roman',
|
|
3101
|
+
'none',
|
|
3102
|
+
];
|
|
3103
|
+
const resolveListKind = (componentAttributes) => {
|
|
3104
|
+
const value = componentAttributes?.[LIST_KIND_ATTRIBUTE];
|
|
3105
|
+
return LIST_KINDS.includes(value) ? value : DEFAULT_LIST_KIND;
|
|
3106
|
+
};
|
|
3107
|
+
/**
|
|
3108
|
+
* A stored value reaches a CSS property here, so the allowlist is the
|
|
3109
|
+
* boundary that keeps it from being whatever a request happened to write.
|
|
3110
|
+
* Unrecognised values mean "unset" and the browser default applies.
|
|
3111
|
+
*/
|
|
3112
|
+
const resolveListMarker = (componentAttributes) => {
|
|
3113
|
+
const value = componentAttributes?.[LIST_MARKER_ATTRIBUTE];
|
|
3114
|
+
return LIST_MARKERS.includes(value) ? value : undefined;
|
|
3115
|
+
};
|
|
3116
|
+
/**
|
|
3117
|
+
* One item per line of the authored text, blank lines dropped. An unset
|
|
3118
|
+
* attribute falls back to the placeholder items, but text the author
|
|
3119
|
+
* deliberately cleared stays cleared — an empty list element still carries
|
|
3120
|
+
* the column's styles and stays selectable in the composer.
|
|
3121
|
+
*/
|
|
3122
|
+
const resolveListItems = (componentAttributes) => {
|
|
3123
|
+
const value = componentAttributes?.[LIST_ITEMS_ATTRIBUTE];
|
|
3124
|
+
const text = value === undefined || value === null
|
|
3125
|
+
? DEFAULT_LIST_ITEMS
|
|
3126
|
+
: typeof value === 'string'
|
|
3127
|
+
? value
|
|
3128
|
+
: String(value);
|
|
3129
|
+
return text
|
|
3130
|
+
.split(/\r?\n/)
|
|
3131
|
+
.map((line) => line.trim())
|
|
3132
|
+
.filter((line) => line !== '');
|
|
3133
|
+
};
|
|
3134
|
+
/**
|
|
3135
|
+
* HTML's `start` takes any integer, including negative ones. The editor
|
|
3136
|
+
* stores a number, but a hand-written or migrated config may carry a numeric
|
|
3137
|
+
* string; both mean the same thing here. Anything else is unset, and only an
|
|
3138
|
+
* ordered list ever reads it.
|
|
3139
|
+
*/
|
|
3140
|
+
const resolveListStart = (componentAttributes) => {
|
|
3141
|
+
const value = componentAttributes?.[LIST_START_ATTRIBUTE];
|
|
3142
|
+
if (typeof value === 'number' && Number.isInteger(value)) {
|
|
3143
|
+
return value;
|
|
3144
|
+
}
|
|
3145
|
+
if (typeof value === 'string' && value.trim() !== '') {
|
|
3146
|
+
const parsed = Number(value.trim());
|
|
3147
|
+
if (Number.isInteger(parsed)) {
|
|
3148
|
+
return parsed;
|
|
3149
|
+
}
|
|
3150
|
+
}
|
|
3151
|
+
return undefined;
|
|
3152
|
+
};
|
|
3153
|
+
/**
|
|
3154
|
+
* Select editors in the composer store `'true'` / `'false'` as strings, while
|
|
3155
|
+
* a hand-written or migrated config may carry a real boolean. Both mean the
|
|
3156
|
+
* same thing here. Only an ordered list ever reads it.
|
|
3157
|
+
*/
|
|
3158
|
+
const resolveListReversed = (componentAttributes) => {
|
|
3159
|
+
const value = componentAttributes?.[LIST_REVERSED_ATTRIBUTE];
|
|
3160
|
+
if (typeof value === 'boolean') {
|
|
3161
|
+
return value;
|
|
3162
|
+
}
|
|
3163
|
+
return typeof value === 'string' && value.trim().toLowerCase() === 'true';
|
|
3164
|
+
};
|
|
3165
|
+
|
|
3166
|
+
class RListComponent {
|
|
3167
|
+
get marker() {
|
|
3168
|
+
return resolveListMarker(this.componentAttributes);
|
|
3169
|
+
}
|
|
3170
|
+
get links() {
|
|
3171
|
+
return this.componentAttributes?.[LIST_LINKS_ATTRIBUTE];
|
|
3172
|
+
}
|
|
3173
|
+
get style() {
|
|
3174
|
+
return {
|
|
3175
|
+
...(this.marker !== undefined ? { listStyleType: this.marker } : {}),
|
|
3176
|
+
...this.styleProps,
|
|
3177
|
+
};
|
|
3178
|
+
}
|
|
3179
|
+
get items() {
|
|
3180
|
+
return resolveListItems(this.componentAttributes);
|
|
3181
|
+
}
|
|
3182
|
+
get kind() {
|
|
3183
|
+
return resolveListKind(this.componentAttributes);
|
|
3184
|
+
}
|
|
3185
|
+
get start() {
|
|
3186
|
+
return resolveListStart(this.componentAttributes);
|
|
3187
|
+
}
|
|
3188
|
+
get reversed() {
|
|
3189
|
+
return resolveListReversed(this.componentAttributes);
|
|
3190
|
+
}
|
|
3191
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.25", ngImport: i0, type: RListComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
3192
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.2.25", type: RListComponent, isStandalone: true, selector: "rulecms-r-list", inputs: { componentAttributes: "componentAttributes", styleProps: "styleProps", className: "className" }, host: { styleAttribute: "display: contents" }, ngImport: i0, template: `
|
|
3193
|
+
@if (kind === 'ordered') {
|
|
3194
|
+
<ol [ngStyle]="style" [class]="className || undefined" [attr.start]="start" [attr.reversed]="reversed ? true : null">
|
|
3195
|
+
@for (item of items; track $index) {
|
|
3196
|
+
<li>
|
|
3197
|
+
<rulecms-inline-markup [text]="item" [links]="links" />
|
|
3198
|
+
</li>
|
|
3199
|
+
}
|
|
3200
|
+
</ol>
|
|
3201
|
+
} @else {
|
|
3202
|
+
<ul [ngStyle]="style" [class]="className || undefined">
|
|
3203
|
+
@for (item of items; track $index) {
|
|
3204
|
+
<li>
|
|
3205
|
+
<rulecms-inline-markup [text]="item" [links]="links" />
|
|
3206
|
+
</li>
|
|
3207
|
+
}
|
|
3208
|
+
</ul>
|
|
3209
|
+
}
|
|
3210
|
+
`, isInline: true, dependencies: [{ kind: "directive", type: NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }, { kind: "component", type: InlineMarkupComponent, selector: "rulecms-inline-markup", inputs: ["text", "links"] }] }); }
|
|
3211
|
+
}
|
|
3212
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.25", ngImport: i0, type: RListComponent, decorators: [{
|
|
3213
|
+
type: Component,
|
|
3214
|
+
args: [{
|
|
3215
|
+
selector: 'rulecms-r-list',
|
|
3216
|
+
standalone: true,
|
|
3217
|
+
imports: [NgStyle, InlineMarkupComponent],
|
|
3218
|
+
template: `
|
|
3219
|
+
@if (kind === 'ordered') {
|
|
3220
|
+
<ol [ngStyle]="style" [class]="className || undefined" [attr.start]="start" [attr.reversed]="reversed ? true : null">
|
|
3221
|
+
@for (item of items; track $index) {
|
|
3222
|
+
<li>
|
|
3223
|
+
<rulecms-inline-markup [text]="item" [links]="links" />
|
|
3224
|
+
</li>
|
|
3225
|
+
}
|
|
3226
|
+
</ol>
|
|
3227
|
+
} @else {
|
|
3228
|
+
<ul [ngStyle]="style" [class]="className || undefined">
|
|
3229
|
+
@for (item of items; track $index) {
|
|
3230
|
+
<li>
|
|
3231
|
+
<rulecms-inline-markup [text]="item" [links]="links" />
|
|
3232
|
+
</li>
|
|
3233
|
+
}
|
|
3234
|
+
</ul>
|
|
3235
|
+
}
|
|
3236
|
+
`,
|
|
3237
|
+
host: { style: 'display: contents' },
|
|
3238
|
+
}]
|
|
3239
|
+
}], propDecorators: { componentAttributes: [{
|
|
3240
|
+
type: Input
|
|
3241
|
+
}], styleProps: [{
|
|
3242
|
+
type: Input
|
|
3243
|
+
}], className: [{
|
|
3244
|
+
type: Input
|
|
3245
|
+
}] } });
|
|
3246
|
+
|
|
3247
|
+
/**
|
|
3248
|
+
* RList always renders a `<ul>` or `<ol>` — both block-level elements that
|
|
3249
|
+
* hold margin, padding, width and border as authored — and puts styleProps on
|
|
3250
|
+
* that element itself. No configuration changes the answer, so no wrapper
|
|
3251
|
+
* div, ever.
|
|
3252
|
+
*/
|
|
3253
|
+
const resolveNoContainer$1 = () => false;
|
|
3254
|
+
|
|
3255
|
+
/**
|
|
3256
|
+
* RList's root is always a real element — `<ul>` or `<ol>` — so it can always
|
|
3257
|
+
* carry the column's className, and the component applies the one it
|
|
3258
|
+
* receives.
|
|
3259
|
+
*/
|
|
3260
|
+
const resolveRendersOwnElement$1 = () => true;
|
|
3261
|
+
|
|
3262
|
+
const defaultAttributeValues$1 = [
|
|
3263
|
+
{
|
|
3264
|
+
name: LIST_ITEMS_ATTRIBUTE,
|
|
3265
|
+
defaultValues: {
|
|
3266
|
+
all: DEFAULT_LIST_ITEMS,
|
|
3267
|
+
},
|
|
3268
|
+
},
|
|
3269
|
+
{
|
|
3270
|
+
name: LIST_KIND_ATTRIBUTE,
|
|
3271
|
+
defaultValues: {
|
|
3272
|
+
all: DEFAULT_LIST_KIND,
|
|
3273
|
+
},
|
|
3274
|
+
},
|
|
3275
|
+
// Marker, start and reversed default to unset on purpose: the browser
|
|
3276
|
+
// already renders disc for unordered and 1, 2, 3 for ordered, and an
|
|
3277
|
+
// attribute that is not set is one the render path does not emit.
|
|
3278
|
+
{
|
|
3279
|
+
name: LIST_MARKER_ATTRIBUTE,
|
|
3280
|
+
defaultValues: {},
|
|
3281
|
+
},
|
|
3282
|
+
{
|
|
3283
|
+
name: LIST_START_ATTRIBUTE,
|
|
3284
|
+
defaultValues: {},
|
|
3285
|
+
},
|
|
3286
|
+
{
|
|
3287
|
+
name: LIST_REVERSED_ATTRIBUTE,
|
|
3288
|
+
defaultValues: {},
|
|
3289
|
+
},
|
|
3290
|
+
{
|
|
3291
|
+
name: LIST_LINKS_ATTRIBUTE,
|
|
3292
|
+
defaultValues: {},
|
|
3293
|
+
},
|
|
3294
|
+
];
|
|
3295
|
+
|
|
3296
|
+
/**
|
|
3297
|
+
* Every setting the list offers, in the order the Modify drawer shows them.
|
|
3298
|
+
*
|
|
3299
|
+
* Order is not cosmetic. The drawer renders these top to bottom and the
|
|
3300
|
+
* documentation registry asserts its pages match that sequence, so moving an
|
|
3301
|
+
* entry here moves it in both places at once.
|
|
3302
|
+
*/
|
|
3303
|
+
const attributeEditModeConfigs$1 = [
|
|
3304
|
+
{
|
|
3305
|
+
name: LIST_ITEMS_ATTRIBUTE,
|
|
3306
|
+
heading: 'List Items',
|
|
3307
|
+
subHeading: 'The items, one per line — each line becomes its own entry in the list. Blank lines are skipped, for 3 different device resolutions',
|
|
3308
|
+
defaultValues: {},
|
|
3309
|
+
},
|
|
3310
|
+
{
|
|
3311
|
+
name: LIST_KIND_ATTRIBUTE,
|
|
3312
|
+
heading: 'List Kind',
|
|
3313
|
+
subHeading: 'Bulleted (unordered) or numbered (ordered). Numbered lists can also set where counting starts and whether it counts down, for 3 different device resolutions',
|
|
3314
|
+
defaultValues: { all: DEFAULT_LIST_KIND },
|
|
3315
|
+
},
|
|
3316
|
+
{
|
|
3317
|
+
name: LIST_MARKER_ATTRIBUTE,
|
|
3318
|
+
heading: 'Marker Style',
|
|
3319
|
+
subHeading: 'What sits in front of each item: a disc, circle or square for bullets; numbers, letters or Roman numerals for numbering; or none to hide the markers. Unset uses the browser default for the list kind, for 3 different device resolutions',
|
|
3320
|
+
defaultValues: {},
|
|
3321
|
+
},
|
|
3322
|
+
{
|
|
3323
|
+
name: LIST_START_ATTRIBUTE,
|
|
3324
|
+
heading: 'Numbering Start',
|
|
3325
|
+
subHeading: 'The number the first item counts from. Only a numbered list reads this; leave unset to count from 1, for 3 different device resolutions',
|
|
3326
|
+
defaultValues: {},
|
|
3327
|
+
},
|
|
3328
|
+
{
|
|
3329
|
+
name: LIST_REVERSED_ATTRIBUTE,
|
|
3330
|
+
heading: 'Reversed Numbering',
|
|
3331
|
+
subHeading: 'Whether a numbered list counts down instead of up, e.g. a top-10 countdown. Only a numbered list reads this, for 3 different device resolutions',
|
|
3332
|
+
defaultValues: {},
|
|
3333
|
+
},
|
|
3334
|
+
{
|
|
3335
|
+
name: LIST_LINKS_ATTRIBUTE,
|
|
3336
|
+
heading: 'Links',
|
|
3337
|
+
subHeading: 'Links the items can point at. To link part of an item, wrap those words in {1|linked text}, where the number is the position of the entry in this list counting from 1; clearing an entry leaves its position in place so the other numbers keep pointing where they did, for 3 different device resolutions',
|
|
3338
|
+
defaultValues: {},
|
|
3339
|
+
},
|
|
3340
|
+
];
|
|
3341
|
+
|
|
3342
|
+
const displayCardEntry$1 = {
|
|
3343
|
+
id: ComponentIdentifiers.rList,
|
|
3344
|
+
resolveNoContainer: resolveNoContainer$1,
|
|
3345
|
+
resolveRendersOwnElement: resolveRendersOwnElement$1,
|
|
3346
|
+
card: RListComponent,
|
|
3347
|
+
defaultAttributeValues: defaultAttributeValues$1,
|
|
3348
|
+
attributeEditModeConfigs: attributeEditModeConfigs$1,
|
|
3349
|
+
};
|
|
3350
|
+
|
|
3351
|
+
/**
|
|
3352
|
+
* The attribute names `r-accordion` reads.
|
|
3353
|
+
*
|
|
3354
|
+
* The component is a disclosure and nothing more specific than that: a summary
|
|
3355
|
+
* a reader clicks and the detail it expands to. An FAQ is one thing to build
|
|
3356
|
+
* out of it, a shipping policy or a "show more" under a truncated bio are
|
|
3357
|
+
* others, and the settings are named after the two halves of the element —
|
|
3358
|
+
* `<summary>` and the detail inside `<details>` — rather than after any one of
|
|
3359
|
+
* those uses.
|
|
3360
|
+
*
|
|
3361
|
+
* One accordion is one disclosure, rather than a component holding N
|
|
3362
|
+
* collapsible sections. A repeating-group attribute would need an editor this
|
|
3363
|
+
* library does not have, and several sections in one component leave the
|
|
3364
|
+
* nested-content slot with no single detail to belong to (see
|
|
3365
|
+
* `rulecms/__docs__/__ARCHITECTURE__/ACCORDION_COMPONENT_ARCHITECTURE.md`).
|
|
3366
|
+
* Several of these in a column is the multi-section case, which is also how an
|
|
3367
|
+
* author reorders one.
|
|
3368
|
+
*
|
|
3369
|
+
* Every name is accordion-specific, including the links, which are the same
|
|
3370
|
+
* numbered list `r-text` already offers. Sharing that attribute name would
|
|
3371
|
+
* have saved a line of editor wiring and cost the documentation contract:
|
|
3372
|
+
* RuleCMS publishes one page per component-scope attribute and requires
|
|
3373
|
+
* exactly one owning component. Reuse happens a level down, where the composer
|
|
3374
|
+
* resolves an attribute to an *editor* by name — the same arrangement `r-list`
|
|
3375
|
+
* uses.
|
|
3376
|
+
*
|
|
3377
|
+
* Nothing here duplicates a column style attribute. Spacing, colour and
|
|
3378
|
+
* typography come from Column Styles on the `<details>` element and inherit
|
|
3379
|
+
* into the summary and the detail; the attributes below are only what is
|
|
3380
|
+
* specific to a disclosure — what it says closed, what it says open, whether
|
|
3381
|
+
* it starts open, and whether the summary also belongs in the page outline.
|
|
3382
|
+
*/
|
|
3383
|
+
const ACCORDION_SUMMARY_ATTRIBUTE = 'accordion-summary-3-resolutions';
|
|
3384
|
+
const ACCORDION_DETAIL_ATTRIBUTE = 'accordion-detail-3-resolutions';
|
|
3385
|
+
const ACCORDION_LINKS_ATTRIBUTE = 'accordion-links-3-resolutions';
|
|
3386
|
+
const ACCORDION_OPEN_ATTRIBUTE = 'accordion-open-3-resolutions';
|
|
3387
|
+
const ACCORDION_HEADING_LEVEL_ATTRIBUTE = 'accordion-heading-level-3-resolutions';
|
|
3388
|
+
/** What a freshly dropped accordion shows before the author types anything. */
|
|
3389
|
+
const DEFAULT_ACCORDION_SUMMARY = 'Summary';
|
|
3390
|
+
const DEFAULT_ACCORDION_DETAIL = 'The detail this summary expands to. Each line becomes its own paragraph.';
|
|
3391
|
+
/**
|
|
3392
|
+
* Heading levels the summary can be given, plus `none` for the common case.
|
|
3393
|
+
*
|
|
3394
|
+
* `h1` is deliberately absent: the page already has one, and ten disclosures
|
|
3395
|
+
* down the page would otherwise offer ten ways to compete with it. Levels
|
|
3396
|
+
* start at `h2` because a summary sits under whatever heading introduces the
|
|
3397
|
+
* section it is in.
|
|
3398
|
+
*
|
|
3399
|
+
* `none` is a real option rather than an unset value so the picker always has
|
|
3400
|
+
* a definite selection — there is no way to clear a select back to nothing —
|
|
3401
|
+
* and it is the default because a disclosure is not automatically part of the
|
|
3402
|
+
* document outline.
|
|
3403
|
+
*/
|
|
3404
|
+
const ACCORDION_HEADING_LEVELS = ['none', 'h2', 'h3', 'h4', 'h5', 'h6'];
|
|
3405
|
+
const DEFAULT_ACCORDION_HEADING_LEVEL = 'none';
|
|
3406
|
+
/**
|
|
3407
|
+
* The summary, on one line. An unset attribute falls back to the placeholder,
|
|
3408
|
+
* but text the author deliberately cleared stays cleared: an empty `<summary>`
|
|
3409
|
+
* still draws its marker and still toggles, and the composer needs the element
|
|
3410
|
+
* to stay selectable on the canvas. Same distinction `r-list` draws for its
|
|
3411
|
+
* items and `r-button` for its label.
|
|
3412
|
+
*
|
|
3413
|
+
* Authored newlines collapse to single spaces. The editor behind this
|
|
3414
|
+
* attribute is the shared multi-line text field, and `<summary>` is a single
|
|
3415
|
+
* line of prose — HTML would collapse the whitespace anyway, so the resolved
|
|
3416
|
+
* value says so rather than leaving it to the browser.
|
|
3417
|
+
*
|
|
3418
|
+
* `{n|…}` markers contribute their inner text and nothing else. The summary
|
|
3419
|
+
* takes no links — inside a `<summary>` a click both follows the anchor and
|
|
3420
|
+
* toggles the disclosure — but the editor behind it is the same field that
|
|
3421
|
+
* offers the marker grammar for the detail, so an author will type one here.
|
|
3422
|
+
* Degrading it to prose is what the rest of the library does with a marker it
|
|
3423
|
+
* cannot resolve; leaving the braces on screen is the one outcome nobody wants.
|
|
3424
|
+
*/
|
|
3425
|
+
const resolveAccordionSummary = (componentAttributes) => {
|
|
3426
|
+
const value = componentAttributes?.[ACCORDION_SUMMARY_ATTRIBUTE];
|
|
3427
|
+
const text = value === undefined || value === null
|
|
3428
|
+
? DEFAULT_ACCORDION_SUMMARY
|
|
3429
|
+
: typeof value === 'string'
|
|
3430
|
+
? value
|
|
3431
|
+
: String(value);
|
|
3432
|
+
return parseInlineLinkMarkers(text)
|
|
3433
|
+
.map((run) => run.text)
|
|
3434
|
+
.join('')
|
|
3435
|
+
.replace(/\s+/g, ' ')
|
|
3436
|
+
.trim();
|
|
3437
|
+
};
|
|
3438
|
+
/**
|
|
3439
|
+
* The detail, as one paragraph per authored line with blank lines dropped —
|
|
3440
|
+
* the same grammar `r-list` uses for its items, so an author who has met one
|
|
3441
|
+
* has met both. An unset attribute falls back to the placeholder; text the
|
|
3442
|
+
* author deliberately cleared stays cleared and the disclosure opens onto
|
|
3443
|
+
* whatever else is in it, or onto nothing.
|
|
3444
|
+
*/
|
|
3445
|
+
const resolveAccordionDetail = (componentAttributes) => {
|
|
3446
|
+
const value = componentAttributes?.[ACCORDION_DETAIL_ATTRIBUTE];
|
|
3447
|
+
const text = value === undefined || value === null
|
|
3448
|
+
? DEFAULT_ACCORDION_DETAIL
|
|
3449
|
+
: typeof value === 'string'
|
|
3450
|
+
? value
|
|
3451
|
+
: String(value);
|
|
3452
|
+
return text
|
|
3453
|
+
.split(/\r?\n/)
|
|
3454
|
+
.map((line) => line.trim())
|
|
3455
|
+
.filter((line) => line !== '');
|
|
3456
|
+
};
|
|
3457
|
+
/**
|
|
3458
|
+
* Whether the disclosure renders already open. Select editors in the composer
|
|
3459
|
+
* store `'true'` / `'false'` as strings, while a hand-written or migrated
|
|
3460
|
+
* config may carry a real boolean; both mean the same thing here.
|
|
3461
|
+
*
|
|
3462
|
+
* This is an attribute rather than runtime state on purpose: `open` has to be
|
|
3463
|
+
* right in the server's HTML, because a disclosure a search engine or a
|
|
3464
|
+
* no-JavaScript reader should see expanded cannot wait for hydration.
|
|
3465
|
+
*/
|
|
3466
|
+
const resolveAccordionOpen = (componentAttributes) => {
|
|
3467
|
+
const value = componentAttributes?.[ACCORDION_OPEN_ATTRIBUTE];
|
|
3468
|
+
if (typeof value === 'boolean') {
|
|
3469
|
+
return value;
|
|
3470
|
+
}
|
|
3471
|
+
return typeof value === 'string' && value.trim().toLowerCase() === 'true';
|
|
3472
|
+
};
|
|
3473
|
+
/**
|
|
3474
|
+
* The heading tag to wrap the summary in, or `undefined` for no heading.
|
|
3475
|
+
*
|
|
3476
|
+
* A stored value reaches an element name here, so the allowlist is the
|
|
3477
|
+
* boundary that keeps it from being whatever a request happened to write.
|
|
3478
|
+
* `none` and anything unrecognised both mean "no heading", which is also the
|
|
3479
|
+
* safe answer: a plain `<summary>` is a valid disclosure, while an invented
|
|
3480
|
+
* tag inside one is not.
|
|
3481
|
+
*/
|
|
3482
|
+
const resolveAccordionHeadingLevel = (componentAttributes) => {
|
|
3483
|
+
const value = componentAttributes?.[ACCORDION_HEADING_LEVEL_ATTRIBUTE];
|
|
3484
|
+
if (value === 'none' || !ACCORDION_HEADING_LEVELS.includes(value)) {
|
|
3485
|
+
return undefined;
|
|
3486
|
+
}
|
|
3487
|
+
return value;
|
|
3488
|
+
};
|
|
3489
|
+
|
|
3490
|
+
const SUMMARY_LAYOUT_STYLE = {
|
|
3491
|
+
display: 'list-item',
|
|
3492
|
+
width: '100%',
|
|
3493
|
+
boxSizing: 'border-box',
|
|
3494
|
+
};
|
|
3495
|
+
const SUMMARY_HEADING_STYLE = {
|
|
3496
|
+
display: 'inline',
|
|
3497
|
+
margin: 0,
|
|
3498
|
+
};
|
|
3499
|
+
class RAccordionComponent {
|
|
3500
|
+
constructor() {
|
|
3501
|
+
this.summaryLayoutStyle = SUMMARY_LAYOUT_STYLE;
|
|
3502
|
+
this.summaryHeadingStyle = SUMMARY_HEADING_STYLE;
|
|
3503
|
+
}
|
|
3504
|
+
get summary() {
|
|
3505
|
+
return resolveAccordionSummary(this.componentAttributes);
|
|
3506
|
+
}
|
|
3507
|
+
get headingLevel() {
|
|
3508
|
+
return resolveAccordionHeadingLevel(this.componentAttributes);
|
|
3509
|
+
}
|
|
3510
|
+
get links() {
|
|
3511
|
+
return this.componentAttributes?.[ACCORDION_LINKS_ATTRIBUTE];
|
|
3512
|
+
}
|
|
3513
|
+
get open() {
|
|
3514
|
+
return resolveAccordionOpen(this.componentAttributes);
|
|
3515
|
+
}
|
|
3516
|
+
get detail() {
|
|
3517
|
+
return resolveAccordionDetail(this.componentAttributes);
|
|
3518
|
+
}
|
|
3519
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.25", ngImport: i0, type: RAccordionComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
3520
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.2.25", type: RAccordionComponent, isStandalone: true, selector: "rulecms-r-accordion", inputs: { componentAttributes: "componentAttributes", styleProps: "styleProps", className: "className" }, host: { styleAttribute: "display: contents" }, ngImport: i0, template: `
|
|
3521
|
+
<details [ngStyle]="styleProps" [class]="className || undefined" [attr.open]="open ? true : null">
|
|
3522
|
+
<summary [ngStyle]="summaryLayoutStyle">
|
|
3523
|
+
@if (headingLevel) {
|
|
3524
|
+
<rulecms-html-tag [tag]="headingLevel" [styleProps]="summaryHeadingStyle">
|
|
3525
|
+
{{ summary }}
|
|
3526
|
+
</rulecms-html-tag>
|
|
3527
|
+
} @else {
|
|
3528
|
+
{{ summary }}
|
|
3529
|
+
}
|
|
3530
|
+
</summary>
|
|
3531
|
+
@for (paragraph of detail; track $index) {
|
|
3532
|
+
<p>
|
|
3533
|
+
<rulecms-inline-markup [text]="paragraph" [links]="links" />
|
|
3534
|
+
</p>
|
|
3535
|
+
}
|
|
3536
|
+
<ng-content />
|
|
3537
|
+
</details>
|
|
3538
|
+
`, isInline: true, dependencies: [{ kind: "directive", type: NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }, { kind: "component", type: InlineMarkupComponent, selector: "rulecms-inline-markup", inputs: ["text", "links"] }, { kind: "component", type: HtmlTagComponent, selector: "rulecms-html-tag", inputs: ["tag", "styleProps", "className"] }] }); }
|
|
3539
|
+
}
|
|
3540
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.25", ngImport: i0, type: RAccordionComponent, decorators: [{
|
|
3541
|
+
type: Component,
|
|
3542
|
+
args: [{
|
|
3543
|
+
selector: 'rulecms-r-accordion',
|
|
3544
|
+
standalone: true,
|
|
3545
|
+
imports: [NgStyle, InlineMarkupComponent, HtmlTagComponent],
|
|
3546
|
+
template: `
|
|
3547
|
+
<details [ngStyle]="styleProps" [class]="className || undefined" [attr.open]="open ? true : null">
|
|
3548
|
+
<summary [ngStyle]="summaryLayoutStyle">
|
|
3549
|
+
@if (headingLevel) {
|
|
3550
|
+
<rulecms-html-tag [tag]="headingLevel" [styleProps]="summaryHeadingStyle">
|
|
3551
|
+
{{ summary }}
|
|
3552
|
+
</rulecms-html-tag>
|
|
3553
|
+
} @else {
|
|
3554
|
+
{{ summary }}
|
|
3555
|
+
}
|
|
3556
|
+
</summary>
|
|
3557
|
+
@for (paragraph of detail; track $index) {
|
|
3558
|
+
<p>
|
|
3559
|
+
<rulecms-inline-markup [text]="paragraph" [links]="links" />
|
|
3560
|
+
</p>
|
|
3561
|
+
}
|
|
3562
|
+
<ng-content />
|
|
3563
|
+
</details>
|
|
3564
|
+
`,
|
|
3565
|
+
host: { style: 'display: contents' },
|
|
3566
|
+
}]
|
|
3567
|
+
}], propDecorators: { componentAttributes: [{
|
|
3568
|
+
type: Input
|
|
3569
|
+
}], styleProps: [{
|
|
3570
|
+
type: Input
|
|
3571
|
+
}], className: [{
|
|
3572
|
+
type: Input
|
|
3573
|
+
}] } });
|
|
3574
|
+
|
|
3575
|
+
/**
|
|
3576
|
+
* RAccordion always renders a `<details>` — a block-level element that holds
|
|
3577
|
+
* margin, padding, width and border as authored — and puts styleProps on that
|
|
3578
|
+
* element itself. No configuration changes the answer, so no wrapper div,
|
|
3579
|
+
* ever.
|
|
3580
|
+
*/
|
|
3581
|
+
const resolveNoContainer = () => false;
|
|
3582
|
+
|
|
3583
|
+
/**
|
|
3584
|
+
* RAccordion's root is always a real element — `<details>` — so it can always
|
|
3585
|
+
* carry the column's className, and the component applies the one it receives.
|
|
3586
|
+
*/
|
|
3587
|
+
const resolveRendersOwnElement = () => true;
|
|
3588
|
+
|
|
3589
|
+
const defaultAttributeValues = [
|
|
3590
|
+
{
|
|
3591
|
+
name: ACCORDION_SUMMARY_ATTRIBUTE,
|
|
3592
|
+
defaultValues: {
|
|
3593
|
+
all: DEFAULT_ACCORDION_SUMMARY,
|
|
3594
|
+
},
|
|
3595
|
+
},
|
|
3596
|
+
{
|
|
3597
|
+
name: ACCORDION_DETAIL_ATTRIBUTE,
|
|
3598
|
+
defaultValues: {
|
|
3599
|
+
all: DEFAULT_ACCORDION_DETAIL,
|
|
3600
|
+
},
|
|
3601
|
+
},
|
|
3602
|
+
// Closed by default, and unset rather than 'false': a disclosure the author
|
|
3603
|
+
// has not thought about should start collapsed, which is what an absent
|
|
3604
|
+
// `open` attribute already means in HTML.
|
|
3605
|
+
{
|
|
3606
|
+
name: ACCORDION_OPEN_ATTRIBUTE,
|
|
3607
|
+
defaultValues: {},
|
|
3608
|
+
},
|
|
3609
|
+
{
|
|
3610
|
+
name: ACCORDION_HEADING_LEVEL_ATTRIBUTE,
|
|
3611
|
+
defaultValues: {
|
|
3612
|
+
all: DEFAULT_ACCORDION_HEADING_LEVEL,
|
|
3613
|
+
},
|
|
3614
|
+
},
|
|
3615
|
+
{
|
|
3616
|
+
name: ACCORDION_LINKS_ATTRIBUTE,
|
|
3617
|
+
defaultValues: {},
|
|
3618
|
+
},
|
|
3619
|
+
];
|
|
3620
|
+
|
|
3621
|
+
/**
|
|
3622
|
+
* Every setting the accordion offers, in the order the Modify drawer shows
|
|
3623
|
+
* them.
|
|
3624
|
+
*
|
|
3625
|
+
* Headings name the two halves of the element — Summary and Detail — not one
|
|
3626
|
+
* use of it. An author building an FAQ reads "Summary" as the question without
|
|
3627
|
+
* being told to; an author building a "show more" would have had to translate
|
|
3628
|
+
* "Question" into something it is not.
|
|
3629
|
+
*
|
|
3630
|
+
* Order is not cosmetic. The drawer renders these top to bottom and the
|
|
3631
|
+
* documentation registry asserts its pages match that sequence, so moving an
|
|
3632
|
+
* entry here moves it in both places at once.
|
|
3633
|
+
*/
|
|
3634
|
+
const attributeEditModeConfigs = [
|
|
3635
|
+
{
|
|
3636
|
+
name: ACCORDION_SUMMARY_ATTRIBUTE,
|
|
3637
|
+
heading: 'Summary',
|
|
3638
|
+
subHeading: 'The line a reader sees closed and clicks to expand. One line — extra lines collapse into it, for 3 different device resolutions',
|
|
3639
|
+
defaultValues: {},
|
|
3640
|
+
},
|
|
3641
|
+
{
|
|
3642
|
+
name: ACCORDION_DETAIL_ATTRIBUTE,
|
|
3643
|
+
heading: 'Detail',
|
|
3644
|
+
subHeading: 'What the summary expands to, one paragraph per line. Blank lines are skipped, for 3 different device resolutions',
|
|
3645
|
+
defaultValues: {},
|
|
3646
|
+
},
|
|
3647
|
+
{
|
|
3648
|
+
name: ACCORDION_OPEN_ATTRIBUTE,
|
|
3649
|
+
heading: 'Open by Default',
|
|
3650
|
+
subHeading: 'Whether the detail is already expanded when the page loads. A reader can still collapse it, for 3 different device resolutions',
|
|
3651
|
+
defaultValues: {},
|
|
3652
|
+
},
|
|
3653
|
+
{
|
|
3654
|
+
name: ACCORDION_HEADING_LEVEL_ATTRIBUTE,
|
|
3655
|
+
heading: 'Summary Heading Level',
|
|
3656
|
+
subHeading: 'Whether the summary also counts as a heading in the page outline, and at which level. Leave it at none unless these summaries are the structure of the page, for 3 different device resolutions',
|
|
3657
|
+
defaultValues: { all: DEFAULT_ACCORDION_HEADING_LEVEL },
|
|
3658
|
+
},
|
|
3659
|
+
{
|
|
3660
|
+
name: ACCORDION_LINKS_ATTRIBUTE,
|
|
3661
|
+
heading: 'Links',
|
|
3662
|
+
subHeading: 'Links the detail can point at. To link part of the detail, wrap those words in {1|linked text}, where the number is the position of the entry in this list counting from 1; clearing an entry leaves its position in place so the other numbers keep pointing where they did, for 3 different device resolutions',
|
|
3663
|
+
defaultValues: {},
|
|
3664
|
+
},
|
|
3665
|
+
];
|
|
3666
|
+
|
|
3667
|
+
const displayCardEntry = {
|
|
3668
|
+
id: ComponentIdentifiers.rAccordion,
|
|
3669
|
+
resolveNoContainer,
|
|
3670
|
+
resolveRendersOwnElement,
|
|
3671
|
+
card: RAccordionComponent,
|
|
3672
|
+
defaultAttributeValues,
|
|
3673
|
+
attributeEditModeConfigs,
|
|
3674
|
+
};
|
|
3675
|
+
|
|
3676
|
+
const cardEntries$1 = [
|
|
3677
|
+
displayCardEntry$8,
|
|
3678
|
+
displayCardEntry$7,
|
|
3679
|
+
displayCardEntry$6,
|
|
3680
|
+
displayCardEntry$5,
|
|
3681
|
+
displayCardEntry$4,
|
|
3682
|
+
displayCardEntry$3,
|
|
3683
|
+
displayCardEntry$2,
|
|
3684
|
+
displayCardEntry$1,
|
|
3685
|
+
displayCardEntry,
|
|
3686
|
+
];
|
|
3687
|
+
|
|
3688
|
+
const cardGroups$1 = [
|
|
3689
|
+
{ title: 'Cards', cards: [{ title: 'Cards', entries: cardEntries$1 }] },
|
|
3690
|
+
];
|
|
3691
|
+
|
|
3692
|
+
let cachedSourceComponentsPlugin;
|
|
3693
|
+
const getComponentsFromRenderCardGroups = (group) => {
|
|
3694
|
+
const entries = group.reduce((all, current) => all.concat(current.cards.reduce((cards, card) => cards.concat(card.entries), [])), []);
|
|
3695
|
+
return entries.reduce((acc, entry) => {
|
|
3696
|
+
acc[entry.id] = {
|
|
3697
|
+
id: entry.id,
|
|
3698
|
+
card: entry.card,
|
|
3699
|
+
defaultAttributeValues: entry.defaultAttributeValues,
|
|
3700
|
+
...(entry.resolveNoContainer !== undefined ? { resolveNoContainer: entry.resolveNoContainer } : {}),
|
|
3701
|
+
...(entry.noContainer !== undefined ? { noContainer: entry.noContainer } : {}),
|
|
3702
|
+
...(entry.resolveRendersOwnElement !== undefined
|
|
3703
|
+
? { resolveRendersOwnElement: entry.resolveRendersOwnElement }
|
|
3704
|
+
: {}),
|
|
3705
|
+
};
|
|
3706
|
+
return acc;
|
|
3707
|
+
}, {});
|
|
3708
|
+
};
|
|
3709
|
+
/**
|
|
3710
|
+
* Returns the source components plugin with a lookup map of render components.
|
|
3711
|
+
* Results are cached after the first call.
|
|
3712
|
+
*/
|
|
3713
|
+
const getSourceComponentsPlugin = () => {
|
|
3714
|
+
if (cachedSourceComponentsPlugin) {
|
|
3715
|
+
return cachedSourceComponentsPlugin;
|
|
3716
|
+
}
|
|
3717
|
+
const components = getComponentsFromRenderCardGroups(cardGroups$1);
|
|
3718
|
+
cachedSourceComponentsPlugin = {
|
|
3719
|
+
components,
|
|
3720
|
+
};
|
|
3721
|
+
return cachedSourceComponentsPlugin;
|
|
3722
|
+
};
|
|
3723
|
+
|
|
3724
|
+
/**
|
|
3725
|
+
* Mapping of attribute names to their corresponding editor components.
|
|
3726
|
+
*
|
|
3727
|
+
* The RuleCMS editor dynamically loads the appropriate editor component for each attribute
|
|
3728
|
+
* that users can edit. This mapping ensures that the correct component is loaded when
|
|
3729
|
+
* editing a given attribute.
|
|
3730
|
+
*
|
|
3731
|
+
* The attributes defined here are shared across all source components available for user modification.
|
|
3732
|
+
*/
|
|
3733
|
+
const componentNameToModifyAttribute = {
|
|
3734
|
+
'icon-name-3-resolutions': 'icon-name-3-resolutions',
|
|
3735
|
+
'content-text-3-resolutions': 'content-text-3-resolutions',
|
|
3736
|
+
// r-button owns its own label and icon attributes — one component per
|
|
3737
|
+
// documented setting — but reuses the editors those names already resolve
|
|
3738
|
+
// to, so no new editor component is needed for either.
|
|
3739
|
+
'button-label-3-resolutions': 'content-text-3-resolutions',
|
|
3740
|
+
'button-icon-3-resolutions': 'icon-name-3-resolutions',
|
|
3741
|
+
// The preset is an object of three axes rather than one scalar, and the
|
|
3742
|
+
// button takes a single link rather than the numbered list rich text uses,
|
|
3743
|
+
// so neither fits an existing editor. Position and disabled are plain
|
|
3744
|
+
// two-value choices and go through the shared select.
|
|
3745
|
+
'button-preset-3-resolutions': 'button-preset-3-resolutions',
|
|
3746
|
+
'button-link-3-resolutions': 'button-link-3-resolutions',
|
|
3747
|
+
'button-icon-position-3-resolutions': 'button-icon-position-3-resolutions',
|
|
3748
|
+
'button-disabled-3-resolutions': 'button-disabled-3-resolutions',
|
|
3749
|
+
// Both of the divider's settings are closed sets of a handful of values, so
|
|
3750
|
+
// both go through the shared select and neither needs an editor built for it.
|
|
3751
|
+
'divider-style-3-resolutions': 'divider-style-3-resolutions',
|
|
3752
|
+
'divider-thickness-3-resolutions': 'divider-thickness-3-resolutions',
|
|
3753
|
+
// The embed URL is the one field in the library whose value is accepted or
|
|
3754
|
+
// refused rather than merely stored, so it gets an editor that can say which
|
|
3755
|
+
// it was while the author is still looking at the field. Its title is plain
|
|
3756
|
+
// text, and the other two are closed sets.
|
|
3757
|
+
'embed-url-3-resolutions': 'embed-url-3-resolutions',
|
|
3758
|
+
'embed-title-3-resolutions': 'image-text-3-resolutions',
|
|
3759
|
+
'embed-aspect-ratio-3-resolutions': 'embed-aspect-ratio-3-resolutions',
|
|
3760
|
+
'embed-loading-3-resolutions': 'embed-loading-3-resolutions',
|
|
3761
|
+
'enclosing-tag-3-resolutions': 'enclosing-tag-3-resolutions',
|
|
3762
|
+
'links-3-resolutions': 'links-3-resolutions',
|
|
3763
|
+
// r-list owns its own attribute names — one component per documented
|
|
3764
|
+
// setting — but reuses existing editors wherever one fits: the multi-line
|
|
3765
|
+
// text field for its items, the numbered link list for its links, and the
|
|
3766
|
+
// plain text field for where numbering starts (the number editor is
|
|
3767
|
+
// pixel-specific, and a numbering start is not a length). Kind, marker and
|
|
3768
|
+
// reversed are closed sets and go through the shared select.
|
|
3769
|
+
'list-items-3-resolutions': 'content-text-3-resolutions',
|
|
3770
|
+
'list-kind-3-resolutions': 'list-kind-3-resolutions',
|
|
3771
|
+
'list-marker-3-resolutions': 'list-marker-3-resolutions',
|
|
3772
|
+
'list-start-3-resolutions': 'image-text-3-resolutions',
|
|
3773
|
+
'list-reversed-3-resolutions': 'list-reversed-3-resolutions',
|
|
3774
|
+
'list-links-3-resolutions': 'links-3-resolutions',
|
|
3775
|
+
// r-accordion owns its own attribute names — one component per documented
|
|
3776
|
+
// setting — and reuses existing editors for everything that fits: the
|
|
3777
|
+
// multi-line text field for the summary and the detail, and the numbered
|
|
3778
|
+
// link list for the detail's links. Whether it starts open and whether the
|
|
3779
|
+
// summary is a heading are closed sets and go through the shared select.
|
|
3780
|
+
'accordion-summary-3-resolutions': 'content-text-3-resolutions',
|
|
3781
|
+
'accordion-detail-3-resolutions': 'content-text-3-resolutions',
|
|
3782
|
+
'accordion-open-3-resolutions': 'accordion-open-3-resolutions',
|
|
3783
|
+
'accordion-heading-level-3-resolutions': 'accordion-heading-level-3-resolutions',
|
|
3784
|
+
'accordion-links-3-resolutions': 'links-3-resolutions',
|
|
3785
|
+
'content-title-3-resolutions': 'content-title-3-resolutions',
|
|
3786
|
+
'media-selector-3-resolutions': 'media-selector-3-resolutions',
|
|
3787
|
+
'pass-through-image-attributes-alt-3-resolutions': 'image-alt-text-3-resolutions',
|
|
3788
|
+
'pass-through-image-attributes-width-3-resolutions': 'image-intrinsic-width-3-resolutions',
|
|
3789
|
+
'pass-through-image-attributes-height-3-resolutions': 'image-intrinsic-height-3-resolutions',
|
|
3790
|
+
'pass-through-image-attributes-sizes-3-resolutions': 'image-sizes-3-resolutions',
|
|
3791
|
+
'pass-through-image-attributes-loading-3-resolutions': 'image-loading-3-resolutions',
|
|
3792
|
+
'pass-through-image-attributes-fetchpriority-3-resolutions': 'image-fetchpriority-3-resolutions',
|
|
3793
|
+
'cloudinary-transformations-3-resolutions': 'cloudinary-transformations-3-resolutions',
|
|
3794
|
+
'video-selector-3-resolutions': 'video-selector-3-resolutions',
|
|
3795
|
+
'video-poster-selector-3-resolutions': 'video-poster-selector-3-resolutions',
|
|
3796
|
+
'pass-through-video-attributes-controls-3-resolutions': 'video-controls-3-resolutions',
|
|
3797
|
+
'pass-through-video-attributes-autoplay-3-resolutions': 'video-autoplay-3-resolutions',
|
|
3798
|
+
'pass-through-video-attributes-loop-3-resolutions': 'video-loop-3-resolutions',
|
|
3799
|
+
'pass-through-video-attributes-muted-3-resolutions': 'video-muted-3-resolutions',
|
|
3800
|
+
'pass-through-video-attributes-playsinline-3-resolutions': 'video-playsinline-3-resolutions',
|
|
3801
|
+
'pass-through-video-attributes-preload-3-resolutions': 'video-preload-3-resolutions',
|
|
3802
|
+
'pass-through-video-attributes-width-3-resolutions': 'video-intrinsic-width-3-resolutions',
|
|
3803
|
+
'pass-through-video-attributes-height-3-resolutions': 'video-intrinsic-height-3-resolutions',
|
|
3804
|
+
'video-transformations-3-resolutions': 'video-transformations-3-resolutions',
|
|
3805
|
+
};
|
|
3806
|
+
|
|
3807
|
+
/**
|
|
3808
|
+
* Mapping of component IDs to their supported modifiable attributes.
|
|
3809
|
+
*
|
|
3810
|
+
* Each component type can have a specific list of attributes that users are allowed to modify
|
|
3811
|
+
* in the editor. This mapping defines which attributes are available for each component type.
|
|
3812
|
+
*
|
|
3813
|
+
* Note: This defines which attributes each component supports, while component-name-to-modify-attribute.ts
|
|
3814
|
+
* defines which editor component should be used to modify each attribute type.
|
|
3815
|
+
* todo: this is a little confusing and might require a deeper look into how RuleCMS uses this.
|
|
3816
|
+
*/
|
|
3817
|
+
/**
|
|
3818
|
+
* Empty on purpose. Every component this library ships declares its editable
|
|
3819
|
+
* attributes through its own `attribute-edit-mode-configs.ts`, which is the
|
|
3820
|
+
* path the composer's Modify drawer reads. Listing an attribute here as well
|
|
3821
|
+
* shows it twice — once through the legacy shared-attribute editor, which has
|
|
3822
|
+
* no entry for modern `-3-resolutions` names and renders an empty section.
|
|
3823
|
+
*/
|
|
3824
|
+
const attributeListForComponentTypes = {};
|
|
3825
|
+
|
|
3826
|
+
let returnValue;
|
|
3827
|
+
// Flatten nested card groups and cards into a flat array of entries, then converts to a lookup map by entry ID
|
|
3828
|
+
const getComponentMetadataForEditing = () => {
|
|
3829
|
+
if (returnValue) {
|
|
3830
|
+
return returnValue;
|
|
3831
|
+
}
|
|
3832
|
+
const entries = cardGroups$1.reduce((all, current) => all.concat(current.cards.reduce((cards, card) => cards.concat(card.entries), [])), []);
|
|
3833
|
+
const componentMetadata = entries.reduce((acc, entry) => {
|
|
3834
|
+
acc[entry.id] = {
|
|
3835
|
+
defaultAttributeValues: entry.defaultAttributeValues,
|
|
3836
|
+
attributeEditModeConfigs: entry.attributeEditModeConfigs,
|
|
3837
|
+
};
|
|
3838
|
+
return acc;
|
|
3839
|
+
}, {});
|
|
3840
|
+
returnValue = {
|
|
3841
|
+
attributeListForComponentTypes,
|
|
3842
|
+
componentNameToModifyAttribute,
|
|
3843
|
+
componentMetadata,
|
|
3844
|
+
};
|
|
3845
|
+
return returnValue;
|
|
3846
|
+
};
|
|
3847
|
+
|
|
3848
|
+
const cardEntries = [
|
|
3849
|
+
{ id: ComponentIdentifiers.rText, icon: 'T', label: 'Text' },
|
|
3850
|
+
{ id: ComponentIdentifiers.cloudinaryAdvancedImage, icon: '🖼️', label: 'Image' },
|
|
3851
|
+
{ id: ComponentIdentifiers.rVideo, icon: '🎬', label: 'Video' },
|
|
3852
|
+
{ id: ComponentIdentifiers.rIcon, icon: '◆', label: 'Icon' },
|
|
3853
|
+
{ id: ComponentIdentifiers.rButton, icon: '⬭', label: 'Button' },
|
|
3854
|
+
{ id: ComponentIdentifiers.rDivider, icon: '━', label: 'Divider' },
|
|
3855
|
+
{ id: ComponentIdentifiers.rEmbed, icon: '⧉', label: 'Embed' },
|
|
3856
|
+
{ id: ComponentIdentifiers.rList, icon: '☰', label: 'List' },
|
|
3857
|
+
{ id: ComponentIdentifiers.rAccordion, icon: '▸', label: 'Accordion' },
|
|
3858
|
+
];
|
|
3859
|
+
|
|
3860
|
+
const cardGroups = [
|
|
3861
|
+
{
|
|
3862
|
+
title: 'Elements',
|
|
3863
|
+
showInitially: true,
|
|
3864
|
+
cards: [{ title: 'Base Elements', entries: cardEntries }],
|
|
3865
|
+
},
|
|
3866
|
+
];
|
|
3867
|
+
|
|
3868
|
+
let cachedPreviewComponents;
|
|
3869
|
+
/**
|
|
3870
|
+
* Returns the preview card groups for drag and drop functionality.
|
|
3871
|
+
* This replaces the dragDropPreviewCardGroups export.
|
|
3872
|
+
*/
|
|
3873
|
+
const getPreviewCardGroups = () => {
|
|
3874
|
+
return cardGroups;
|
|
3875
|
+
};
|
|
3876
|
+
/**
|
|
3877
|
+
* Returns a lookup map of preview components by entry ID.
|
|
3878
|
+
* Results are cached after the first call.
|
|
3879
|
+
*/
|
|
3880
|
+
const getPreviewComponents = () => {
|
|
3881
|
+
if (cachedPreviewComponents) {
|
|
3882
|
+
return cachedPreviewComponents;
|
|
3883
|
+
}
|
|
3884
|
+
// Flatten nested card groups and cards into a flat array of entries
|
|
3885
|
+
const entries = [];
|
|
3886
|
+
for (const group of cardGroups) {
|
|
3887
|
+
for (const card of group.cards) {
|
|
3888
|
+
entries.push(...card.entries);
|
|
3889
|
+
}
|
|
3890
|
+
}
|
|
3891
|
+
// Convert to a lookup map by entry ID
|
|
3892
|
+
cachedPreviewComponents = entries.reduce((acc, entry) => {
|
|
3893
|
+
acc[entry.id] = entry;
|
|
3894
|
+
return acc;
|
|
3895
|
+
}, {});
|
|
3896
|
+
return cachedPreviewComponents;
|
|
3897
|
+
};
|
|
3898
|
+
|
|
3899
|
+
/**
|
|
3900
|
+
* ComponentLibraryModule conformance (the @rulecms/widget-angular contract).
|
|
3901
|
+
*
|
|
3902
|
+
* This package is the DEFAULT component library. It conforms to the contract
|
|
3903
|
+
* STRUCTURALLY — it has no runtime dependency on @rulecms/widget-angular.
|
|
3904
|
+
* Consumers register it, e.g.:
|
|
3905
|
+
*
|
|
3906
|
+
* libraries: { default: () => import('@rulecms/source-components-angular') }
|
|
3907
|
+
*
|
|
3908
|
+
* or statically import the module for SSR pages.
|
|
3909
|
+
*/
|
|
3910
|
+
const manifest = {
|
|
3911
|
+
id: 'default',
|
|
3912
|
+
version: '0.1.0',
|
|
3913
|
+
angularPeerRange: '>=17.0.0',
|
|
3914
|
+
};
|
|
3915
|
+
/** The render surface: component type → renderable entry. */
|
|
3916
|
+
const components = getSourceComponentsPlugin().components;
|
|
3917
|
+
/** The editor surface: palette + attribute metadata for the RuleCMS composer. */
|
|
3918
|
+
const editor = {
|
|
3919
|
+
previewComponents: getPreviewComponents(),
|
|
3920
|
+
previewCardGroups: getPreviewCardGroups(),
|
|
3921
|
+
componentMetadata: getComponentMetadataForEditing(),
|
|
3922
|
+
};
|
|
3923
|
+
|
|
3924
|
+
/**
|
|
3925
|
+
* Canonical letter order, so the same span always serializes the same way and
|
|
3926
|
+
* a source-mode author sees `{bi1|…}` rather than whichever order an editor
|
|
3927
|
+
* happened to apply the marks in.
|
|
3928
|
+
*/
|
|
3929
|
+
const MARK_LETTERS = [
|
|
3930
|
+
['bold', 'b'],
|
|
3931
|
+
['italic', 'i'],
|
|
3932
|
+
['strike', 's'],
|
|
3933
|
+
];
|
|
3934
|
+
const markLetters = (marks) => MARK_LETTERS.filter(([mark]) => marks[mark])
|
|
3935
|
+
.map(([, letter]) => letter)
|
|
3936
|
+
.join('');
|
|
3937
|
+
/**
|
|
3938
|
+
* A span back to its marker. Text that carries neither marks nor an index is
|
|
3939
|
+
* emitted bare: a marker with nothing to say would only be noise an author has
|
|
3940
|
+
* to read past, and it round-trips to the same runs either way.
|
|
3941
|
+
*/
|
|
3942
|
+
const formatSpan = (run) => {
|
|
3943
|
+
const letters = markLetters(run.marks);
|
|
3944
|
+
const index = run.index === undefined ? '' : `${run.index}`;
|
|
3945
|
+
if (letters === '' && index === '') {
|
|
3946
|
+
return run.text;
|
|
3947
|
+
}
|
|
3948
|
+
return `{${letters}${index}|${run.text}}`;
|
|
3949
|
+
};
|
|
3950
|
+
/** One run as authored text. */
|
|
3951
|
+
function formatInlineMarker(run) {
|
|
3952
|
+
switch (run.kind) {
|
|
3953
|
+
case 'text':
|
|
3954
|
+
return run.text;
|
|
3955
|
+
case 'break':
|
|
3956
|
+
return '{br|}';
|
|
3957
|
+
default:
|
|
3958
|
+
return formatSpan(run);
|
|
3959
|
+
}
|
|
3960
|
+
}
|
|
3961
|
+
/**
|
|
3962
|
+
* Runs back to the string they were parsed from.
|
|
3963
|
+
*
|
|
3964
|
+
* This is the writer half of the grammar, and it lives beside the parser so
|
|
3965
|
+
* that an editor never hand-assembles marker syntax. `parseInlineMarkers` and
|
|
3966
|
+
* this function are inverses for anything the parser produces, which is what
|
|
3967
|
+
* lets a rich editor round-trip through source mode without rewriting content
|
|
3968
|
+
* the author did not touch.
|
|
3969
|
+
*/
|
|
3970
|
+
function serializeInlineMarkers(runs) {
|
|
3971
|
+
return runs.map(formatInlineMarker).join('');
|
|
3972
|
+
}
|
|
3973
|
+
|
|
3974
|
+
/**
|
|
3975
|
+
* Generated bundle index. Do not edit.
|
|
3976
|
+
*/
|
|
3977
|
+
|
|
3978
|
+
export { ACCORDION_HEADING_LEVELS, BLOCK_ENCLOSING_TAGS, BUTTON_CLICK_HANDLER_PROP, BUTTON_CSS_VARIABLE_PREFIX, BUTTON_SIZES, BUTTON_TONES, BUTTON_VARIANTS, ComponentIdentifiers, DEFAULT_ACCORDION_HEADING_LEVEL, DEFAULT_BUTTON_LABEL, DEFAULT_BUTTON_PRESET, DEFAULT_EMBED_LOADING, DEFAULT_ICON_POSITION, DEFAULT_LIST_KIND, EMBED_ASPECT_RATIOS, EMBED_LOADING_VALUES, EMBED_PROVIDERS, EMBED_UNSHAPED_HEIGHT, ENCLOSING_TAGS, ICON_POSITIONS, INLINE_ENCLOSING_TAGS, InlineMarkupComponent, LIST_KINDS, LIST_MARKERS, REJECTED_HREF_SCHEMES, SIZE_SCALES, TONE_PALETTES, buildButtonPresetStyles, disabledStyles as buttonDisabledStyles, cardGroups$1 as cardGroups, components, editor, formatInlineMarker, getComponentMetadataForEditing, getPreviewCardGroups, getPreviewComponents, getSourceComponentsPlugin, isAllowedEmbedUrl, isBlockEnclosingTag, isButtonSize, isButtonTone, isButtonVariant, isEnclosingTag, isSafeHref, manifest, normaliseHost, parseInlineLinkMarkers, parseInlineMarkers, resolveButtonPreset, resolveEmbedUrl, resolveLinkAttributes, serializeInlineMarkers, toneCssVariable };
|
|
3979
|
+
//# sourceMappingURL=rulecms-source-components-angular.mjs.map
|