@financial-times/custom-code-component 1.11.2 → 2.0.1-alpha.10

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.
Files changed (35) hide show
  1. package/dist/index.js +598 -0
  2. package/dist/index.js.map +1 -0
  3. package/dist/webcomponent/src/custom-code-component.d.ts +55 -0
  4. package/dist/webcomponent/src/environment.d.ts +15 -0
  5. package/dist/webcomponent/src/errors.d.ts +27 -0
  6. package/dist/webcomponent/src/events.d.ts +10 -0
  7. package/dist/webcomponent/src/get-trace.d.ts +8 -0
  8. package/dist/webcomponent/src/index.d.ts +1 -0
  9. package/dist/webcomponent/src/logger.d.ts +20 -0
  10. package/dist/webcomponent/src/path.d.ts +23 -0
  11. package/dist/{tracking.d.ts → webcomponent/src/tracking.d.ts} +12 -4
  12. package/dist/webcomponent/src/util.d.ts +33 -0
  13. package/dist/webcomponent/test/environment.test.d.ts +1 -0
  14. package/dist/webcomponent/test/error-handling.test.d.ts +8 -0
  15. package/dist/webcomponent/test/example.d.ts +11 -0
  16. package/dist/webcomponent/test/generate-readable-stream.d.ts +8 -0
  17. package/dist/webcomponent/test/path.test.d.ts +5 -0
  18. package/dist/webcomponent/test/ssr.test.d.ts +4 -0
  19. package/dist/webcomponent/test/utils.test.d.ts +1 -0
  20. package/dist/webcomponent/vite.config.d.ts +2 -0
  21. package/dist/webcomponent/vitest.config.d.ts +2 -0
  22. package/package.json +25 -17
  23. package/src/custom-code-component.ts +247 -167
  24. package/src/environment.ts +77 -0
  25. package/src/errors.ts +73 -0
  26. package/src/events.ts +22 -0
  27. package/src/{get-trace.js → get-trace.ts} +23 -14
  28. package/src/index.ts +8 -0
  29. package/src/logger.ts +71 -0
  30. package/src/path.ts +83 -0
  31. package/src/tracking.ts +20 -11
  32. package/src/util.ts +66 -0
  33. package/dist/custom-code-component.d.ts +0 -21
  34. package/dist/custom-code-component.js +0 -380
  35. package/src/custom-code-component.xsd +0 -67
@@ -2,12 +2,17 @@
2
2
  import {
3
3
  sanitise,
4
4
  assignIfUndefined,
5
- } from "@financial-times/o-tracking/src/javascript/utils.js";
5
+ } from "@financial-times/o-tracking/src/javascript/utils";
6
6
 
7
+ type PropMap = { [k: string]: any };
7
8
  // For a given container element, get the number of elements that match the
8
9
  // original element (siblings); and the index of the original element (position).
