Skip to content
Draft
2 changes: 1 addition & 1 deletion base/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

<groupId>com.flowingcode.vaadin.addons.demo</groupId>
<artifactId>commons-demo</artifactId>
<version>5.3.2-SNAPSHOT</version>
<version>5.4.0-SNAPSHOT</version>

<name>Commons Demo</name>
<description>Common classes for add-ons demo</description>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
/*-
* #%L
* Commons Demo
* %%
* Copyright (C) 2020 - 2025 Flowing Code
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
package com.flowingcode.vaadin.addons.demo;

import com.vaadin.flow.component.dependency.JsModule;
import com.vaadin.flow.component.icon.IconFactory;
import java.util.Locale;

/**
* CommonsDemo icons.
*
* @author Javier Godoy / Flowing Code
*/
public enum CommonsDemoIcons implements IconFactory {
ROTATE, FLIP, HIDE_SOURCE, SHOW_SOURCE;

/**
* The Iconset name, i.e. {@code "fab"}."
*/
public static final String ICONSET = "commons-demo";

/**
* Return the full icon name.
*
* @return the full icon name, i.e. {@code "commons-demo:name"}..
*/
public String getIconName() {
return ICONSET + ':' + getIconPart();
}

/**
* Return the icon name within the iconset.
*
* @return the icon name, i.e. {@code "name"}..
*/
public String getIconPart() {
return name().toLowerCase(Locale.ENGLISH).replace('_', '-').replaceFirst("^-", "");
}

/**
* Create a new {@link Icon} instance with the icon determined by the name.
*
* @return a new instance of {@link Icon} component
*/
@Override
public Icon create() {
return new Icon(getIconPart());
}

/**
* Server side component for CommonsDemo icons.
*/
@JsModule("./commons-demo-iconset.ts")
@SuppressWarnings("serial")
public static final class Icon extends com.vaadin.flow.component.icon.Icon {
private Icon(String icon) {
super(ICONSET, icon);
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,20 @@ public MultiSourceCodeViewer(List<SourceCodeTab> sourceCodeTabs, Map<String, Str
getStyle().set("flex-direction", "column");
}

/**
* Adds the overlay controls to the underlying source code viewer. See
* {@link SourceCodeViewer#withButtons()} for the events they fire and the coordination they
* require from the enclosing layout.
*
* @return this viewer, for method chaining
*/
public MultiSourceCodeViewer withButtons() {
if (codeViewer != null) {
codeViewer.withButtons();
}
return this;
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}

public boolean isEmpty() {
return selectedTab == null;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@
import com.flowingcode.vaadin.jsonmigration.JsonMigration;
import com.vaadin.flow.component.ClickNotifier;
import com.vaadin.flow.component.Component;
import com.vaadin.flow.component.ComponentEvent;
import com.vaadin.flow.component.ComponentUtil;
import com.vaadin.flow.component.HasElement;
import com.vaadin.flow.component.HasSize;
import com.vaadin.flow.component.UI;
Expand All @@ -37,11 +39,14 @@

@SuppressWarnings("serial")
@JsModule("./code-viewer.ts")
@JsModule("./source-code-viewer-buttons.ts")
@ExtensionMethod(value = JsonMigration.class, suppressBaseMethods = true)
public class SourceCodeViewer extends Div implements HasSize {

private final Element codeViewer;

private final Div buttonsWrapper;

public SourceCodeViewer(String sourceUrl) {
this(sourceUrl, null);
}
Expand All @@ -51,13 +56,114 @@
}

public SourceCodeViewer(String url, String language, Map<String, String> properties) {
addClassName("source-code-viewer");
addClassName("has-code-viewer-gutter");

codeViewer = new Element("code-viewer");
getElement().appendChild(codeViewer);
getElement().getStyle().set("overflow", "auto");
getElement().getStyle().set("display", "flex");
codeViewer.getStyle().set("flex-grow", "1");

Div codeViewerWrapper = new Div();
codeViewerWrapper.addClassName("source-code-viewer-codeviewer-wrapper");
codeViewerWrapper.getElement().appendChild(codeViewer);

// Non-scrolling overlay so the buttons stay pinned while the code scrolls
buttonsWrapper = new Div();
buttonsWrapper.addClassName("source-code-viewer-buttons-wrapper");

add(codeViewerWrapper, buttonsWrapper);

setProperties(properties);
addAttachListener(ev -> fetchContents(url, language));
addAttachListener(ev -> {
fetchContents(url, language);
observeScrollbar();
});
}

/**
* Adds the overlay controls (show, hide, flip and rotate) pinned over the source code.
* <p>
* The buttons do not change this viewer directly. Instead, each one dispatches a bubbling DOM
* event that is expected to be handled by an enclosing layout, which is what actually collapses,
* repositions or reorients the source panel:
* <ul>
* <li>show / hide dispatch {@code source-collapse-changed} (carrying {@code detail.collapsed});
* <li>flip dispatches {@code source-flip};
* <li>rotate dispatches {@code source-rotate}.
* </ul>
* The buttons are therefore only useful when this viewer is placed inside a layout that listens
* for those events and coordinates the response (see {@link TabbedDemo}); otherwise they emit
* events that nothing consumes.
* <p>
* The controls are rendered by the {@code source-code-viewer-buttons} client-side web component,
* which dispatches the events locally on click; the enclosing layout (see {@link TabbedDemo})
* handles them through Vaadin server-side listeners. This method is idempotent: the component is
* added only once.
*/
public void withButtons() {
if (buttonsWrapper.getElement().getChildCount() == 0) {
buttonsWrapper.getElement().appendChild(new Element("source-code-viewer-buttons"));
}
}

/**
* Observes the scrollable wrapper. Whenever the vertical scrollbar appears or disappears, sets (or
* clears) the {@code --code-viewer-gutter} custom property on the nearest ancestor (or self)
* carrying the {@code has-code-viewer-gutter} class. Whenever the wrapper collapses below 24px in
* either axis, sets {@code --source-code-viewer-show-button-display} so the show button becomes
* visible (and clears it otherwise).
*/
private void observeScrollbar() {
getElement().executeJs(
"""
const root = this;
const wrapper = root.querySelector('.source-code-viewer-codeviewer-wrapper');
if (!wrapper) return;
root.__scrollbarObserver?.disconnect();
root.__scrollbarMutation?.disconnect();
let hasScrollbar = null;
const update = () => {
if (wrapper.offsetWidth < 24 || wrapper.offsetHeight < 10) {
root.style.setProperty('--source-code-viewer-show-button-display', 'block');
} else {
root.style.removeProperty('--source-code-viewer-show-button-display');
}
const current = wrapper.scrollHeight > wrapper.clientHeight;
if (current === hasScrollbar) return;
hasScrollbar = current;
let target = root;
while (target && !target.classList.contains('has-code-viewer-gutter')) {
target = target.parentElement;
}
if (target) {
if (current) {
const scrollbarWidth = wrapper.offsetWidth - wrapper.clientWidth;
target.style.setProperty('--code-viewer-gutter', scrollbarWidth + 'px');
} else {
target.style.removeProperty('--code-viewer-gutter');
}
}
};
let frame = 0;
const scheduleUpdate = () => {
if (frame) return;
frame = requestAnimationFrame(() => { frame = 0; update(); });
};
const resizeObserver = new ResizeObserver(scheduleUpdate);
resizeObserver.observe(wrapper);
root.__scrollbarObserver = resizeObserver;
const codeViewer = root.querySelector('code-viewer');
if (codeViewer) {
const mutationObserver = new MutationObserver(scheduleUpdate);
mutationObserver.observe(codeViewer, {childList: true, subtree: true});
root.__scrollbarMutation = mutationObserver;
}
update();
""");
}

private void dispatchEvent(ComponentEvent<? extends Component> ev) {

Check warning on line 163 in base/src/main/java/com/flowingcode/vaadin/addons/demo/SourceCodeViewer.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Remove this unused private "dispatchEvent" method.

See more on https://sonarcloud.io/project/issues?id=FlowingCode_CommonsDemo&issues=AZ8fRoMoTK5zFVh6CKUN&open=AZ8fRoMoTK5zFVh6CKUN&pullRequest=161
for (Component c = this; c != null; c = c.getParent().orElse(null)) {
ComponentUtil.fireEvent(c, ev);
}
}

public void fetchContents(String url, String language) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ public SplitLayoutDemo(Component demo, List<SourceCodeTab> tabs) {
properties.put("vaadin", VaadinVersion.getVaadinVersion());
properties.put("flow", Version.getFullVersion());

code = new MultiSourceCodeViewer(tabs, properties);
code = new MultiSourceCodeViewer(tabs, properties).withButtons();
this.demo = demo;
setSourcePosition(code.getSourcePosition());

Expand All @@ -59,7 +59,11 @@ public boolean isEmpty() {
return code.isEmpty();
}

private void setSourcePosition(SourcePosition position) {
public SourcePosition getSourcePosition() {
return sourcePosition;
}

public void setSourcePosition(SourcePosition position) {
if (!position.equals(sourcePosition)) {
getContent().removeAll();
switch (position) {
Expand All @@ -76,10 +80,6 @@ private void setSourcePosition(SourcePosition position) {
}
}

public void toggleSourcePosition() {
setSourcePosition(sourcePosition.toggle());
}

public void setOrientation(Orientation o) {
getContent().setOrientation(o);
getContent()
Expand All @@ -93,26 +93,22 @@ public Orientation getOrientation() {
return getContent().getOrientation();
}

public void setSplitterPosition(int pos) {
getContent().setSplitterPosition(pos);
}

public void setSizeFull() {
getContent().setSizeFull();
}

public void showSourceCode() {
getContent().setSplitterPosition(50);
}

public void hideSourceCode() {
switch (sourcePosition) {
case PRIMARY:
getContent().setSplitterPosition(0);
break;
case SECONDARY:
getContent().setSplitterPosition(100);
break;
public void setSourceCollapsed(boolean collapsed) {
if (!collapsed) {
getContent().setSplitterPosition(50);
} else {
switch (sourcePosition) {
case PRIMARY:
getContent().setSplitterPosition(0);
break;
case SECONDARY:
getContent().setSplitterPosition(100);
break;
}
}
}

Expand Down
Loading
Loading