Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 51 additions & 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 Expand Up @@ -317,6 +317,56 @@
</dependencies>
</profile>

<profile>
<id>dance</id>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-clean-plugin</artifactId>
<version>3.3.2</version>
<configuration>
<filesets>
<fileset>
<directory>${project.basedir}</directory>
<includes>
<include>package.json</include>
<include>package-lock.json</include>
<include>tsconfig.json</include>
<include>tsconfig.json.*</include>
<include>types.d.ts</include>
<include>types.d.ts.*</include>
<include>vite.config.ts</include>
<include>vite.generated.ts</include>
<include>webpack.config.js</include>
<include>webpack.generated.js</include>
</includes>
</fileset>
<fileset>
<directory>${project.basedir}/frontend</directory>
<includes>
<include>index.html</include>
</includes>
</fileset>
<fileset>
<directory>${project.basedir}/frontend/generated</directory>
</fileset>
<fileset>
<directory>${project.basedir}/node_modules</directory>
</fileset>
<fileset>
<directory>${project.basedir}/src/main/bundles</directory>
</fileset>
<fileset>
<directory>${project.basedir}/src/main/dev-bundle</directory>
</fileset>
</filesets>
</configuration>
</plugin>
</plugins>
</build>
</profile>

</profiles>

</project>
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