//
// Copyright © 2011-2025 Cambridge Intelligence Limited.
// All rights reserved.
//
// Sample Code
//
//! Drag items to and from combos.
import KeyLines from "keylines";
import data, {
selectionColour,
nodeColours,
nodeBorders,
ocColours,
acceptOcColours,
plusGlyph,
createGlyph,
} from "./combodrag-data.js";
let chart;
let mode;
let propsToReset = [];
let isCombining = false;
const arrangeCheckbox = document.getElementById("arrange");
const transferCheckbox = document.getElementById("transfer");
const resizeButton = document.getElementById("resize");
const arrangeItemsButton = document.getElementById("arrangeItems");
const arrangeButtons = Array.from(document.querySelectorAll("#rhsForm .btn"));
const arrangeTextElement = document.getElementById("arrangeText");
function isCombo(id) {
return chart.combo().isCombo(id);
}
function getComboNodeItem(id) {
const item = chart.getItem(id);
if (item && item.type === "node") {
if (isCombo(id)) {
return item;
}
const comboId = item.parentId;
if (comboId) {
return chart.getItem(comboId);
}
}
return null;
}
function getAllComboNodes() {
const comboIdSet = new Set();
chart.each({ type: "node", items: "all" }, (node) => {
if (isCombo(node.id)) {
comboIdSet.add(node.id);
}
});
return comboIdSet;
}
function arrangeAllCombos(options) {
chart.combo().arrange(Array.from(getAllComboNodes()), options);
}
function handleResizeCombos() {
arrangeAllCombos({ name: "none" });
}
function handleRespositionCombos() {
arrangeAllCombos({ resize: false, name: "concentric" });
}
function handleDragStart({ setDragOptions, preventDefault }) {
if (isCombining) {
preventDefault();
return;
}
// Allow combo contents to be dragged separately from the top level combo
setDragOptions({ dragCombos: false });
propsToReset = [];
}
// Darken the colour of the combo to indicate that if the drag ended here
// the node(s) being dragged would transfer into it
function styleOverCombo(combo) {
const { id, c } = combo;
propsToReset.push({
id,
oc: { c: ocColours[nodeColours.indexOf(c)] },
});
chart.setProperties({
id,
oc: { c: acceptOcColours[nodeColours.indexOf(c)] },
});
}
// Add a + glyph to the node to indicate that if the drag ended here
// a combo would be created containing this node and the node(s) being dragged
function addGlyphToNode(id) {
propsToReset.push({ id, g: [] });
chart.setProperties({ id, g: [plusGlyph] });
}
function handleDragOver({ id }) {
// Clear previous styling
chart.setProperties(propsToReset);
// Apply styling to indicate the result of ending the drag here
if (id !== null) {
const currentTargetCombo = getComboNodeItem(id);
if (currentTargetCombo === null) {
// We're over a node, meaning that if the drag ends here we can create a combo
addGlyphToNode(id);
} else {
// We're over a combo, meaning that if the drag ends here we can transfer into it
styleOverCombo(currentTargetCombo);
}
}
}
async function makeNewCombo(idsToCombine, idBeingDragged) {
const item = chart.getItem(idBeingDragged);
const style = {
c: item.c,
e: 1.4,
b: nodeBorders[nodeColours.indexOf(item.c)],
};
const openStyle = {
c: ocColours[nodeColours.indexOf(item.c)],
b: nodeBorders[nodeColours.indexOf(item.c)],
bw: 2,
};
await chart.combo().combine(
{
ids: idsToCombine,
open: true,
style,
glyph: createGlyph,
openStyle,
label: "",
},
{
arrange: mode.arrange ? "none" : "concentric",
}
);
}
// Get an array of all the nested combos that contain a given node (starting at innermost)
function getAncestors(nodeId) {
if (nodeId === null) return [];
const ancestors = [];
let ancestor = chart.getItem(nodeId).parentId;
while (ancestor) {
ancestors.push(ancestor);
ancestor = chart.getItem(ancestor).parentId;
}
return ancestors;
}
function getNodesToTransfer(nodeIds, idToTransferInto) {
// Exclude nodes from being transferred
// if a combo that contains them is being transferred
// or they are already in the combo being transferred into
return nodeIds.filter((nodeId) => {
return (
!getAncestors(nodeId).some((id) => nodeIds.includes(id)) &&
(chart.getItem(nodeId).parentId ?? null) !== idToTransferInto
);
});
}
async function handleDragEnd({ id, dragIds, type, preventDefault }) {
chart.setProperties(propsToReset);
if (type === "node") {
// Check the type it has been dragged onto is the chart background (null) or a node
const inBackground = chart.getItem(id) === null;
const isNode = chart.getItem(id)?.type === "node";
if (inBackground || isNode) {
// Set a flag to prevent another drag starting while async combo functions are running
isCombining = true;
const transferOptions = {
arrange: mode.arrange ? "none" : "concentric",
resize: !mode.arrange,
};
const currentTargetCombo = getComboNodeItem(id);
if (currentTargetCombo === null && !inBackground) {
// Create a new combo
const idsToCombine = [id, ...dragIds];
// Ensure all the items being combined are at the top level
await chart.combo().transfer(idsToCombine, null, transferOptions);
await makeNewCombo(idsToCombine, id);
} else {
const currentTargetComboId = currentTargetCombo
? currentTargetCombo.id
: null;
const nodesToTransfer = getNodesToTransfer(
dragIds,
currentTargetComboId
);
// Check there is at least one item to transfer
if (nodesToTransfer.length > 0) {
await chart.combo().open(currentTargetComboId);
await chart
.combo()
.transfer(nodesToTransfer, currentTargetComboId, transferOptions);
}
}
// Clear the flag, as it is now safe for another drag to start
isCombining = false;
}
if (!mode.arrange) {
preventDefault();
}
}
}
function setMode() {
arrangeCheckbox.disabled = !transferCheckbox.checked;
arrangeCheckbox.checked = arrangeCheckbox.checked && transferCheckbox.checked;
arrangeTextElement.style.color = transferCheckbox.checked ? "" : "silver";
mode = {
arrange: arrangeCheckbox.checked,
transfer: transferCheckbox.checked,
};
removeTransferDragEvents();
if (mode.transfer) {
addTransferDragEvents();
}
arrangeButtons.forEach((btn) => {
btn.disabled = !mode.arrange;
});
// update whether open combos have resize handles on selection
const updateRe = Array.from(getAllComboNodes()).map((id) => ({
id,
oc: { re: mode.arrange },
}));
chart.setProperties(updateRe);
}
function addTransferDragEvents() {
chart.on("drag-start", handleDragStart);
chart.on("drag-over", handleDragOver);
chart.on("drag-end", handleDragEnd);
}
function removeTransferDragEvents() {
chart.off("drag-start", handleDragStart);
chart.off("drag-over", handleDragOver);
chart.off("drag-end", handleDragEnd);
}
function initialiseEvents() {
addTransferDragEvents();
arrangeCheckbox.addEventListener("change", setMode);
transferCheckbox.addEventListener("change", setMode);
resizeButton.addEventListener("click", handleResizeCombos);
arrangeItemsButton.addEventListener("click", handleRespositionCombos);
}
async function startKeyLines() {
const options = {
logo: { u: "/images/Logo.png" },
selectedNode: {
b: selectionColour,
bw: 4,
oc: {
b: selectionColour,
bw: 4,
},
},
handMode: true,
};
chart = await KeyLines.create({ container: "klchart", options });
chart.load(data);
chart.zoom("fit");
setMode();
initialiseEvents();
}
window.addEventListener("DOMContentLoaded", startKeyLines);
<!DOCTYPE html>
<html lang="en" style="background-color: #2d383f;">
<head>
<meta charset="UTF-8">
<title>Combo Dragging</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">
<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="/combodrag.js" crossorigin="use-credentials" 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%;">Combo Dragging</text>
</svg>
</div>
<div class="tab-content-panel" data-tab-group="rhs">
<div class="toggle-content is-visible tabcontent" id="controlsTab">
<form autocomplete="off" onsubmit="return false" id="rhsForm">
<div class="cicontent">
<fieldset>
<p>Transfer items by dragging them to and from combos.</p>
<label class="checkbox">
<input id="transfer" type="checkbox" checked>Allow transfer between combos
</label>
<label class="checkbox" id="arrangeText">
<input id="arrange" type="checkbox">Allow manual arrangement of items
</label>
<input class="btn btn-kl" type="button" value="Arrange items" disabled style="margin-left: 20px;" id="arrangeItems">
<input class="btn btn-kl" type="button" value="Resize combos" disabled id="resize">
</fieldset>
</div>
</form>
</div>
</div>
</div>
</div>
<div id="moreParent">
<div id="moreContainer">
</div>
</div>
<div id="lazyScripts">
</div>
</body>
</html>
Loading source
