From fb66ae10bfe5d2c72d10de6d4bc14af6efb44cd2 Mon Sep 17 00:00:00 2001 From: Javier Godoy <11554739+javier-godoy@users.noreply.github.com> Date: Tue, 25 Jun 2024 12:36:34 -0300 Subject: [PATCH] feat: add support for multiple source highlighting Close #96 --- README.md | 11 +++-- .../addons/demo/MultiSourceCodeViewer.java | 24 +++++++++- .../vaadin/addons/demo/SourceCodeViewer.java | 46 ++++++++++++++++--- .../resources/frontend/code-viewer.ts | 30 +++++++++--- .../vaadin/addons/demo/AdditionalSources.java | 7 +++ .../vaadin/addons/demo/MultiSourceDemo.java | 20 +++++--- 6 files changed, 114 insertions(+), 24 deletions(-) create mode 100644 src/test/java/com/flowingcode/vaadin/addons/demo/AdditionalSources.java diff --git a/README.md b/README.md index 62e56b1..0949a81 100644 --- a/README.md +++ b/README.md @@ -166,18 +166,21 @@ Strictly, the constructor of `SourceCodeViewer` receives a map with arbitrary va This feature supports highlighting a source code fragment in order to emphasize a section of the code snippet. The highlighted fragment is automatically scrolled into view. -A fragment is highlighted either by calling `SourceCodeViewer.highlight(name)` or when hovering a component that has been configured with `SourceCodeViewer.highlightOnHover(component,name)`, where `name` is the name of the fragment. `SourceCodeViewer.highlight(null)` turn off the highlighting. -`SourceCodeViewer.highlightOnClick` allows configuring a click listener that turns highlight on. +A fragment is highlighted either by calling `SourceCodeViewer.highlight(filenameAndId)` or when clicking/hovering a component that has been configured with `SourceCodeViewer.highlightOnClick` or `SourceCodeViewer.highlightOnHover`, where `filenameAndId` is the name of the fragment. If the component is in an additional source file, `filenameAndId` can be given as a string in the format `filename#id`. If no `'#'` is present, it is assumed that the identifier corresponds to a block in the first source panel. `SourceCodeViewer.highlight(null)` turns off the highlighting. -In the source code, a fragment is delimited by `// begin-block name` and `// end-block` comments. Nested fragments are not supported. +In the source code, a fragment is delimited by `// begin-block filenameAndId` and `// end-block` comments. Nested fragments are not supported. The `// begin-block` and `// end-block` comments are removed after post-processing. ``` // begin-block first - Div first = new Div(new Text("First")); + Div first = new Div(new Text("Highlight block in first panel")); SourceCodeViewer.highlightOnHover(first, "first"); add(first); // end-block + + Div other = new Div(new Text("Highlight additional source")); + SourceCodeViewer.highlightOnHover(other, "AdditionalSource.java#other"); + add(other); ``` diff --git a/src/main/java/com/flowingcode/vaadin/addons/demo/MultiSourceCodeViewer.java b/src/main/java/com/flowingcode/vaadin/addons/demo/MultiSourceCodeViewer.java index eda3b6d..b2138c0 100644 --- a/src/main/java/com/flowingcode/vaadin/addons/demo/MultiSourceCodeViewer.java +++ b/src/main/java/com/flowingcode/vaadin/addons/demo/MultiSourceCodeViewer.java @@ -6,6 +6,7 @@ import com.vaadin.flow.component.tabs.Tabs; import java.util.List; import java.util.Map; +import java.util.Optional; public class MultiSourceCodeViewer extends Div { @@ -15,13 +16,21 @@ public class MultiSourceCodeViewer extends Div { private SourceCodeViewer codeViewer; private Tab selectedTab; + private Tabs tabs; public MultiSourceCodeViewer(List sourceCodeTabs, Map properties) { if (sourceCodeTabs.size() > 1) { - Tabs tabs = new Tabs(createTabs(sourceCodeTabs)); + tabs = new Tabs(createTabs(sourceCodeTabs)); tabs.addSelectedChangeListener(ev -> onTabSelected(ev.getSelectedTab())); add(tabs); selectedTab = tabs.getSelectedTab(); + + getElement().addEventListener("fragment-request", ev -> { + String filename = ev.getEventData().get("event.detail.filename").asString(); + findTabWithFilename(filename).ifPresent(tab -> { + tabs.setSelectedTab(tab); + }); + }).addEventData("event.detail.filename"); } else { selectedTab = createTab(sourceCodeTabs.get(0)); } @@ -89,7 +98,7 @@ private String getExtension(String filename) { } private void onTabSelected(Tab tab) { - this.selectedTab = tab; + selectedTab = tab; String url = (String) ComponentUtil.getData(tab, DATA_URL); String language = (String) ComponentUtil.getData(tab, DATA_LANGUAGE); @@ -104,4 +113,15 @@ public SourcePosition getSourcePosition() { return (SourcePosition) ComponentUtil.getData(selectedTab, DATA_POSITION); } + private Optional findTabWithFilename(String filename) { + if (tabs != null) { + return tabs.getChildren().filter(Tab.class::isInstance).map(Tab.class::cast).filter(tab -> { + String url = (String) ComponentUtil.getData(tab, DATA_URL); + return filename == null || getFilename(url).equals(filename); + }).findFirst(); + } else { + return Optional.empty(); + } + } + } diff --git a/src/main/java/com/flowingcode/vaadin/addons/demo/SourceCodeViewer.java b/src/main/java/com/flowingcode/vaadin/addons/demo/SourceCodeViewer.java index beb1786..300cceb 100644 --- a/src/main/java/com/flowingcode/vaadin/addons/demo/SourceCodeViewer.java +++ b/src/main/java/com/flowingcode/vaadin/addons/demo/SourceCodeViewer.java @@ -89,20 +89,54 @@ private void setProperties(Map properties) { } } - public static void highlightOnHover(Component c, String id) { + /** + * Highlights the block identified by {@code filenameAndId} when the component is hovered over. + *

