Public
Edited
Nov 18, 2022
Importers
2 stars
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
defaultCreateSelector = (level, depth, showTitle, onAddAll = null) => {
const { id, title } = level;
const addAllButton = !!onAddAll
? html`<button class="add-all-button" style="float:right">All</button>`
: "";
const selectElement = html`<select name="${id}"></select>`;
const selector = html`<div class="cascading-hierarchy-selector">
${
showTitle
? html`<div style="text-transform: uppercase;">${title}${addAllButton}</div>`
: addAllButton
}
${selectElement}
</div>`;
if (!!onAddAll) {
d3.select(addAllButton).on("click", () =>
onAddAll(level, selectElement, depth)
);
}
selector.setAddAllEnabled = (isEnabled) => {
!!onAddAll
? $(addAllButton).prop("disabled", !isEnabled).trigger("change")
: null;
};
return selector;
}
Insert cell
function* renderHierarchyPicker(
levels,
{
showTitle = true,
createSelector = defaultCreateSelector,
elementId = null,
getSelect = (node) => {
const $node = $(node);
return $node.prop("tagName") === "SELECT" ? $node : $node.find("select");
},
multiple = false,
onSelect = () => {},
showIntermediate = false,
defaultToFirst = true,
width = "20em",
wrapSelectors = (selectors) => html`
<div
class="cascading-hierarchy-picker"
style="display: flex; flex-direction: row; overflow-x: scroll;"
>
${joinArray(selectors, "&nbsp;&nbsp;")}
</div>`,
showAddAll = false
} = {}
) {
select2;

const onAddAll =
multiple && showAddAll
? (level, selectElement, depth) => {
// This `if` is redundant; the Add All button shouldn't be clickable unless there are cachedOptions.
if (!!level.cachedOptions) {
const optionIds = level.cachedOptions.map((x) => x.id);
$(selectElement).val(optionIds);
$(selectElement).trigger("change");
}
}
: null;
const selectors = levels.map((level, i) =>
createSelector(
level,
i,
showTitle,
level.selectOptions?.maximumSelectionLength ? null : onAddAll
)
);
// Guard against custom createSelectors that don't have setAddAllEnabled:
const setAddAllEnabled = (selector, isEnabled) => {
if (!!selector.setAddAllEnabled) {
selector.setAddAllEnabled(isEnabled);
}
};

const wrap = html`
<div ${!!elementId ? `id="${elementId}"` : ""} class="table-select">
<style>
.table-select {
font-family: 'Questrial', sans-serif;
font-weight: bold;
}
.select2-dropdown, ._select2-selection__rendered {
font-family: 'Questrial', sans-serif;
font-weight: bold;
}
</style>
<link href="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/css/select2.min.css" rel="stylesheet" />
<link href="https://fonts.googleapis.com/css2?family=Questrial&display=swap" rel="stylesheet">
${wrapSelectors(selectors)}
</div>`;

// value.levels: [{id: "l1", title: "Level 1", ...}, ...]
// value.selections: currently selected ones. So: {l1: ["A2"], l2: ["B2", "B3"]}
// This function dispatches events if anything changes.
const setValue = (value) => {
const [v1, v2] = [JSON.stringify(value), JSON.stringify(wrap.value)];

const isDifferent = v1 !== v2;

if (isDifferent && (showIntermediate || value.selectionComplete)) {
wrap.value = _.cloneDeep(value);
wrap.dispatchEvent(new CustomEvent("input"));
onSelect(wrap.value);
}
};

const getValue = () => wrap.value;
const isValidValue = (candidate) =>
multiple ? candidate?.length > 0 : !!candidate;

// finds items that match what you typed
const matcher = ({ term }, data) => {
if (!term || term.length === 0) return data;
const search = term.toLowerCase();
return data.text.toLowerCase().indexOf(search) < 0
? null
: {
...data,
editDistance: editDistance(search, data.id)
};
};

const sorter = (options) => {
return options.sort((a, b) => b.editDistance - a.editDistance);
};

const defaultOptions = {
width,
allowClear: true,
matcher,
sorter,
multiple
};

const selections = {};

// Clears all selections.
const resetChildren = (levelIndex, isClear) => {
return selectors.forEach((selector, i) => {
if (isClear && i === levelIndex) {
delete selections[levels[i].id];
getSelect(selector).val(null).trigger("change");
}

if (i > levelIndex) {
delete selections[levels[i].id];
getSelect(selector)
.empty()
.select2({ ...defaultOptions, disabled: true })
.val(null)
.trigger("change");
setAddAllEnabled(selector, false);
}
});
};

// Runs when a new level becomes available.
function activateLevel(levelIndex) {
const {
title,
getOptions,
defaultValueId,
selectOptions = {}
} = levels[levelIndex];

const selectionArray = levels
.filter((_, i) => i < levelIndex)
.map(({ id }) => selections[id]);

wrapAsPromise(getOptions(selectionArray, levelIndex, levels)).then(
(options) => {
const defaultValue =
isValidValue(defaultValueId) &&
(multiple
? options.filter((d) => wrapAsArray(defaultValueId).includes(d.id))
: options.find((d) => d.id === defaultValueId));
const foundDefaultValue =
!isValidValue(defaultValue) && defaultToFirst
? multiple
? [options[0]]
: options[0]
: defaultValue;

getSelect(selectors[levelIndex])
.empty()
.select2({
...defaultOptions,
placeholder: `Select ${title}`,
data: options,
disabled: false,
...selectOptions
})
.val(null)
.trigger("change");

if (isValidValue(foundDefaultValue)) {
getSelect(selectors[levelIndex])
.val(
multiple
? foundDefaultValue.map((d) => d.id)
: foundDefaultValue.id
)
.trigger("change");
}
resetChildren(levelIndex);

// Cache the options, so the addAllButton knows what to select
levels[levelIndex].cachedOptions = options;
setAddAllEnabled(selectors[levelIndex], true);
}
);
}

// Runs when you select something
const handleLevelSelect = (value, levelIndex) => {
if (!isValidValue(value)) {
return;
}

selections[levels[levelIndex].id] = value;
const isLastLevel = levelIndex === levels.length - 1;
if (!isLastLevel) {
levels
.filter((_, idx) => idx > levelIndex)
.forEach((level, i) => {
// e.g. if you handleLevelSelect(levelIndex=2), disable the addAll for levels 3 and 4 (i=0, 1)
setAddAllEnabled(selectors[levelIndex + i + 1], false);
delete level.cachedOptions;

delete selections[level.id];
});

activateLevel(levelIndex + 1);
}

setValue({
selections,
selectionComplete: isLastLevel,
levelIndex,
levels
});
};

// initialize the levels

levels.forEach((level, levelIndex) => {
const select = getSelect(selectors[levelIndex]);
select
.on("change", (event) => {
const value = select.val();
if (!!value) {
handleLevelSelect(value, levelIndex);
}
})
.on("select2:clear", () => {
resetChildren(levelIndex, true);
setValue({
selections,
selectionComplete: false,
levelIndex,
levels
});
})
.select2({ ...defaultOptions, ...level.customOptions, disabled: true })
.empty();

setAddAllEnabled(selectors[levelIndex], true);
});

activateLevel(0);
setValue({
selections,
selectionComplete: false,
levels,
initialUpdate: true
});
yield wrap;
}
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell

Purpose-built for displays of data

Observable is your go-to platform for exploring data and creating expressive data visualizations. Use reactive JavaScript notebooks for prototyping and a collaborative canvas for visual data exploration and dashboard creation.
Learn more