@salesforcedevs/docs-components 1.17.5-edit → 1.17.6-hover-edit

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/lwc.config.json CHANGED
@@ -19,6 +19,7 @@
19
19
  "doc/heading",
20
20
  "doc/headingAnchor",
21
21
  "doc/markdownEditor",
22
+ "doc/textSelectionSearch",
22
23
  "doc/overview",
23
24
  "doc/phase",
24
25
  "doc/specificationContent",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@salesforcedevs/docs-components",
3
- "version": "1.17.5-edit",
3
+ "version": "1.17.6-hover-edit",
4
4
  "description": "Docs Lightning web components for DSC",
5
5
  "license": "MIT",
6
6
  "main": "index.js",
@@ -0,0 +1,185 @@
1
+ # Text Selection Search Component
2
+
3
+ A Lightning Web Component that detects text selection on the page and shows a search popover for finding related content.
4
+
5
+ ## Features
6
+
7
+ - **Automatic Text Detection**: Detects text selection via mouse and keyboard
8
+ - **Positioned Popover**: Shows search interface near the selected text
9
+ - **Search Functionality**: Makes API calls to backend search endpoints
10
+ - **Modern UI**: Clean, responsive interface with loading states
11
+ - **Keyboard Navigation**: Escape key to close popover
12
+ - **Event Handling**: Dispatches events for search results and clicks
13
+ - **Customizable**: Configurable appearance and behavior
14
+
15
+ ## Usage
16
+
17
+ ### Basic Implementation
18
+
19
+ ```html
20
+ <doc-text-selection-search
21
+ search-api-url="/api/search"
22
+ placeholder="Search for..."
23
+ popover-position="top"
24
+ max-results="10">
25
+ </doc-text-selection-search>
26
+ ```
27
+
28
+ ### With Event Listeners
29
+
30
+ ```html
31
+ <doc-text-selection-search
32
+ search-api-url="/api/search"
33
+ @searchresults="handleSearchResults"
34
+ @resultclick="handleResultClick">
35
+ </doc-text-selection-search>
36
+ ```
37
+
38
+ ```javascript
39
+ handleSearchResults(event) {
40
+ const { query, results, selectedText } = event.detail;
41
+ console.log('Search results:', results);
42
+ }
43
+
44
+ handleResultClick(event) {
45
+ const { result, query, selectedText } = event.detail;
46
+ console.log('Result clicked:', result);
47
+ // Navigate to result or perform action
48
+ }
49
+ ```
50
+
51
+ ## API Reference
52
+
53
+ ### Properties
54
+
55
+ | Property | Type | Default | Description |
56
+ |----------|------|---------|-------------|
57
+ | `searchApiUrl` | string | `""` | The URL endpoint for the search API |
58
+ | `placeholder` | string | `"Search for..."` | Placeholder text for the search input |
59
+ | `popoverPosition` | `"top" \| "bottom"` | `"top"` | Position of popover relative to selected text |
60
+ | `maxResults` | number | `10` | Maximum number of search results to display |
61
+
62
+ ### Events
63
+
64
+ | Event | Detail | Description |
65
+ |-------|--------|-------------|
66
+ | `searchresults` | `{ query, results, selectedText }` | Fired when search results are received |
67
+ | `resultclick` | `{ result, query, selectedText }` | Fired when a search result is clicked |
68
+
69
+ ### Backend API Format
70
+
71
+ The component expects your backend API to:
72
+
73
+ **Request (POST):**
74
+ ```json
75
+ {
76
+ "query": "search term",
77
+ "maxResults": 10,
78
+ "selectedText": "originally selected text"
79
+ }
80
+ ```
81
+
82
+ **Response:**
83
+ ```json
84
+ {
85
+ "results": [
86
+ {
87
+ "title": "Result Title",
88
+ "description": "Result description or snippet",
89
+ "url": "https://example.com/result"
90
+ }
91
+ ]
92
+ }
93
+ ```
94
+
95
+ ## Examples
96
+
97
+ ### Knowledge Base Search
98
+
99
+ ```html
100
+ <doc-text-selection-search
101
+ search-api-url="https://api.example.com/kb/search"
102
+ placeholder="Search knowledge base..."
103
+ popover-position="bottom"
104
+ max-results="15">
105
+ </doc-text-selection-search>
106
+ ```
107
+
108
+ ### Documentation Search
109
+
110
+ ```html
111
+ <doc-text-selection-search
112
+ search-api-url="/api/docs/search"
113
+ placeholder="Search documentation..."
114
+ popover-position="top"
115
+ max-results="8">
116
+ </doc-text-selection-search>
117
+ ```
118
+
119
+ ### Custom Styling
120
+
121
+ You can customize the appearance by overriding CSS classes:
122
+
123
+ ```css
124
+ .text-selection-search-popover {
125
+ /* Custom popover styling */
126
+ border-radius: 12px;
127
+ box-shadow: 0 8px 24px rgba(0, 0, 0, 0.2);
128
+ }
129
+
130
+ .result-item:hover {
131
+ /* Custom hover effects */
132
+ background: #f0f8ff;
133
+ border-color: #0066cc;
134
+ }
135
+ ```
136
+
137
+ ## Browser Support
138
+
139
+ - Chrome 60+
140
+ - Firefox 55+
141
+ - Safari 12+
142
+ - Edge 79+
143
+
144
+ ## Accessibility
145
+
146
+ - Keyboard navigation support (Escape to close)
147
+ - Screen reader friendly
148
+ - High contrast mode support
149
+ - Focus management
150
+
151
+ ## Performance Considerations
152
+
153
+ - Event listeners are properly cleaned up on component destruction
154
+ - Debounced text selection detection
155
+ - Efficient DOM updates
156
+ - Minimal re-renders
157
+
158
+ ## Troubleshooting
159
+
160
+ ### Common Issues
161
+
162
+ 1. **Popover not appearing**: Ensure text selection is enabled and the component is properly mounted
163
+ 2. **API calls failing**: Check the `searchApiUrl` and ensure CORS is configured
164
+ 3. **Positioning issues**: The popover position calculation may need adjustment for complex layouts
165
+
166
+ ### Debug Mode
167
+
168
+ Enable console logging for debugging:
169
+
170
+ ```javascript
171
+ // Add to your component
172
+ connectedCallback() {
173
+ this.debug = true; // Enable debug logging
174
+ }
175
+ ```
176
+
177
+ ## Contributing
178
+
179
+ When contributing to this component:
180
+
181
+ 1. Follow the existing code style and patterns
182
+ 2. Add appropriate tests for new features
183
+ 3. Update documentation for API changes
184
+ 4. Ensure accessibility compliance
185
+ 5. Test across different browsers and devices
@@ -0,0 +1,286 @@
1
+ .text-selection-search-popover {
2
+ background: white;
3
+ border: 1px solid #e1e5e9;
4
+ border-radius: 8px;
5
+ box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);
6
+ padding: 0;
7
+ min-width: 320px;
8
+ max-width: 480px;
9
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
10
+ position: relative;
11
+ overflow: hidden;
12
+ opacity: 1;
13
+ visibility: visible;
14
+ transition: opacity 0.1s ease, visibility 0.1s ease;
15
+ }
16
+
17
+ .text-selection-search-popover.hidden {
18
+ opacity: 0;
19
+ visibility: hidden;
20
+ pointer-events: none;
21
+ position: absolute;
22
+ top: -9999px;
23
+ left: -9999px;
24
+ }
25
+
26
+ /* Header Styles */
27
+ .popover-header {
28
+ background: #f8f9fa;
29
+ border-bottom: 1px solid #e9ecef;
30
+ padding: 12px 16px;
31
+ margin: 0;
32
+ }
33
+
34
+ .popover-title {
35
+ margin: 0;
36
+ font-size: 14px;
37
+ font-weight: 600;
38
+ color: #495057;
39
+ text-transform: uppercase;
40
+ letter-spacing: 0.5px;
41
+ }
42
+
43
+ /* Content Container */
44
+ .text-selection-search-popover > *:not(.popover-header):not(.popover-close) {
45
+ padding: 16px;
46
+ }
47
+
48
+ /* Section Labels */
49
+ .section-label {
50
+ display: block;
51
+ font-weight: 600;
52
+ font-size: 12px;
53
+ color: #6c757d;
54
+ margin-bottom: 6px;
55
+ text-transform: uppercase;
56
+ letter-spacing: 0.5px;
57
+ }
58
+
59
+ /* Selected Text Display */
60
+ .selected-text-section {
61
+ margin-bottom: 16px;
62
+ padding-bottom: 12px;
63
+ border-bottom: 1px solid #f1f3f4;
64
+ }
65
+
66
+ .selected-text-display {
67
+ background: #f8f9fa;
68
+ border: 1px solid #e9ecef;
69
+ border-radius: 4px;
70
+ padding: 8px 12px;
71
+ font-size: 13px;
72
+ line-height: 1.4;
73
+ color: #495057;
74
+ min-height: 32px;
75
+ max-height: 60px;
76
+ overflow-y: auto;
77
+ word-wrap: break-word;
78
+ font-style: italic;
79
+ }
80
+
81
+ /* Edit Form */
82
+ .edit-form {
83
+ margin-bottom: 16px;
84
+ }
85
+
86
+ .input-group {
87
+ margin-bottom: 16px;
88
+ }
89
+
90
+ .input-group:last-child {
91
+ margin-bottom: 0;
92
+ }
93
+
94
+ .input-with-button {
95
+ display: flex;
96
+ gap: 8px;
97
+ align-items: flex-end;
98
+ }
99
+
100
+ .edit-input {
101
+ flex: 1;
102
+ padding: 12px 16px;
103
+ border: 2px solid #e5e7eb;
104
+ border-radius: 6px;
105
+ font-size: 14px;
106
+ transition: border-color 0.2s ease;
107
+ background: white;
108
+ min-height: 48px;
109
+ }
110
+
111
+ .edit-input:focus {
112
+ outline: none;
113
+ border-color: #0070d2;
114
+ box-shadow: 0 0 0 3px rgba(0, 112, 210, 0.1);
115
+ }
116
+
117
+ .edit-input::placeholder {
118
+ color: #9ca3af;
119
+ }
120
+
121
+ .edit-button {
122
+ padding: 8px 16px;
123
+ background: #0070d2;
124
+ color: white;
125
+ border: none;
126
+ border-radius: 4px;
127
+ font-weight: 500;
128
+ font-size: 13px;
129
+ cursor: pointer;
130
+ transition: all 0.2s ease;
131
+ white-space: nowrap;
132
+ min-height: 48px;
133
+ text-transform: uppercase;
134
+ letter-spacing: 0.5px;
135
+ }
136
+
137
+ .edit-button:hover:not(:disabled) {
138
+ background: #005fb2;
139
+ transform: translateY(-1px);
140
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
141
+ }
142
+
143
+ .edit-button:active:not(:disabled) {
144
+ transform: translateY(0);
145
+ box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
146
+ }
147
+
148
+ .edit-button:disabled {
149
+ background: #9ca3af;
150
+ cursor: not-allowed;
151
+ transform: none;
152
+ box-shadow: none;
153
+ }
154
+
155
+ /* Loading State */
156
+ .loading-state {
157
+ display: flex;
158
+ align-items: center;
159
+ gap: 12px;
160
+ padding: 12px;
161
+ background: #f8f9fa;
162
+ border-radius: 6px;
163
+ margin-top: 12px;
164
+ }
165
+
166
+ .loading-text {
167
+ font-size: 13px;
168
+ color: #6c757d;
169
+ }
170
+
171
+ /* Error State */
172
+ .error-state {
173
+ padding: 12px;
174
+ background: #fef2f2;
175
+ border: 1px solid #fecaca;
176
+ border-radius: 6px;
177
+ margin-top: 12px;
178
+ }
179
+
180
+ .error-message {
181
+ color: #dc2626;
182
+ font-size: 13px;
183
+ }
184
+
185
+ /* Success State */
186
+ .success-state {
187
+ padding: 12px;
188
+ background: #f0fdf4;
189
+ border: 1px solid #bbf7d0;
190
+ border-radius: 6px;
191
+ margin-top: 12px;
192
+ }
193
+
194
+ .success-message {
195
+ color: #16a34a;
196
+ font-size: 13px;
197
+ font-weight: 500;
198
+ }
199
+
200
+ /* Close Button */
201
+ .popover-close {
202
+ position: absolute;
203
+ top: 8px;
204
+ right: 8px;
205
+ z-index: 10;
206
+ }
207
+
208
+ .close-button {
209
+ background: rgba(108, 117, 125, 0.1);
210
+ border: none;
211
+ color: #6c757d;
212
+ width: 24px;
213
+ height: 24px;
214
+ border-radius: 4px;
215
+ display: flex;
216
+ align-items: center;
217
+ justify-content: center;
218
+ cursor: pointer;
219
+ font-size: 14px;
220
+ font-weight: bold;
221
+ transition: background-color 0.2s ease;
222
+ }
223
+
224
+ .close-button:hover {
225
+ background: rgba(108, 117, 125, 0.2);
226
+ }
227
+
228
+ /* Responsive Design */
229
+ @media (max-width: 480px) {
230
+ .text-selection-search-popover {
231
+ min-width: 280px;
232
+ max-width: 90vw;
233
+ }
234
+
235
+ .input-with-button {
236
+ flex-direction: column;
237
+ align-items: stretch;
238
+ }
239
+
240
+ .edit-button {
241
+ margin-top: 8px;
242
+ }
243
+
244
+ .popover-header {
245
+ padding: 10px 12px;
246
+ }
247
+
248
+ .popover-title {
249
+ font-size: 13px;
250
+ }
251
+ }
252
+
253
+ /* Animation for popover appearance */
254
+ @keyframes popoverFadeIn {
255
+ from {
256
+ opacity: 0;
257
+ transform: translateY(-10px);
258
+ }
259
+ to {
260
+ opacity: 1;
261
+ transform: translateY(0);
262
+ }
263
+ }
264
+
265
+ .text-selection-search-popover {
266
+ animation: popoverFadeIn 0.2s ease-out;
267
+ }
268
+
269
+ /* Scrollbar styling for results */
270
+ .results-list::-webkit-scrollbar {
271
+ width: 6px;
272
+ }
273
+
274
+ .results-list::-webkit-scrollbar-track {
275
+ background: #f1f1f1;
276
+ border-radius: 3px;
277
+ }
278
+
279
+ .results-list::-webkit-scrollbar-thumb {
280
+ background: #c1c1c1;
281
+ border-radius: 3px;
282
+ }
283
+
284
+ .results-list::-webkit-scrollbar-thumb:hover {
285
+ background: #a8a8a8;
286
+ }
@@ -0,0 +1,90 @@
1
+ <template>
2
+ <!-- Text Selection Search Popover -->
3
+ <template if:true={isVisible}>
4
+ <div
5
+ class="text-selection-search-popover"
6
+ class:hidden={isHidden}
7
+ onkeydown={handleKeyDown}
8
+ onclick={handlePopoverClick}
9
+ >
10
+ <!-- Header with Title -->
11
+ <div class="popover-header">
12
+ <h3 class="popover-title">Text Editor</h3>
13
+ </div>
14
+
15
+ <!-- Selected Text Display -->
16
+ <div class="selected-text-section">
17
+ <label class="section-label">Selected Text</label>
18
+ <div class="selected-text-display">
19
+ {selectedText}
20
+ </div>
21
+ </div>
22
+
23
+ <!-- Edit Form -->
24
+ <div class="edit-form">
25
+ <form onsubmit={handleSearchSubmit}>
26
+ <div class="input-group">
27
+ <label class="section-label">Edit to:</label>
28
+ <div class="input-with-button">
29
+ <input
30
+ type="text"
31
+ class="edit-input"
32
+ value={searchQuery}
33
+ onchange={handleSearchInputChange}
34
+ onclick={handleInputClick}
35
+ placeholder="Enter your edited text here..."
36
+ autocomplete="off"
37
+ autofocus
38
+ />
39
+ <dx-button
40
+ type="submit"
41
+ variant="primary"
42
+ disabled={searchButtonDisabled}
43
+ class="edit-button"
44
+ >
45
+ Edit
46
+ </dx-button>
47
+ </div>
48
+ </div>
49
+ </form>
50
+ </div>
51
+
52
+ <!-- Loading State -->
53
+ <template if:true={isLoading}>
54
+ <div class="loading-state">
55
+ <dx-spinner
56
+ size="small"
57
+ alternative-text="Editing..."
58
+ ></dx-spinner>
59
+ <span class="loading-text">Processing your edit...</span>
60
+ </div>
61
+ </template>
62
+
63
+ <!-- Error State -->
64
+ <template if:true={hasError}>
65
+ <div class="error-state">
66
+ <span class="error-message">{errorMessage}</span>
67
+ </div>
68
+ </template>
69
+
70
+ <!-- Success State -->
71
+ <template if:true={isSuccess}>
72
+ <div class="success-state">
73
+ <span class="success-message">Text edited successfully!</span>
74
+ </div>
75
+ </template>
76
+
77
+ <!-- Close Button -->
78
+ <div class="popover-close">
79
+ <dx-button
80
+ onclick={hidePopover}
81
+ variant="tertiary"
82
+ class="close-button"
83
+ title="Close editor"
84
+ >
85
+ ×
86
+ </dx-button>
87
+ </div>
88
+ </div>
89
+ </template>
90
+ </template>
@@ -0,0 +1,236 @@
1
+ import { LightningElement, api, track } from "lwc";
2
+
3
+ export default class TextSelectionSearch extends LightningElement {
4
+ @api searchApiUrl = "";
5
+ @api repoUrl = "";
6
+ @api placeholder = "Search for...";
7
+ @api popoverPosition = "top";
8
+ @api maxResults = 10;
9
+
10
+ @track isVisible = false;
11
+ @track isHidden = false;
12
+ @track searchQuery = "";
13
+ @track isLoading = false;
14
+ @track isSuccess = false;
15
+ @track errorMessage = "";
16
+ @track popoverStyle = "";
17
+
18
+ selectedText = "";
19
+ selectionStart = 0;
20
+ selectionEnd = 0;
21
+ initialPopoverStyle = "";
22
+
23
+ connectedCallback() {
24
+ this.setupTextSelectionListener();
25
+ }
26
+
27
+ disconnectedCallback() {
28
+ this.removeTextSelectionListener();
29
+ }
30
+
31
+ setupTextSelectionListener() {
32
+ console.log('TextSelectionSearch: Setting up text selection listeners');
33
+ document.addEventListener("mouseup", (event) => this.handleTextSelection(event));
34
+ document.addEventListener("keyup", (event) => this.handleTextSelection(event));
35
+ }
36
+
37
+ removeTextSelectionListener() {
38
+ console.log('TextSelectionSearch: Removing text selection listeners');
39
+ document.removeEventListener("mouseup", (event) => this.handleTextSelection(event));
40
+ document.removeEventListener("keyup", (event) => this.handleTextSelection(event));
41
+ }
42
+
43
+ handleTextSelection(event) {
44
+ if (this.isVisible) {
45
+ console.log('TextSelectionSearch: Popover is visible, ignoring text selection event');
46
+ return;
47
+ }
48
+
49
+ console.log('TextSelectionSearch: Text selection event triggered');
50
+
51
+ if (event && event.target) {
52
+ const target = event.target;
53
+ const popover = this.template.querySelector('.text-selection-search-popover');
54
+ if (popover && popover.contains(target)) {
55
+ console.log('TextSelectionSearch: Click inside popover, ignoring selection event');
56
+ return;
57
+ }
58
+ }
59
+
60
+ const selection = window.getSelection();
61
+
62
+ if (!selection || selection.toString().trim() === "") {
63
+ if (this.isVisible) {
64
+ console.log('TextSelectionSearch: No text selected, but popover is visible - keeping it open');
65
+ return;
66
+ }
67
+ console.log('TextSelectionSearch: No text selected, hiding popover');
68
+ this.hidePopover();
69
+ return;
70
+ }
71
+
72
+ this.selectedText = selection.toString().trim();
73
+ console.log('TextSelectionSearch: Selected text:', this.selectedText);
74
+
75
+ if (this.selectedText.length > 0) {
76
+ this.showPopover();
77
+ }
78
+ }
79
+
80
+ showPopover() {
81
+ console.log('TextSelectionSearch: Showing popover');
82
+ const selection = window.getSelection();
83
+ if (!selection || selection.rangeCount === 0) {
84
+ console.log('TextSelectionSearch: No selection range found');
85
+ return;
86
+ }
87
+
88
+ const range = selection.getRangeAt(0);
89
+ const rect = range.getBoundingClientRect();
90
+
91
+ const popoverTop = this.popoverPosition === "top"
92
+ ? Math.max(10, rect.top - 80)
93
+ : Math.min(window.innerHeight - 200, rect.bottom + 20);
94
+
95
+ const popoverLeft = Math.max(10, Math.min(window.innerWidth - 350, rect.left + (rect.width / 2) - 175));
96
+
97
+ this.popoverStyle = `position: fixed; top: ${popoverTop}px; left: ${popoverLeft}px; z-index: 9999; width: 350px;`;
98
+ this.initialPopoverStyle = this.popoverStyle;
99
+ console.log('TextSelectionSearch: Popover style:', this.popoverStyle);
100
+
101
+ this.isVisible = true;
102
+ this.isHidden = false;
103
+ this.searchQuery = this.selectedText;
104
+ this.errorMessage = "";
105
+ this.isSuccess = false;
106
+
107
+ console.log('TextSelectionSearch: Popover should now be visible');
108
+ }
109
+
110
+ hidePopover() {
111
+ console.log('TextSelectionSearch: Hiding popover');
112
+ this.isHidden = true;
113
+
114
+ setTimeout(() => {
115
+ this.isVisible = false;
116
+ this.searchQuery = "";
117
+ this.errorMessage = "";
118
+ this.isSuccess = false;
119
+ this.popoverStyle = "";
120
+ this.initialPopoverStyle = "";
121
+
122
+ const selection = window.getSelection();
123
+ if (selection) {
124
+ selection.removeAllRanges();
125
+ }
126
+ }, 100);
127
+ }
128
+
129
+ handlePopoverClick(event) {
130
+ event.stopPropagation();
131
+ }
132
+
133
+ handleInputClick(event) {
134
+ event.stopPropagation();
135
+ }
136
+
137
+ handleSearchInputChange(event) {
138
+ const target = event.target;
139
+ this.searchQuery = target.value;
140
+ }
141
+
142
+ async handleSearchSubmit(event) {
143
+ event.preventDefault();
144
+
145
+ if (!this.searchQuery.trim()) {
146
+ this.errorMessage = "Please enter edited text";
147
+ return;
148
+ }
149
+
150
+ if (!this.repoUrl.trim()) {
151
+ this.errorMessage = "Repository URL not configured";
152
+ return;
153
+ }
154
+
155
+ if (!this.searchApiUrl) {
156
+ this.errorMessage = "Search API URL not configured";
157
+ return;
158
+ }
159
+
160
+ await this.performSearch();
161
+ }
162
+
163
+ async performSearch() {
164
+ this.isLoading = true;
165
+ this.errorMessage = "";
166
+ this.isSuccess = false;
167
+
168
+ try {
169
+ const response = await fetch(this.searchApiUrl, {
170
+ method: "POST",
171
+ headers: {
172
+ "Content-Type": "application/json"
173
+ },
174
+ body: JSON.stringify({
175
+ repoUrl: this.repoUrl,
176
+ selectedText: this.selectedText,
177
+ editedText: this.searchQuery
178
+ })
179
+ });
180
+
181
+ if (response.ok) {
182
+ this.isSuccess = true;
183
+
184
+ this.dispatchEvent(
185
+ new CustomEvent("textedited", {
186
+ detail: {
187
+ repoUrl: this.repoUrl,
188
+ selectedText: this.selectedText,
189
+ editedText: this.searchQuery,
190
+ success: true
191
+ }
192
+ })
193
+ );
194
+
195
+ setTimeout(() => {
196
+ this.hidePopover();
197
+ }, 2000);
198
+ } else {
199
+ throw new Error(`Edit failed: ${response.status}`);
200
+ }
201
+ } catch (error) {
202
+ console.error("Edit failed:", error);
203
+ this.errorMessage = "Edit failed. Please try again.";
204
+ } finally {
205
+ this.isLoading = false;
206
+ }
207
+ }
208
+
209
+ handleKeyDown(event) {
210
+ if (event.key === "Escape") {
211
+ this.hidePopover();
212
+ }
213
+ }
214
+
215
+ get hasError() {
216
+ return this.errorMessage.length > 0;
217
+ }
218
+
219
+ get searchButtonDisabled() {
220
+ return this.isLoading || !this.searchQuery.trim();
221
+ }
222
+
223
+ renderedCallback() {
224
+ if (this.isVisible && this.initialPopoverStyle) {
225
+ const popover = this.template.querySelector('.text-selection-search-popover');
226
+ if (popover) {
227
+ popover.style.cssText = this.initialPopoverStyle;
228
+ }
229
+ } else if (!this.isVisible) {
230
+ const popover = this.template.querySelector('.text-selection-search-popover');
231
+ if (popover) {
232
+ popover.style.cssText = '';
233
+ }
234
+ }
235
+ }
236
+ }
@@ -0,0 +1,257 @@
1
+ import { LightningElement, api, track } from "lwc";
2
+
3
+ export default class TextSelectionSearch extends LightningElement {
4
+ @api searchApiUrl: string = "";
5
+ @api repoUrl: string = "";
6
+ @api placeholder: string = "Search for...";
7
+ @api popoverPosition: "top" | "bottom" = "top";
8
+ @api maxResults: number = 10;
9
+
10
+ @track isVisible: boolean = false;
11
+ @track isHidden: boolean = false;
12
+ @track searchQuery: string = "";
13
+ @track isLoading: boolean = false;
14
+ @track isSuccess: boolean = false;
15
+ @track errorMessage: string = "";
16
+ @track popoverStyle: string = "";
17
+
18
+ private selectedText: string = "";
19
+ private selectionStart: number = 0;
20
+ private selectionEnd: number = 0;
21
+ private initialPopoverStyle: string = ""; // Store initial position
22
+
23
+ connectedCallback() {
24
+ this.setupTextSelectionListener();
25
+ }
26
+
27
+ disconnectedCallback() {
28
+ this.removeTextSelectionListener();
29
+ }
30
+
31
+ // Setup text selection listener
32
+ setupTextSelectionListener() {
33
+ console.log('TextSelectionSearch: Setting up text selection listeners');
34
+ document.addEventListener("mouseup", (event) => this.handleTextSelection(event));
35
+ document.addEventListener("keyup", (event) => this.handleTextSelection(event));
36
+ }
37
+
38
+ removeTextSelectionListener() {
39
+ console.log('TextSelectionSearch: Removing text selection listeners');
40
+ document.removeEventListener("mouseup", (event) => this.handleTextSelection(event));
41
+ document.removeEventListener("keyup", (event) => this.handleTextSelection(event));
42
+ }
43
+
44
+ // Handle text selection
45
+ handleTextSelection(event?: Event) {
46
+ // If popover is visible, completely ignore all text selection events
47
+ if (this.isVisible) {
48
+ console.log('TextSelectionSearch: Popover is visible, ignoring text selection event');
49
+ return;
50
+ }
51
+
52
+ console.log('TextSelectionSearch: Text selection event triggered');
53
+
54
+ // Check if the event target is inside our popover
55
+ if (event && event.target) {
56
+ const target = event.target as Element;
57
+ const popover = this.template.querySelector('.text-selection-search-popover');
58
+ if (popover && popover.contains(target)) {
59
+ console.log('TextSelectionSearch: Click inside popover, ignoring selection event');
60
+ return;
61
+ }
62
+ }
63
+
64
+ const selection = window.getSelection();
65
+
66
+ if (!selection || selection.toString().trim() === "") {
67
+ // Only hide popover if it's currently visible and we're not clicking inside it
68
+ if (this.isVisible) {
69
+ console.log('TextSelectionSearch: No text selected, but popover is visible - keeping it open');
70
+ return;
71
+ }
72
+ console.log('TextSelectionSearch: No text selected, hiding popover');
73
+ this.hidePopover();
74
+ return;
75
+ }
76
+
77
+ this.selectedText = selection.toString().trim();
78
+ console.log('TextSelectionSearch: Selected text:', this.selectedText);
79
+
80
+ if (this.selectedText.length > 0) {
81
+ this.showPopover();
82
+ }
83
+ }
84
+
85
+ // Show popover at selection position
86
+ showPopover() {
87
+ console.log('TextSelectionSearch: Showing popover');
88
+ const selection = window.getSelection();
89
+ if (!selection || selection.rangeCount === 0) {
90
+ console.log('TextSelectionSearch: No selection range found');
91
+ return;
92
+ }
93
+
94
+ const range = selection.getRangeAt(0);
95
+ const rect = range.getBoundingClientRect();
96
+
97
+ // Calculate popover position with more conservative positioning
98
+ const popoverTop = this.popoverPosition === "top"
99
+ ? Math.max(10, rect.top - 80) // Ensure it doesn't go off-screen
100
+ : Math.min(window.innerHeight - 200, rect.bottom + 20);
101
+
102
+ const popoverLeft = Math.max(10, Math.min(window.innerWidth - 350, rect.left + (rect.width / 2) - 175));
103
+
104
+ this.popoverStyle = `position: fixed; top: ${popoverTop}px; left: ${popoverLeft}px; z-index: 9999; width: 350px;`;
105
+ this.initialPopoverStyle = this.popoverStyle; // Store the initial position
106
+ console.log('TextSelectionSearch: Popover style:', this.popoverStyle);
107
+
108
+ this.isVisible = true;
109
+ this.isHidden = false;
110
+ this.searchQuery = this.selectedText;
111
+ this.errorMessage = "";
112
+ this.isSuccess = false;
113
+
114
+ console.log('TextSelectionSearch: Popover should now be visible');
115
+ }
116
+
117
+ // Hide popover
118
+ hidePopover() {
119
+ console.log('TextSelectionSearch: Hiding popover');
120
+ this.isHidden = true;
121
+
122
+ // Use setTimeout to ensure the hidden class is applied before hiding
123
+ setTimeout(() => {
124
+ this.isVisible = false;
125
+ this.searchQuery = "";
126
+ this.errorMessage = "";
127
+ this.isSuccess = false;
128
+ this.popoverStyle = ""; // Reset the positioning style
129
+ this.initialPopoverStyle = ""; // Reset the initial position
130
+
131
+ // Clear any text selection to prevent immediate re-showing
132
+ const selection = window.getSelection();
133
+ if (selection) {
134
+ selection.removeAllRanges();
135
+ }
136
+ }, 100);
137
+ }
138
+
139
+ // Handle popover click to prevent closing
140
+ handlePopoverClick(event: Event) {
141
+ event.stopPropagation();
142
+ }
143
+
144
+ // Handle input click to prevent closing
145
+ handleInputClick(event: Event) {
146
+ event.stopPropagation();
147
+ }
148
+
149
+ // Handle search input change
150
+ handleSearchInputChange(event: Event) {
151
+ const target = event.target as HTMLInputElement;
152
+ this.searchQuery = target.value;
153
+ }
154
+
155
+ // Handle search submission
156
+ async handleSearchSubmit(event: Event) {
157
+ event.preventDefault();
158
+
159
+ if (!this.searchQuery.trim()) {
160
+ this.errorMessage = "Please enter edited text";
161
+ return;
162
+ }
163
+
164
+ if (!this.repoUrl.trim()) {
165
+ this.errorMessage = "Repository URL not configured";
166
+ return;
167
+ }
168
+
169
+ if (!this.searchApiUrl) {
170
+ this.errorMessage = "Search API URL not configured";
171
+ return;
172
+ }
173
+
174
+ await this.performSearch();
175
+ }
176
+
177
+ // Perform search API call
178
+ async performSearch() {
179
+ this.isLoading = true;
180
+ this.errorMessage = "";
181
+ this.isSuccess = false;
182
+
183
+ try {
184
+ const response = await fetch(this.searchApiUrl, {
185
+ method: "POST",
186
+ headers: {
187
+ "Content-Type": "application/json"
188
+ },
189
+ body: JSON.stringify({
190
+ repoUrl: this.repoUrl,
191
+ selectedText: this.selectedText,
192
+ editedText: this.searchQuery
193
+ })
194
+ });
195
+
196
+ if (response.ok) {
197
+ this.isSuccess = true;
198
+
199
+ // Dispatch success event
200
+ this.dispatchEvent(
201
+ new CustomEvent("textedited", {
202
+ detail: {
203
+ repoUrl: this.repoUrl,
204
+ selectedText: this.selectedText,
205
+ editedText: this.searchQuery,
206
+ success: true
207
+ }
208
+ })
209
+ );
210
+
211
+ // Auto-hide popover after success
212
+ setTimeout(() => {
213
+ this.hidePopover();
214
+ }, 2000);
215
+ } else {
216
+ throw new Error(`Edit failed: ${response.status}`);
217
+ }
218
+ } catch (error) {
219
+ console.error("Edit failed:", error);
220
+ this.errorMessage = "Edit failed. Please try again.";
221
+ } finally {
222
+ this.isLoading = false;
223
+ }
224
+ }
225
+
226
+ // Handle escape key
227
+ handleKeyDown(event: KeyboardEvent) {
228
+ if (event.key === "Escape") {
229
+ this.hidePopover();
230
+ }
231
+ }
232
+
233
+ // Getters for UI state
234
+ get hasError() {
235
+ return this.errorMessage.length > 0;
236
+ }
237
+
238
+ get searchButtonDisabled() {
239
+ return this.isLoading || !this.searchQuery.trim();
240
+ }
241
+
242
+ renderedCallback() {
243
+ if (this.isVisible && this.initialPopoverStyle) {
244
+ const popover = this.template.querySelector('.text-selection-search-popover') as HTMLElement;
245
+ if (popover) {
246
+ // Always use the initial position, never recalculate
247
+ popover.style.cssText = this.initialPopoverStyle;
248
+ }
249
+ } else if (!this.isVisible) {
250
+ // Clear the style when popover is hidden
251
+ const popover = this.template.querySelector('.text-selection-search-popover') as HTMLElement;
252
+ if (popover) {
253
+ popover.style.cssText = '';
254
+ }
255
+ }
256
+ }
257
+ }