@skhema/web-component 0.0.14 → 0.0.16
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/README.md +10 -10
- package/dist/cdn.d.ts +0 -1
- package/dist/cdn.d.ts.map +1 -1
- package/dist/components/SkhemaElement.d.ts +6 -8
- package/dist/components/SkhemaElement.d.ts.map +1 -1
- package/dist/components/types.d.ts +3 -4
- package/dist/components/types.d.ts.map +1 -1
- package/dist/index.cjs +192 -72
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +4 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.es.js +192 -72
- package/dist/index.es.js.map +1 -1
- package/dist/utils/analytics.d.ts +1 -2
- package/dist/utils/analytics.d.ts.map +1 -1
- package/dist/utils/sanitization.d.ts +31 -0
- package/dist/utils/sanitization.d.ts.map +1 -0
- package/dist/utils/seo.d.ts +0 -1
- package/dist/utils/seo.d.ts.map +1 -1
- package/dist/utils/validation.d.ts +0 -1
- package/dist/utils/validation.d.ts.map +1 -1
- package/dist/web-component.min.js +1 -1
- package/dist/web-component.min.js.map +1 -1
- package/package.json +17 -3
package/README.md
CHANGED
|
@@ -19,23 +19,23 @@ npm install @skhema/web-component
|
|
|
19
19
|
```
|
|
20
20
|
|
|
21
21
|
```javascript
|
|
22
|
-
import '@skhema/web-component'
|
|
22
|
+
import '@skhema/web-component'
|
|
23
23
|
// Component is automatically registered
|
|
24
24
|
```
|
|
25
25
|
|
|
26
26
|
## Attributes
|
|
27
27
|
|
|
28
|
-
| Attribute
|
|
29
|
-
|
|
30
|
-
| `element-type`
|
|
31
|
-
| `contributor-id` | ✓
|
|
32
|
-
| `content`
|
|
33
|
-
| `theme`
|
|
28
|
+
| Attribute | Required | Description |
|
|
29
|
+
| ---------------- | -------- | ------------------------------------- |
|
|
30
|
+
| `element-type` | ✓ | Type of strategic element |
|
|
31
|
+
| `contributor-id` | ✓ | Your contributor identifier |
|
|
32
|
+
| `content` | | Alternative to inner text |
|
|
33
|
+
| `theme` | | Visual theme: `light`, `dark`, `auto` |
|
|
34
34
|
|
|
35
35
|
## Element Types
|
|
36
36
|
|
|
37
37
|
- `key_challenge` - Business challenges
|
|
38
|
-
- `supporting_fact` - Evidence and data points
|
|
38
|
+
- `supporting_fact` - Evidence and data points
|
|
39
39
|
- `guiding_policy` - Strategic approaches
|
|
40
40
|
- `solution_alternative` - Potential solutions
|
|
41
41
|
- And more...
|
|
@@ -45,7 +45,7 @@ import '@skhema/web-component';
|
|
|
45
45
|
```html
|
|
46
46
|
<article>
|
|
47
47
|
<p>The automotive industry is undergoing transformation...</p>
|
|
48
|
-
|
|
48
|
+
|
|
49
49
|
<skhema-element element-type="key_challenge" contributor-id="analyst">
|
|
50
50
|
Traditional automakers face retooling challenges while competing with Tesla.
|
|
51
51
|
</skhema-element>
|
|
@@ -54,4 +54,4 @@ import '@skhema/web-component';
|
|
|
54
54
|
|
|
55
55
|
## License
|
|
56
56
|
|
|
57
|
-
MIT
|
|
57
|
+
MIT
|
package/dist/cdn.d.ts
CHANGED
package/dist/cdn.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cdn.d.ts","sourceRoot":"","sources":["../src/cdn.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,aAAa,EAAE,MAAM,+BAA+B,
|
|
1
|
+
{"version":3,"file":"cdn.d.ts","sourceRoot":"","sources":["../src/cdn.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,aAAa,EAAE,MAAM,+BAA+B,CAAA;AAiB7D,OAAO,EAAE,aAAa,EAAE,CAAA"}
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
1
|
+
import { ContentData, SkhemaElementAttributes, SkhemaElementEventMap } from './types.js';
|
|
3
2
|
export declare class SkhemaElement extends HTMLElement {
|
|
4
3
|
private shadow;
|
|
5
4
|
private contentData;
|
|
@@ -23,12 +22,11 @@ export declare class SkhemaElement extends HTMLElement {
|
|
|
23
22
|
declare global {
|
|
24
23
|
interface HTMLElementEventMap extends SkhemaElementEventMap {
|
|
25
24
|
}
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
}
|
|
25
|
+
interface SkhemaElementJSX extends Partial<SkhemaElementAttributes> {
|
|
26
|
+
[key: string]: unknown;
|
|
27
|
+
}
|
|
28
|
+
interface JSXIntrinsicElements {
|
|
29
|
+
'skhema-element': SkhemaElementJSX;
|
|
32
30
|
}
|
|
33
31
|
}
|
|
34
32
|
//# sourceMappingURL=SkhemaElement.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"SkhemaElement.d.ts","sourceRoot":"","sources":["../../src/components/SkhemaElement.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"SkhemaElement.d.ts","sourceRoot":"","sources":["../../src/components/SkhemaElement.ts"],"names":[],"mappings":"AAiBA,OAAO,KAAK,EACV,WAAW,EAEX,uBAAuB,EACvB,qBAAqB,EACtB,MAAM,YAAY,CAAA;AA+TnB,qBAAa,aAAc,SAAQ,WAAW;IAC5C,OAAO,CAAC,MAAM,CAAY;IAC1B,OAAO,CAAC,WAAW,CAA2B;IAC9C,OAAO,CAAC,kBAAkB,CAAQ;;IAOlC,MAAM,KAAK,kBAAkB,IAAI,CAAC,MAAM,uBAAuB,CAAC,EAAE,CASjE;IAED,iBAAiB;IAYjB,wBAAwB,CACtB,KAAK,EAAE,MAAM,uBAAuB,EACpC,QAAQ,EAAE,MAAM,GAAG,IAAI,EACvB,QAAQ,EAAE,MAAM,GAAG,IAAI;IAOzB,OAAO,CAAC,MAAM;IAwCd,OAAO,CAAC,UAAU;IAIlB,OAAO,CAAC,aAAa;IAuErB,OAAO,CAAC,qBAAqB;IAQ7B,OAAO,CAAC,WAAW;IASnB,OAAO,CAAC,WAAW;IAyBnB,OAAO,CAAC,iBAAiB;YA0BX,SAAS;YAyBT,eAAe;IAkBtB,cAAc,IAAI,WAAW,GAAG,IAAI;IAIpC,OAAO,IAAI,IAAI;CAGvB;AAGD,OAAO,CAAC,MAAM,CAAC;IAEb,UAAU,mBAAoB,SAAQ,qBAAqB;KAAG;IAE9D,UAAU,gBAAiB,SAAQ,OAAO,CAAC,uBAAuB,CAAC;QACjE,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAA;KACvB;IAGD,UAAU,oBAAoB;QAC5B,gBAAgB,EAAE,gBAAgB,CAAA;KACnC;CACF"}
|
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
import { ElementValue } from '@skhema/types';
|
|
2
|
-
|
|
3
2
|
export interface SkhemaElementAttributes {
|
|
4
3
|
'element-type': ElementValue;
|
|
5
4
|
'contributor-id': string;
|
|
6
|
-
|
|
5
|
+
content?: string;
|
|
7
6
|
'source-url'?: string;
|
|
8
|
-
|
|
7
|
+
theme?: 'light' | 'dark' | 'auto';
|
|
9
8
|
'track-analytics'?: 'true' | 'false';
|
|
10
9
|
}
|
|
11
10
|
export interface EmbedAnalytics {
|
|
@@ -38,7 +37,7 @@ export interface SkhemaElementEventMap {
|
|
|
38
37
|
'skhema:click': CustomEvent<ContentData>;
|
|
39
38
|
'skhema:error': CustomEvent<{
|
|
40
39
|
error: string;
|
|
41
|
-
details?:
|
|
40
|
+
details?: unknown;
|
|
42
41
|
}>;
|
|
43
42
|
}
|
|
44
43
|
//# sourceMappingURL=types.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/components/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,eAAe,
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/components/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,eAAe,CAAA;AAEjD,MAAM,WAAW,uBAAuB;IACtC,cAAc,EAAE,YAAY,CAAA;IAC5B,gBAAgB,EAAE,MAAM,CAAA;IACxB,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,KAAK,CAAC,EAAE,OAAO,GAAG,MAAM,GAAG,MAAM,CAAA;IACjC,iBAAiB,CAAC,EAAE,MAAM,GAAG,OAAO,CAAA;CACrC;AAED,MAAM,WAAW,cAAc;IAC7B,aAAa,EAAE,MAAM,CAAA;IACrB,WAAW,EAAE,YAAY,CAAA;IACzB,WAAW,EAAE,MAAM,CAAA;IACnB,OAAO,EAAE,MAAM,CAAA;IACf,OAAO,EAAE,MAAM,CAAA;IACf,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,SAAS,EAAE,MAAM,CAAA;IACjB,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AAED,MAAM,WAAW,WAAW;IAC1B,cAAc,EAAE,MAAM,CAAA;IACtB,YAAY,EAAE,YAAY,CAAA;IAC1B,OAAO,EAAE,MAAM,CAAA;IACf,YAAY,EAAE,MAAM,CAAA;IACpB,UAAU,EAAE,MAAM,CAAA;IAClB,SAAS,EAAE,MAAM,CAAA;IACjB,UAAU,CAAC,EAAE,MAAM,CAAA;CACpB;AAED,MAAM,WAAW,kBAAkB;IACjC,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,WAAW,CAAC,EAAE,MAAM,CAAA;CACrB;AAED,MAAM,WAAW,qBAAqB;IACpC,aAAa,EAAE,WAAW,CAAC,cAAc,CAAC,CAAA;IAC1C,cAAc,EAAE,WAAW,CAAC,WAAW,CAAC,CAAA;IACxC,cAAc,EAAE,WAAW,CAAC;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,OAAO,CAAA;KAAE,CAAC,CAAA;CAClE"}
|
package/dist/index.cjs
CHANGED
|
@@ -1,45 +1,13 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperties(exports, { __esModule: { value: true }, [Symbol.toStringTag]: { value: "Module" } });
|
|
3
3
|
const types = require("@skhema/types");
|
|
4
|
-
function isValidElementType(elementType) {
|
|
5
|
-
const validTypes = Object.values(types.ELEMENT_TYPES).map((type) => type.value);
|
|
6
|
-
return validTypes.includes(elementType);
|
|
7
|
-
}
|
|
8
|
-
function validateAttributes(element) {
|
|
9
|
-
const errors = [];
|
|
10
|
-
const elementType = element.getAttribute("element-type");
|
|
11
|
-
const contributorId = element.getAttribute("contributor-id");
|
|
12
|
-
if (!elementType) {
|
|
13
|
-
errors.push("Missing required attribute: element-type");
|
|
14
|
-
} else if (!isValidElementType(elementType)) {
|
|
15
|
-
const validTypes = Object.values(types.ELEMENT_TYPES).map((t) => t.value).join(", ");
|
|
16
|
-
errors.push(`Invalid element-type "${elementType}". Valid types: ${validTypes}`);
|
|
17
|
-
}
|
|
18
|
-
if (!contributorId) {
|
|
19
|
-
errors.push("Missing required attribute: contributor-id");
|
|
20
|
-
} else if (contributorId.trim().length === 0) {
|
|
21
|
-
errors.push("contributor-id cannot be empty");
|
|
22
|
-
}
|
|
23
|
-
return {
|
|
24
|
-
isValid: errors.length === 0,
|
|
25
|
-
errors,
|
|
26
|
-
elementType: isValidElementType(elementType || "") ? elementType : void 0,
|
|
27
|
-
contributorId: contributorId || void 0
|
|
28
|
-
};
|
|
29
|
-
}
|
|
30
|
-
function getElementTypeLabel(elementType) {
|
|
31
|
-
const type = Object.values(types.ELEMENT_TYPES).find((t) => t.value === elementType);
|
|
32
|
-
return (type == null ? void 0 : type.label) || elementType;
|
|
33
|
-
}
|
|
34
|
-
function getElementTypeAcronym(elementType) {
|
|
35
|
-
const type = Object.values(types.ELEMENT_TYPES).find((t) => t.value === elementType);
|
|
36
|
-
return (type == null ? void 0 : type.acronym) || elementType.substring(0, 2).toUpperCase();
|
|
37
|
-
}
|
|
38
4
|
function toUrlSafeBase64(str) {
|
|
39
|
-
const base64 = btoa(
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
5
|
+
const base64 = btoa(
|
|
6
|
+
encodeURIComponent(str).replace(
|
|
7
|
+
/%([0-9A-F]{2})/g,
|
|
8
|
+
(_, p1) => String.fromCharCode(parseInt(p1, 16))
|
|
9
|
+
)
|
|
10
|
+
);
|
|
43
11
|
return base64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
44
12
|
}
|
|
45
13
|
async function trackEmbedLoad(analytics) {
|
|
@@ -55,7 +23,10 @@ async function trackEmbedLoad(analytics) {
|
|
|
55
23
|
user_agent: analytics.userAgent || ""
|
|
56
24
|
});
|
|
57
25
|
if (navigator.sendBeacon) {
|
|
58
|
-
navigator.sendBeacon(
|
|
26
|
+
navigator.sendBeacon(
|
|
27
|
+
"https://api.skhema.com/api:XGdoUqHx/component/embed",
|
|
28
|
+
data
|
|
29
|
+
);
|
|
59
30
|
} else {
|
|
60
31
|
fetch("https://api.skhema.com/api:XGdoUqHx/component/embed", {
|
|
61
32
|
method: "POST",
|
|
@@ -104,26 +75,131 @@ function generateContentHash(content) {
|
|
|
104
75
|
}
|
|
105
76
|
return Math.abs(hash).toString(36).substring(0, 12);
|
|
106
77
|
}
|
|
78
|
+
function sanitizeContent(content) {
|
|
79
|
+
const htmlEncode = (str) => {
|
|
80
|
+
const div = document.createElement("div");
|
|
81
|
+
div.textContent = str;
|
|
82
|
+
return div.innerHTML;
|
|
83
|
+
};
|
|
84
|
+
let sanitized = stripUrls(content);
|
|
85
|
+
sanitized = htmlEncode(sanitized);
|
|
86
|
+
sanitized = sanitized.replace(/\n/g, "<br>");
|
|
87
|
+
sanitized = applyTextWrapping(sanitized);
|
|
88
|
+
return sanitized;
|
|
89
|
+
}
|
|
90
|
+
function stripUrls(text) {
|
|
91
|
+
const patterns = [
|
|
92
|
+
// Standard URLs with protocols
|
|
93
|
+
/https?:\/\/[^\s<>"{}|\\^`[\]]+/gi,
|
|
94
|
+
// FTP URLs
|
|
95
|
+
/ftp:\/\/[^\s<>"{}|\\^`[\]]+/gi,
|
|
96
|
+
// URLs without protocol but with www
|
|
97
|
+
/www\.[^\s<>"{}|\\^`[\]]+/gi,
|
|
98
|
+
// Email-like patterns
|
|
99
|
+
/[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/gi,
|
|
100
|
+
// Common domain patterns (anything.com, anything.org, etc.)
|
|
101
|
+
/(?:^|\s)([a-zA-Z0-9-]+\.)+[a-zA-Z]{2,}(?:\/[^\s]*)?/gi
|
|
102
|
+
];
|
|
103
|
+
let stripped = text;
|
|
104
|
+
patterns.forEach((pattern) => {
|
|
105
|
+
stripped = stripped.replace(pattern, "");
|
|
106
|
+
});
|
|
107
|
+
stripped = stripped.replace(/\s+/g, " ").trim();
|
|
108
|
+
return stripped;
|
|
109
|
+
}
|
|
110
|
+
function applyTextWrapping(text) {
|
|
111
|
+
const words = text.split(/(\s+)/);
|
|
112
|
+
return words.map((word) => {
|
|
113
|
+
if (/^\s+$/.test(word) || word.includes("<")) {
|
|
114
|
+
return word;
|
|
115
|
+
}
|
|
116
|
+
if (word.length > 30) {
|
|
117
|
+
return word.replace(/(.{10})/g, "$1");
|
|
118
|
+
}
|
|
119
|
+
return word;
|
|
120
|
+
}).join("");
|
|
121
|
+
}
|
|
122
|
+
function validateContentSecurity(content) {
|
|
123
|
+
const issues = [];
|
|
124
|
+
if (/<script[\s>]/i.test(content)) {
|
|
125
|
+
issues.push("Script tags detected");
|
|
126
|
+
}
|
|
127
|
+
if (/on\w+\s*=/i.test(content)) {
|
|
128
|
+
issues.push("Event handlers detected");
|
|
129
|
+
}
|
|
130
|
+
if (/javascript:/i.test(content)) {
|
|
131
|
+
issues.push("JavaScript protocol detected");
|
|
132
|
+
}
|
|
133
|
+
if (/data:[^,]*script/i.test(content)) {
|
|
134
|
+
issues.push("Data URL with script detected");
|
|
135
|
+
}
|
|
136
|
+
if (/<iframe[\s>]/i.test(content)) {
|
|
137
|
+
issues.push("Iframe tags detected");
|
|
138
|
+
}
|
|
139
|
+
if (/https?:\/\//i.test(content) || /www\./i.test(content)) {
|
|
140
|
+
issues.push("URLs detected in content");
|
|
141
|
+
}
|
|
142
|
+
return {
|
|
143
|
+
isSecure: issues.length === 0,
|
|
144
|
+
issues
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
function isValidElementType(elementType) {
|
|
148
|
+
const validTypes = Object.values(types.ELEMENT_TYPES).map((type) => type.value);
|
|
149
|
+
return validTypes.includes(elementType);
|
|
150
|
+
}
|
|
151
|
+
function validateAttributes(element) {
|
|
152
|
+
const errors = [];
|
|
153
|
+
const elementType = element.getAttribute("element-type");
|
|
154
|
+
const contributorId = element.getAttribute("contributor-id");
|
|
155
|
+
if (!elementType) {
|
|
156
|
+
errors.push("Missing required attribute: element-type");
|
|
157
|
+
} else if (!isValidElementType(elementType)) {
|
|
158
|
+
const validTypes = Object.values(types.ELEMENT_TYPES).map((t) => t.value).join(", ");
|
|
159
|
+
errors.push(
|
|
160
|
+
`Invalid element-type "${elementType}". Valid types: ${validTypes}`
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
if (!contributorId) {
|
|
164
|
+
errors.push("Missing required attribute: contributor-id");
|
|
165
|
+
} else if (contributorId.trim().length === 0) {
|
|
166
|
+
errors.push("contributor-id cannot be empty");
|
|
167
|
+
}
|
|
168
|
+
return {
|
|
169
|
+
isValid: errors.length === 0,
|
|
170
|
+
errors,
|
|
171
|
+
elementType: isValidElementType(elementType || "") ? elementType : void 0,
|
|
172
|
+
contributorId: contributorId || void 0
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
function getElementTypeLabel(elementType) {
|
|
176
|
+
const type = Object.values(types.ELEMENT_TYPES).find((t) => t.value === elementType);
|
|
177
|
+
return type?.label || elementType;
|
|
178
|
+
}
|
|
179
|
+
function getElementTypeAcronym(elementType) {
|
|
180
|
+
const type = Object.values(types.ELEMENT_TYPES).find((t) => t.value === elementType);
|
|
181
|
+
return type?.acronym || elementType.substring(0, 2).toUpperCase();
|
|
182
|
+
}
|
|
107
183
|
function generateStructuredData(content, elementType, contributorId, sourceUrl) {
|
|
108
184
|
return {
|
|
109
185
|
"@context": "https://schema.org",
|
|
110
186
|
"@type": "AnalysisContent",
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
187
|
+
text: content,
|
|
188
|
+
analysisType: elementType,
|
|
189
|
+
category: getElementTypeLabel(elementType),
|
|
190
|
+
contributor: contributorId,
|
|
191
|
+
url: generateRedirectUrl(content, elementType, contributorId),
|
|
192
|
+
provider: {
|
|
117
193
|
"@type": "Organization",
|
|
118
|
-
|
|
119
|
-
|
|
194
|
+
name: "Skhema",
|
|
195
|
+
url: "https://skhema.com"
|
|
120
196
|
},
|
|
121
|
-
|
|
197
|
+
isPartOf: {
|
|
122
198
|
"@type": "WebPage",
|
|
123
|
-
|
|
199
|
+
url: sourceUrl
|
|
124
200
|
},
|
|
125
|
-
|
|
126
|
-
|
|
201
|
+
dateCreated: (/* @__PURE__ */ new Date()).toISOString(),
|
|
202
|
+
platform: "Skhema"
|
|
127
203
|
};
|
|
128
204
|
}
|
|
129
205
|
function generateRedirectUrl(content, elementType, contributorId, options = {}) {
|
|
@@ -156,7 +232,7 @@ function createMetaTags(content, elementType, contributorId) {
|
|
|
156
232
|
function createAriaAttributes(elementType) {
|
|
157
233
|
const label = getElementTypeLabel(elementType);
|
|
158
234
|
return {
|
|
159
|
-
|
|
235
|
+
role: "article",
|
|
160
236
|
"aria-label": `${label} - Strategic insight`,
|
|
161
237
|
"aria-describedby": "skhema-description"
|
|
162
238
|
};
|
|
@@ -298,6 +374,10 @@ const styles = `
|
|
|
298
374
|
margin: 0;
|
|
299
375
|
font-style: italic;
|
|
300
376
|
position: relative;
|
|
377
|
+
word-wrap: break-word;
|
|
378
|
+
overflow-wrap: break-word;
|
|
379
|
+
hyphens: auto;
|
|
380
|
+
max-width: 100%;
|
|
301
381
|
}
|
|
302
382
|
|
|
303
383
|
.skhema-content-text::before {
|
|
@@ -480,7 +560,14 @@ class SkhemaElement extends HTMLElement {
|
|
|
480
560
|
this.shadow = this.attachShadow({ mode: "closed" });
|
|
481
561
|
}
|
|
482
562
|
static get observedAttributes() {
|
|
483
|
-
return [
|
|
563
|
+
return [
|
|
564
|
+
"element-type",
|
|
565
|
+
"contributor-id",
|
|
566
|
+
"content",
|
|
567
|
+
"source-url",
|
|
568
|
+
"theme",
|
|
569
|
+
"track-analytics"
|
|
570
|
+
];
|
|
484
571
|
}
|
|
485
572
|
connectedCallback() {
|
|
486
573
|
if (this.componentConnected) return;
|
|
@@ -505,7 +592,17 @@ class SkhemaElement extends HTMLElement {
|
|
|
505
592
|
}
|
|
506
593
|
const content = this.getContent();
|
|
507
594
|
if (!content.trim()) {
|
|
508
|
-
this.renderError("Component requires content", [
|
|
595
|
+
this.renderError("Component requires content", [
|
|
596
|
+
"Add content between the opening and closing tags, or use the content attribute"
|
|
597
|
+
]);
|
|
598
|
+
return;
|
|
599
|
+
}
|
|
600
|
+
const securityValidation = validateContentSecurity(content);
|
|
601
|
+
if (!securityValidation.isSecure) {
|
|
602
|
+
this.renderError(
|
|
603
|
+
"Content security validation failed",
|
|
604
|
+
securityValidation.issues
|
|
605
|
+
);
|
|
509
606
|
return;
|
|
510
607
|
}
|
|
511
608
|
this.contentData = {
|
|
@@ -527,7 +624,11 @@ class SkhemaElement extends HTMLElement {
|
|
|
527
624
|
if (!this.contentData) return;
|
|
528
625
|
const { element_type, contributor_id, content } = this.contentData;
|
|
529
626
|
const label = getElementTypeLabel(element_type);
|
|
530
|
-
const redirectUrl = generateRedirectUrl(
|
|
627
|
+
const redirectUrl = generateRedirectUrl(
|
|
628
|
+
content,
|
|
629
|
+
element_type,
|
|
630
|
+
contributor_id
|
|
631
|
+
);
|
|
531
632
|
const theme = this.getAttribute("theme") || "auto";
|
|
532
633
|
const displayName = this.formatContributorName(contributor_id);
|
|
533
634
|
const initials = this.getInitials(displayName);
|
|
@@ -555,7 +656,7 @@ class SkhemaElement extends HTMLElement {
|
|
|
555
656
|
</div>
|
|
556
657
|
|
|
557
658
|
<div class="skhema-content">
|
|
558
|
-
<div class="skhema-content-text">${content}</div>
|
|
659
|
+
<div class="skhema-content-text">${sanitizeContent(content)}</div>
|
|
559
660
|
</div>
|
|
560
661
|
|
|
561
662
|
<div class="skhema-footer">
|
|
@@ -572,7 +673,9 @@ class SkhemaElement extends HTMLElement {
|
|
|
572
673
|
</div>
|
|
573
674
|
</div>
|
|
574
675
|
`;
|
|
575
|
-
const saveBtn = this.shadow.querySelector(
|
|
676
|
+
const saveBtn = this.shadow.querySelector(
|
|
677
|
+
".skhema-save-btn"
|
|
678
|
+
);
|
|
576
679
|
if (saveBtn) {
|
|
577
680
|
saveBtn.addEventListener("click", (event) => {
|
|
578
681
|
this.handleSaveClick(event);
|
|
@@ -599,15 +702,22 @@ class SkhemaElement extends HTMLElement {
|
|
|
599
702
|
</div>
|
|
600
703
|
</div>
|
|
601
704
|
`;
|
|
602
|
-
this.dispatchEvent(
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
705
|
+
this.dispatchEvent(
|
|
706
|
+
new CustomEvent("skhema:error", {
|
|
707
|
+
detail: { error: title, details: errors },
|
|
708
|
+
bubbles: true
|
|
709
|
+
})
|
|
710
|
+
);
|
|
606
711
|
}
|
|
607
712
|
addStructuredData() {
|
|
608
713
|
if (!this.contentData) return;
|
|
609
714
|
const { content, element_type, contributor_id, source_url } = this.contentData;
|
|
610
|
-
const structuredData = generateStructuredData(
|
|
715
|
+
const structuredData = generateStructuredData(
|
|
716
|
+
content,
|
|
717
|
+
element_type,
|
|
718
|
+
contributor_id,
|
|
719
|
+
source_url
|
|
720
|
+
);
|
|
611
721
|
const script = document.createElement("script");
|
|
612
722
|
script.type = "application/ld+json";
|
|
613
723
|
script.textContent = JSON.stringify(structuredData);
|
|
@@ -631,20 +741,24 @@ class SkhemaElement extends HTMLElement {
|
|
|
631
741
|
userAgent: navigator.userAgent
|
|
632
742
|
};
|
|
633
743
|
await trackEmbedLoad(analytics);
|
|
634
|
-
this.dispatchEvent(
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
744
|
+
this.dispatchEvent(
|
|
745
|
+
new CustomEvent("skhema:load", {
|
|
746
|
+
detail: analytics,
|
|
747
|
+
bubbles: true
|
|
748
|
+
})
|
|
749
|
+
);
|
|
638
750
|
}
|
|
639
751
|
async handleSaveClick(_event) {
|
|
640
752
|
if (!this.contentData) return;
|
|
641
753
|
if (shouldTrackAnalytics(this)) {
|
|
642
754
|
await trackClick(this.contentData);
|
|
643
755
|
}
|
|
644
|
-
this.dispatchEvent(
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
756
|
+
this.dispatchEvent(
|
|
757
|
+
new CustomEvent("skhema:click", {
|
|
758
|
+
detail: this.contentData,
|
|
759
|
+
bubbles: true
|
|
760
|
+
})
|
|
761
|
+
);
|
|
648
762
|
}
|
|
649
763
|
// Public API methods
|
|
650
764
|
getContentData() {
|
|
@@ -656,11 +770,17 @@ class SkhemaElement extends HTMLElement {
|
|
|
656
770
|
}
|
|
657
771
|
function registerSkhemaElement() {
|
|
658
772
|
if (typeof window !== "undefined" && !customElements.get("skhema-element")) {
|
|
659
|
-
customElements.define(
|
|
773
|
+
customElements.define(
|
|
774
|
+
"skhema-element",
|
|
775
|
+
SkhemaElement
|
|
776
|
+
);
|
|
660
777
|
}
|
|
661
778
|
}
|
|
662
779
|
if (typeof window !== "undefined" && !customElements.get("skhema-element")) {
|
|
663
|
-
customElements.define(
|
|
780
|
+
customElements.define(
|
|
781
|
+
"skhema-element",
|
|
782
|
+
SkhemaElement
|
|
783
|
+
);
|
|
664
784
|
}
|
|
665
785
|
exports.SkhemaElement = SkhemaElement;
|
|
666
786
|
exports.default = SkhemaElement;
|