js-draw
Version:
Draw pictures using a pen, touchscreen, or mouse! JS-draw is a drawing library for JavaScript and TypeScript.
194 lines (193 loc) • 7.12 kB
JavaScript
import OverflowWidget from './widgets/OverflowWidget.mjs';
import AbstractToolbar from './AbstractToolbar.mjs';
import { toolbarCSSPrefix } from './constants.mjs';
/**
* @example
*
* ```ts,runnable
* import { makeDropdownToolbar, Editor } from 'js-draw';
*
* const editor = new Editor(document.body);
* const toolbar = makeDropdownToolbar(editor);
* toolbar.addDefaults();
*
* toolbar.addExitButton(editor => {
* // TODO
* });
*
* toolbar.addSaveButton(editor => {
* // TODO
* });
* ```
*
* Returns a subclass of {@link AbstractToolbar}.
*
* @see
* - {@link makeEdgeToolbar}
* - {@link AbstractToolbar.addSaveButton}
* - {@link AbstractToolbar.addExitButton}
*/
export const makeDropdownToolbar = (editor) => {
return new DropdownToolbar(editor, editor.getRootElement(), editor.localization);
};
export default class DropdownToolbar extends AbstractToolbar {
/** @internal */
constructor(editor, parent, localizationTable) {
super(editor, localizationTable);
// Flex-order of the next widget to be added.
this.widgetOrderCounter = 0;
// Widget to toggle overflow menu.
this.overflowWidget = null;
this.reLayoutQueued = false;
this.container = document.createElement('div');
this.container.classList.add(`${toolbarCSSPrefix}root`);
this.container.classList.add(`${toolbarCSSPrefix}element`);
this.container.classList.add(`${toolbarCSSPrefix}dropdown-toolbar`);
this.container.setAttribute('role', 'toolbar');
parent.appendChild(this.container);
if ('ResizeObserver' in window) {
this.resizeObserver = new ResizeObserver((_entries) => {
this.reLayout();
});
this.resizeObserver.observe(this.container);
}
else {
console.warn('ResizeObserver not supported. Toolbar will not resize.');
}
}
queueReLayout() {
if (!this.reLayoutQueued) {
this.reLayoutQueued = true;
requestAnimationFrame(() => this.reLayout());
}
}
reLayout() {
this.reLayoutQueued = false;
if (!this.overflowWidget) {
return;
}
const getTotalWidth = (widgetList) => {
let totalWidth = 0;
for (const widget of widgetList) {
if (!widget.isHidden()) {
totalWidth += widget.getButtonWidth();
}
}
return totalWidth;
};
// Returns true if there is enough empty space to move the first child
// from the overflow menu to the main menu.
const canRemoveFirstChildFromOverflow = (freeSpaceInMainMenu) => {
const overflowChildren = this.overflowWidget?.getChildWidgets() ?? [];
if (overflowChildren.length === 0) {
return false;
}
return overflowChildren[0].getButtonWidth() <= freeSpaceInMainMenu;
};
const allWidgets = this.getAllWidgets();
let overflowWidgetsWidth = getTotalWidth(this.overflowWidget.getChildWidgets());
let shownWidgetWidth = getTotalWidth(allWidgets) - overflowWidgetsWidth;
let availableWidth = this.container.clientWidth * 0.87;
// If on a device that has enough vertical space, allow
// showing two rows of buttons.
// TODO: Fix magic numbers
if (window.innerHeight > availableWidth * 1.75) {
availableWidth *= 1.75;
}
let updatedChildren = false;
// If we can remove at least one child from the overflow menu,
if (canRemoveFirstChildFromOverflow(availableWidth - shownWidgetWidth)) {
// Move widgets to the main menu.
const overflowChildren = this.overflowWidget.clearChildren();
for (const child of overflowChildren) {
child.addTo(this.container);
child.setIsToplevel(true);
if (!child.isHidden()) {
shownWidgetWidth += child.getButtonWidth();
}
}
overflowWidgetsWidth = 0;
updatedChildren = true;
}
if (shownWidgetWidth >= availableWidth) {
// Move widgets to the overflow menu.
// Start with the rightmost widget, move to the leftmost
for (let i = allWidgets.length - 1; i >= 0 && shownWidgetWidth >= availableWidth; i--) {
const child = allWidgets[i];
if (this.overflowWidget.hasAsChild(child)) {
continue;
}
if (child.canBeInOverflowMenu()) {
shownWidgetWidth -= child.getButtonWidth();
this.overflowWidget.addToOverflow(child);
}
}
updatedChildren = true;
}
// Hide/show the overflow widget.
this.overflowWidget.setHidden(this.overflowWidget.getChildWidgets().length === 0);
if (updatedChildren) {
this.setupColorPickers();
}
}
addWidgetInternal(widget) {
const container = widget.addTo(this.container);
// Ensure that the widget gets displayed in the correct
// place in the toolbar, even if it's removed and re-added.
container.style.order = `${this.widgetOrderCounter++}`;
this.queueReLayout();
}
removeWidgetInternal(widget) {
widget.remove();
this.queueReLayout();
}
addSpacer(options = {}) {
const spacer = document.createElement('div');
spacer.classList.add(`${toolbarCSSPrefix}spacer`);
if (options.grow) {
spacer.style.flexGrow = `${options.grow}`;
}
if (options.minSize) {
spacer.style.minWidth = options.minSize;
}
if (options.maxSize) {
spacer.style.maxWidth = options.maxSize;
}
spacer.style.order = `${this.widgetOrderCounter++}`;
this.container.appendChild(spacer);
}
/**
* Adds a widget that toggles the overflow menu. Call `addOverflowWidget` to ensure
* that this widget is in the correct space (if shown).
*
* @example
* ```ts
* toolbar.addDefaultToolWidgets();
* toolbar.addOverflowWidget();
* toolbar.addDefaultActionButtons();
* ```
* shows the overflow widget between the default tool widgets and the default action buttons,
* if shown.
*/
addOverflowWidget() {
this.overflowWidget = new OverflowWidget(this.editor, this.localizationTable);
this.addWidget(this.overflowWidget);
}
/**
* Adds both the default tool widgets and action buttons. Equivalent to
* ```ts
* toolbar.addDefaultToolWidgets();
* toolbar.addOverflowWidget();
* toolbar.addDefaultActionButtons();
* ```
*/
addDefaults() {
this.addDefaultToolWidgets();
this.addOverflowWidget();
this.addDefaultActionButtons();
}
onRemove() {
this.container.remove();
this.resizeObserver.disconnect();
}
}