js-draw
Version:
Draw pictures using a pen, touchscreen, or mouse! JS-draw is a drawing library for JavaScript and TypeScript.
144 lines (143 loc) • 5.92 kB
JavaScript
import { MutableReactiveValue } from '../../../util/ReactiveValue.mjs';
import stopPropagationOfScrollingWheelEvents from '../../../util/stopPropagationOfScrollingWheelEvents.mjs';
import addLongPressOrHoverCssClasses from '../../../util/addLongPressOrHoverCssClasses.mjs';
import { toolbarCSSPrefix } from '../../constants.mjs';
let idCounter = 0;
/**
* Creates a widget that allows users to select one of serveral items from a list.
*
* `ChoiceIdType` should be `string`, a `number`, or an `enum` (or similar).
*
* If this input is set to an ID that is not in `choices`, no item is selected.
*/
const makeGridSelector = (
// Text before the grid selector used as a label
labelText, defaultId, choices) => {
const outerContainer = document.createElement('div');
outerContainer.classList.add(`${toolbarCSSPrefix}grid-selector`);
const selectedValue = MutableReactiveValue.fromInitialValue(defaultId);
const menuContainer = document.createElement('div');
menuContainer.role = 'group';
menuContainer.id = `${toolbarCSSPrefix}-grid-select-id-${idCounter++}`;
stopPropagationOfScrollingWheelEvents(menuContainer);
const label = document.createElement('label');
label.textContent = labelText;
label.htmlFor = menuContainer.id;
outerContainer.appendChild(label);
// All buttons in a radiogroup need the same name attribute.
let radiogroupName = `${toolbarCSSPrefix}-grid-selector-${idCounter++}`;
const createChoiceButton = (record) => {
const buttonContainer = document.createElement('div');
buttonContainer.classList.add('choice-button');
const button = document.createElement('input');
button.type = 'radio';
button.id = `${toolbarCSSPrefix}-grid-select-button-${idCounter++}`;
// Some toolbars only show the label on hover. Having long press or hover
// CSS classes are helpful here.
addLongPressOrHoverCssClasses(buttonContainer);
// Clicking any part of labelContainer triggers the radio button.
const labelContainer = document.createElement('label');
const rebuildLabel = () => {
labelContainer.setAttribute('title', record.title);
const labelText = document.createElement('span');
labelText.classList.add('button-label-text');
const icon = record.makeIcon();
icon.classList.add('icon');
// The title of the record
labelText.innerText = record.title;
labelContainer.htmlFor = button.id;
labelContainer.replaceChildren(icon, labelText);
};
rebuildLabel();
// Mark the button as belonging to the current group (causes
// other buttons in the same group to automatically uncheck
// when this button is checked).
const updateButtonRadiogroupName = () => {
button.name = radiogroupName;
};
updateButtonRadiogroupName();
const updateButtonCSS = () => {
if (button.checked) {
buttonContainer.classList.add('checked');
}
else {
buttonContainer.classList.remove('checked');
}
};
button.oninput = () => {
// Setting the selected value fires an event that causes the value
// of this button to be set.
if (button.checked) {
selectedValue.set(record.id);
}
updateButtonCSS();
};
button.onfocus = () => {
if (buttonContainer.querySelector(':focus-visible')) {
buttonContainer.classList.add('focus-visible');
}
};
button.onblur = () => {
buttonContainer.classList.remove('focus-visible');
};
// Prevent the right-click menu from being shown on long-press
// (important for some toolbars that use long-press gestures to
// show grid selector labels).
buttonContainer.oncontextmenu = (event) => {
event.preventDefault();
};
buttonContainer.replaceChildren(button, labelContainer);
menuContainer.appendChild(buttonContainer);
// Set whether the current button is checked
const setChecked = (checked) => {
button.checked = checked;
updateButtonCSS();
};
setChecked(false);
// Updates the factory's icon based on the current style of the tool.
const updateIcon = () => {
rebuildLabel();
};
return {
choiceRecord: record,
setChecked,
updateIcon,
updateButtonRadiogroupName,
};
};
const buttons = [];
for (const choice of choices) {
buttons.push(createChoiceButton(choice));
}
// invariant: buttons.length = choices.length
// However, it is still possible that selectedValue does not correspond
// to a choice in `choices`. This is acceptable.
outerContainer.appendChild(menuContainer);
selectedValue.onUpdateAndNow((choiceId) => {
for (let i = 0; i < buttons.length; i++) {
buttons[i].setChecked(buttons[i].choiceRecord.id === choiceId);
}
});
const result = {
value: selectedValue,
_radiogroupName: radiogroupName,
linkWith: (other) => {
result._radiogroupName = other._radiogroupName;
radiogroupName = other._radiogroupName;
for (const button of buttons) {
button.updateButtonRadiogroupName();
}
},
updateIcons: () => {
buttons.forEach((button) => button.updateIcon());
},
getRootElement() {
return outerContainer;
},
addTo: (parent) => {
parent.appendChild(outerContainer);
},
};
return result;
};
export default makeGridSelector;