Skip to content
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

Custom outline item names - Feature Request #634

Open
FedericoBonel opened this issue Oct 1, 2024 · 1 comment
Open

Custom outline item names - Feature Request #634

FedericoBonel opened this issue Oct 1, 2024 · 1 comment

Comments

@FedericoBonel
Copy link

Description

As edits grow in size and complexity, the outline can become quite large. The outline is a great way to help the user understand what is in the edit and where, but it can become confusing as well when you start having multiple instances of the same component at the top level.
Consider the following example:

image

In that case there aren't many instances, but the user will most likely have multiple groups at top level, and in each group they could have multiple role entities, so when you have 20 or 50 groups each with maybe 10 or 20 role entities this can be more confusing than clarifying.

Proposed solution

This can be easily fixed in a flexible manner if there would be an outline items array in the outline override, or a specific override for each outline item. That way you could do something like this:

Outline item override:

const overrides = {
  // These "props" would hold the props given to the component the item represents. Here I'm assuming there is a title.
  outlineItem: ({ props }) => <Puck.OutlineItem>{data.title}</Puck.OutlineItem>,
};
@OsamuBlack
Copy link

I believe you have been working on this issue and might have implemented the solution already. In case it helps here is my implementation using the override and puck action.

Demo:

image

The main component is coming from packages/core/components/Puck/components/Outline/ file which does not have an useStated array declared to keep record of items opened

The second one is packages/core/components/LayerTree

// All import statements which basically includes the /lib

// Type declarations for outline overrides
export type OutlineItemRenderProps = {
  id: string;
  zone: string | undefined;
  isSelected: boolean;
  isHovering: boolean;
  index: number;
};

export type OutlineZonesRenderProps = {
  zone: string | undefined;
  label: string;
};

const getClassName = getClassNameFactory("LayerTree", styles);
const getClassNameLayer = getClassNameFactory("Layer", styles);