+ * If the component is in an additional source, {@code filenameAndId} can be given as a string in + * the format {@code filename#id}. If no {@code '#'} is present, it is assumed that the identifier + * corresponds to a block in the first source panel. + * + * @param c The component that triggers the highlight action when hovered over. + * @param filenameAndId The identifier string that combines filename and id separated by + * {@code '#'}. + */ + public static void highlightOnHover(Component c, String filenameAndId) { c.addAttachListener(ev -> { - c.getElement().executeJs("Vaadin.Flow.fcCodeViewerConnector.highlightOnHover(this,$0)", id); + c.getElement().executeJs("Vaadin.Flow.fcCodeViewerConnector.highlightOnHover(this,$0)", filenameAndId); }); } - public static > void highlightOnClick(T c, String id) { + /** + * Highlight block {@code id} when the component is clicked. + *

+ * If the component is in an additional source, {@code filenameAndId} can be given as a string in + * the format {@code filename#id}. If no {@code '#'} is present, it is assumed that the identifier + * corresponds to a block in the first source panel. + * + * @param c The component that triggers the highlight action when clicked. + * @param filenameAndId The identifier string that combines filename and id separated by + * {@code '#'}. + */ + public static > void highlightOnClick(T c, + String filenameAndId) { c.addClickListener(ev -> { - c.getElement().executeJs("Vaadin.Flow.fcCodeViewerConnector.highlight($0)", id); + c.getElement().executeJs("Vaadin.Flow.fcCodeViewerConnector.highlight($0)", filenameAndId); }); } - public static void highlight(String id) { - UI.getCurrent().getElement().executeJs("Vaadin.Flow.fcCodeViewerConnector.highlight($0)", id); + /** + * Highlights the block identified by {@code filenameAndId}. + *

+ * If the component is in an additional source, {@code filenameAndId} can be given as a string in + * the format {@code filename#id}. If no {@code '#'} is present, it is assumed that the identifier + * corresponds to a block in the first source panel. + * + * @param filenameAndId The identifier string that combines filename and id separated by + * {@code '#'}. + */ + + public static void highlight(String filenameAndId) { + UI.getCurrent().getElement().executeJs("Vaadin.Flow.fcCodeViewerConnector.highlight($0)", filenameAndId); } } diff --git a/src/main/resources/META-INF/resources/frontend/code-viewer.ts b/src/main/resources/META-INF/resources/frontend/code-viewer.ts index 9d71a75..44f1a72 100644 --- a/src/main/resources/META-INF/resources/frontend/code-viewer.ts +++ b/src/main/resources/META-INF/resources/frontend/code-viewer.ts @@ -2,7 +2,7 @@ * #%L * Commons Demo * %% - * Copyright (C) 2020 - 2023 Flowing Code + * Copyright (C) 2020 - 2024 Flowing Code * %% * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -51,6 +51,8 @@ export class CodeViewer extends LitElement { private __license : Element[] = []; + private __highlightedBlock : string | null = null; + env: any = {}; createRenderRoot() { @@ -234,6 +236,7 @@ pre[class*="language-"] { (window as any).Prism.highlightAllUnder(self); self.__license.reverse().forEach(e=>self.querySelector('pre code')?.prepend(e)); self.process(code); + self._highlight(self.__highlightedBlock, false); }}; xhr.open('GET', sourceUrl, true); xhr.send(); @@ -435,20 +438,35 @@ pre[class*="language-"] { } /** @deprecated Use highlight(id: string|null) instead */ - highligth(id:string|null) { - this.highlight(id); + highligth(filenameAndId:string|null) { + this.highlight(filenameAndId); } //highlight a marked block - highlight(id:string|null) { + highlight(filenameAndId:string|null) { + this._highlight(filenameAndId, true); + } + + //highlight a marked block. If request is true, dispatch a fragment request if the block is not found. + private _highlight(filenameAndId:string|null, request:boolean) { + this.__highlightedBlock=filenameAndId; const div = this.querySelector('.highlight') as HTMLElement; div.style.removeProperty('top'); div.style.removeProperty('height'); - if (id!==null) { + if (filenameAndId!==null) { + + var ss = filenameAndId!.split('#',2); + var id = ss!.pop(); + var filename = ss!.pop(); + var begin = this.querySelector('.begin-'+id) as HTMLElement; var end = this.querySelector('.end-'+id) as HTMLElement; - if (begin && end && begin.offsetTop<=end.offsetTop) { + if (!begin || !end) { + if (request) { + this.dispatchEvent(new CustomEvent('fragment-request', {bubbles: true, detail: {filename}})); + } + } else if (begin.offsetTop<=end.offsetTop) { var top = begin.offsetTop; var height = end.offsetTop+end.offsetHeight-top; div.style.top= `calc( ${top}px + 0.75em)`; diff --git a/src/test/java/com/flowingcode/vaadin/addons/demo/AdditionalSources.java b/src/test/java/com/flowingcode/vaadin/addons/demo/AdditionalSources.java new file mode 100644 index 0000000..bf52132 --- /dev/null +++ b/src/test/java/com/flowingcode/vaadin/addons/demo/AdditionalSources.java @@ -0,0 +1,7 @@ +package com.flowingcode.vaadin.addons.demo; + +public class AdditionalSources { + // begin-block fragment + // this class has more sources + // end-block fragment +} diff --git a/src/test/java/com/flowingcode/vaadin/addons/demo/MultiSourceDemo.java b/src/test/java/com/flowingcode/vaadin/addons/demo/MultiSourceDemo.java index c059d54..e7ceba9 100644 --- a/src/test/java/com/flowingcode/vaadin/addons/demo/MultiSourceDemo.java +++ b/src/test/java/com/flowingcode/vaadin/addons/demo/MultiSourceDemo.java @@ -18,26 +18,34 @@ */ package com.flowingcode.vaadin.addons.demo; +import com.vaadin.flow.component.button.Button; import com.vaadin.flow.component.dependency.StyleSheet; import com.vaadin.flow.component.html.Div; -import com.vaadin.flow.component.html.Span; import com.vaadin.flow.router.PageTitle; import com.vaadin.flow.router.Route; @Route(value = "demo/multisource", layout = Demo.class) @PageTitle("Demo with multiple sources") // show-source @DemoSource +// show-source @DemoSource(clazz = AdditionalSources.class) // show-source @DemoSource("/src/test/resources/META-INF/resources/frontend/multi-source-demo.css") -// show-source @DemoSource(clazz = Demo.class) @DemoSource +@DemoSource(clazz = AdditionalSources.class) @DemoSource("/src/test/resources/META-INF/resources/frontend/multi-source-demo.css") -@DemoSource(clazz = Demo.class) @StyleSheet("./multi-source-demo.css") public class MultiSourceDemo extends Div { public MultiSourceDemo() { - Span span = new Span("This is the main source"); - span.addClassName("custom-style"); - add(span); + + // begin-block main + Div div = new Div("This is the main source"); + div.addClassName("custom-style"); + SourceCodeViewer.highlightOnHover(div, "main"); + add(div); + // end-block + + Button button1 = new Button("Highlight code in AdditionalSources"); + SourceCodeViewer.highlightOnClick(button1, "AdditionalSources.java#fragment"); + add(button1); } }