Merge pull request #3988 from SiriusXT/Highlighted-Text

Show highlighted text in the right pane
This commit is contained in:
zadam 2023-06-04 12:37:19 +02:00 committed by GitHub
commit eff3e1df85
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 320 additions and 4 deletions

View File

@ -44,6 +44,7 @@ import BacklinksWidget from "../widgets/floating_buttons/zpetne_odkazy.js";
import SharedInfoWidget from "../widgets/shared_info.js";
import FindWidget from "../widgets/find.js";
import TocWidget from "../widgets/toc.js";
import HighlightedTextWidget from "../widgets/highlighted_text.js";
import BulkActionsDialog from "../widgets/dialogs/bulk_actions.js";
import AboutDialog from "../widgets/dialogs/about.js";
import HelpDialog from "../widgets/dialogs/help.js";
@ -184,6 +185,7 @@ export default class DesktopLayout {
)
.child(new RightPaneContainer()
.child(new TocWidget())
.child(new HighlightedTextWidget())
.child(...this.customWidgets.get('right-pane'))
)
)

View File

@ -0,0 +1,255 @@
/**
* Widget: Show highlighted text in the right pane
*
* By design there's no support for nonsensical or malformed constructs:
* - 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";
const TPL = `<div class="highlighted-text-widget">
<style>
.highlighted-text-widget {
padding: 10px;
contain: none;
overflow: auto;
position: relative;
}
.highlighted-text > ol {
padding-left: 20px;
}
.highlighted-text li {
cursor: pointer;
margin-bottom: 3px;
text-align: justify;
text-justify: distribute;
word-wrap: break-word;
hyphens: auto;
}
.highlighted-text li:hover {
font-weight: bold;
}
.close-highlighted-text {
position: absolute;
top: 2px;
right: 2px;
}
</style>
<span class="highlighted-text"></span>
</div>`;
export default class HighlightedTextWidget extends RightPanelWidget {
constructor() {
super();
this.closeHltButton = new CloseHltButton();
this.child(this.closeHltButton);
}
get widgetTitle() {
return "Highlighted Text";
}
isEnabled() {
return super.isEnabled()
&& this.note.type === 'text'
&& !this.noteContext.viewScope.highlightedTextTemporarilyHidden
&& this.noteContext.viewScope.viewMode === 'default';
}
async doRenderBody() {
this.$body.empty().append($(TPL));
this.$hlt = this.$body.find('.highlighted-text');
this.$body.find('.highlighted-text-widget').append(this.closeHltButton.render());
}
async refreshWithNote(note) {
/*The reason for adding highlightedTextPreviousVisible is to record whether the previous state of the highlightedText is hidden or displayed,
* and then let it be displayed/hidden at the initial time.
* 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*/
if (this.noteContext.viewScope.highlightedTextPreviousVisible == true) {
this.toggleInt(true);
} else {
this.toggleInt(false);
}
const hltLabel = note.getLabel('hideHighlightWidget');
const optionsHlt = JSON.parse(options.get('highlightedText'));
if (hltLabel?.value == "" || hltLabel?.value === "true" || optionsHlt == "") {
this.toggleInt(false);
this.triggerCommand("reEvaluateRightPaneVisibility");
return;
}
let $hlt = "", hltLiCount = -1;
// Check for type text unconditionally in case alwaysShowWidget is set
if (this.note.type === 'text') {
const { content } = await note.getNoteComplement();
({ $hlt, hltLiCount } = await this.getHlt(content, optionsHlt));
}
this.$hlt.html($hlt);
if ([undefined, "false"].includes(hltLabel?.value) && hltLiCount > 0) {
this.toggleInt(true);
this.noteContext.viewScope.highlightedTextPreviousVisible = true;
} else {
this.toggleInt(false);
this.noteContext.viewScope.highlightedTextPreviousVisible = false;
}
this.triggerCommand("reEvaluateRightPaneVisibility");
}
/**
* Builds a table of helight text.
*/
getHlt(html, optionsHlt) {
// matches a span containing background-color
const regex1 = /<span[^>]*style\s*=\s*[^>]*background-color:[^>]*?>[\s\S]*?<\/span>/gi;
// 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;
// Possible values in optionsHlt '["bold","italic","underline","color","bgColor"]'
// element priority span>i>strong>u
let findSubStr="", combinedRegexStr = "";
if (optionsHlt.indexOf("bgColor") >= 0){
findSubStr+=`,span[style*="background-color"]`;
combinedRegexStr+=`|${regex1.source}`;
}
if (optionsHlt.indexOf("color") >= 0){
findSubStr+=`,span[style*="color"]`;
combinedRegexStr+=`|${regex2.source}`;
}
if (optionsHlt.indexOf("italic") >= 0){
findSubStr+=`,i`;
combinedRegexStr+=`|${regex3.source}`;
}
if (optionsHlt.indexOf("bold") >= 0){
findSubStr+=`,strong`;
combinedRegexStr+=`|${regex4.source}`;
}
if (optionsHlt.indexOf("underline") >= 0){
findSubStr+=`,u`;
combinedRegexStr+=`|${regex5.source}`;
}
findSubStr = findSubStr.substring(1)
combinedRegexStr = `(` + combinedRegexStr.substring(1) + `)`;
const combinedRegex = new RegExp(combinedRegexStr, 'gi');
let $hlt = $("<ol>");
let prevEndIndex = -1, hltLiCount = 0;
for (let match = null, hltIndex=0; ((match = combinedRegex.exec(html)) !== null); hltIndex++) {
var subHtml = match[0];
const startIndex = match.index;
const endIndex = combinedRegex.lastIndex;
if (prevEndIndex != -1 && startIndex === prevEndIndex) {
//If the previous element is connected to this element in HTML, then concatenate them into one.
$hlt.children().last().append(subHtml);
} else {
//hide li if its text content is empty
if ([...subHtml.matchAll(/(?<=^|>)[^><]+?(?=<|$)/g)].map(matchTmp => matchTmp[0]).join('').trim() != ""){
var $li = $('<li>');
$li.html(subHtml);
$li.on("click", () => this.jumpToHlt(findSubStr,hltIndex));
$hlt.append($li);
hltLiCount++;
}else{
continue
}
}
prevEndIndex = endIndex;
}
return {
$hlt,
hltLiCount
};
}
async jumpToHlt(findSubStr,hltIndex) {
const isReadOnly = await this.noteContext.isReadOnly();
let targetElement;
if (isReadOnly) {
const $container = await this.noteContext.getContentElement();
targetElement=$container.find(findSubStr).filter(function() {
if (findSubStr.indexOf("color")>=0 && findSubStr.indexOf("background-color")<0){
let color = this.style.color;
return $(this).prop('tagName')=="SPAN" && color==""?false:true;
}else{
return true;
}
}).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;
})
} else {
const textEditor = await this.noteContext.getTextEditor();
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"],
// the background-color error will be regarded as color, so it needs to be filtered
if (findSubStr.indexOf("color")>=0 && findSubStr.indexOf("background-color")<0){
let color = this.style.color;
return $(this).prop('tagName')=="SPAN" && color==""?false:true;
}else{
return true;
}
}).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;
})
}
targetElement[hltIndex].scrollIntoView({
behavior: "smooth", block: "center"
});
}
async closeHltCommand() {
this.noteContext.viewScope.highlightedTextTemporarilyHidden = true;
await this.refresh();
this.triggerCommand('reEvaluateRightPaneVisibility');
}
async entitiesReloadedEvent({ loadResults }) {
if (loadResults.isNoteContentReloaded(this.noteId)) {
await this.refresh();
} else if (loadResults.getAttributes().find(attr => attr.type === 'label'
&& (attr.name.toLowerCase().includes('readonly') || attr.name === 'hideHighlightWidget')
&& attributeService.isAffecting(attr, this.note))) {
await this.refresh();
}
}
}
class CloseHltButton extends OnClickButtonWidget {
constructor() {
super();
this.icon("bx-x")
.title("Close HighlightedTextWidget")
.titlePlacement("bottom")
.onClick((widget, e) => {
e.stopPropagation();
widget.triggerCommand("closeHlt");
})
.class("icon-action close-highlighted-text");
}
}

