//
//     Copyright © 2011-2025 Cambridge Intelligence Limited.
//     All rights reserved.
//
//     Sample Code
//
//!    Create nodes with custom shapes, multiple labels and advanced styles.

import {
  data,
  invertedInteractiveNode,
  normalInteractiveNode,
  interactionStylePresets,
} from "./advancednodestyles-data.js";

import KeyLines from "keylines";

let chart;
// keeps track of the current state/styling of items on the chart
const itemsState = [...data.items];

function highlightSelection(id) {
  // if the user hasn't clicked an item resize back to fit
  if (!id) {
    chart.foreground(() => true);
    return;
  }
  // if there is a clicked item set it to be foregrounded
  const clickedItem = chart.getItem(id);
  if (clickedItem) {
    chart.foreground((item) => clickedItem.id === item.id);
  }
}

function formatPrintString(key, value) {
  if (key === "x" || key === "y" || key === "bg") {
    return undefined;
  }
  // prints the borderRadius array as a single value if all directions are set to the same value
  if (key === "borderRadius") {
    const set = new Set(value);
    if (set.size === 1) {
      return value[0];
    }
  }
  return value;
}

// Formats the code snippets neatly in the display box on the right-hand side
function prettyPrint(id) {
  const item = chart.getItem(id);
  if (item) {
    const json = JSON.stringify(item, formatPrintString, 1);
    document.getElementById("display").innerHTML = json;
  } else {
    document.getElementById("display").innerHTML = "No node selected";
  }
}

