Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
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
21 changes: 17 additions & 4 deletions 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 @@ -75,6 +75,12 @@
<artifactId>vaadin-core</artifactId>
<optional>true</optional>
</dependency>

<dependency>
<groupId>com.vaadin</groupId>
<artifactId>vaadin</artifactId>
<scope>test</scope>
</dependency>

<dependency>
<groupId>org.projectlombok</groupId>
Expand All @@ -99,13 +105,13 @@
<dependency>
<groupId>com.flowingcode.vaadin</groupId>
<artifactId>json-migration-helper</artifactId>
<version>0.9.2</version>
<version>0.9.5</version>
</dependency>

<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.11</version>
<version>4.13.2</version>
<scope>test</scope>
</dependency>

Expand All @@ -126,7 +132,14 @@
</exclusion>
</exclusions>
</dependency>


<dependency>
<groupId>com.flowingcode.vaadin.test</groupId>
<artifactId>testbench-rpc</artifactId>
<version>1.5.0</version>
<scope>test</scope>
</dependency>

<dependency>
<groupId>io.github.bonigarcia</groupId>
<artifactId>webdrivermanager</artifactId>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
/*-
* #%L
* Commons Demo
* %%
* Copyright (C) 2020 - 2026 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, GITHUB, HOME;

/**
* The Iconset name, i.e. {@code "commons-demo"}.
*/
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 @@ -22,6 +22,7 @@
import com.flowingcode.vaadin.addons.GithubBranch;
import com.flowingcode.vaadin.addons.GithubLink;
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Repeatable;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
Expand All @@ -44,6 +45,7 @@
@Repeatable(DemoSources.class)
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Inherited
public @interface DemoSource {

static final String DEFAULT_VALUE = "__DEFAULT__";
Expand Down Expand Up @@ -74,8 +76,13 @@
*/
String language() default DEFAULT_VALUE;

/** Source code position in the layout */
SourcePosition sourcePosition() default SourcePosition.SECONDARY;
/**
* Source code position in the layout. Defaults to {@link SourcePosition#DEFAULT}, which adopts the
* position carried over from the previously shown demo, or {@link SourcePosition#SECONDARY} if
* there is none. An explicit {@link SourcePosition#PRIMARY} or {@link SourcePosition#SECONDARY}
* always takes precedence over the carried-over position.
*/
SourcePosition sourcePosition() default SourcePosition.DEFAULT;

String condition() default "";

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,14 @@
package com.flowingcode.vaadin.addons.demo;

import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Inherited
public @interface DemoSources {

DemoSource[] value();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,22 @@
/*-
* #%L
* Commons Demo
* %%
* Copyright (C) 2020 - 2026 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.Component;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,22 @@
/*-
* #%L
* Commons Demo
* %%
* Copyright (C) 2020 - 2026 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.server.ServiceInitEvent;
Expand Down
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 Expand Up @@ -148,7 +162,7 @@ public SourcePosition getSourcePosition() {
if (selectedTab != null) {
return (SourcePosition) ComponentUtil.getData(selectedTab, DATA_POSITION);
} else {
return SourcePosition.SECONDARY;
return SourcePosition.DEFAULT;
}

}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,11 +37,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 +54,108 @@ public SourceCodeViewer(String sourceUrl, Map<String, String> properties) {
}

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
* width or 10px in height, 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();
""");
}

public void fetchContents(String url, String language) {
Expand Down
Loading
Loading