2023-05-31 18:32:33 +08:00
|
|
|
|
/**
|
|
|
|
|
* Widget: Show highlighted text in the right pane
|
|
|
|
|
*
|
2023-06-04 17:46:37 +02:00
|
|
|
|
* By design, there's no support for nonsensical or malformed constructs:
|
2023-05-31 18:32:33 +08:00
|
|
|
|
* - For example, if there is a formula in the middle of the highlighted text, the two ends of the formula will be regarded as two entries
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
import attributeService from "../services/attributes.js";
|
|
|
|
|
import RightPanelWidget from "./right_panel_widget.js";
|
|
|
|
|
import options from "../services/options.js";
|
|
|
|
|
import OnClickButtonWidget from "./buttons/onclick_button.js";
|
2023-11-03 10:44:14 +01:00
|
|
|
|
import appContext from "../components/app_context.js";
|
2023-05-31 18:32:33 +08:00
|
|
|
|
|
2023-06-22 15:38:36 +08:00
|
|
|
|
const TPL = `<div class="highlights-list-widget">
|
2023-05-31 18:32:33 +08:00
|
|
|
|
<style>
|
2023-06-22 15:38:36 +08:00
|
|
|
|
.highlights-list-widget {
|
2023-05-31 18:32:33 +08:00
|
|
|
|
padding: 10px;
|
|
|
|
|
contain: none;
|
|
|
|
|
overflow: auto;
|
|
|
|
|
position: relative;
|
|
|
|
|
}
|
|
|
|
|
|
2023-06-22 15:38:36 +08:00
|
|
|
|
.highlights-list > ol {
|
2023-05-31 18:32:33 +08:00
|
|
|
|
padding-left: 20px;
|
|
|
|
|
}
|
|
|
|
|
|
2023-06-22 15:38:36 +08:00
|
|
|
|
.highlights-list li {
|
2023-05-31 18:32:33 +08:00
|
|
|
|
cursor: pointer;
|
|
|
|
|
margin-bottom: 3px;
|
|
|
|
|
text-align: justify;
|
|
|
|
|
text-justify: distribute;
|
2023-06-03 14:43:20 +08:00
|
|
|
|
word-wrap: break-word;
|
|
|
|
|
hyphens: auto;
|
2023-05-31 18:32:33 +08:00
|
|
|
|
}
|
|
|
|
|
|
2023-06-22 15:38:36 +08:00
|
|
|
|
.highlights-list li:hover {
|
2023-05-31 18:32:33 +08:00
|
|
|
|
font-weight: bold;
|
|
|
|
|
}
|
|
|
|
|
</style>
|
|
|
|
|
|
2023-06-22 15:38:36 +08:00
|
|
|
|
<span class="highlights-list"></span>
|
2023-05-31 18:32:33 +08:00
|
|
|
|
</div>`;
|
|
|
|
|
|
2023-06-04 17:46:37 +02:00
|
|
|
|
export default class HighlightsListWidget extends RightPanelWidget {
|
2023-05-31 18:32:33 +08:00
|
|
|
|
get widgetTitle() {
|
2023-06-22 15:38:36 +08:00
|
|
|
|
return "Highlights List";
|
2023-05-31 18:32:33 +08:00
|
|
|
|
}
|
|
|
|
|
|
2023-11-03 10:44:14 +01:00
|
|
|
|
get widgetButtons() {
|
|
|
|
|
return [
|
|
|
|
|
new OnClickButtonWidget()
|
|
|
|
|
.icon("bx-slider")
|
|
|
|
|
.title("Options")
|
|
|
|
|
.titlePlacement("left")
|
|
|
|
|
.onClick(() => appContext.tabManager.openContextWithNote('_optionsTextNotes', {activate: true}))
|
|
|
|
|
.class("icon-action"),
|
|
|
|
|
new OnClickButtonWidget()
|
|
|
|
|
.icon("bx-x")
|
|
|
|
|
.title("Close Highlights List")
|
|
|
|
|
.titlePlacement("left")
|
|
|
|
|
.onClick(widget => widget.triggerCommand("closeHlt"))
|
|
|
|
|
.class("icon-action")
|
|
|
|
|
];
|
|
|
|
|
}
|
|
|
|
|
|
2023-05-31 18:32:33 +08:00
|
|
|
|
isEnabled() {
|
|
|
|
|
return super.isEnabled()
|
|
|
|
|
&& this.note.type === 'text'
|
2023-06-22 15:38:36 +08:00
|
|
|
|
&& !this.noteContext.viewScope.highlightsListTemporarilyHidden
|
2023-05-31 18:32:33 +08:00
|
|
|
|
&& this.noteContext.viewScope.viewMode === 'default';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async doRenderBody() {
|
|
|
|
|
this.$body.empty().append($(TPL));
|
2023-06-22 15:38:36 +08:00
|
|
|
|
this.$highlightsList = this.$body.find('.highlights-list');
|
2023-05-31 18:32:33 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async refreshWithNote(note) {
|
2023-06-22 15:38:36 +08:00
|
|
|
|
/* The reason for adding highlightsListPreviousVisible is to record whether the previous state
|
|
|
|
|
of the highlightsList is hidden or displayed, and then let it be displayed/hidden at the initial time.
|
2023-06-04 17:46:37 +02:00
|
|
|
|
If there is no such value, when the right panel needs to display toc but not highlighttext,
|
|
|
|
|
every time the note content is changed, highlighttext Widget will appear and then close immediately,
|
|
|
|
|
because getHlt function will consume time */
|
2023-06-22 15:38:36 +08:00
|
|
|
|
if (this.noteContext.viewScope.highlightsListPreviousVisible) {
|
2023-06-01 20:17:00 +08:00
|
|
|
|
this.toggleInt(true);
|
|
|
|
|
} else {
|
|
|
|
|
this.toggleInt(false);
|
|
|
|
|
}
|
2023-05-31 18:32:33 +08:00
|
|
|
|
|
2023-06-22 15:38:36 +08:00
|
|
|
|
const optionsHighlightsList = JSON.parse(options.get('highlightsList'));
|
2023-06-01 20:38:55 +08:00
|
|
|
|
|
2023-07-22 19:13:03 +00:00
|
|
|
|
if (note.isLabelTruthy('hideHighlightWidget') || !optionsHighlightsList.length) {
|
2023-05-31 18:32:33 +08:00
|
|
|
|
this.toggleInt(false);
|
|
|
|
|
this.triggerCommand("reEvaluateRightPaneVisibility");
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2023-06-22 15:38:36 +08:00
|
|
|
|
let $highlightsList = "", hlLiCount = -1;
|
2023-05-31 18:32:33 +08:00
|
|
|
|
// Check for type text unconditionally in case alwaysShowWidget is set
|
|
|
|
|
if (this.note.type === 'text') {
|
2023-06-04 17:46:37 +02:00
|
|
|
|
const {content} = await note.getNoteComplement();
|
2023-06-22 15:38:36 +08:00
|
|
|
|
({$highlightsList, hlLiCount} = this.getHighlightList(content, optionsHighlightsList));
|
2023-05-31 18:32:33 +08:00
|
|
|
|
}
|
2023-06-04 17:46:37 +02:00
|
|
|
|
this.$highlightsList.empty().append($highlightsList);
|
2023-06-22 15:38:36 +08:00
|
|
|
|
if (hlLiCount > 0) {
|
2023-06-01 20:17:00 +08:00
|
|
|
|
this.toggleInt(true);
|
2023-06-22 15:38:36 +08:00
|
|
|
|
this.noteContext.viewScope.highlightsListPreviousVisible = true;
|
2023-06-01 20:17:00 +08:00
|
|
|
|
} else {
|
|
|
|
|
this.toggleInt(false);
|
2023-06-22 15:38:36 +08:00
|
|
|
|
this.noteContext.viewScope.highlightsListPreviousVisible = false;
|
2023-05-31 18:32:33 +08:00
|
|
|
|
}
|
|
|
|
|
|
2023-06-01 20:17:00 +08:00
|
|
|
|
this.triggerCommand("reEvaluateRightPaneVisibility");
|
2023-05-31 18:32:33 +08:00
|
|
|
|
}
|
2023-06-01 20:17:00 +08:00
|
|
|
|
|
2023-06-22 15:38:36 +08:00
|
|
|
|
getHighlightList(content, optionsHighlightsList) {
|
2023-06-03 11:55:50 +08:00
|
|
|
|
// matches a span containing background-color
|
2023-06-04 17:46:37 +02:00
|
|
|
|
const regex1 = /<span[^>]*style\s*=\s*[^>]*background-color:[^>]*?>[\s\S]*?<\/span>/gi;
|
2023-06-03 11:55:50 +08:00
|
|
|
|
// matches a span containing color
|
|
|
|
|
const regex2 = /<span[^>]*style\s*=\s*[^>]*[^-]color:[^>]*?>[\s\S]*?<\/span>/gi;
|
|
|
|
|
// match italics
|
|
|
|
|
const regex3 = /<i>[\s\S]*?<\/i>/gi;
|
|
|
|
|
// match bold
|
|
|
|
|
const regex4 = /<strong>[\s\S]*?<\/strong>/gi;
|
|
|
|
|
// match underline
|
|
|
|
|
const regex5 = /<u>[\s\S]*?<\/u>/g;
|
2023-06-22 15:38:36 +08:00
|
|
|
|
// Possible values in optionsHighlightsList: '["bold","italic","underline","color","bgColor"]'
|
2023-06-04 16:02:30 +08:00
|
|
|
|
// element priority: span>i>strong>u
|
2023-06-04 17:46:37 +02:00
|
|
|
|
let findSubStr = "", combinedRegexStr = "";
|
2023-06-22 15:38:36 +08:00
|
|
|
|
if (optionsHighlightsList.includes("bgColor")) {
|
|
|
|
|
findSubStr += `,span[style*="background-color"]:not(section.include-note span[style*="background-color"])`;
|
2023-06-04 17:46:37 +02:00
|
|
|
|
combinedRegexStr += `|${regex1.source}`;
|
2023-06-03 11:55:50 +08:00
|
|
|
|
}
|
2023-06-22 15:38:36 +08:00
|
|
|
|
if (optionsHighlightsList.includes("color")) {
|
|
|
|
|
findSubStr += `,span[style*="color"]:not(section.include-note span[style*="color"])`;
|
2023-06-04 17:46:37 +02:00
|
|
|
|
combinedRegexStr += `|${regex2.source}`;
|
2023-06-03 11:55:50 +08:00
|
|
|
|
}
|
2023-06-22 15:38:36 +08:00
|
|
|
|
if (optionsHighlightsList.includes("italic")) {
|
|
|
|
|
findSubStr += `,i:not(section.include-note i)`;
|
2023-06-04 17:46:37 +02:00
|
|
|
|
combinedRegexStr += `|${regex3.source}`;
|
2023-06-03 11:55:50 +08:00
|
|
|
|
}
|
2023-06-22 15:38:36 +08:00
|
|
|
|
if (optionsHighlightsList.includes("bold")) {
|
|
|
|
|
findSubStr += `,strong:not(section.include-note strong)`;
|
2023-06-04 17:46:37 +02:00
|
|
|
|
combinedRegexStr += `|${regex4.source}`;
|
2023-06-03 11:55:50 +08:00
|
|
|
|
}
|
2023-06-22 15:38:36 +08:00
|
|
|
|
if (optionsHighlightsList.includes("underline")) {
|
|
|
|
|
findSubStr += `,u:not(section.include-note u)`;
|
2023-06-04 17:46:37 +02:00
|
|
|
|
combinedRegexStr += `|${regex5.source}`;
|
2023-06-03 11:55:50 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
findSubStr = findSubStr.substring(1)
|
|
|
|
|
combinedRegexStr = `(` + combinedRegexStr.substring(1) + `)`;
|
|
|
|
|
const combinedRegex = new RegExp(combinedRegexStr, 'gi');
|
2023-06-04 17:46:37 +02:00
|
|
|
|
const $highlightsList = $("<ol>");
|
2023-06-22 15:38:36 +08:00
|
|
|
|
let prevEndIndex = -1, hlLiCount = 0;
|
2023-06-04 17:46:37 +02:00
|
|
|
|
for (let match = null, hltIndex = 0; ((match = combinedRegex.exec(content)) !== null); hltIndex++) {
|
|
|
|
|
const subHtml = match[0];
|
2023-06-01 20:17:00 +08:00
|
|
|
|
const startIndex = match.index;
|
2023-06-03 11:55:50 +08:00
|
|
|
|
const endIndex = combinedRegex.lastIndex;
|
2023-06-04 17:46:37 +02:00
|
|
|
|
if (prevEndIndex !== -1 && startIndex === prevEndIndex) {
|
|
|
|
|
// If the previous element is connected to this element in HTML, then concatenate them into one.
|
|
|
|
|
$highlightsList.children().last().append(subHtml);
|
2023-06-03 11:55:50 +08:00
|
|
|
|
} else {
|
2023-06-04 17:46:37 +02:00
|
|
|
|
// TODO: can't be done with $(subHtml).text()?
|
2023-06-22 15:38:36 +08:00
|
|
|
|
//Can’t remember why regular expressions are used here, but modified to $(subHtml).text() works as expected
|
|
|
|
|
//const hasText = [...subHtml.matchAll(/(?<=^|>)[^><]+?(?=<|$)/g)].map(matchTmp => matchTmp[0]).join('').trim();
|
|
|
|
|
const hasText = $(subHtml).text().trim();
|
2023-06-04 17:46:37 +02:00
|
|
|
|
|
|
|
|
|
if (hasText) {
|
|
|
|
|
$highlightsList.append(
|
|
|
|
|
$('<li>')
|
|
|
|
|
.html(subHtml)
|
2023-06-22 15:38:36 +08:00
|
|
|
|
.on("click", () => this.jumpToHighlightsList(findSubStr, hltIndex))
|
2023-06-04 17:46:37 +02:00
|
|
|
|
);
|
|
|
|
|
|
2023-06-22 15:38:36 +08:00
|
|
|
|
hlLiCount++;
|
2023-06-04 17:46:37 +02:00
|
|
|
|
} else {
|
|
|
|
|
// hide li if its text content is empty
|
|
|
|
|
continue;
|
2023-06-03 14:43:20 +08:00
|
|
|
|
}
|
2023-05-31 18:32:33 +08:00
|
|
|
|
}
|
2023-06-01 20:17:00 +08:00
|
|
|
|
prevEndIndex = endIndex;
|
|
|
|
|
}
|
2023-05-31 18:32:33 +08:00
|
|
|
|
return {
|
2023-06-04 17:46:37 +02:00
|
|
|
|
$highlightsList,
|
2023-06-22 15:38:36 +08:00
|
|
|
|
hlLiCount
|
2023-05-31 18:32:33 +08:00
|
|
|
|
};
|
|
|
|
|
}
|
2023-06-04 17:46:37 +02:00
|
|
|
|
|
2023-06-22 15:38:36 +08:00
|
|
|
|
async jumpToHighlightsList(findSubStr, itemIndex) {
|
2023-05-31 18:32:33 +08:00
|
|
|
|
const isReadOnly = await this.noteContext.isReadOnly();
|
2023-06-03 11:55:50 +08:00
|
|
|
|
let targetElement;
|
2023-05-31 18:32:33 +08:00
|
|
|
|
if (isReadOnly) {
|
|
|
|
|
const $container = await this.noteContext.getContentElement();
|
2023-06-04 17:46:37 +02:00
|
|
|
|
targetElement = $container.find(findSubStr).filter(function () {
|
|
|
|
|
if (findSubStr.indexOf("color") >= 0 && findSubStr.indexOf("background-color") < 0) {
|
2023-06-03 11:55:50 +08:00
|
|
|
|
let color = this.style.color;
|
2023-06-04 17:46:37 +02:00
|
|
|
|
return !($(this).prop('tagName') === "SPAN" && color === "");
|
|
|
|
|
} else {
|
2023-06-03 11:55:50 +08:00
|
|
|
|
return true;
|
2023-06-04 17:46:37 +02:00
|
|
|
|
}
|
|
|
|
|
}).filter(function () {
|
|
|
|
|
return $(this).parent(findSubStr).length === 0
|
|
|
|
|
&& $(this).parent().parent(findSubStr).length === 0
|
|
|
|
|
&& $(this).parent().parent().parent(findSubStr).length === 0
|
|
|
|
|
&& $(this).parent().parent().parent().parent(findSubStr).length === 0;
|
2023-06-03 11:55:50 +08:00
|
|
|
|
})
|
2023-05-31 18:32:33 +08:00
|
|
|
|
} else {
|
|
|
|
|
const textEditor = await this.noteContext.getTextEditor();
|
2023-06-04 17:46:37 +02:00
|
|
|
|
targetElement = $(textEditor.editing.view.domRoots.values().next().value).find(findSubStr).filter(function () {
|
|
|
|
|
// When finding span[style*="color"] but not looking for span[style*="background-color"],
|
2023-06-03 11:55:50 +08:00
|
|
|
|
// the background-color error will be regarded as color, so it needs to be filtered
|
2023-06-04 17:46:37 +02:00
|
|
|
|
if (findSubStr.indexOf("color") >= 0 && findSubStr.indexOf("background-color") < 0) {
|
2023-06-03 11:55:50 +08:00
|
|
|
|
let color = this.style.color;
|
2023-06-04 17:46:37 +02:00
|
|
|
|
return !($(this).prop('tagName') === "SPAN" && color === "");
|
|
|
|
|
} else {
|
2023-06-03 11:55:50 +08:00
|
|
|
|
return true;
|
2023-06-04 17:46:37 +02:00
|
|
|
|
}
|
|
|
|
|
}).filter(function () {
|
|
|
|
|
// Need to filter out the child elements of the element that has been found
|
|
|
|
|
return $(this).parent(findSubStr).length === 0
|
|
|
|
|
&& $(this).parent().parent(findSubStr).length === 0
|
|
|
|
|
&& $(this).parent().parent().parent(findSubStr).length === 0
|
|
|
|
|
&& $(this).parent().parent().parent().parent(findSubStr).length === 0;
|
2023-06-03 11:55:50 +08:00
|
|
|
|
})
|
2023-05-31 18:32:33 +08:00
|
|
|
|
}
|
2023-06-04 17:46:37 +02:00
|
|
|
|
targetElement[itemIndex].scrollIntoView({
|
2023-06-03 11:55:50 +08:00
|
|
|
|
behavior: "smooth", block: "center"
|
|
|
|
|
});
|
2023-05-31 18:32:33 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async closeHltCommand() {
|
2023-06-22 15:38:36 +08:00
|
|
|
|
this.noteContext.viewScope.highlightsListTemporarilyHidden = true;
|
2023-05-31 18:32:33 +08:00
|
|
|
|
await this.refresh();
|
|
|
|
|
this.triggerCommand('reEvaluateRightPaneVisibility');
|
|
|
|
|
}
|
|
|
|
|
|
2023-06-04 17:46:37 +02:00
|
|
|
|
async entitiesReloadedEvent({loadResults}) {
|
2023-05-31 18:32:33 +08:00
|
|
|
|
if (loadResults.isNoteContentReloaded(this.noteId)) {
|
|
|
|
|
await this.refresh();
|
2023-06-05 16:12:02 +02:00
|
|
|
|
} else if (loadResults.getAttributeRows().find(attr => attr.type === 'label'
|
2023-06-01 20:17:00 +08:00
|
|
|
|
&& (attr.name.toLowerCase().includes('readonly') || attr.name === 'hideHighlightWidget')
|
2023-05-31 18:32:33 +08:00
|
|
|
|
&& attributeService.isAffecting(attr, this.note))) {
|
|
|
|
|
await this.refresh();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|