Skip to content

Icons expansion #338

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Feb 20, 2024
Merged
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
103 changes: 75 additions & 28 deletions client/packages/lowcoder-design/src/components/iconSelect/index.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,24 @@
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import type { IconDefinition } from "@fortawesome/free-regular-svg-icons";
import { default as Popover } from "antd/es/popover";
import { ActionType } from '@rc-component/trigger/lib/interface';
import { Popover } from "antd";
import { ActionType } from "@rc-component/trigger/lib/interface";
import { TacoInput } from "components/tacoInput";
import { Tooltip } from "components/toolTip";
import { trans } from "i18n/design";
import _ from "lodash";
import { ReactNode, useEffect, useCallback, useMemo, useRef, useState } from "react";
import {
ReactNode,
useEffect,
useCallback,
useMemo,
useRef,
useState,
} from "react";
import Draggable from "react-draggable";
import { default as List, ListRowProps } from "react-virtualized/dist/es/List";
import styled from "styled-components";
import { CloseIcon, SearchIcon } from "icons";
import { ANTDICON } from "../../../../lowcoder/src/comps/comps/timelineComp/antIcon";
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is ANTDICON in timelineComp?


const PopupContainer = styled.div`
width: 408px;
Expand Down Expand Up @@ -110,11 +118,23 @@ const IconItemContainer = styled.div`

class Icon {
readonly title: string;
constructor(readonly def: IconDefinition, readonly names: string[]) {
this.title = def.iconName.split("-").map(_.upperFirst).join(" ");
constructor(readonly def: IconDefinition | any, readonly names: string[]) {
if (def?.iconName) {
this.title = def.iconName.split("-").map(_.upperFirst).join(" ");
} else {
this.title = names[0].slice(5);
this.def = def;
}
}
getView() {
return <FontAwesomeIcon icon={this.def} style={{ width: "1em", height: "1em" }} />;
if (this.names[0]?.startsWith("antd/")) return this.def;
else
return (
<FontAwesomeIcon
icon={this.def}
style={{ width: "1em", height: "1em" }}
/>
);
}
}

Expand Down Expand Up @@ -144,14 +164,25 @@ async function getAllIcons() {
}
}
}
//append ant icon
for (let key of Object.keys(ANTDICON)) {
ret["antd/" + key] = new Icon(
ANTDICON[key.toLowerCase() as keyof typeof ANTDICON],
["antd/" + key]
);
}
allIcons = ret;
return ret;
}

export const iconPrefix = "/icon:";

export function removeQuote(value?: string) {
return value ? (value.startsWith('"') && value.endsWith('"') ? value.slice(1, -1) : value) : "";
return value
? value.startsWith('"') && value.endsWith('"')
? value.slice(1, -1)
: value
: "";
}

