Skip to content

Highlight spots#105

Merged
veronika-tseleva-cleantalk merged 14 commits into
devfrom
highlight-spots
May 14, 2026
Merged

Highlight spots#105
veronika-tseleva-cleantalk merged 14 commits into
devfrom
highlight-spots

Conversation

@veronika-tseleva-cleantalk
Copy link
Copy Markdown
Contributor

@veronika-tseleva-cleantalk veronika-tseleva-cleantalk commented May 12, 2026

Copilot AI review requested due to automatic review settings May 12, 2026 19:13
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR updates the SpotFix doBoard widget to improve “spot” highlighting behavior (especially around finished spots), adjusts selection/highlight internals, and tweaks the widget layout/badge count behavior.

Changes:

  • Add a minimum height to the widget container to stabilize layout.
  • Update highlighting logic to (de)highlight fixed spots depending on whether the finished section is expanded.
  • Rework text highlighting to use DOM Ranges/TreeWalker instead of string-based HTML insertion; adjust selection prechecks; update the wrap badge to show notification count.

Reviewed changes

Copilot reviewed 3 out of 7 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
styles/doboard-widget.css Adds min-height to the widget container.
js/src/widget.js Changes task loading, finished-section highlighting toggles, and badge count logic (notifications).
js/src/selections.js Relaxes selection constraints and rewrites text highlighting to use DOM operations.
js/src/api.js Minor whitespace cleanup.
dist/doboard-widget-bundle.js Rebuilt bundle reflecting the source changes.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread js/src/widget.js Outdated
Comment thread js/src/selections.js
Comment on lines 61 to +70
const range = selection.getRangeAt(0);
// Selection must be within a single DOM element.
if (range.startContainer !== range.endContainer) { spotFixDebugLog('`spotFixGetSelectedData` skip by `Selection within several tags nodes`'); return null; }
const commonNode = range.commonAncestorContainer;
const commonElement = commonNode.nodeType === Node.ELEMENT_NODE ? commonNode : commonNode.parentElement;
if (commonElement === document.body && range.startContainer !== range.endContainer) {
spotFixDebugLog('`spotFixGetSelectedData` skip: Selection is too broad');
return null;
}

// if (range.startContainer !== range.endContainer) { spotFixDebugLog('`spotFixGetSelectedData` skip by `Selection within several tags nodes`'); return null; }
Comment thread js/src/selections.js Outdated
Comment on lines +323 to +324
const walker = document.createTreeWalker(element, NodeFilter.SHOW_TEXT, null, false);

Comment thread js/src/widget.js
Copilot AI review requested due to automatic review settings May 14, 2026 09:33
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 3 out of 7 changed files in this pull request and generated 10 comments.

Comment thread js/src/widget.js
Comment thread js/src/widget.js
Comment on lines +1992 to +1998
} else {
notificationsCount = localStorage.getItem('spotfix-tasks-notifications-count');
}

