Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

🔥 Allow highlighting for old browsers #5751

Open
wants to merge 5 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -31,17 +31,21 @@
}"
>
<MarkdownRenderer v-if="useMarkdown" :markdown="text" />
<span v-else v-html="text" /><template>
<style :key="id" scoped>
::highlight(search-text-highlight-{{id}}) {
color: #ff675f;
}
</style>
</template>
<span v-else v-html="text" />
</div>
</span>
</div>
</div>
<template>
<style :key="id" scoped>
.search-text-highlight-{{id}} {
color: #ff675f;
}
::highlight(search-text-highlight-{{id}}) {
color: #ff675f;
}
</style>
</template>
</div>
</template>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,47 +63,53 @@
:entity-name="selectedEntity.text"
:message="$t('spanAnnotation.shortcutHelper')"
/>
<template>
<template v-for="{ id, color } in spanQuestion.answer.options">
<style :key="id" scoped>
.span-annotation__field::highlight(hl-{{id}}), .span-annotation__field::highlight(hl-{{id}}-selection) {
background-color: {{color}};
}
[data-theme="dark"] .span-annotation__field::highlight(hl-{{id}}), [data-theme="dark"] .span-annotation__field::highlight(hl-{{id}}-selection) {
background-color: {{color.palette.veryDark}};
}
.span-annotation__field::highlight(hl-{{id}}-pre-selection) {
background: {{color.palette.light}};
}
[data-theme="dark"] .span-annotation__field::highlight(hl-{{id}}-pre-selection) {
background: {{color.palette.dark}};
}
.span-annotation__field--overlapped::highlight(hl-{{id}}-selection) {
background: {{color}};
}
[data-theme="dark"] .span-annotation__field--overlapped::highlight(hl-{{id}}-selection) {
background: {{color.palette.veryDark}};
}
.span-annotation__field--overlapped::highlight(hl-{{id}}-pre-selection) {
background: {{color.palette.light}};
color: inherit;
}
[data-theme="dark"] .span-annotation__field--overlapped::highlight(hl-{{id}}-pre-selection) {
background: {{color.palette.dark}};
}
.span-annotation__field--overlapped::highlight(hl-{{id}}-hover) {
background: {{color}};
}
[data-theme="dark"] .span-annotation__field--overlapped::highlight(hl-{{id}}-hover) {
background: {{color.palette.veryDark}};
}
::highlight(search-text-highlight-{{id}}) {
color: #ff675f;
}
</style>
</template>
</template>
</div>
<template>
<template v-for="{ id, color } in spanQuestion.answer.options">
<style :key="id" scoped>
.span-annotation__field::highlight(hl-{{id}}), .span-annotation__field::highlight(hl-{{id}}-selection) {
background-color: {{color}};
}
[data-theme="dark"] .span-annotation__field::highlight(hl-{{id}}), [data-theme="dark"] .span-annotation__field::highlight(hl-{{id}}-selection) {
background-color: {{color.palette.veryDark}};
}
.span-annotation__field::highlight(hl-{{id}}-pre-selection) {
background: {{color.palette.light}};
}
[data-theme="dark"] .span-annotation__field::highlight(hl-{{id}}-pre-selection) {
background: {{color.palette.dark}};
}
.span-annotation__field--overlapped::highlight(hl-{{id}}-selection) {
background: {{color}};
}
[data-theme="dark"] .span-annotation__field--overlapped::highlight(hl-{{id}}-selection) {
background: {{color.palette.veryDark}};
}
.span-annotation__field--overlapped::highlight(hl-{{id}}-pre-selection) {
background: {{color.palette.light}};
color: inherit;
}
[data-theme="dark"] .span-annotation__field--overlapped::highlight(hl-{{id}}-pre-selection) {
background: {{color.palette.dark}};
}
.span-annotation__field--overlapped::highlight(hl-{{id}}-hover) {
background: {{color}};
}
[data-theme="dark"] .span-annotation__field--overlapped::highlight(hl-{{id}}-hover) {
background: {{color.palette.veryDark}};
}
</style>
</template>