// Called when KeyLines is created
function initializeInteractions() {
  // Zoom the view to its original position when double-clicking the background
  chart.on("double-click", ({ id, preventDefault }) => {
    if (!chart.getItem(id)) {
      chart.zoom("fit", { animate: true, time: 500 });
    }
    // Default behaviour is to zoom in - we preventDefault to override this
    preventDefault();
  });

  let interactiveNodesState = {};
  const labelButtons = {};
  const buttonGroupLabels = {};

  function registerLabelButton(
    itemId,
    labelIndex,
    onItemInteractionLabelStyle,
    onClick
  ) {
    labelButtons[itemId] = labelButtons[itemId] || {};
    labelButtons[itemId][labelIndex] = { onClick };
    labelButtons[itemId][
      onItemInteractionLabelStyle
    ] = onItemInteractionLabelStyle;
  }

  function registerLabelButtonGroup(
    itemId,
    labelIndexArray,
    onItemInteractionLabelStyle,
    onClick
  ) {
    for (const labelIndex of labelIndexArray) {
      registerLabelButton(
        itemId,
        labelIndex,
        onItemInteractionLabelStyle,
        onClick
      );
      buttonGroupLabels[itemId] = {
        ...buttonGroupLabels[itemId],
        [labelIndex]: labelIndexArray,
      };
    }
  }

  // Highlight and pretty print when a selection is made or if we click an interactive label
  chart.on("click", ({ id, subItem, preventDefault }) => {
    prettyPrint(id);
    highlightSelection(id);

    if (subItem.type === "label") {
      const labelIndex = subItem.index;
      if (labelButtons[id] && labelButtons[id][labelIndex]) {
        labelButtons[id][labelIndex].onClick();
        prettyPrint(id);
      }
    }
  });

  let hoveredButton = null;

  chart.on("hover", async ({ id, subItem }) => {
    const style = {};

    const labelIndex = subItem?.type === "label" ? subItem.index : -1;
    const isHoverableLabel =
      id !== null && labelButtons[id] && labelButtons[id][labelIndex];
    const isAButtonGroup =
      isHoverableLabel && !!buttonGroupLabels?.[id]?.[labelIndex];
    const hoverStyle = isHoverableLabel && labelButtons[id][style];
    const sourceItem = itemsState.find((item) => item.id === id);
    const chartElement = document.querySelector("#klchart canvas[style]");

    // Always clear any hovered button first
    if (hoveredButton) {
      const hoveredButtonSourceItem = itemsState.find(
        (item) => item.id === hoveredButton
      );
      await chart.setProperties({
        id: hoveredButton,
        t: hoveredButtonSourceItem?.t,
      });
      hoveredButton = null;
    }
    // clear cursor style
    chartElement.classList.remove("cursor");

    if (isHoverableLabel && !isAButtonGroup) {
      const label = [...sourceItem.t];

      label[labelIndex] = {
        ...label[labelIndex],
        ...hoverStyle,
      };

      await chart.setProperties({ id, t: label });
      hoveredButton = id;
      chartElement.classList.toggle("cursor");
    } else if (isAButtonGroup) {
      const label = [...sourceItem.t];

      for (const index of buttonGroupLabels[id][labelIndex]) {
        label[index] = {
          ...label[index],
          ...hoverStyle,
        };
      }

      await chart.setProperties({ id, t: label });
      hoveredButton = id;
      chartElement.classList.toggle("cursor");
    }
  });

  async function toggleNodeStyle(itemId) {
    const interactiveNodeState = interactiveNodesState[itemId];

    const nodeStyle = !interactiveNodeState
      ? invertedInteractiveNode
      : normalInteractiveNode;
    // remove x and y from the styling to prevent the node snapping back to it's original position
    delete nodeStyle.x;
    delete nodeStyle.y;

    // updtate the item state with the new toggled style
    const itemIndex = itemsState.findIndex((item) => item.id === itemId);
    itemsState[itemIndex] = nodeStyle;

    await chart.setProperties({ id: itemId, ...nodeStyle });

    interactiveNodesState[itemId] = !interactiveNodesState[itemId];
  }

  async function toggleLabelButtons(itemId, labelIndex, style) {
    // track which buttons are toggled
    const currentNodeInteractiveState = interactiveNodesState[itemId] ?? [];

    currentNodeInteractiveState[labelIndex] =
      currentNodeInteractiveState[labelIndex] === undefined
        ? true
        : !currentNodeInteractiveState[labelIndex];

    interactiveNodesState[itemId] = currentNodeInteractiveState;

    // make sure all buttons are styled correctly depending on their toggle state
    const currentStyle = itemsState.find((item) => item.id === itemId);
    const originalStyle = data.items.find((item) => item.id === itemId);

    const toggleStyle = { ...currentStyle, t: [...currentStyle.t] };
    toggleStyle.t[labelIndex] = {
      ...toggleStyle.t[labelIndex],
      ...style,
    };

    // remove x and y from the styling to prevent the node snapping back to it's original position
    delete toggleStyle.x;
    delete toggleStyle.y;

    // update the item state with the new toggled style
    const itemIndex = itemsState.findIndex((item) => item.id === itemId);
    itemsState[itemIndex] = toggleStyle;

    currentNodeInteractiveState.forEach((toggleValue, index) => {
      const originalLabelStyle = { ...originalStyle.t[index] };
      const labelStyle = toggleValue ? style : originalLabelStyle;
      toggleStyle.t[index] = {
        ...toggleStyle.t[index],
        ...labelStyle,
      };
    });

    await chart.setProperties({ id: itemId, ...toggleStyle });
  }

  registerLabelButton(
    "t2",
    2,
    interactionStylePresets.interactiveNodeHover,
    () => {
      toggleNodeStyle("t2");
    }
  );

  registerLabelButtonGroup(
    "t2",
    [3, 4],
    interactionStylePresets.interactiveNodeHover,
    () => window.open("/stylingnodes.htm#advancedlabelstyling", "_blank")
  );

  registerLabelButton(
    "outside-buttons-rectangle",
    3,
    interactionStylePresets.outsideLabelActive,
    () => {
      toggleLabelButtons(
        "outside-buttons-rectangle",
        3,
        interactionStylePresets.outsideLabelHover
      );
    }
  );
  registerLabelButton(
    "outside-buttons-rectangle",
    5,
    interactionStylePresets.outsideLabelActive,
    () => {
      toggleLabelButtons(
        "outside-buttons-rectangle",
        5,
        interactionStylePresets.outsideLabelHover
      );
    }
  );
  registerLabelButton(
    "outside-buttons-rectangle",
    7,
    interactionStylePresets.outsideLabelActive,
    () => {
      toggleLabelButtons(
        "outside-buttons-rectangle",
        7,
        interactionStylePresets.outsideLabelHover
      );
    }
  );

  registerLabelButton(
    "outside-buttons-circle",
    3,
    interactionStylePresets.outsideLabelActive,
    () => {
      toggleLabelButtons(
        "outside-buttons-circle",
        3,
        interactionStylePresets.outsideLabelHover
      );
    }
  );
  registerLabelButton(
    "outside-buttons-circle",
    4,
    interactionStylePresets.outsideLabelActive,
    () => {
      toggleLabelButtons(
        "outside-buttons-circle",
        4,
        interactionStylePresets.outsideLabelHover
      );
    }
  );
  registerLabelButton(
    "outside-buttons-circle",
    5,
    interactionStylePresets.outsideLabelActive,
    () => {
      toggleLabelButtons(
        "outside-buttons-circle",
        5,
        interactionStylePresets.outsideLabelHover
      );
    }
  );
}

