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.
Macro for printing all the spaces
Retrieve and display current exchange rates for major currencies against a specified base currency.
Basic greeting for 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 (for instance, partner's logo) in one place across the whole Confluence instance without re-uploading them on each page. User parameters allow setting the image size.
Show confetti bursts and a motivational message upon saving an edited page
Get the latest cryptocurrency rates for major coins.
Generate a list of labels from all spaces leading to corresponding content pages, organized in alphabetical order.
Create dropdown menus in Confluence Cloud with predefined sets of options, multiselect features, and permission gates