<style :key="id" scoped>
::highlight(search-text-highlight-{{id}}) {
color: #ff675f;
}
.search-text-highlight-{{ id }} {
color: #ff675f;
}
</style>
</template>
</div>
</template>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ type Styles = {

type InitialConfiguration = Partial<Omit<Configuration, "lineHeight">>;

const isCSSHighlightsSupported = !!CSS.highlights;

export class Highlighting {
private readonly spanSelection = SpanSelection.getInstance();
private node: HTMLElement | undefined;
Expand Down Expand Up @@ -108,7 +110,7 @@ export class Highlighting {
}

mount(selections: LoadedSpan[] = []) {
if (!CSS.highlights) {
if (!isCSSHighlightsSupported) {
throw new Error(
"The CSS Custom Highlight API is not supported in this browser!"
);
Expand Down Expand Up @@ -292,7 +294,7 @@ export class Highlighting {
}

private applyStyles() {
if (!CSS.highlights) return;
if (!isCSSHighlightsSupported) return;

this.applyHighlightStyle();
this.applyEntityStyle();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,17 @@
<MarkdownRenderer v-if="useMarkdown" :markdown="fieldText" />
<Sandbox v-else-if="isHTML" :content="fieldText" />
<div v-else :class="classes" v-html="fieldText" />
<template>
<style :key="id" scoped>
::highlight(search-text-highlight-{{id}}) {
color: #ff675f;
}
</style>
</template>
</div>
<template>
<style :key="id" scoped>
.search-text-highlight-{{id}} {
color: #ff675f;
}
::highlight(search-text-highlight-{{id}}) {
color: #ff675f;
}
</style>
</template>
</div>
</template>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,35 +10,38 @@ declare namespace CSS {
};
}

type Indexes = { start: number; end: number }[];
type Coincidences = {
textNode: Node;
indexes: Indexes;
}[];

const DSLChars = ["|", "+", "-", "*"];
const isCSSHighlightsSupported = !!CSS.highlights;

export const useSearchTextHighlight = (fieldId: string) => {
const FIELD_ID_TO_HIGHLIGHT = `fields-content-${fieldId}`;
const HIGHLIGHT_CLASS = `search-text-highlight-${fieldId}`;

const scapeDSLChars = (value: string) => {
let output = value;

for (const char of DSLChars) {
output = output.replaceAll(char, " ");
}

return output
.split(" ")
.map((w) => w.trim())
.filter(Boolean);
};

const createRangesToHighlight = (
const createIndexesToHighlight = (
fieldComponent: HTMLElement,
searchText: string
) => {
CSS.highlights.delete(HIGHLIGHT_CLASS);
): Coincidences => {
const scapeDSLChars = (value: string) => {
let output = value;

const ranges = [];
for (const char of DSLChars) {
output = output.replaceAll(char, " ");
}

return output
.split(" ")
.map((w) => w.trim())
.filter(Boolean);
};

const getTextNodesUnder = (el) => {
const textNodes = [];
const textNodes: Node[] = [];
const walker = document.createTreeWalker(el, NodeFilter.SHOW_TEXT);

while (walker.nextNode()) {
Expand All @@ -48,8 +51,12 @@ export const useSearchTextHighlight = (fieldId: string) => {
return textNodes.filter((node) => node.nodeValue.trim().length > 0);
};

const getAllCoincidences = (textNode, word, mode: "PARTIAL" | "WORD") => {
const indexes = [];
const getAllCoincidences = (
textNode: Node,
word: string,
mode: "PARTIAL" | "WORD"
) => {
const indexes: Indexes = [];

if (mode === "PARTIAL") {
let startIndex = 0;
Expand Down Expand Up @@ -88,46 +95,102 @@ export const useSearchTextHighlight = (fieldId: string) => {
return indexes;
};

const createRanges = (textNode, indexes) => {
const ranges = [];
const textNodes = getTextNodesUnder(fieldComponent);
const words = scapeDSLChars(searchText);

for (const index of indexes) {
const range = new Range();
const coincidences = [];

range.setStart(textNode, index.start);
range.setEnd(textNode, index.end);
for (const textNode of textNodes) {
const indexes = [];
for (const word of words) {
const index = getAllCoincidences(textNode, word, "WORD");

ranges.push(range);
indexes.push(...index);
}

return ranges;
};
coincidences.push({
textNode,
indexes,
});
}

const textNodes = getTextNodesUnder(fieldComponent);
const words = scapeDSLChars(searchText);
return coincidences;
};

for (const textNode of textNodes) {
for (const word of words) {
const indexes = getAllCoincidences(textNode, word, "WORD");
const highlightCoincidences = (coincidences: Coincidences) => {
if (isCSSHighlightsSupported) {
const createRanges = (coincidences: Coincidences) => {
const ranges = [];

const newRanges = createRanges(textNode, indexes);
for (const coincidence of coincidences) {
for (const index of coincidence.indexes) {
const range = new Range();

ranges.push(...newRanges);
}
range.setStart(coincidence.textNode, index.start);
range.setEnd(coincidence.textNode, index.end);

ranges.push(range);
}
}

return ranges;
};

const ranges = createRanges(coincidences);

return CSS.highlights.set(HIGHLIGHT_CLASS, new Highlight(...ranges));
}

return ranges;
for (const coincidence of coincidences) {
let highlightedHTML = "";
let offset = 0;
const textContent = coincidence.textNode.nodeValue;

coincidence.indexes
.sort((a, b) => a.start - b.start)
.forEach(({ start, end }) => {
highlightedHTML += textContent.slice(offset, start);

highlightedHTML += `<span class=${HIGHLIGHT_CLASS}>${textContent.slice(
start,
end
)}</span>`;

offset = end;
});

highlightedHTML += textContent.slice(offset);

if (coincidence.textNode.parentElement)
coincidence.textNode.parentElement.innerHTML = highlightedHTML;
}
};

const highlightText = (searchText: string) => {
const fieldComponent = document.getElementById(FIELD_ID_TO_HIGHLIGHT);

if (isCSSHighlightsSupported) CSS.highlights.delete(HIGHLIGHT_CLASS);
else {
const currentSpans = document.getElementsByClassName(HIGHLIGHT_CLASS);

for (const span of Array.from(currentSpans)) {
const parent = span.parentElement;
if (!parent) continue;

parent.innerHTML = parent.innerHTML.replaceAll(
span.outerHTML,
span.innerHTML
);
}
}

if (!searchText || !fieldComponent) {
CSS.highlights.delete(HIGHLIGHT_CLASS);
return;
}
const ranges = createRangesToHighlight(fieldComponent, searchText);

CSS.highlights.set(HIGHLIGHT_CLASS, new Highlight(...ranges));
const coincidences = createIndexesToHighlight(fieldComponent, searchText);

highlightCoincidences(coincidences);
};

return {
Expand Down
Loading
Loading