@salesforce/webapp-experimental 1.80.1 → 1.82.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/dist/api/clients.js +72 -77
- package/dist/api/index.d.ts +4 -4
- package/dist/api/index.d.ts.map +1 -1
- package/dist/api/index.js +13 -11
- package/dist/api/utils/accounts.js +16 -30
- package/dist/api/utils/records.js +21 -20
- package/dist/api/utils/user.js +20 -28
- package/dist/app/index.d.ts +4 -4
- package/dist/app/index.d.ts.map +1 -1
- package/dist/app/index.js +7 -7
- package/dist/app/manifest.js +23 -37
- package/dist/app/org.js +45 -58
- package/dist/design/index.js +12 -19
- package/dist/design/interactions/interactionsController.d.ts +1 -6
- package/dist/design/interactions/interactionsController.d.ts.map +1 -1
- package/dist/index.d.ts +4 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +25 -10
- package/dist/package.json.js +4 -0
- package/dist/proxy/handler.d.ts +2 -7
- package/dist/proxy/handler.d.ts.map +1 -1
- package/dist/proxy/handler.js +462 -523
- package/dist/proxy/index.d.ts +2 -2
- package/dist/proxy/index.d.ts.map +1 -1
- package/dist/proxy/index.js +7 -6
- package/dist/proxy/livePreviewScript.js +11 -26
- package/dist/proxy/routing.d.ts +1 -6
- package/dist/proxy/routing.d.ts.map +1 -1
- package/dist/proxy/routing.js +73 -103
- package/package.json +7 -6
- package/dist/api/clients.test.d.ts +0 -7
- package/dist/api/clients.test.d.ts.map +0 -1
- package/dist/api/clients.test.js +0 -167
- package/dist/api/graphql-operations-types.js +0 -44
- package/dist/api/utils/records.test.d.ts +0 -7
- package/dist/api/utils/records.test.d.ts.map +0 -1
- package/dist/api/utils/records.test.js +0 -190
- package/dist/api/utils/user.test.d.ts +0 -7
- package/dist/api/utils/user.test.d.ts.map +0 -1
- package/dist/api/utils/user.test.js +0 -108
- package/dist/design/interactions/communicationManager.js +0 -108
- package/dist/design/interactions/componentMatcher.js +0 -80
- package/dist/design/interactions/editableManager.js +0 -95
- package/dist/design/interactions/eventHandlers.js +0 -125
- package/dist/design/interactions/index.js +0 -47
- package/dist/design/interactions/interactionsController.js +0 -135
- package/dist/design/interactions/styleManager.js +0 -97
- package/dist/design/interactions/utils/cssUtils.js +0 -72
- package/dist/design/interactions/utils/sourceUtils.js +0 -99
- package/dist/proxy/livePreviewScript.test.d.ts +0 -7
- package/dist/proxy/livePreviewScript.test.d.ts.map +0 -1
- package/dist/proxy/livePreviewScript.test.js +0 -96
|
@@ -1,190 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Copyright (c) 2026, Salesforce, Inc.,
|
|
3
|
-
* All rights reserved.
|
|
4
|
-
* For full license text, see the LICENSE.txt file
|
|
5
|
-
*/
|
|
6
|
-
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
7
|
-
import { getRecord, createRecord, updateRecord, deleteRecord } from "./records.js";
|
|
8
|
-
// Mock the uiApiClient
|
|
9
|
-
vi.mock("../clients.js", () => {
|
|
10
|
-
const mockGet = vi.fn();
|
|
11
|
-
const mockPost = vi.fn();
|
|
12
|
-
const mockPatch = vi.fn();
|
|
13
|
-
const mockDelete = vi.fn();
|
|
14
|
-
return {
|
|
15
|
-
uiApiClient: {
|
|
16
|
-
get: mockGet,
|
|
17
|
-
post: mockPost,
|
|
18
|
-
patch: mockPatch,
|
|
19
|
-
delete: mockDelete,
|
|
20
|
-
},
|
|
21
|
-
};
|
|
22
|
-
});
|
|
23
|
-
// Get references to the mocked functions for assertions
|
|
24
|
-
const { uiApiClient } = await import("../clients.js");
|
|
25
|
-
const mockGet = vi.mocked(uiApiClient.get);
|
|
26
|
-
const mockPost = vi.mocked(uiApiClient.post);
|
|
27
|
-
const mockPatch = vi.mocked(uiApiClient.patch);
|
|
28
|
-
const mockDelete = vi.mocked(uiApiClient.delete);
|
|
29
|
-
describe("Records API Utils", () => {
|
|
30
|
-
beforeEach(() => {
|
|
31
|
-
vi.clearAllMocks();
|
|
32
|
-
});
|
|
33
|
-
describe("getRecord", () => {
|
|
34
|
-
const data = {
|
|
35
|
-
fields: {
|
|
36
|
-
Name: { value: "Test Account" },
|
|
37
|
-
Industry: { value: "Technology" },
|
|
38
|
-
Phone: { value: "555-0123" },
|
|
39
|
-
},
|
|
40
|
-
};
|
|
41
|
-
const mockRecordResponse = { json: vi.fn().mockResolvedValue(data) };
|
|
42
|
-
it("fetches a record by ID with default params", async () => {
|
|
43
|
-
mockGet.mockResolvedValue(mockRecordResponse);
|
|
44
|
-
const result = await getRecord("001000000000001");
|
|
45
|
-
expect(mockGet).toHaveBeenCalledWith("/records/001000000000001");
|
|
46
|
-
expect(result).toEqual(data);
|
|
47
|
-
});
|
|
48
|
-
it("fetches a record by ID with custom params", async () => {
|
|
49
|
-
mockGet.mockResolvedValue(mockRecordResponse);
|
|
50
|
-
const params = { fields: "Name,Industry,Phone" };
|
|
51
|
-
const result = await getRecord("001000000000001", params);
|
|
52
|
-
expect(mockGet).toHaveBeenCalledWith("/records/001000000000001?fields=Name%2CIndustry%2CPhone");
|
|
53
|
-
expect(result).toEqual(data);
|
|
54
|
-
});
|
|
55
|
-
it("handles API errors correctly", async () => {
|
|
56
|
-
const apiError = new Error("Record not found");
|
|
57
|
-
mockGet.mockRejectedValue(apiError);
|
|
58
|
-
await expect(getRecord("001000000000001")).rejects.toThrow("Record not found");
|
|
59
|
-
expect(mockGet).toHaveBeenCalledWith("/records/001000000000001");
|
|
60
|
-
});
|
|
61
|
-
});
|
|
62
|
-
describe("createRecord", () => {
|
|
63
|
-
const data = {
|
|
64
|
-
id: "001000000000002",
|
|
65
|
-
success: true,
|
|
66
|
-
};
|
|
67
|
-
const mockCreateResponse = { json: vi.fn().mockResolvedValue(data) };
|
|
68
|
-
it("creates a record with correct API name and fields", async () => {
|
|
69
|
-
mockPost.mockResolvedValue(mockCreateResponse);
|
|
70
|
-
const apiName = "Account";
|
|
71
|
-
const fields = {
|
|
72
|
-
Name: "New Test Account",
|
|
73
|
-
Industry: "Healthcare",
|
|
74
|
-
Phone: "555-0456",
|
|
75
|
-
};
|
|
76
|
-
const result = await createRecord(apiName, fields);
|
|
77
|
-
expect(mockPost).toHaveBeenCalledWith("/records", { apiName, fields });
|
|
78
|
-
expect(result).toEqual(data);
|
|
79
|
-
});
|
|
80
|
-
it("handles empty fields object", async () => {
|
|
81
|
-
mockPost.mockResolvedValue(mockCreateResponse);
|
|
82
|
-
const result = await createRecord("Account", {});
|
|
83
|
-
expect(mockPost).toHaveBeenCalledWith("/records", {
|
|
84
|
-
apiName: "Account",
|
|
85
|
-
fields: {},
|
|
86
|
-
});
|
|
87
|
-
expect(result).toEqual(data);
|
|
88
|
-
});
|
|
89
|
-
it("handles API errors during creation", async () => {
|
|
90
|
-
const apiError = new Error("Invalid field value");
|
|
91
|
-
mockPost.mockRejectedValue(apiError);
|
|
92
|
-
const fields = { Name: "" }; // Invalid empty name
|
|
93
|
-
await expect(createRecord("Account", fields)).rejects.toThrow("Invalid field value");
|
|
94
|
-
expect(mockPost).toHaveBeenCalledWith("/records", {
|
|
95
|
-
apiName: "Account",
|
|
96
|
-
fields,
|
|
97
|
-
});
|
|
98
|
-
});
|
|
99
|
-
});
|
|
100
|
-
describe("updateRecord", () => {
|
|
101
|
-
const data = { success: true };
|
|
102
|
-
const mockUpdateResponse = { json: vi.fn().mockResolvedValue(data) };
|
|
103
|
-
it("updates a record with correct ID and fields", async () => {
|
|
104
|
-
mockPatch.mockResolvedValue(mockUpdateResponse);
|
|
105
|
-
const recordId = "001000000000001";
|
|
106
|
-
const fields = {
|
|
107
|
-
Name: "Updated Account Name",
|
|
108
|
-
Industry: "Finance",
|
|
109
|
-
};
|
|
110
|
-
const result = await updateRecord(recordId, fields);
|
|
111
|
-
expect(mockPatch).toHaveBeenCalledWith(`/records/${recordId}`, {
|
|
112
|
-
fields,
|
|
113
|
-
});
|
|
114
|
-
expect(result).toEqual(data);
|
|
115
|
-
});
|
|
116
|
-
it("handles partial field updates", async () => {
|
|
117
|
-
mockPatch.mockResolvedValue(mockUpdateResponse);
|
|
118
|
-
const recordId = "001000000000001";
|
|
119
|
-
const fields = { Industry: "Manufacturing" };
|
|
120
|
-
const result = await updateRecord(recordId, fields);
|
|
121
|
-
expect(mockPatch).toHaveBeenCalledWith(`/records/${recordId}`, {
|
|
122
|
-
fields,
|
|
123
|
-
});
|
|
124
|
-
expect(result).toEqual(data);
|
|
125
|
-
});
|
|
126
|
-
it("handles API errors during update", async () => {
|
|
127
|
-
const apiError = new Error("Record is locked");
|
|
128
|
-
mockPatch.mockRejectedValue(apiError);
|
|
129
|
-
const recordId = "001000000000001";
|
|
130
|
-
const fields = { Name: "New Name" };
|
|
131
|
-
await expect(updateRecord(recordId, fields)).rejects.toThrow("Record is locked");
|
|
132
|
-
expect(mockPatch).toHaveBeenCalledWith(`/records/${recordId}`, {
|
|
133
|
-
fields,
|
|
134
|
-
});
|
|
135
|
-
});
|
|
136
|
-
it("handles empty fields object for update", async () => {
|
|
137
|
-
mockPatch.mockResolvedValue(mockUpdateResponse);
|
|
138
|
-
const result = await updateRecord("001000000000001", {});
|
|
139
|
-
expect(mockPatch).toHaveBeenCalledWith("/records/001000000000001", {
|
|
140
|
-
fields: {},
|
|
141
|
-
});
|
|
142
|
-
expect(result).toEqual(data);
|
|
143
|
-
});
|
|
144
|
-
});
|
|
145
|
-
describe("deleteRecord", () => {
|
|
146
|
-
it("deletes a record by ID and returns true", async () => {
|
|
147
|
-
mockDelete.mockResolvedValue({});
|
|
148
|
-
const result = await deleteRecord("001000000000001");
|
|
149
|
-
expect(mockDelete).toHaveBeenCalledWith("/records/001000000000001");
|
|
150
|
-
expect(result).toBe(true);
|
|
151
|
-
});
|
|
152
|
-
it("handles API errors during deletion", async () => {
|
|
153
|
-
const apiError = new Error("Cannot delete record");
|
|
154
|
-
mockDelete.mockRejectedValue(apiError);
|
|
155
|
-
await expect(deleteRecord("001000000000001")).rejects.toThrow("Cannot delete record");
|
|
156
|
-
expect(mockDelete).toHaveBeenCalledWith("/records/001000000000001");
|
|
157
|
-
});
|
|
158
|
-
it("always returns true on successful deletion", async () => {
|
|
159
|
-
mockDelete.mockResolvedValue({ status: 204 });
|
|
160
|
-
const result = await deleteRecord("001000000000001");
|
|
161
|
-
expect(result).toBe(true);
|
|
162
|
-
});
|
|
163
|
-
});
|
|
164
|
-
describe("Error Handling", () => {
|
|
165
|
-
it("propagates network errors from getRecord", async () => {
|
|
166
|
-
const networkError = new Error("Network timeout");
|
|
167
|
-
networkError.name = "NetworkError";
|
|
168
|
-
mockGet.mockRejectedValue(networkError);
|
|
169
|
-
await expect(getRecord("001000000000001")).rejects.toThrow("Network timeout");
|
|
170
|
-
});
|
|
171
|
-
it("propagates validation errors from createRecord", async () => {
|
|
172
|
-
const validationError = new Error("Required field missing");
|
|
173
|
-
validationError.name = "ValidationError";
|
|
174
|
-
mockPost.mockRejectedValue(validationError);
|
|
175
|
-
await expect(createRecord("Account", {})).rejects.toThrow("Required field missing");
|
|
176
|
-
});
|
|
177
|
-
it("propagates permission errors from updateRecord", async () => {
|
|
178
|
-
const permissionError = new Error("Insufficient permissions");
|
|
179
|
-
permissionError.name = "PermissionError";
|
|
180
|
-
mockPatch.mockRejectedValue(permissionError);
|
|
181
|
-
await expect(updateRecord("001000000000001", { Name: "Test" })).rejects.toThrow("Insufficient permissions");
|
|
182
|
-
});
|
|
183
|
-
it("propagates not found errors from deleteRecord", async () => {
|
|
184
|
-
const notFoundError = new Error("Record not found");
|
|
185
|
-
notFoundError.name = "NotFoundError";
|
|
186
|
-
mockDelete.mockRejectedValue(notFoundError);
|
|
187
|
-
await expect(deleteRecord("001000000000001")).rejects.toThrow("Record not found");
|
|
188
|
-
});
|
|
189
|
-
});
|
|
190
|
-
});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"user.test.d.ts","sourceRoot":"","sources":["../../../src/api/utils/user.test.ts"],"names":[],"mappings":"AAAA;;;;GAIG"}
|
|
@@ -1,108 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Copyright (c) 2026, Salesforce, Inc.,
|
|
3
|
-
* All rights reserved.
|
|
4
|
-
* For full license text, see the LICENSE.txt file
|
|
5
|
-
*/
|
|
6
|
-
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
7
|
-
import { getCurrentUser } from "./user.js";
|
|
8
|
-
const mockFetch = vi.fn();
|
|
9
|
-
vi.mock("@salesforce/sdk-data", () => ({
|
|
10
|
-
getDataSDK: vi.fn(() => Promise.resolve({
|
|
11
|
-
fetch: mockFetch,
|
|
12
|
-
})),
|
|
13
|
-
}));
|
|
14
|
-
// Mock console.error to avoid noise in test output
|
|
15
|
-
const mockConsoleError = vi.spyOn(console, "error").mockImplementation(() => {
|
|
16
|
-
/* noop */
|
|
17
|
-
});
|
|
18
|
-
describe("User API Utils", () => {
|
|
19
|
-
beforeEach(() => {
|
|
20
|
-
vi.clearAllMocks();
|
|
21
|
-
});
|
|
22
|
-
afterEach(() => {
|
|
23
|
-
mockConsoleError.mockClear();
|
|
24
|
-
});
|
|
25
|
-
describe("getCurrentUser", () => {
|
|
26
|
-
const mockChatterUser = {
|
|
27
|
-
id: "005000000000001",
|
|
28
|
-
name: "John Doe",
|
|
29
|
-
email: "john@example.com",
|
|
30
|
-
username: "john@example.com",
|
|
31
|
-
};
|
|
32
|
-
it("successfully fetches current user", async () => {
|
|
33
|
-
mockFetch.mockResolvedValue({
|
|
34
|
-
ok: true,
|
|
35
|
-
json: () => Promise.resolve(mockChatterUser),
|
|
36
|
-
});
|
|
37
|
-
const result = await getCurrentUser();
|
|
38
|
-
expect(mockFetch).toHaveBeenCalledWith("/services/data/v99.0/chatter/users/me");
|
|
39
|
-
expect(result).toEqual({
|
|
40
|
-
id: "005000000000001",
|
|
41
|
-
name: "John Doe",
|
|
42
|
-
});
|
|
43
|
-
});
|
|
44
|
-
it('falls back to "User" when name is empty', async () => {
|
|
45
|
-
mockFetch.mockResolvedValue({
|
|
46
|
-
ok: true,
|
|
47
|
-
json: () => Promise.resolve({
|
|
48
|
-
id: "005000000000003",
|
|
49
|
-
name: "",
|
|
50
|
-
}),
|
|
51
|
-
});
|
|
52
|
-
const result = await getCurrentUser();
|
|
53
|
-
expect(result).toEqual({
|
|
54
|
-
id: "005000000000003",
|
|
55
|
-
name: "User",
|
|
56
|
-
});
|
|
57
|
-
});
|
|
58
|
-
it('falls back to "User" when name is null', async () => {
|
|
59
|
-
mockFetch.mockResolvedValue({
|
|
60
|
-
ok: true,
|
|
61
|
-
json: () => Promise.resolve({
|
|
62
|
-
id: "005000000000004",
|
|
63
|
-
name: null,
|
|
64
|
-
}),
|
|
65
|
-
});
|
|
66
|
-
const result = await getCurrentUser();
|
|
67
|
-
expect(result).toEqual({
|
|
68
|
-
id: "005000000000004",
|
|
69
|
-
name: "User",
|
|
70
|
-
});
|
|
71
|
-
});
|
|
72
|
-
it('falls back to "User" when name is undefined', async () => {
|
|
73
|
-
mockFetch.mockResolvedValue({
|
|
74
|
-
ok: true,
|
|
75
|
-
json: () => Promise.resolve({
|
|
76
|
-
id: "005000000000005",
|
|
77
|
-
}),
|
|
78
|
-
});
|
|
79
|
-
const result = await getCurrentUser();
|
|
80
|
-
expect(result).toEqual({
|
|
81
|
-
id: "005000000000005",
|
|
82
|
-
name: "User",
|
|
83
|
-
});
|
|
84
|
-
});
|
|
85
|
-
it("throws error when request fails", async () => {
|
|
86
|
-
mockFetch.mockResolvedValue({
|
|
87
|
-
ok: false,
|
|
88
|
-
status: 500,
|
|
89
|
-
});
|
|
90
|
-
await expect(getCurrentUser()).rejects.toThrow("HTTP 500");
|
|
91
|
-
expect(mockConsoleError).toHaveBeenCalled();
|
|
92
|
-
});
|
|
93
|
-
it("throws error when no user data is found", async () => {
|
|
94
|
-
mockFetch.mockResolvedValue({
|
|
95
|
-
ok: true,
|
|
96
|
-
json: () => Promise.resolve({}),
|
|
97
|
-
});
|
|
98
|
-
await expect(getCurrentUser()).rejects.toThrow("No user data found in Chatter API response");
|
|
99
|
-
expect(mockConsoleError).toHaveBeenCalled();
|
|
100
|
-
});
|
|
101
|
-
it("throws error when fetch rejects", async () => {
|
|
102
|
-
const apiError = new Error("Network error");
|
|
103
|
-
mockFetch.mockRejectedValue(apiError);
|
|
104
|
-
await expect(getCurrentUser()).rejects.toThrow("Network error");
|
|
105
|
-
expect(mockConsoleError).toHaveBeenCalledWith("Error fetching user data:", apiError);
|
|
106
|
-
});
|
|
107
|
-
});
|
|
108
|
-
});
|
|
@@ -1,108 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Copyright (c) 2026, Salesforce, Inc.,
|
|
3
|
-
* All rights reserved.
|
|
4
|
-
* For full license text, see the LICENSE.txt file
|
|
5
|
-
*/
|
|
6
|
-
/**
|
|
7
|
-
* Communication Manager Module
|
|
8
|
-
* Handles communication with the parent window (VS Code extension)
|
|
9
|
-
*/
|
|
10
|
-
import { TEXT_TAGS } from "./editableManager.js";
|
|
11
|
-
import { getElementStyles } from "./utils/cssUtils.js";
|
|
12
|
-
import { getLabelFromSource, getSourceFromDataAttributes } from "./utils/sourceUtils.js";
|
|
13
|
-
export class CommunicationManager {
|
|
14
|
-
constructor() {
|
|
15
|
-
// CommunicationManager has no required initialization
|
|
16
|
-
}
|
|
17
|
-
/**
|
|
18
|
-
* Notify the extension about a selected component
|
|
19
|
-
* @param element - The selected element
|
|
20
|
-
*/
|
|
21
|
-
notifyComponentSelected(element) {
|
|
22
|
-
const label = getLabelFromSource(element);
|
|
23
|
-
// Temporarily remove design-mode-selected class to get original styles
|
|
24
|
-
const wasSelected = element.classList.contains("design-mode-selected");
|
|
25
|
-
if (wasSelected) {
|
|
26
|
-
element.classList.remove("design-mode-selected");
|
|
27
|
-
}
|
|
28
|
-
const styles = getElementStyles(element);
|
|
29
|
-
// Restore design-mode-selected class if it was present
|
|
30
|
-
if (wasSelected) {
|
|
31
|
-
element.classList.add("design-mode-selected");
|
|
32
|
-
}
|
|
33
|
-
// Source location metadata injected at compile time (babel-plugin-enhanced-locator)
|
|
34
|
-
const debugSource = getSourceFromDataAttributes(element);
|
|
35
|
-
const textType = element.dataset?.textType ?? "none";
|
|
36
|
-
const hasNonEditableText = TEXT_TAGS.includes(element.tagName) && (textType === "dynamic" || textType === "mixed");
|
|
37
|
-
// Send message to parent window
|
|
38
|
-
try {
|
|
39
|
-
if (window.parent !== window) {
|
|
40
|
-
window.parent.postMessage({
|
|
41
|
-
type: "component-selected",
|
|
42
|
-
component: {
|
|
43
|
-
name: label,
|
|
44
|
-
tagName: element.tagName,
|
|
45
|
-
styles: {
|
|
46
|
-
...styles,
|
|
47
|
-
},
|
|
48
|
-
debugSource: debugSource,
|
|
49
|
-
hasNonEditableText,
|
|
50
|
-
},
|
|
51
|
-
}, "*");
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
catch (error) {
|
|
55
|
-
console.log("Could not notify extension:", error);
|
|
56
|
-
}
|
|
57
|
-
// Store in global for polling fallback
|
|
58
|
-
window.selectedComponentInfo =
|
|
59
|
-
{
|
|
60
|
-
name: label,
|
|
61
|
-
element: element,
|
|
62
|
-
tagName: element.tagName,
|
|
63
|
-
};
|
|
64
|
-
}
|
|
65
|
-
/**
|
|
66
|
-
* Notify the extension about a text change
|
|
67
|
-
* @param element - The element that changed
|
|
68
|
-
* @param originalText - The original text
|
|
69
|
-
* @param newText - The new text
|
|
70
|
-
*/
|
|
71
|
-
notifyTextChange(element, originalText, newText) {
|
|
72
|
-
const label = getLabelFromSource(element);
|
|
73
|
-
const debugSource = getSourceFromDataAttributes(element);
|
|
74
|
-
try {
|
|
75
|
-
if (window.parent !== window) {
|
|
76
|
-
window.parent.postMessage({
|
|
77
|
-
type: "text-changed",
|
|
78
|
-
change: {
|
|
79
|
-
componentName: label,
|
|
80
|
-
tagName: element.tagName,
|
|
81
|
-
originalText,
|
|
82
|
-
newText,
|
|
83
|
-
debugSource,
|
|
84
|
-
},
|
|
85
|
-
}, "*");
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
catch (error) {
|
|
89
|
-
console.log("Could not notify extension about text change:", error);
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
/**
|
|
93
|
-
* Notify the parent window that interactions initialization is complete
|
|
94
|
-
*/
|
|
95
|
-
notifyInitializationComplete() {
|
|
96
|
-
try {
|
|
97
|
-
if (typeof window !== "undefined" && window.parent && window.parent !== window) {
|
|
98
|
-
window.parent.postMessage({
|
|
99
|
-
type: "interactions-initialized",
|
|
100
|
-
}, "*");
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
catch (error) {
|
|
104
|
-
const err = error;
|
|
105
|
-
console.warn("Could not send initialization message to parent:", err.message);
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
}
|
|
@@ -1,80 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Copyright (c) 2026, Salesforce, Inc.,
|
|
3
|
-
* All rights reserved.
|
|
4
|
-
* For full license text, see the LICENSE.txt file
|
|
5
|
-
*/
|
|
6
|
-
export class ComponentMatcher {
|
|
7
|
-
allowlist;
|
|
8
|
-
constructor(options = {}) {
|
|
9
|
-
this.allowlist = options.allowlist ?? [];
|
|
10
|
-
}
|
|
11
|
-
/**
|
|
12
|
-
* Check whether an element contains compile-time source metadata attributes.
|
|
13
|
-
* @param element - The element to check
|
|
14
|
-
* @returns True if the data-source-file attribute is present
|
|
15
|
-
*/
|
|
16
|
-
hasSourceMetadata(element) {
|
|
17
|
-
if (!element) {
|
|
18
|
-
return false;
|
|
19
|
-
}
|
|
20
|
-
return element.hasAttribute("data-source-file");
|
|
21
|
-
}
|
|
22
|
-
matchesList(element, selectors) {
|
|
23
|
-
return selectors.some((selector) => element.matches(selector));
|
|
24
|
-
}
|
|
25
|
-
/**
|
|
26
|
-
* Checks if a label represents a component name (not a fallback like tag name, ID, or text content)
|
|
27
|
-
* @param label - The label to check
|
|
28
|
-
* @param tagName - The element's tag name (lowercase)
|
|
29
|
-
* @returns True if the label is a component name
|
|
30
|
-
*/
|
|
31
|
-
isComponentNameLabel(label, tagName) {
|
|
32
|
-
return (label !== tagName && !label.includes("#") && !label.includes("(") && !label.includes('"'));
|
|
33
|
-
}
|
|
34
|
-
/**
|
|
35
|
-
* Check if an element is highlightable.
|
|
36
|
-
* @param element - The element to check
|
|
37
|
-
* @returns True if the element should be highlighted
|
|
38
|
-
*/
|
|
39
|
-
isHighlightableElement(element) {
|
|
40
|
-
if (!element || element === document.body || element === document.documentElement) {
|
|
41
|
-
return false;
|
|
42
|
-
}
|
|
43
|
-
if (!this.hasSourceMetadata(element)) {
|
|
44
|
-
return false;
|
|
45
|
-
}
|
|
46
|
-
if (this.allowlist.length > 0) {
|
|
47
|
-
return this.matchesList(element, this.allowlist);
|
|
48
|
-
}
|
|
49
|
-
return true;
|
|
50
|
-
}
|
|
51
|
-
/**
|
|
52
|
-
* Find the nearest highlightable element by walking up the DOM tree
|
|
53
|
-
* @param target - The target element
|
|
54
|
-
* @returns The highlightable element or null
|
|
55
|
-
*/
|
|
56
|
-
findHighlightableElement(target) {
|
|
57
|
-
if (!target || target === document.body || target === document.documentElement) {
|
|
58
|
-
return null;
|
|
59
|
-
}
|
|
60
|
-
// Fast path: use closest() if supported.
|
|
61
|
-
const closest = typeof target.closest === "function"
|
|
62
|
-
? target.closest("[data-source-file]")
|
|
63
|
-
: null;
|
|
64
|
-
if (closest &&
|
|
65
|
-
closest !== document.body &&
|
|
66
|
-
closest !== document.documentElement &&
|
|
67
|
-
this.isHighlightableElement(closest)) {
|
|
68
|
-
return closest;
|
|
69
|
-
}
|
|
70
|
-
// Fallback: manual walk.
|
|
71
|
-
let current = target;
|
|
72
|
-
while (current && current !== document.body && current !== document.documentElement) {
|
|
73
|
-
if (this.isHighlightableElement(current)) {
|
|
74
|
-
return current;
|
|
75
|
-
}
|
|
76
|
-
current = current.parentElement;
|
|
77
|
-
}
|
|
78
|
-
return null;
|
|
79
|
-
}
|
|
80
|
-
}
|
|
@@ -1,95 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Copyright (c) 2026, Salesforce, Inc.,
|
|
3
|
-
* All rights reserved.
|
|
4
|
-
* For full license text, see the LICENSE.txt file
|
|
5
|
-
*/
|
|
6
|
-
/**
|
|
7
|
-
* Editable Manager Module
|
|
8
|
-
* Handles making elements editable and notifying the extension of text changes.
|
|
9
|
-
* History/undo for style changes is owned by the design property panel (host); this module does not use history.
|
|
10
|
-
*/
|
|
11
|
-
import { findElementsBySourceLocation, getSourceFromDataAttributes } from "./utils/sourceUtils.js";
|
|
12
|
-
export const TEXT_TAGS = ["H1", "H2", "H3", "H4", "H5", "H6", "P", "SPAN", "A", "BUTTON", "LABEL"];
|
|
13
|
-
export class EditableManager {
|
|
14
|
-
communicationManager;
|
|
15
|
-
boundHandleBlur;
|
|
16
|
-
boundHandleKeydown;
|
|
17
|
-
boundHandleInput;
|
|
18
|
-
editableGroup;
|
|
19
|
-
constructor(communicationManager) {
|
|
20
|
-
this.communicationManager = communicationManager;
|
|
21
|
-
this.boundHandleBlur = this._handleBlur.bind(this);
|
|
22
|
-
this.boundHandleKeydown = this._handleKeydown.bind(this);
|
|
23
|
-
this.boundHandleInput = this._handleInput.bind(this);
|
|
24
|
-
this.editableGroup = [];
|
|
25
|
-
}
|
|
26
|
-
makeEditableIfText(element) {
|
|
27
|
-
if (!this._isTextElement(element)) {
|
|
28
|
-
return;
|
|
29
|
-
}
|
|
30
|
-
const source = getSourceFromDataAttributes(element);
|
|
31
|
-
const siblings = source ? findElementsBySourceLocation(source) : [];
|
|
32
|
-
const others = siblings.filter((el) => el !== element);
|
|
33
|
-
this.editableGroup = [element, ...others];
|
|
34
|
-
for (const el of this.editableGroup) {
|
|
35
|
-
el.dataset.originalText = el.textContent ?? "";
|
|
36
|
-
}
|
|
37
|
-
element.contentEditable = "true";
|
|
38
|
-
element.addEventListener("blur", this.boundHandleBlur);
|
|
39
|
-
element.addEventListener("keydown", this.boundHandleKeydown);
|
|
40
|
-
element.addEventListener("input", this.boundHandleInput);
|
|
41
|
-
}
|
|
42
|
-
removeEditable(element) {
|
|
43
|
-
if (element.contentEditable === "true") {
|
|
44
|
-
element.contentEditable = "false";
|
|
45
|
-
}
|
|
46
|
-
element.removeEventListener("blur", this.boundHandleBlur);
|
|
47
|
-
element.removeEventListener("keydown", this.boundHandleKeydown);
|
|
48
|
-
element.removeEventListener("input", this.boundHandleInput);
|
|
49
|
-
for (const el of this.editableGroup) {
|
|
50
|
-
delete el.dataset.originalText;
|
|
51
|
-
}
|
|
52
|
-
this.editableGroup = [];
|
|
53
|
-
}
|
|
54
|
-
_isTextElement(element) {
|
|
55
|
-
return (TEXT_TAGS.includes(element.tagName) &&
|
|
56
|
-
(element.textContent ?? "").trim().length > 0 &&
|
|
57
|
-
element.dataset.textType === "static");
|
|
58
|
-
}
|
|
59
|
-
_handleInput(e) {
|
|
60
|
-
const primary = e.target;
|
|
61
|
-
const text = primary.textContent ?? "";
|
|
62
|
-
for (let i = 1; i < this.editableGroup.length; i++) {
|
|
63
|
-
this.editableGroup[i].textContent = text;
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
_handleBlur(e) {
|
|
67
|
-
const element = e.target;
|
|
68
|
-
const newText = element.textContent ?? "";
|
|
69
|
-
const originalText = element.dataset.originalText ?? "";
|
|
70
|
-
if (newText !== originalText) {
|
|
71
|
-
for (const el of this.editableGroup) {
|
|
72
|
-
el.dataset.originalText = newText;
|
|
73
|
-
}
|
|
74
|
-
if (this.communicationManager) {
|
|
75
|
-
this.communicationManager.notifyTextChange(element, originalText, newText);
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
this.removeEditable(element);
|
|
79
|
-
}
|
|
80
|
-
_handleKeydown(e) {
|
|
81
|
-
const element = e.target;
|
|
82
|
-
if (e.key === "Enter" && !e.shiftKey) {
|
|
83
|
-
e.preventDefault();
|
|
84
|
-
element.blur();
|
|
85
|
-
}
|
|
86
|
-
if (e.key === "Escape") {
|
|
87
|
-
for (const el of this.editableGroup) {
|
|
88
|
-
if (el.dataset.originalText) {
|
|
89
|
-
el.textContent = el.dataset.originalText;
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
element.blur();
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
}
|