From 86861f6ec36b6015c20dd0b4529b627fc8f92667 Mon Sep 17 00:00:00 2001 From: SiriusXT <1160925501@qq.com> Date: Wed, 31 May 2023 18:32:33 +0800 Subject: [PATCH] Show highlighted text in the left pane --- src/public/app/layouts/desktop_layout.js | 2 + src/public/app/widgets/highlighted_text.js | 267 ++++++++++++++++++ .../widgets/type_widgets/content_widget.js | 2 + .../options/text_notes/highlighted_text.js | 90 ++++++ src/routes/api/options.js | 2 + src/services/options_init.js | 2 + 6 files changed, 365 insertions(+) create mode 100644 src/public/app/widgets/highlighted_text.js create mode 100644 src/public/app/widgets/type_widgets/options/text_notes/highlighted_text.js diff --git a/src/public/app/layouts/desktop_layout.js b/src/public/app/layouts/desktop_layout.js index 17dcc99a7..3a9ec2930 100644 --- a/src/public/app/layouts/desktop_layout.js +++ b/src/public/app/layouts/desktop_layout.js @@ -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 HltWidget 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"; @@ -181,6 +182,7 @@ export default class DesktopLayout { ) .child(new RightPaneContainer() .child(new TocWidget()) + .child(new HltWidget()) .child(...this.customWidgets.get('right-pane')) ) ) diff --git a/src/public/app/widgets/highlighted_text.js b/src/public/app/widgets/highlighted_text.js new file mode 100644 index 000000000..3dbfc9b5c --- /dev/null +++ b/src/public/app/widgets/highlighted_text.js @@ -0,0 +1,267 @@ +/** + * 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 = `
+ + + +
`; + +export default class HltWidget 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.hltTemporarilyHidden + && this.noteContext.viewScope.viewMode === 'default'; + } + + async doRenderBody() { + this.$body.empty().append($(TPL)); + this.$hlt = this.$body.find('.hlt'); + this.$body.find('.hlt-widget').append(this.closeHltButton.render()); + } + + async refreshWithNote(note) { + const hltLabel = note.getLabel('hlt'); + + if (hltLabel?.value === 'hide') { + this.toggleInt(false); + this.triggerCommand("reEvaluateRightPaneVisibility"); + return; + } + + let $hlt = "", hltColors = [], hltBgColors = []; + + let optionsHltColors = JSON.parse(options.get('highlightedTextColors')); + let optionsHltBgColors = JSON.parse(options.get('highlightedTextBgColors')); + // Check for type text unconditionally in case alwaysShowWidget is set + if (this.note.type === 'text') { + const { content } = await note.getNoteComplement(); + //hltColors/hltBgColors are the colors/background-color that appear in notes and in options + ({ $hlt, hltColors, hltBgColors } = await this.getHlt(content, optionsHltColors, optionsHltBgColors)); + } + this.$hlt.html($hlt); + this.toggleInt( + ["", "show"].includes(hltLabel?.value) + || hltColors!="" + || hltBgColors!="" + ); + + this.triggerCommand("reEvaluateRightPaneVisibility"); + } + //Converts color values in RGB, RGBA, or HSL format to hexadecimal format, removing transparency + colorToHex(color) { + function rgbToHex(rgb) { + // Converts color values in RGB or RGBA format to hexadecimal format + var rgba = rgb.match(/\d+/g); + var r = parseInt(rgba[0]); + var g = parseInt(rgba[1]); + var b = parseInt(rgba[2]); + var hex = "#"; + hex += (r < 16 ? "0" : "") + r.toString(16); + hex += (g < 16 ? "0" : "") + g.toString(16); + hex += (b < 16 ? "0" : "") + b.toString(16); + return hex; + } + + function hslToHex(hsl) { + // Convert color values in HSL format to RGB format and then to hexadecimal format + var hslValues = hsl.match(/\d+(\.\d+)?/g); + var h = parseFloat(hslValues[0]) / 360; + var s = parseFloat(hslValues[1]) / 100; + var l = parseFloat(hslValues[2]) / 100; + var r, g, b; + + if (s === 0) { + r = g = b = l; // achromatic + } else { + function hueToRgb(p, q, t) { + if (t < 0) t += 1; + if (t > 1) t -= 1; + if (t < 1 / 6) return p + (q - p) * 6 * t; + if (t < 1 / 2) return q; + if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6; + return p; + } + + var q = l < 0.5 ? l * (1 + s) : l + s - l * s; + var p = 2 * l - q; + r = hueToRgb(p, q, h + 1 / 3); + g = hueToRgb(p, q, h); + b = hueToRgb(p, q, h - 1 / 3); + } + + var hex = "#"; + hex += (Math.round(r * 255) < 16 ? "0" : "") + Math.round(r * 255).toString(16); + hex += (Math.round(g * 255) < 16 ? "0" : "") + Math.round(g * 255).toString(16); + hex += (Math.round(b * 255) < 16 ? "0" : "") + Math.round(b * 255).toString(16); + return hex; + } + if (color.indexOf("rgb") !== -1) { + return rgbToHex(color); + } else if (color.indexOf("hsl") !== -1) { + return hslToHex(color); + } else { + return ""; + } + } + // Determine whether the highlighted color is in the options, avoid errors caused by errors in color conversion, + // and the error of each value is acceptable within 2 + hexIsInOptionHexs(targetColor, optionColors){ + for (let i = 0; i < optionColors.length; i++) { + if (Math.abs(parseInt(optionColors[i].slice(1, 3), 16) - parseInt(targetColor.slice(1, 3), 16)) > 2) { continue; } + if (Math.abs(parseInt(optionColors[i].slice(3, 5), 16) - parseInt(targetColor.slice(3, 5), 16)) > 2) { continue; } + if (Math.abs(parseInt(optionColors[i].slice(5, 7), 16) - parseInt(targetColor.slice(5, 7), 16)) > 2) { continue; } + return true; + } + return false; + } + /** + * Builds a jquery table of helight text. + */ + getHlt(html, optionsHltColors, optionsHltBgColors) { + const hltBCs = $(html).find(`span[style*="background-color"],span[style*="color"]`) + const $hlt = $("
    "); + let hltColors = []; + let hltBgColors = []; + for (let hltIndex = 0; hltIndex'); + + if (color != "") { + var hexColor = this.colorToHex(color); + if (this.hexIsInOptionHexs(hexColor,optionsHltColors)) { + $li.html(hltText) + hltColors.push(hexColor); + liDisplay=true; + } + } + if (bgColor != "") { + var hexBgColor = this.colorToHex(bgColor); + if (this.hexIsInOptionHexs(hexBgColor,optionsHltBgColors)) { + //When you need to add a background color, in order to make the display more comfortable, change the background color to transparent + $li.html(hltText.css("background-color", hexBgColor+"80")) + hltBgColors.push(hexBgColor); + liDisplay=true; + } + } + if(!liDisplay){ + $li.css("display","none"); + } + //The font color and background color may be nested or adjacent to each other. At this time, connect the front and back li to avoid interruption + if(hltIndex!=0 && hltBCs[hltIndex-1].nextSibling ===hltBCs[hltIndex] && $hlt.children().last().css("display")!="none"){ + $hlt.children().last().append($li.html()); + }else{ + $li.on("click", () => this.jumpToHlt(hltIndex)); + $hlt.append($li); + } + + }; + return { + $hlt, + hltColors, + hltBgColors + }; + } + + async jumpToHlt(hltIndex) { + const isReadOnly = await this.noteContext.isReadOnly(); + if (isReadOnly) { + const $container = await this.noteContext.getContentElement(); + const hltElement = $container.find(`span[style*="background-color"],span[style*="color"]`)[hltIndex]; + + if (hltElement != null) { + hltElement.scrollIntoView({ behavior: "smooth", block: "center" }); + } + } else { + const textEditor = await this.noteContext.getTextEditor(); + $(textEditor.editing.view.domRoots.values().next().value).find(`span[style*="background-color"],span[style*="color"]`)[hltIndex].scrollIntoView({ + behavior: "smooth", block: "center" + }); + } + } + + async closeHltCommand() { + this.noteContext.viewScope.hltTemporarilyHidden = 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 === 'hlt') + && attributeService.isAffecting(attr, this.note))) { + await this.refresh(); + } + } +} + + +class CloseHltButton extends OnClickButtonWidget { + constructor() { + super(); + + this.icon("bx-x") + .title("Close HLT") + .titlePlacement("bottom") + .onClick((widget, e) => { + e.stopPropagation(); + + widget.triggerCommand("closeHlt"); + }) + .class("icon-action close-hlt"); + } +} diff --git a/src/public/app/widgets/type_widgets/content_widget.js b/src/public/app/widgets/type_widgets/content_widget.js index 967c996e5..f7a4846bd 100644 --- a/src/public/app/widgets/type_widgets/content_widget.js +++ b/src/public/app/widgets/type_widgets/content_widget.js @@ -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: [ diff --git a/src/public/app/widgets/type_widgets/options/text_notes/highlighted_text.js b/src/public/app/widgets/type_widgets/options/text_notes/highlighted_text.js new file mode 100644 index 000000000..7fc2ce825 --- /dev/null +++ b/src/public/app/widgets/type_widgets/options/text_notes/highlighted_text.js @@ -0,0 +1,90 @@ +import OptionsWidget from "../options_widget.js"; + +const TPL = ` +
    + +

    Highlighted Text

    + + Displays highlighted text in the left pane. You can customize the highlighted text displayed in the left pane: +
    Text color:
    + + + + + + + + + + + + + + + +
    Background color:
    + + + + + + + + + + + + + + + +
    `; + +export default class HighlightedTextOptions extends OptionsWidget { + doRender() { + this.$widget = $(TPL); + this.$hltColors = this.$widget.find(".hlt-color"); + this.$hltColors.on('change', () => { + const hltColorVals=this.$widget.find('input.hlt-color[type="checkbox"]:checked').map(function() { + return this.value; + }).get(); + this.updateOption('highlightedTextColors', JSON.stringify(hltColorVals)); + + }); + this.$hltBgColors = this.$widget.find(".hlt-background-color"); + this.$hltBgColors.on('change', () =>{ + const hltBgColorVals=this.$widget.find('input.hlt-background-color[type="checkbox"]:checked').map(function() { + return this.value; + }).get(); + this.updateOption('highlightedTextBgColors', JSON.stringify(hltBgColorVals)); + }); + + } + + async optionsLoaded(options) { + const hltColorVals=JSON.parse(options.highlightedTextColors); + const hltBgColorVals=JSON.parse(options.highlightedTextBgColors); + this.$widget.find('input.hlt-color[type="checkbox"]').each(function () { + if ($.inArray($(this).val(), hltColorVals) !== -1) { + $(this).prop("checked", true); + } else { + $(this).prop("checked", false); + } + }); + this.$widget.find('input.hlt-background-color[type="checkbox"]').each(function () { + if ($.inArray($(this).val(), hltBgColorVals) !== -1) { + $(this).prop("checked", true); + } else { + $(this).prop("checked", false); + } + }); + } +} diff --git a/src/routes/api/options.js b/src/routes/api/options.js index e98b1795f..48f2590b9 100644 --- a/src/routes/api/options.js +++ b/src/routes/api/options.js @@ -60,6 +60,8 @@ const ALLOWED_OPTIONS = new Set([ 'compressImages', 'downloadImagesAutomatically', 'minTocHeadings', + 'highlightedTextColors', + 'highlightedTextBgColors', 'checkForUpdates', 'disableTray', 'customSearchEngineName', diff --git a/src/services/options_init.js b/src/services/options_init.js index 1ce5bbd5d..ba34fba03 100644 --- a/src/services/options_init.js +++ b/src/services/options_init.js @@ -87,6 +87,8 @@ const defaultOptions = [ { name: 'compressImages', value: 'true', isSynced: true }, { name: 'downloadImagesAutomatically', value: 'true', isSynced: true }, { name: 'minTocHeadings', value: '5', isSynced: true }, + { name: 'highlightedTextColors', value: '["#e64c4c","#e6994c","#e6e64c","#99e64c","#4ce64c","#4ce699","#4ce6e6","#4c99e6","#4c4ce6","#994ce6"]', isSynced: true }, + { name: 'highlightedTextBgColors', value: '["#e64c4c","#e6994c","#e6e64c","#99e64c","#4ce64c","#4ce699","#4ce6e6","#4c99e6","#4c4ce6","#994ce6"]', isSynced: true }, { name: 'checkForUpdates', value: 'true', isSynced: true }, { name: 'disableTray', value: 'false', isSynced: false }, { name: 'customSearchEngineName', value: 'Duckduckgo', isSynced: false },