This Confluence Cloud macro injects an interactive, in-page fuzzy search component that natively parses the current page's Atlas Doc Format (ADF) payload. Built purely with modern JavaScript and Atlassian Design System CSS variables, it operates securely without external library dependencies. It leverages a lightweight DP Levenshtein algorithm for typo-tolerant matching and utilizes browser Text Fragments to automatically scroll to and highlight precise text results.
<div class="in-page-search-container" style="position: relative; width: 75%; width: 27rem; max-width: 37.5rem;">
<div id="in-page-search-wrapper" style="display: flex; align-items: center; position: relative; height: 2.5rem; background: var(--ds-background-input, #fff); border: 2px solid var(--ds-border, #0B120E24); border-radius: var(--ds-radius-medium, 6px); transition: border-color 0.2s ease;">
<span class="aui-icon aui-icon-small aui-iconfont-search" style="position: absolute; left: 0.75rem; color: var(--ds-icon-subtle, #505F79);" role="img" aria-label="Search"></span>
<input class="text"
type="text"
id="in-page-search-input"
placeholder="Search"
style="flex: 1; height: 100%; box-sizing: border-box; padding: 0.5rem 2.25rem; font-size: 0.875rem; margin: 0; background: transparent; color: var(--ds-text, #172b4d); border: none; outline: none; width: 100%;" />
<div id="in-page-search-clear" style="display: none; position: absolute; right: 0.375rem; width: 1.5rem; height: 1.5rem; justify-content: center; align-items: center; cursor: pointer; border-radius: 50%; color: var(--ds-icon-subtle, #505F79); transition: background-color 0.2s ease;">
<span class="aui-icon aui-icon-small aui-iconfont-cross" role="img" aria-label="Clear"></span>
</div>
</div>
<div id="in-page-search-dropdown"
class="aui-dropdown2"
style="display: none; position: absolute; top: 100%; left: 0; width: 27rem; max-width: 37.5rem; margin-top: 0.5rem; background: var(--ds-surface-overlay, #fff); border: 0.0625rem solid var(--ds-border, #dfe1e6); border-radius: var(--ds-radius-medium, 6px); box-shadow: var(--ds-shadow-overlay, 0 0.25rem 0.5rem -0.125rem rgba(9, 30, 66, 0.25), 0 0 0.0625rem rgba(9, 30, 66, 0.31)); z-index: 1000; max-height: 18.75rem; overflow-y: auto;">
</div>
</div>
<script>
(() => {
const pageId = "$page.id";
const baseUrl = "$baseUrl";
const searchInput = document.getElementById('in-page-search-input');
const searchInputWrapper = document.getElementById('in-page-search-wrapper');
const clearBtn = document.getElementById('in-page-search-clear');
const dropdown = document.getElementById('in-page-search-dropdown');
const searchableBlocks = [];
let initRetries = 0;
let indexError = null;
// Focus state handling for the outer wrapper
searchInput.addEventListener('focus', () => searchInputWrapper.style.borderColor = 'var(--ds-border-focused, #4c9aff)');
searchInput.addEventListener('blur', () => searchInputWrapper.style.borderColor = 'var(--ds-border, #0B120E24)');
// Clear button functionality
clearBtn.addEventListener('mouseover', () => clearBtn.style.backgroundColor = 'var(--ds-background-neutral-hovered, rgba(9, 30, 66, 0.08))');
clearBtn.addEventListener('mouseout', () => clearBtn.style.backgroundColor = 'transparent');
clearBtn.addEventListener('click', () => {
searchInput.value = '';
searchInput.focus();
performSearch();
});
// Dynamic iframe resizing
const resizeIframe = () => {
if (typeof AP !== 'undefined' && AP.resize) {
setTimeout(() => {
const inputRect = searchInputWrapper.getBoundingClientRect();
let requiredHeight = inputRect.bottom + 10;
if (dropdown.style.display === 'block') {
requiredHeight = dropdown.getBoundingClientRect().bottom + 20;
}
AP.resize('100%', requiredHeight + 'px');
}, 50);
}
};
// Lightweight DP Levenshtein substring search
const fuzzySearchSubstring = (text, query, maxTolerance = 0.3) => {
const t = text.toLowerCase();
const q = query.toLowerCase();
const m = q.length;
const n = t.length;
const maxErrors = Math.floor(m * maxTolerance);
if (!m || !n) return null;
const matrix = Array.from({ length: m + 1 }, () => new Int32Array(n + 1));
for (let i = 0; i <= m; i++) matrix[i][0] = i;
for (let j = 0; j <= n; j++) matrix[0][j] = 0;
for (let i = 1; i <= m; i++) {
for (let j = 1; j <= n; j++) {
const cost = q[i - 1] === t[j - 1] ? 0 : 1;
matrix[i][j] = Math.min(
matrix[i - 1][j] + 1,
matrix[i][j - 1] + 1,
matrix[i - 1][j - 1] + cost
);
}
}
let minEdits = Infinity;
let endIdx = -1;
for (let j = 1; j <= n; j++) {
if (matrix[m][j] < minEdits) {
minEdits = matrix[m][j];
endIdx = j;
}
}
// Backtrack to find exact bounds
if (minEdits <= maxErrors) {
let i = m, j = endIdx;
while (i > 0 && j > 0) {
const current = matrix[i][j];
const cost = q[i - 1] === t[j - 1] ? 0 : 1;
if (current === matrix[i - 1][j - 1] + cost) {
i--; j--;
} else if (current === matrix[i][j - 1] + 1) {
j--;
} else {
i--;
}
}
return { edits: minEdits, indices: [[j, endIdx - 1]] };
}
return null;
};
// Extract text from Atlas Doc Format
const extractText = (n) => {
const texts = [];
if (n.type === "text" && n.text) texts.push(n.text);
if (n.attrs && n.attrs.text) texts.push(n.attrs.text);
if (["extension", "inlineExtension", "bodiedExtension"].includes(n.type) && n.attrs && n.attrs.parameters && n.attrs.parameters.macroParams) {
const params = n.attrs.parameters.macroParams;
for (const key in params) {
if (params[key] && typeof params[key].value === 'string') {
texts.push(params[key].value);
}
}
}
if (Array.isArray(n.content)) {
n.content.forEach((child) => {
const childText = extractText(child);
if (childText) texts.push(childText);
});
}
return texts.join("");
};
const walkAdf = (n) => {
const blockTypes = ["paragraph", "heading", "listItem", "tableCell", "extension", "bodiedExtension", "codeBlock", "panel", "blockquote"];
if (blockTypes.includes(n.type)) {
const text = extractText(n).replace(/\s+/g, ' ').trim();
if (text) searchableBlocks.push({ text });
} else if (Array.isArray(n.content)) {
n.content.forEach(walkAdf);
}
};
// Initial load
const doRequest = (urlToTry) => {
if (typeof AP === 'undefined' || !AP.request) {
indexError = "AP.request is undefined. App context missing.";
return;
}
AP.request({
url: urlToTry,
type: "GET",
success: (response) => {
try {
const data = typeof response === 'string' ? JSON.parse(response) : response;
const adfString = data.body && data.body.atlas_doc_format ? data.body.atlas_doc_format.value : null;
if (!adfString) {
indexError = "API succeeded but atlas_doc_format was missing.";
return;
}
const adf = typeof adfString === 'string' ? JSON.parse(adfString) : adfString;
if (Array.isArray(adf.content)) adf.content.forEach(walkAdf);
if (!searchableBlocks.length) {
const allText = extractText(adf).replace(/\s+/g, ' ').trim();
if (allText) searchableBlocks.push({ text: allText });
}
if (dropdown.style.display === 'block' && searchInput.value.trim()) performSearch();
} catch(e) {
console.error("ADF Parsing Exception", e);
indexError = "Exception parsing page text structure.";
}
},
error: (err) => {
if (urlToTry.indexOf("/api/v2/") === 0) {
doRequest("/wiki/api/v2/pages/" + pageId + "?body-format=atlas_doc_format");
return;
}
console.error("API error", err);
indexError = "API request failed: " + (err.status || "Unknown Error");
}
});
};
const initSearchData = () => {
if (typeof AP === 'undefined' || !AP.request) {
if (initRetries < 50) {
initRetries++;
setTimeout(initSearchData, 200);
return;
} else {
console.warn("AP library load timeout.");
}
}
doRequest("/api/v2/pages/" + pageId + "?body-format=atlas_doc_format");
};
initSearchData();
// UI Utilities
const escapeHtml = (str) => {
const map = { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' };
return str.replace(/[&<>"']/g, (m) => map[m]);
};
const performSearch = () => {
const query = searchInput.value.trim();
dropdown.innerHTML = '';
if (!query) {
dropdown.style.display = 'none';
clearBtn.style.display = 'none';
resizeIframe();
return;
}
clearBtn.style.display = 'flex';
if (indexError) {
dropdown.innerHTML = '<div style="padding: 0.75rem; color: var(--ds-text-danger, #de350b); text-align: center; font-size: 0.8125rem;">' + escapeHtml(indexError) + '</div>';
dropdown.style.display = 'block';
resizeIframe();
return;
}
if (!searchableBlocks.length) {
dropdown.innerHTML = '<div style="padding: 0.75rem; color: var(--ds-text-subtle, #6b778c); text-align: center; font-size: 0.8125rem;">Loading search index...</div>';
dropdown.style.display = 'block';
resizeIframe();
return;
}
const results = searchableBlocks
.map((block) => {
const tolerance = query.length > 5 ? 0.4 : 0.3;
const matchInfo = fuzzySearchSubstring(block.text, query, tolerance);
return matchInfo ? { text: block.text, edits: matchInfo.edits, indices: matchInfo.indices } : null;
})
.filter(Boolean)
.sort((a, b) => a.edits - b.edits);
if (!results.length) {
dropdown.innerHTML = '<div style="box-sizing: border-box; white-space: normal; word-wrap: break-word; padding: 0.75rem; color: var(--ds-text-subtle, #6b778c); text-align: center; font-size: 0.8125rem;">ℹ️ No matches found</div>';
} else {
results.forEach((res) => {
const text = res.text;
const matchStart = res.indices[0][0];
const matchEnd = res.indices[0][1];
const center = Math.floor((matchStart + matchEnd) / 2);
const start = Math.max(0, center - 40);
const end = Math.min(text.length, center + 40);
let snippet = text.substring(start, end);
const localStart = Math.max(0, matchStart - start);
const localEnd = Math.min(snippet.length - 1, matchEnd - start);
const beforeMatch = escapeHtml(snippet.substring(0, localStart));
const matchHtml = '<mark style="background-color: var(--ds-background-warning-bold, #ffab00); font-weight: 600; padding: 0 0.125rem; border-radius: 0.1875rem; color: var(--ds-text-warning-inverse, #172b4d);">' + escapeHtml(snippet.substring(localStart, localEnd + 1)) + '</mark>';
const afterMatch = escapeHtml(snippet.substring(localEnd + 1));
let snippetHtml = beforeMatch + matchHtml + afterMatch;
if (start > 0) snippetHtml = "..." + snippetHtml;
if (end < text.length) snippetHtml = snippetHtml + "...";
const item = document.createElement('a');
const snippetFragmentText = snippet.trim();
item.href = baseUrl + '/pages/viewpage.action?pageId=' + pageId + '#:~:text=' + encodeURIComponent(snippetFragmentText);
item.target = '_blank';
Object.assign(item.style, {
display: 'block',
padding: '0.625rem 0.75rem',
color: 'var(--ds-text, #172b4d)',
textDecoration: 'none',
borderBottom: '0.0625rem solid var(--ds-border, #ebecf0)',
fontSize: '0.875rem',
lineHeight: '1.4'
});
item.innerHTML = snippetHtml;
item.addEventListener('mouseover', () => item.style.backgroundColor = 'var(--ds-background-neutral-hovered, #f4f5f7)');
item.addEventListener('mouseout', () => item.style.backgroundColor = 'transparent');
dropdown.appendChild(item);
});
if (dropdown.lastChild) dropdown.lastChild.style.borderBottom = 'none';
}
dropdown.style.display = 'block';
resizeIframe();
};
searchInput.addEventListener('input', performSearch);
searchInput.addEventListener('keypress', (e) => { if (e.key === 'Enter') performSearch(); });
document.addEventListener('click', (e) => {
if (!searchInputWrapper.contains(e.target) && !dropdown.contains(e.target)) {
if (dropdown.style.display === 'block') {
dropdown.style.display = 'none';
resizeIframe();
}
}
});
})();
</script>Perform text searches within pages, blog posts, and attachments
List all Confluence spaces with details such as space name, author, and creation date
Retrieve and display current exchange rates for major currencies against a specified base currency
Display a personalized greeting message for a logged-in user
Embed external content such as web pages, forms, slides and charts as iframe in Confluence pages, write custom HTML, and use CSS and Javascript for better looks and features
Manage frequently used images (e.g., partner logos) in one central location across the entire Confluence instance without re-uploading them on each page
Show confetti bursts and a motivational message upon saving an edited page
Get the latest cryptocurrency exchange rates for major coins
Generate an alphabetical list of labels from all spaces, linking each label to its corresponding content pages
Create dropdown menus in Confluence Cloud with predefined option sets, multiselect support, and permission-based access controls