9
- const getSiblingsAndPosition = (el, originalEl, selector) => {
10
- const siblings = Array.from(el.querySelectorAll(selector));
10
+ const getSiblingsAndPosition = (
11
+ el: Element | null,
12
+ originalEl: Element,
13
+ selector: string
14
+ ) => {
15
+ const siblings = Array.from(el?.querySelectorAll(selector) ?? []);
11
16
  const position = siblings.findIndex((item) => item === originalEl);
12
17
  if (position === -1) {
13
18
  return;
@@ -27,8 +32,8 @@ const elementPropertiesToCollect = [
27
32
  "role",
28
33
  ];
29
34
  // Get all (sanitised) properties of a given element.
30
- const getAllElementProperties = (element) => {
31
- const properties = {};
35
+ const getAllElementProperties = (element: any) => {
36
+ const properties: PropMap = {};
32
37
  for (const property of elementPropertiesToCollect) {
33
38
  const value =
34
39
  element[property] ||
@@ -46,7 +51,7 @@ const getAllElementProperties = (element) => {
46
51
  return properties;
47
52
  };
48
53
 
49
- const parseRawValue = (rawValue) => {
54
+ const parseRawValue = (rawValue: string) => {
50
55
  try {
51
56
  const parsedValue = JSON.parse(rawValue);
52
57
  const type = Object.prototype.toString.call(parsedValue);
@@ -58,14 +63,14 @@ const parseRawValue = (rawValue) => {
58
63
  }
59
64
  };
60
65
 
61
- const getAttributeValue = (rawValue) => {
66
+ const getAttributeValue = (rawValue: string) => {
62
67
  const [isJSON, value] = parseRawValue(rawValue);
63
68
 
64
69
  return isJSON ? value : rawValue;
65
70
  };
66
71
 
67
72
  // Get some properties of a given element.
68
- const getDomPathProps = (attrs, props) => {
73
+ const getDomPathProps = (attrs: Attr[], props: PropMap) => {
69
74
  // Collect any attribute that matches given strings.
70
75
  attrs
71
76
  .filter((attribute) =>
@@ -79,8 +84,12 @@ const getDomPathProps = (attrs, props) => {
79
84
  };
80
85
 
81
86
  // Get only the custom data-trackable-context-? properties of a given element
82
- const getContextProps = (attrs, props, isOriginalEl) => {
83
- const customProps = {};
87
+ const getContextProps = (
88
+ attrs: Attr[],
89
+ props: PropMap,
90
+ isOriginalEl: boolean
91
+ ) => {
92
+ const customProps: { [k: string]: any } = {};
84
93
 
85
94
  // for the original element collect properties like className, nodeName
86
95
  if (isOriginalEl) {
@@ -102,11 +111,11 @@ const getContextProps = (attrs, props, isOriginalEl) => {
102
111
  return customProps;
103
112
  };
104
113
 
105
- export function getTrace(el, rootEl) {
114
+ export function getTrace(el: Element, rootEl: Element) {
106
115
  const originalEl = el;
107
- const selector = originalEl.getAttribute("data-trackable")
116
+ const selector = originalEl?.getAttribute("data-trackable")
108
117
  ? `[data-trackable="${originalEl.getAttribute("data-trackable")}"]`
109
- : originalEl.nodeName;
118
+ : originalEl?.nodeName;
110
119
  const trace = [];
111
120
  const customContext = {};
112
121
  while (el && el !== rootEl) {
@@ -129,7 +138,7 @@ export function getTrace(el, rootEl) {
129
138
 
130
139
  assignIfUndefined(contextProps, customContext);
131
140
 
132
- el = el.parentNode;
141
+ el = el.parentNode as Element;
133
142
  }
134
143
  return { trace, customContext };
135
144
  }
package/src/index.ts ADDED
@@ -0,0 +1,8 @@
1
+ import { FTCustomCodeComponent } from "./custom-code-component";
2
+
3
+ // Register the custom element
4
+ export const init = () =>
5
+ customElements.define("custom-code-component", FTCustomCodeComponent);
6
+
7
+ // Allows module to be imported multiple times but custom element only registered once
8
+ if (customElements && !customElements.get("custom-code-component")) init();
package/src/logger.ts ADDED
@@ -0,0 +1,71 @@
1
+ export const LogLevel = Object.freeze({
2
+ DEBUG: 0,
3
+ INFO: 1,
4
+ WARN: 2,
5
+ ERROR: 3,
6
+ TEST: 4,
7
+ DEFAULT: 2,
8
+ });
9
+
10
+ export function convertStringLogLevel(value: string | null) {
11
+ const level = value?.toLowerCase();
12
+
13
+ if (level === "debug") {
14
+ return LogLevel.DEBUG;
15
+ }
16
+
17
+ if (level === "info") {
18
+ return LogLevel.INFO;
19
+ }
20
+
21
+ if (level === "warn") {
22
+ return LogLevel.WARN;
23
+ }
24
+
25
+ if (level === "error") {
26
+ return LogLevel.ERROR;
27
+ }
28
+
29
+ if (level === "test") {
30
+ return LogLevel.TEST;
31
+ }
32
+
33
+ return LogLevel.DEFAULT;
34
+ }
35
+
36
+ export class Logger {
37
+ level: number;
38
+ constructor(
39
+ { level = LogLevel.DEFAULT }: { level: number } = {
40
+ level: LogLevel.DEFAULT,
41
+ }
42
+ ) {
43
+ this.level = level;
44
+ }
45
+
46
+ debug(...args: any[]) {
47
+ if (this.level <= LogLevel.DEBUG) {
48
+ console.info(...args);
49
+ }
50
+ }
51
+
52
+ log = this.debug;
53
+
54
+ info(...args: any[]) {
55
+ if (this.level <= LogLevel.INFO) {
56
+ console.info(...args);
57
+ }
58
+ }
59
+
60
+ warn(...args: any[]) {
61
+ if (this.level <= LogLevel.WARN) {
62
+ console.warn(...args);
63
+ }
64
+ }
65
+
66
+ error(...args: any[]) {
67
+ if (this.level <= LogLevel.ERROR) {
68
+ console.error(...args);
69
+ }
70
+ }
71
+ }
package/src/path.ts ADDED
@@ -0,0 +1,83 @@
1
+ import { CCCError } from "./errors";
2
+
3
+ export type ComponentPathType = {
4
+ org: string;
5
+ repo: string;
6
+ component: string;
7
+ versionRange: string;
8
+ };
9
+ export class ComponentPath {
10
+ org: string;
11
+ repo: string;
12
+ component: string;
13
+ versionRange: string;
14
+
15
+ constructor(path: ComponentPathType | string) {
16
+ const { org, repo, component, versionRange } = isValidComponentPathObject(
17
+ path
18
+ )
19
+ ? path
20
+ : ComponentPath.fromString(path);
21
+ this.org = org;
22
+ this.repo = repo;
23
+ this.component = component;
24
+ this.versionRange = versionRange;
25
+ }
26
+
27
+ set path(path: ComponentPathType | string) {
28
+ const { org, repo, component, versionRange } = isValidComponentPathObject(
29
+ path
30
+ )
31
+ ? path
32
+ : ComponentPath.fromString(path);
33
+ this.org = org;
34
+ this.repo = repo;
35
+ this.component = component;
36
+ this.versionRange = versionRange;
37
+ }
38
+
39
+ get path(): string {
40
+ return `${this.org}/${this.repo}@${this.versionRange}/${this.component}`;
41
+ }
42
+
43
+ toString(): string {
44
+ return this.path;
45
+ }
46
+
47
+ static fromString(path: string | null, v?: string | null): ComponentPath {
48
+ if (!path) throw new CCCError("No path specified");
49
+
50
+ const versionRange =
51
+ v ??
52
+ path
53
+ .match(/@[^\/]+/)
54
+ ?.toString()
55
+ .replace("@", "") ??
56
+ "unknown";
57
+
58
+ if (!versionRange) throw new CCCError("No version specified");
59
+
60
+ const [component, repo, org] = path
61
+ .replace(/@[^\/]+/, "")
62
+ .split("/")
63
+ .reverse();
64
+
65
+ return new ComponentPath({ org, repo, component, versionRange });
66
+ }
67
+ }
68
+
69
+ export type DetailType = {
70
+ component: ComponentPath;
71
+ source?: string;
72
+ cause?: string;
73
+ };
74
+
75
+ export function isValidComponentPathObject(
76
+ value: unknown
77
+ ): value is ComponentPathType {
78
+ if (typeof value === "object" && value !== null) {
79
+ return "org" in value && "repo" in value && "component" in value;
80
+ }
81
+
82
+ return false;
83
+ }
package/src/tracking.ts CHANGED
@@ -1,9 +1,11 @@
1
- import Delegate from "ftdomdelegate";
2
- import { getTrace } from "./get-trace";
1
+ import Delegate from "ftdomdelegate/main";
2
+
3
3
  import {
4
4
  sanitise,
5
5
  assignIfUndefined,
6
- } from "@financial-times/o-tracking/src/javascript/utils.js";
6
+ } from "@financial-times/o-tracking/src/javascript/utils";
7
+ import { Logger } from "./logger";
8
+ import { getTrace } from "./get-trace";
7
9
 
8
10
  const eventPropertiesToCollect = ["ctrlKey", "altKey", "shiftKey", "metaKey"];
9
11
 
@@ -16,6 +18,7 @@ class Tracking {
16
18
  category: string;
17
19
  elements: string | string[];
18
20
  isInitialised: boolean;
21
+ log: Logger;
19
22
 
20
23
  constructor({
21
24
  id = "00000000-0000-0000-0000-000000000000",
@@ -25,6 +28,7 @@ class Tracking {
25
28
  shadowRoot = null,
26
29
  category = "cta",
27
30
  elements = 'a, button, input, [role="button"]',
31
+ logger,
28
32
  }: {
29
33
  id?: string;
30
34
  name: string;
@@ -33,6 +37,7 @@ class Tracking {
33
37
  shadowRoot: ShadowRoot | null;
34
38
  category?: string;
35
39
  elements?: string | string[];
40
+ logger: Logger;
36
41
  }) {
37
42
  this.cccId = id;
38
43
  this.cccName = name;
@@ -42,17 +47,18 @@ class Tracking {
42
47
  this.category = category;
43
48
  this.elements = elements;
44
49
  this.isInitialised = false;
50
+ this.log = logger ?? new Logger();
45
51
  }
46
52
 
47
53
  // Get properties for the event (as opposed to properties of the clicked element)
48
- getEventProperties(event) {
49
- const eventProperties = {};
54
+ getEventProperties(event: any) {
55
+ const eventProperties: { [k: string]: any } = {};
50
56
  for (const property of eventPropertiesToCollect) {
51
57
  if (event[property]) {
52
58
  try {
53
59
  eventProperties[property] = sanitise(event[property]);
54
60
  } catch (e) {
55
- console.log(e);
61
+ this.log.info(e);
56
62
  }
57
63
  }
58
64
  }
@@ -60,8 +66,11 @@ class Tracking {
60
66
  }
61
67
 
62
68
  // Controller for handling click events
63
- handleClickEvent(eventData, root) {
64
- return (clickEvent, clickElement) => {
69
+ handleClickEvent(
70
+ eventData: { action: string; category: string },
71
+ root: Element
72
+ ) {
73
+ return (clickEvent: Event, clickElement: HTMLElement) => {
65
74
  const context: any = this.getEventProperties(clickEvent);
66
75
  const { trace, customContext } = getTrace(clickElement, root);
67
76
  context.custom =
@@ -94,7 +103,7 @@ class Tracking {
94
103
  };
95
104
  }
96
105
 
97
- sendSpoorEvent(triggerAction, extraDetail) {
106
+ sendSpoorEvent(triggerAction: any, extraDetail: any) {
98
107
  const eventData = {
99
108
  category: "component",
100
109
  action: "act",
@@ -120,7 +129,7 @@ class Tracking {
120
129
  );
121
130
  }
122
131
 
123
- init(id) {
132
+ init(id: string) {
124
133
  if (!this.isInitialised) {
125
134
  this.isInitialised = true;
126
135
  this.cccId = id ? id : this.cccId;
@@ -132,7 +141,7 @@ class Tracking {
132
141
 
133
142
  const root = this.shadowRoot?.querySelector("[data-component-root]");
134
143
 
135
- if (this.shadowRoot) {
144
+ if (root) {
136
145
  const shadowDelegate = new Delegate(root);
137
146
  shadowDelegate.on(
138
147
  "click",
package/src/util.ts ADDED
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Used to convert camelCase to kebab-case
3
+ * @param str
4
+ * @returns
5
+ */
6
+ export const kebabize = (str: string) =>
7
+ str.replace(
8
+ /[A-Z]+(?![a-z])|[A-Z]/g,
9
+ ($, ofs) => (ofs ? "-" : "") + $.toLowerCase()
10
+ );
11
+
12
+ /**
13
+ * Checks if the provided host is part of the allowed test environments.
14
+ *
15
+ * @param host - The hostname to check.
16
+ * @returns True if the hostname is a safe test environment, false otherwise.
17
+ */
18
+ export function isSafeTestEnv(host: string | undefined) {
19
+ return isAllowed([
20
+ 'localhost',
21
+ 'local.ft.com',
22
+ /^.*\.apps\.in\.ft\.com$/,
23
+ ], host);
24
+ }
25
+
26
+ /**
27
+ * Checks if the current window location hostname is considered a local environment.
28
+ *
29
+ * @returns True if the hostname is local, false otherwise.
30
+ */
31
+ export function isLocalEnv() {
32
+ return isAllowed([
33
+ 'localhost',
34
+ 'local.ft.com',
35
+ /^.*\.in\.ft\.com$/,
36
+ ], window.location.hostname);
37
+ }
38
+
39
+ /**
40
+ * Checks if the current window location host is one of the Spark environments.
41
+ *
42
+ * @returns True if the host is spark.ft.com or spark-staging.ft.com, false otherwise.
43
+ */
44
+ export function isSparkEnv() {
45
+ return isAllowed([
46
+ 'spark.ft.com',
47
+ 'spark-staging.ft.com',
48
+ ], window.location.host);
49
+ }
50
+
51
+ /**
52
+ * Checks if the given hostname is in the provided allowlist.
53
+ *
54
+ * @param allowlist - A list of allowed hostnames or RegExp matchers.
55
+ * @param hostname - The hostname to validate.
56
+ * @returns True if the hostname matches any item in the allowlist, false otherwise.
57
+ */
58
+ export function isAllowed(allowlist: (string|RegExp)[], hostname: string | undefined) {
59
+ if (!hostname) return false;
60
+ return allowlist.some(item => {
61
+ if (typeof item === 'string') {
62
+ return item === hostname;
63
+ }
64
+ return item.test(hostname);
65
+ });
66
+ }
@@ -1,21 +0,0 @@
1
- /**
2
- * @file
3
- * Main component definition for custom-code-component
4
- */
5
- import { ContentTree } from "@financial-times/content-tree";
6
- export declare const init: () => void;
7
- export interface CustomCodeComponent extends ContentTree.Node {
8
- type: "CustomCodeComponent";
9
- path: string;
10
- versionRange: string;
11
- altText: string;
12
- lastModified: string;
13
- fallbackImage?: ContentTree.Image;
14
- displayFallbackText: boolean;
15
- layout: "in-line" | "mid-grid" | "full-grid" | "full-bleed";
16
- attributes: {
17
- [key: string]: string | boolean | undefined;
18
- } | {
19
- children?: CustomCodeComponent | Array<CustomCodeComponent>;
20
- };
21
- }