View File

@ -38,6 +38,10 @@ const TPL = `<div class="toc-widget">
.toc li {
cursor: pointer;
text-align: justify;
text-justify: distribute;
word-wrap: break-word;
hyphens: auto;
}
.toc li:hover {
@ -80,6 +84,16 @@ export default class TocWidget extends RightPanelWidget {
}
async refreshWithNote(note) {
/*The reason for adding tocPreviousVisible is to record whether the previous state of the toc is hidden or displayed,
* and then let it be displayed/hidden at the initial time. If there is no such value,
* when the right panel needs to display highlighttext but not toc, every time the note content is changed,
* toc will appear and then close immediately, because getToc(html) function will consume time*/
if (this.noteContext.viewScope.tocPreviousVisible ==true){
this.toggleInt(true);
}else{
this.toggleInt(false);
}
const tocLabel = note.getLabel('toc');
if (tocLabel?.value === 'hide') {
@ -96,10 +110,13 @@ export default class TocWidget extends RightPanelWidget {
}
this.$toc.html($toc);
this.toggleInt(
["", "show"].includes(tocLabel?.value)
|| headingCount >= options.getInt('minTocHeadings')
);
if (["", "show"].includes(tocLabel?.value) || headingCount >= options.getInt('minTocHeadings')){
this.toggleInt(true);
this.noteContext.viewScope.tocPreviousVisible=true;
}else{
this.toggleInt(false);
this.noteContext.viewScope.tocPreviousVisible=false;
}
this.triggerCommand("reEvaluateRightPaneVisibility");
}

View File

@ -7,6 +7,7 @@ import MaxContentWidthOptions from "./options/appearance/max_content_width.js";
import KeyboardShortcutsOptions from "./options/shortcuts.js";
import HeadingStyleOptions from "./options/text_notes/heading_style.js";
import TableOfContentsOptions from "./options/text_notes/table_of_contents.js";
import HighlightedTextOptions from "./options/text_notes/highlighted_text.js";
import TextAutoReadOnlySizeOptions from "./options/text_notes/text_auto_read_only_size.js";
import VimKeyBindingsOptions from "./options/code_notes/vim_key_bindings.js";
import WrapLinesOptions from "./options/code_notes/wrap_lines.js";
@ -61,6 +62,7 @@ const CONTENT_WIDGETS = {
_optionsTextNotes: [
HeadingStyleOptions,
TableOfContentsOptions,
HighlightedTextOptions,
TextAutoReadOnlySizeOptions
],
_optionsCodeNotes: [

View File

@ -0,0 +1,38 @@
import OptionsWidget from "../options_widget.js";
const TPL = `
<div class="options-section">
<h4>Highlighted Text</h4>
You can customize the highlighted text displayed in the right panel:<br>
<label><input type="checkbox" class="highlighted-text-check" value="bold"> Bold font &nbsp;</label>
<label><input type="checkbox" class="highlighted-text-check" value="italic"> Italic font &nbsp;</label>
<label><input type="checkbox" class="highlighted-text-check" value="underline"> Underlined font &nbsp;</label>
<label><input type="checkbox" class="highlighted-text-check" value="color"> Font with color &nbsp;</label>
<label><input type="checkbox" class="highlighted-text-check" value="bgColor"> Font with background color &nbsp;</label>
</div>`;
export default class HighlightedTextOptions extends OptionsWidget {
doRender() {
this.$widget = $(TPL);
this.$hlt = this.$widget.find("input.highlighted-text-check");
this.$hlt.on('change', () => {
const hltVals=this.$widget.find('input.highlighted-text-check[type="checkbox"]:checked').map(function() {
return this.value;
}).get();
this.updateOption('highlightedText', JSON.stringify(hltVals));
});
}
async optionsLoaded(options) {
const hltVals=JSON.parse(options.highlightedText);
this.$widget.find('input.highlighted-text-check[type="checkbox"]').each(function () {
if ($.inArray($(this).val(), hltVals) !== -1) {
$(this).prop("checked", true);
} else {
$(this).prop("checked", false);
}
});
}
}

View File

@ -60,6 +60,7 @@ const ALLOWED_OPTIONS = new Set([
'compressImages',
'downloadImagesAutomatically',
'minTocHeadings',
'highlightedText',
'checkForUpdates',
'disableTray',
'customSearchEngineName',

View File

@ -87,6 +87,7 @@ const defaultOptions = [
{ name: 'compressImages', value: 'true', isSynced: true },
{ name: 'downloadImagesAutomatically', value: 'true', isSynced: true },
{ name: 'minTocHeadings', value: '5', isSynced: true },
{ name: 'highlightedText', value: '["bold","italic","underline","color","bgColor"]', isSynced: true },
{ name: 'checkForUpdates', value: 'true', isSynced: true },
{ name: 'disableTray', value: 'false', isSynced: false },
{ name: 'customSearchEngineName', value: 'Duckduckgo', isSynced: false },