async function startKeyLines() {
  const options = {
    logo: { u: "/images/Logo.png" },
    handMode: true,
    selectionColour: "#FF9933",
    iconFontFamily: "Font Awesome 5 Free",
    overview: { icon: false },
    navigation: { shown: false },
    hover: 0,
  };

  chart = await KeyLines.create({ container: "klchart", options });
  await chart.load(data);
  await chart.zoom("fit");
  initializeInteractions();
}

function loadFontsAndStart() {
  document.fonts.ready
    .then(() =>
      Promise.all([
        document.fonts.load('1em "Font Awesome 5 Free"'),
        document.fonts.load('1em "Raleway"'),
        document.fonts.load('1em "Montserrat"'),
        document.fonts.load('1em "Varela Round"'),
        document.fonts.load('1em "Material Icons"'),
      ])
    )
    .then(startKeyLines);
}

window.addEventListener("DOMContentLoaded", loadFontsAndStart);
<!DOCTYPE html>
<html lang="en" style="background-color: #2d383f;">
  <head>
    <meta charset="UTF-8">
    <title>Advanced Node Styles</title>
    <link rel="stylesheet" type="text/css" href="/css/keylines.css">
    <link rel="stylesheet" type="text/css" href="/css/minimalsdk.css">
    <link rel="stylesheet" type="text/css" href="/css/sdk-layout.css">
    <link rel="stylesheet" type="text/css" href="/css/demo.css">
    <link rel="stylesheet" type="text/css" href="/css/fontawesome5/fontawesome.css">
    <link rel="stylesheet" type="text/css" href="/css/fontawesome5/solid.css">
    <link rel="stylesheet" type="text/css" href="https://fonts.googleapis.com/css2?family=Raleway&amp;family=Montserrat&amp;family=Varela+Round">
    <link rel="stylesheet" type="text/css" href="/advancednodestyles.css">
    <script src="/vendor/jquery.js" defer type="text/javascript"></script>
    <script id="keylines" src="/public/keylines.umd.cjs" defer type="text/javascript"></script>
    <script src="/advancednodestyles.js" crossorigin="use-credentials" defer type="module"></script>
  </head>
  <body>
    <div class="chart-wrapper demo-cols">
      <div class="tab-content-panel flex1" id="lhs" data-tab-group="rhs">
        <div class="toggle-content is-visible" id="lhsVisual" style="width:100%; height: 100%;">
          <div class="klchart" id="klchart">
          </div>
        </div>
      </div>
      <div class="rhs citext closed" id="demorhscontent">
        <div class="title-bar">
          <svg viewBox="0 0 360 60" style="max-width: 360px; max-height: 60px; font-size: 24px; font-family: Raleway; flex: 1;">
            <text x="0" y="38" style="width: 100%;">Advanced Node Styles</text>
          </svg>
        </div>
        <div class="tab-content-panel" data-tab-group="rhs">
          <div class="toggle-content is-visible tabcontent" id="controlsTab">
            <p>Select a node to view its source code and interact with the buttons on the Interactive Node.</p>
            <form autocomplete="off" onsubmit="return false" id="rhsForm">
              <div class="cicontent">
                <fieldset>
                  <pre id="display" style="-webkit-user-select: text; -khtml-user-select: text; -moz-user-select: text; -ms-user-select: text; word-break: keep-all;font-family: monospace, &quot;Material Icons&quot;">No node selected</pre>
                </fieldset>
              </div>
            </form>
          </div>
        </div>
      </div>
    </div>
    <div id="moreParent">
      <div id="moreContainer">
      </div>
    </div>
    <div id="lazyScripts">
    </div>
  </body>
</html>
@import url("https://fonts.googleapis.com/icon?family=Material+Icons");

/* Enable cursor overriding while hovering the chart */
.cursor {
  cursor: pointer !important;
}
Loading source