export const LayerTree = ({
  data,
  config,
  zoneContent,
  itemSelector,
  setItemSelector,
  zone,
  label,
  openList, // Basically supports opening multiple outline items. Its just an useStated array declared in the 
  setOpen,
  render,
}: {
  data: Data;
  config: Config;
  zoneContent: Data["content"];
  itemSelector?: ItemSelector | null;
  setItemSelector: (item: ItemSelector | null) => void;
  zone?: string;
  label?: string;
  openList: string[];
  setOpen: (id: string) => void;
  render?: {
    outlineItemRender?: (props: OutlineItemRenderProps) => ReactNode;
    zoneRender?: (props: OutlineZonesRenderProps) => ReactNode;
  };
}) => {
  const zones = data.zones || {};
  const ctx = useContext(dropZoneContext);

  return (
    <>
      {label ? (
        <div className={getClassName("zoneTitle")}>
          <div className={getClassName("zoneIcon")}>
            <LuLayers size="12" />
          </div>{" "}
          {label}
          {render?.zoneRender ? (
            <render.zoneRender zone={zone} label={label} />
          ) : (
            ""
          )}
        </div>
      ) : (
        <div className={getClassName("zoneTitle")}>
          <div className={getClassName("zoneIcon")}>
            <LuLayers size="12" />
          </div>{" "}
          {"Container"}
          {render?.zoneRender ? (
            <render.zoneRender zone={rootDroppableId} label={"Container"} />
          ) : (
            ""
          )}
        </div>
      )}
      <ul className={getClassName()}>
        {zoneContent.length === 0 && (
          <div className={getClassName("helper")}>No items</div>
        )}
        {zoneContent.map((item, i) => {
          const isSelected =
            itemSelector?.index === i &&
            (itemSelector.zone === zone ||
              (itemSelector.zone === rootDroppableId && !zone));

          const zonesForItem = findZonesForArea(data, item.props.id);
          const containsZone = Object.keys(zonesForItem).length > 0;

          const {
            setHoveringArea = () => {},
            setHoveringComponent = () => {},
            hoveringComponent,
          } = ctx || {};

          const selectedItem =
            itemSelector && data ? getItem(itemSelector, data) : null;

          const isHovering = hoveringComponent === item.props.id;

          const isOpen = openList.includes(item.props.id);

          const childIsSelected = isChildOfZone(
            item,
            selectedItem,
            ctx ? { data, pathData: ctx.pathData } : undefined
          );

          return (
            <li
              className={getClassNameLayer({
                isSelected,
                isOpen,
                isHovering,
                containsZone,
                childIsSelected,
              })}
              key={`${item.props.id}_${i}`}
            >
              <div className={getClassNameLayer("inner")}>
                <div
                  className={getClassNameLayer("innerWrapper")}
                  onMouseOver={(e) => {
                    e.stopPropagation();
                    setHoveringArea(item.props.id);
                    setHoveringComponent(item.props.id);
                  }}
                  onMouseOut={(e) => {
                    e.stopPropagation();
                    setHoveringArea(null);
                    setHoveringComponent(null);
                  }}
                >
                  <div className={getClassNameLayer("innerContent")}>
                    {containsZone && (
                      <IconButton
                        title={isOpen ? "Collapse" : "Expand"}
                        onClick={() => {
                          setOpen(item.props.id);
                        }}
                      >
                        <LuChevronDown
                          className={getClassNameLayer("chevron")}
                          size="16"
                        />
                      </IconButton>
                    )}
                    <div
                      onClick={(e) => {
                        if (isSelected) {
                          setItemSelector(null);
                          return;
                        }

                        setItemSelector({
                          index: i,
                          zone,
                        });

                        const id = zoneContent[i].props.id;

                        const frame = getFrame();

                        scrollIntoView(
                          frame?.querySelector(
                            `[data-rfd-drag-handle-draggable-id="draggable-${id}"]`
                          ) as HTMLElement
                        );
                      }}
                      className={getClassNameLayer("title")}
                    >
                      <div className={getClassNameLayer("icon")}>
                        {item.type === "Text" || item.type === "Heading" ? (
                          <LuType size="16" />
                        ) : (
                          <LuLayoutGrid size="16" />
                        )}
                      </div>
                      <div className={getClassNameLayer("name")}>
                        {config.components[item.type]["label"] ?? item.type}
                      </div>
                    </div>
                  </div>
                  {render?.outlineItemRender ? (
                    <div className={getClassNameLayer("innerContent")}>
                      <render.outlineItemRender
                        id={item.props.id}
                        zone={zone}
                        isSelected={isSelected}
                        isHovering={isHovering}
                        index={i}
                      />
                    </div>
                  ) : (
                    ""
                  )}
                </div>
              </div>
              {containsZone &&
                Object.keys(zonesForItem).map((zoneKey, idx) => (
                  <div key={idx} className={getClassNameLayer("zones")}>
                    <LayerTree
                      config={config}
                      data={data}
                      zoneContent={zones[zoneKey]}
                      setItemSelector={setItemSelector}
                      itemSelector={itemSelector}
                      zone={zoneKey}
                      label={getZoneId(zoneKey)[1]}
                      openList={openList}
                      setOpen={setOpen}
                      render={render}
                    />
                  </div>
                ))}
            </li>
          );
        })}
      </ul>
      <>

There were a few modifications to the style.module.css for this file as well, most of which allow overflow of items and flex row + justify-between.

One issue i faced was the the jittering of screen when items revealed on hover were overflowing. To fix this I added overflow: scrollX to the wrapper component which is not elegant but prevents jittering.


Only including the chaged css classes

/* Original */
.Layer-clickable {
  align-items: center;
  background: none;
  border: 0;
  border-radius: 4px;
  color: inherit;
  cursor: pointer;
  display: flex;
  font: inherit;
  padding-left: 12px;
  padding-right: 4px;
  width: 100%;
}

.Layer-clickable:focus-visible {
...
}

.Layer--isSelected > .Layer-inner > .Layer-clickable > .Layer-chevron,
.Layer--childIsSelected > .Layer-inner > .Layer-clickable > .Layer-chevron {
  transform: scaleY(-1);
}

.Layer--isSelected > .Layer-zones,

.Layer-title,

.LayerTree-zoneTitle {
  display: flex;
  gap: 8px;
  align-items: center;
  margin: 8px 4px;
  overflow-x: hidden;
}
/* Changed */

// Changed the class from .Layer-clickable
.Layer-innerWrapper {
  align-items: center;
  background: none;
  border: 0;
  border-radius: 4px;
  color: inherit;
  display: flex;
  gap: 8px;
  justify-content: space-between;
  font: inherit;
  padding-left: 8px;
  padding-right: 8px;
  width: 100%;
}

// To handle the content inside the outline item
.Layer-innerContent { 
  align-items: center;
  display: flex;
  gap: 8px;
  align-items: center;
}

.Layer-innerWrapper:focus-visible {
  outline: 2px solid var(--puck-color-azure-05);
  outline-offset: 2px;
  position: relative;
  z-index: 1;
}

.Layer--isOpen > .Layer-inner > .Layer-innerWrapper .Layer-chevron,
.Layer--childIsSelected > .Layer-inner > .Layer-innerWrapper .Layer-chevron {
  transform: scaleY(-1);
}

.Layer--isOpen > .Layer-zones,

.Layer--childIsSelected > .Layer-zones {
  display: block;
}

.Layer-title,
.LayerTree-zoneTitle {
  display: flex;
  gap: 8px;
  align-items: center;
  margin: 8px 4px;
  overflow-x: visible; // To avoid jttering
}

.Layer-title {
  cursor: pointer; // Clickable to open items other than selected
}

.LayerTree-zoneTitle {
  cursor: default;
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

3 participants