function getIconKey(value?: string) {
Expand All @@ -171,7 +202,8 @@ export function useIcon(value?: string) {
function search(
allIcons: Record<string, Icon>,
searchText: string,
searchKeywords?: Record<string, string>
searchKeywords?: Record<string, string>,
IconType?: "OnlyAntd" | "All" | "default" | undefined
) {
const tokens = searchText
.toLowerCase()
Expand All @@ -182,6 +214,8 @@ function search(
if (icon.names.length === 0) {
return false;
}
if (IconType === "OnlyAntd" && !key.startsWith("antd/")) return false;
if (IconType === "default" && key.startsWith("antd/")) return false;
let text = icon.names
.flatMap((name) => [name, searchKeywords?.[name]])
.filter((t) => t)
Expand All @@ -198,16 +232,20 @@ const IconPopup = (props: {
label?: ReactNode;
onClose: () => void;
searchKeywords?: Record<string, string>;
IconType?: "OnlyAntd" | "All" | "default" | undefined;
}) => {
const [searchText, setSearchText] = useState("");
const [allIcons, setAllIcons] = useState<Record<string, Icon>>({});
const searchResults = useMemo(
() => search(allIcons, searchText, props.searchKeywords),
() => search(allIcons, searchText, props.searchKeywords, props.IconType),
[searchText, allIcons]
);
const onChangeRef = useRef(props.onChange);
onChangeRef.current = props.onChange;
const onChangeIcon = useCallback((key: string) => onChangeRef.current(iconPrefix + key), []);
const onChangeIcon = useCallback(
(key: string) => onChangeRef.current(iconPrefix + key),
[]
);
const columnNum = 8;

useEffect(() => {
Expand All @@ -217,24 +255,26 @@ const IconPopup = (props: {
const rowRenderer = useCallback(
(p: ListRowProps) => (
<IconRow key={p.key} style={p.style}>
{searchResults.slice(p.index * columnNum, (p.index + 1) * columnNum).map(([key, icon]) => (
<Tooltip
key={key}
title={icon.title}
placement="bottom"
align={{ offset: [0, -7, 0, 0] }}
destroyTooltipOnHide
>
<IconItemContainer
tabIndex={0}
onClick={() => {
onChangeIcon(key);
}}
{searchResults
.slice(p.index * columnNum, (p.index + 1) * columnNum)
.map(([key, icon]) => (
<Tooltip
key={key}
title={icon.title}
placement="bottom"
align={{ offset: [0, -7, 0, 0] }}
destroyTooltipOnHide
>
{icon.getView()}
</IconItemContainer>
</Tooltip>
))}
<IconItemContainer
tabIndex={0}
onClick={() => {
onChangeIcon(key);
}}
>
{icon.getView()}
</IconItemContainer>
</Tooltip>
))}
</IconRow>
),
[searchResults, allIcons, onChangeIcon]
Expand Down Expand Up @@ -279,6 +319,7 @@ export const IconSelectBase = (props: {
leftOffset?: number;
parent?: HTMLElement | null;
searchKeywords?: Record<string, string>;
IconType?: "OnlyAntd" | "All" | "default" | undefined;
}) => {
const { setVisible, parent } = props;
return (
Expand All @@ -290,7 +331,11 @@ export const IconSelectBase = (props: {
onOpenChange={setVisible}
getPopupContainer={parent ? () => parent : undefined}
// hide the original background when dragging the popover is allowed
overlayInnerStyle={{ border: "none", boxShadow: "none", background: "transparent" }}
overlayInnerStyle={{
border: "none",
boxShadow: "none",
background: "transparent",
}}
// when dragging is allowed, always re-location to avoid the popover exceeds the screen
destroyTooltipOnHide
content={
Expand All @@ -299,6 +344,7 @@ export const IconSelectBase = (props: {
label={props.label}
onClose={() => setVisible?.(false)}
searchKeywords={props.searchKeywords}
IconType={props.IconType}
/>
}
>
Expand All @@ -312,6 +358,7 @@ export const IconSelect = (props: {
label?: ReactNode;
children?: ReactNode;
searchKeywords?: Record<string, string>;
IconType?: "OnlyAntd" | "All" | "default" | undefined;
}) => {
const [visible, setVisible] = useState(false);
return (
Expand Down
1 change: 1 addition & 0 deletions client/packages/lowcoder-design/src/icons/IconCompIcon.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
25 changes: 24 additions & 1 deletion client/packages/lowcoder-design/src/icons/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -278,6 +278,28 @@ export { ReactComponent as SignatureIcon } from "./icon-signature.svg";
export { ReactComponent as ManualIcon } from "./icon-manual.svg";
export { ReactComponent as WarnIcon } from "./icon-warn.svg";
export { ReactComponent as SyncManualIcon } from "./icon-sync-manual.svg";

export { ReactComponent as DangerIcon } from "icons/icon-danger.svg";
export { ReactComponent as TableMinusIcon } from "icons/icon-table-minus.svg";
export { ReactComponent as TablePlusIcon } from "icons/icon-table-plus.svg";
export { ReactComponent as MobileAppIcon } from "icons/icon-mobile-app.svg";
export { ReactComponent as MobileNavIcon } from "icons/icon-navigation-mobile.svg";
export { ReactComponent as PcNavIcon } from "icons/icon-navigation-pc.svg";
export { ReactComponent as UnLockIcon } from "icons/icon-unlock.svg";
export { ReactComponent as CalendarDeleteIcon } from "icons/icon-calendar-delete.svg";
export { ReactComponent as TableCheckedIcon } from "icons/icon-table-checked.svg";
export { ReactComponent as TableUnCheckedIcon } from "icons/icon-table-boolean-false.svg";
export { ReactComponent as FileFolderIcon } from "icons/icon-editor-folder.svg";
export { ReactComponent as ExpandIcon } from "icons/icon-expand.svg";
export { ReactComponent as CompressIcon } from "icons/icon-compress.svg";
export { ReactComponent as TableCellsIcon } from "icons/icon-table-cells.svg"; // Added By Aqib Mirza
export { ReactComponent as TimeLineIcon } from "icons/icon-timeline-comp.svg"
export { ReactComponent as LottieIcon } from "icons/icon-lottie.svg";
export { ReactComponent as MentionIcon } from "icons/icon-mention-comp.svg";
export { ReactComponent as AutoCompleteCompIcon } from "icons/icon-autocomplete-comp.svg";

export { ReactComponent as IconCompIcon } from "icons/IconCompIcon.svg";

export { ReactComponent as DangerIcon } from "./icon-danger.svg";
export { ReactComponent as TableMinusIcon } from "./icon-table-minus.svg";
export { ReactComponent as TablePlusIcon } from "./icon-table-plus.svg";
Expand Down Expand Up @@ -612,4 +634,5 @@ export { ReactComponent as MentionIcon } from "./icon-mention-comp.svg";
export { ReactComponent as AutoCompleteCompIcon } from "./icon-autocomplete-comp.svg";
export { ReactComponent as WidthIcon } from "./icon-width.svg";
export { ReactComponent as ResponsiveLayoutCompIcon } from "./remix/layout-column-line.svg"; // Closest match for responsive layout component
export { ReactComponent as TextSizeIcon } from "./icon-text-size.svg"; */
export { ReactComponent as TextSizeIcon } from "./icon-text-size.svg"; */

142 changes: 142 additions & 0 deletions client/packages/lowcoder/src/comps/comps/iconComp.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
import { useEffect, useRef, useState } from "react";
import styled, { css } from "styled-components";
import { RecordConstructorToView } from "lowcoder-core";
import { styleControl } from "comps/controls/styleControl";
import _ from "lodash";
import {
IconStyle,
IconStyleType,
heightCalculator,
widthCalculator,
} from "comps/controls/styleControlConstants";
import { UICompBuilder } from "comps/generators/uiCompBuilder";
import { withDefault } from "../generators";
import {
NameConfigHidden,
withExposingConfigs,
} from "comps/generators/withExposing";
import { Section, sectionNames } from "lowcoder-design";
import { hiddenPropertyView } from "comps/utils/propertyUtils";
import { trans } from "i18n";
import { NumberControl } from "comps/controls/codeControl";
import { IconControl } from "comps/controls/iconControl";
import ReactResizeDetector from "react-resize-detector";
import { AutoHeightControl } from "../controls/autoHeightControl";
import {
clickEvent,
eventHandlerControl,
} from "../controls/eventHandlerControl";

const Container = styled.div<{ $style: IconStyleType | undefined }>`
height: 100%;
width: 100%;
display: flex;
align-items: center;
justify-content: center;
svg {
object-fit: contain;
pointer-events: auto;
}
${(props) => props.$style && getStyle(props.$style)}
`;

const getStyle = (style: IconStyleType) => {
return css`
svg {
color: ${style.fill};
}
padding: ${style.padding};
border: 1px solid ${style.border};
border-radius: ${style.radius};
margin: ${style.margin};
max-width: ${widthCalculator(style.margin)};
max-height: ${heightCalculator(style.margin)};
`;
};

const EventOptions = [clickEvent] as const;

const childrenMap = {
style: styleControl(IconStyle),
icon: withDefault(IconControl, "/icon:antd/homefilled"),
autoHeight: withDefault(AutoHeightControl, "auto"),
iconSize: withDefault(NumberControl, 20),
onEvent: eventHandlerControl(EventOptions),
};

const IconView = (props: RecordConstructorToView<typeof childrenMap>) => {
const conRef = useRef<HTMLDivElement>(null);
const [width, setWidth] = useState(0);
const [height, setHeight] = useState(0);

useEffect(() => {
if (height && width) {
onResize();
}
}, [height, width]);

const onResize = () => {
const container = conRef.current;
setWidth(container?.clientWidth ?? 0);
setHeight(container?.clientHeight ?? 0);
};

return (
<ReactResizeDetector onResize={onResize}>
<Container
ref={conRef}
$style={props.style}
style={{
fontSize: props.autoHeight
? `${height < width ? height : width}px`
: props.iconSize,
background: props.style.background,
}}
onClick={() => props.onEvent("click")}
>
{props.icon}
</Container>
</ReactResizeDetector>
);
};

let IconBasicComp = (function () {
return new UICompBuilder(childrenMap, (props) => <IconView {...props} />)
.setPropertyViewFn((children) => (
<>
<Section name={sectionNames.basic}>
{children.icon.propertyView({
label: trans("iconComp.icon"),
IconType: "All",
})}
{children.autoHeight.propertyView({
label: trans("iconComp.autoSize"),
})}
{!children.autoHeight.getView() &&
children.iconSize.propertyView({
label: trans("iconComp.iconSize"),
})}
</Section>
<Section name={sectionNames.interaction}>
{children.onEvent.getPropertyView()}
</Section>
<Section name={sectionNames.layout}>
{hiddenPropertyView(children)}
</Section>
<Section name={sectionNames.style}>
{children.style.getPropertyView()}
</Section>
</>
))
.build();
})();

IconBasicComp = class extends IconBasicComp {
override autoHeight(): boolean {
return false;
}
};

export const IconComp = withExposingConfigs(IconBasicComp, [
NameConfigHidden,
]);
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export interface ControlParams extends CodeEditorControlParams {
preInputNode?: ReactNode;
childrenWrapperStyle?: CSSProperties;
extraChildren?: ReactNode;
IconType?: "OnlyAntd" | "All" | "default" | undefined;
}

export interface ControlType {
Expand Down
Loading