const taskCountElement = document.getElementById('doboard_task_widget-task_count');
if ( taskCountElement && +tasksCount ) {
taskCountElement.innerText = ksesFilter(tasksCount);
if ( taskCountElement && +notificationsCount ) {
taskCountElement.innerText = ksesFilter(notificationsCount);
Comment thread js/src/selections.js
Comment on lines +310 to +353
validRanges.forEach(rangeData => {
const { startPos, endPos } = rangeData;

const highlightWrapper = document.createElement('span');
highlightWrapper.className = 'doboard_task_widget-text_selection';

const tooltipSpan = document.createElement('span');
tooltipSpan.className = 'doboard_task_widget-text_selection_tooltip';
tooltipSpan.innerHTML = tooltipHtml;

highlightWrapper.appendChild(tooltipSpan);

const range = document.createRange();
const walker = document.createTreeWalker(element, NodeFilter.SHOW_TEXT, null, false);

let node;
let currentPos = 0;
let startNode = null, startOffset = 0;
let endNode = null, endOffset = 0;

while ((node = walker.nextNode())) {
let nodeLength = node.nodeValue.length;

if (!startNode && currentPos + nodeLength >= startPos) {
startNode = node;
startOffset = startPos - currentPos;
}
if (!endNode && currentPos + nodeLength >= endPos) {
endNode = node;
endOffset = endPos - currentPos;
break;
}
currentPos += nodeLength;
}

if (startNode && endNode) {
try {
range.setStart(startNode, startOffset);
range.setEnd(endNode, endOffset);

const contents = range.extractContents();
highlightWrapper.appendChild(contents);

range.insertNode(highlightWrapper);
Comment thread js/src/selections.js
Comment on lines +63 to +70
const commonNode = range.commonAncestorContainer;
const commonElement = commonNode.nodeType === Node.ELEMENT_NODE ? commonNode : commonNode.parentElement;
if (commonElement === document.body && range.startContainer !== range.endContainer) {
spotFixDebugLog('`spotFixGetSelectedData` skip: Selection is too broad');
return null;
}

// if (range.startContainer !== range.endContainer) { spotFixDebugLog('`spotFixGetSelectedData` skip by `Selection within several tags nodes`'); return null; }
Comment thread js/src/selections.js
Comment on lines +70 to 71
// if (range.startContainer !== range.endContainer) { spotFixDebugLog('`spotFixGetSelectedData` skip by `Selection within several tags nodes`'); return null; }

Comment thread styles/doboard-widget.css
display: flex;
flex-direction: column;
-moz-flex-direction: column;
min-height: 490px;
Comment thread js/src/selections.js
const insertText = marker.type === 'start'
? spotfixHighlightOpen
: spotfixHighlightClose;
let tooltipTitleText = spots[0].isFixed
Comment thread js/src/selections.js
? spotfixHighlightOpen
: spotfixHighlightClose;
let tooltipTitleText = spots[0].isFixed
? `This issue already fixed.`
Comment thread js/src/selections.js Outdated
Comment on lines 371 to 373
} catch (error) {
spotFixDebugLog('Error updating element content: ' + error);
}
Comment thread js/src/widget.js
Comment on lines +1991 to +1993
localStorage.setItem('spotfix-tasks-notifications-count', `${notificationsCount}`);
} else {
notificationsCount = localStorage.getItem('spotfix-tasks-notifications-count');
Copilot AI review requested due to automatic review settings May 14, 2026 10:19
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 3 out of 7 changed files in this pull request and generated 13 comments.

Comments suppressed due to low confidence (1)

js/src/selections.js:231

  • Indentation of the case clauses (lines 213–227) was changed to a shallower level than the surrounding switch body, while default: (line 229) was left at the original deeper indentation. The mixed indentation makes the block hard to read and inconsistent.
        switch (selectionType) {
        case 'image':
            this.spotFixHighlightImageElement(element);
            break;

        case 'element':
            this.spotFixHighlightNestedElement(element);
            break;

        case 'text':
            const uniqueSpot = [spots[0]];
            try {
                this.spotFixHighlightTextInElement(element, uniqueSpot, widgetInstance);
            } catch (e) {
            }
            break;

            default:
                spotFixDebugLog('Unknown selection type: ' + selectionType);
        }

Comment thread js/src/selections.js
Comment on lines +221 to 230
case 'text':
const uniqueSpot = [spots[0]];
try {
this.spotFixHighlightTextInElement(element, uniqueSpot, widgetInstance);
} catch (e) {
}
break;

default:
spotFixDebugLog('Unknown selection type: ' + selectionType);
Comment thread js/src/selections.js
const uniqueSpot = [spots[0]];
try {
this.spotFixHighlightTextInElement(element, uniqueSpot, widgetInstance);
} catch (e) {
Comment thread js/src/selections.js
Comment on lines +222 to +224
const uniqueSpot = [spots[0]];
try {
this.spotFixHighlightTextInElement(element, uniqueSpot, widgetInstance);
Comment thread js/src/selections.js
Comment on lines +70 to 71
// if (range.startContainer !== range.endContainer) { spotFixDebugLog('`spotFixGetSelectedData` skip by `Selection within several tags nodes`'); return null; }

Comment thread js/src/selections.js Outdated
Comment on lines +304 to +338
const tooltipHtml = `
<div class="doboard_task_widget-text_selection_tooltip_element">
<span class="doboard_task_widget-text_selection_tooltip_icon"></span>
<span>
<div>${tooltipTitleText}</div>
<div>You can see history <span class="doboard_task_widget-see-task doboard_task_widget-see-task__task-id-${spots[0].taskId}">Here</span></div>
</span>
</div>
`;

validRanges.forEach(rangeData => {
const { startPos, endPos } = rangeData;

const highlightWrapper = document.createElement('span');
highlightWrapper.className = 'doboard_task_widget-text_selection';

const tooltipSpan = document.createElement('span');
tooltipSpan.className = 'doboard_task_widget-text_selection_tooltip';

// Convert block-level divs to inline-block spans to prevent list breakages
const safeTooltipHtml = tooltipHtml
.replace(/<div/g, '<span style="display:block"')
.replace(/<\/div>/g, '</span>');
tooltipSpan.innerHTML = safeTooltipHtml;

highlightWrapper.appendChild(tooltipSpan);

const range = document.createRange();
const walker = document.createTreeWalker(element, NodeFilter.SHOW_TEXT, null, false);

let node;
let currentPos = 0;
let startNode = null, startOffset = 0;
let endNode = null, endOffset = 0;

Comment thread js/src/widget.js
Comment on lines +1986 to +1987
notificationsCount = await getNotificationsDoboard(this.params.projectToken, sessionId, this.params.accountId, this.params.projectId);
notificationsCount = [...new Map(notificationsCount.map((item) => [item.task_id, item])).values()];
Comment thread js/src/widget.js
Comment on lines +1178 to +1183
if (finishedSpotsListHeader?.classList?.contains('expanded')) {
spotFixHighlightElements(spotsToBeHighlighted, this);
} else {
spotFixRemoveHighlights();
spotFixHighlightElements(spotsToBeHighlighted.filter(item => !item.isFixed), this);
}
Comment thread js/src/widget.js
Comment on lines +1989 to +1992
localStorage.setItem('spotfix-tasks-notifications-count', `${notificationsCount}`);
} else {
notificationsCount = localStorage.getItem('spotfix-tasks-notifications-count');
}
Comment thread js/src/widget.js
Comment on lines +1995 to 1998
if ( taskCountElement && +notificationsCount ) {
taskCountElement.innerText = ksesFilter(notificationsCount);
taskCountElement.classList.remove('hidden');
if (localStorage.getItem('horizontalPosition') === 'left' || window.SpotfixWidgetConfig?.horizontalPosition === 'left') taskCountElement.style.left = '10px';
Comment thread styles/doboard-widget.css
display: flex;
flex-direction: column;
-moz-flex-direction: column;
min-height: 490px;
Copilot AI review requested due to automatic review settings May 14, 2026 21:09
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 3 out of 7 changed files in this pull request and generated 7 comments.

Comments suppressed due to low confidence (4)

js/src/selections.js:269

  • Replacing the LI/OL/UL element with a plain text node permanently destroys the page's list structure (markers, nesting, sibling list items). When spotFixHighlightTextInElement is called for a <li> whose nodePath resolves to that element, the entire list item — including any unrelated children/markup — is collapsed into a flat text node before highlighting. This mutation is not reversible by spotFixRemoveHighlights, will visibly alter the host page, and will break subsequent re-highlight passes (e.g. when toggling the "finished tasks" panel) because the original element no longer exists in the DOM. Consider walking the inner text nodes (as the else branch already does) instead of replacing the list element.
    const listTags = ['LI', 'OL', 'UL'];
    if (listTags.includes(element.tagName)) {
        const textNode = document.createTextNode(element.textContent);
        element.parentNode.replaceChild(textNode, element);
        element = textNode;
    }

js/src/selections.js:226

  • The catch (e) {} swallows every error from spotFixHighlightTextInElement without even calling spotFixDebugLog. Other catches in this file consistently log via spotFixDebugLog, so silent failures here will make highlight bugs very hard to diagnose. At minimum log the error.
            try {
                this.spotFixHighlightTextInElement(element, uniqueSpot, widgetInstance);
            } catch (e) {
            }

js/src/selections.js:227

  • Declaring const uniqueSpot directly inside a case clause without a block scope is a known footgun (lexical declarations leak to sibling case clauses and trigger no-case-declarations). Wrap the body of this case in { ... } to avoid hoisting/TDZ issues if more cases are added.
        case 'text':
            const uniqueSpot = [spots[0]];
            try {
                this.spotFixHighlightTextInElement(element, uniqueSpot, widgetInstance);
            } catch (e) {
            }
            break;

js/src/selections.js:70

  • Leaving the old guard as a commented-out line adds noise without any explanation. Either delete it or add a comment explaining why the broader-selection check was relaxed so future readers understand the intent.
    // if (range.startContainer !== range.endContainer) { spotFixDebugLog('`spotFixGetSelectedData` skip by `Selection within several tags nodes`'); return null; }

Comment thread js/src/selections.js
Comment on lines +222 to +224
const uniqueSpot = [spots[0]];
try {
this.spotFixHighlightTextInElement(element, uniqueSpot, widgetInstance);
Comment thread js/src/widget.js
Comment on lines +1178 to 1186
if (finishedSpotsListHeader?.classList?.contains('expanded')) {
spotFixHighlightElements(spotsToBeHighlighted, this);
} else {
spotFixRemoveHighlights();
spotFixHighlightElements(spotsToBeHighlighted.filter(item => !item.isFixed), this);
}
const headerSpan = document.querySelector('.doboard_task_widget-header span');
if (headerSpan) {
headerSpan.innerHTML = ksesFilter('All spots ' + getIssuesCounterString(this.savedIssuesQuantityOnPage, this.savedIssuesQuantityAll));
Comment thread js/src/widget.js
Comment on lines 1980 to +1996
tasksCount = filteredTasks.length;

let notificationsCount = 0;

if(!this.nonRequesting) {
const activeTasksIds = filteredTasks.map(item => item.taskId);
notificationsCount = await getNotificationsDoboard(this.params.projectToken, sessionId, this.params.accountId, this.params.projectId);
notificationsCount = [...new Map(notificationsCount.map((item) => [item.task_id, item])).values()];
notificationsCount = notificationsCount.filter((item) => activeTasksIds.includes(item.task_id)).length;
localStorage.setItem('spotfix-tasks-notifications-count', `${notificationsCount}`);
} else {
notificationsCount = localStorage.getItem('spotfix-tasks-notifications-count');
}

const taskCountElement = document.getElementById('doboard_task_widget-task_count');
if ( taskCountElement && +tasksCount ) {
taskCountElement.innerText = ksesFilter(tasksCount);
if ( taskCountElement && +notificationsCount ) {
taskCountElement.innerText = ksesFilter(notificationsCount);
Comment thread js/src/widget.js
Comment on lines +1989 to +1992
localStorage.setItem('spotfix-tasks-notifications-count', `${notificationsCount}`);
} else {
notificationsCount = localStorage.getItem('spotfix-tasks-notifications-count');
}
Comment thread js/src/selections.js
Comment on lines +63 to +70
const commonNode = range.commonAncestorContainer;
const commonElement = commonNode.nodeType === Node.ELEMENT_NODE ? commonNode : commonNode.parentElement;
if (commonElement === document.body && range.startContainer !== range.endContainer) {
spotFixDebugLog('`spotFixGetSelectedData` skip: Selection is too broad');
return null;
}

// if (range.startContainer !== range.endContainer) { spotFixDebugLog('`spotFixGetSelectedData` skip by `Selection within several tags nodes`'); return null; }
Comment thread js/src/widget.js

if(!this.nonRequesting) {
const activeTasksIds = filteredTasks.map(item => item.taskId);
notificationsCount = await getNotificationsDoboard(this.params.projectToken, sessionId, this.params.accountId, this.params.projectId);
Comment thread styles/doboard-widget.css
display: flex;
flex-direction: column;
-moz-flex-direction: column;
min-height: 490px;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants