js-draw
Version:
Draw pictures using a pen, touchscreen, or mouse! JS-draw is a drawing library for JavaScript and TypeScript.
249 lines (248 loc) • 12.6 kB
JavaScript
import Erase from '../../commands/Erase.mjs';
import uniteCommands from '../../commands/uniteCommands.mjs';
import BackgroundComponent, { BackgroundType } from '../../components/BackgroundComponent.mjs';
import { EditorImageEventType } from '../../image/EditorImage.mjs';
import { Rect2 } from '@js-draw/math';
import { EditorEventType } from '../../types.mjs';
import { toolbarCSSPrefix } from '../constants.mjs';
import makeColorInput from './components/makeColorInput.mjs';
import BaseWidget from './BaseWidget.mjs';
class DocumentPropertiesWidget extends BaseWidget {
constructor(editor, localizationTable) {
super(editor, 'document-properties-widget', localizationTable);
this.updateDropdownContent = () => { };
this.dropdownUpdateQueued = false;
// Make it possible to open the dropdown, even if this widget isn't selected.
this.container.classList.add('dropdownShowable');
this.editor.notifier.on(EditorEventType.UndoRedoStackUpdated, () => {
this.queueDropdownUpdate();
});
this.editor.image.notifier.on(EditorImageEventType.ExportViewportChanged, () => {
this.queueDropdownUpdate();
});
}
getTitle() {
return this.localizationTable.documentProperties;
}
createIcon() {
return this.editor.icons.makeConfigureDocumentIcon();
}
handleClick() {
this.setDropdownVisible(!this.isDropdownVisible());
this.queueDropdownUpdate();
}
queueDropdownUpdate() {
if (!this.dropdownUpdateQueued) {
requestAnimationFrame(() => this.updateDropdown());
this.dropdownUpdateQueued = true;
}
}
updateDropdown() {
this.dropdownUpdateQueued = false;
if (this.isDropdownVisible()) {
this.updateDropdownContent();
}
}
setBackgroundColor(color) {
this.editor.dispatch(this.editor.setBackgroundColor(color));
}
getBackgroundColor() {
return this.editor.estimateBackgroundColor();
}
removeBackgroundComponents() {
const previousBackgrounds = [];
for (const component of this.editor.image.getBackgroundComponents()) {
if (component instanceof BackgroundComponent) {
previousBackgrounds.push(component);
}
}
return new Erase(previousBackgrounds);
}
/** Replace existing background components with a background of the given type. */
setBackgroundType(backgroundType) {
const prevBackgroundColor = this.editor.estimateBackgroundColor();
const newBackground = new BackgroundComponent(backgroundType, prevBackgroundColor);
const addBackgroundCommand = this.editor.image.addComponent(newBackground);
return uniteCommands([this.removeBackgroundComponents(), addBackgroundCommand]);
}
/** Returns the type of the topmost background component */
getBackgroundType() {
const backgroundComponents = this.editor.image.getBackgroundComponents();
for (let i = backgroundComponents.length - 1; i >= 0; i--) {
const component = backgroundComponents[i];
if (component instanceof BackgroundComponent) {
return component.getBackgroundType();
}
}
return BackgroundType.None;
}
updateImportExportRectSize(size) {
const filterDimension = (dim) => {
if (dim !== undefined && (!isFinite(dim) || dim <= 0)) {
dim = 100;
}
return dim;
};
const width = filterDimension(size.width);
const height = filterDimension(size.height);
const currentRect = this.editor.getImportExportRect();
const newRect = new Rect2(currentRect.x, currentRect.y, width ?? currentRect.w, height ?? currentRect.h);
this.editor.dispatch(this.editor.image.setImportExportRect(newRect));
this.editor.queueRerender();
}
getHelpText() {
return this.localizationTable.pageDropdown__baseHelpText;
}
fillDropdown(dropdown, helpDisplay) {
const container = document.createElement('div');
container.classList.add(`${toolbarCSSPrefix}spacedList`, `${toolbarCSSPrefix}nonbutton-controls-main-list`, `${toolbarCSSPrefix}document-properties-widget`);
// Background color input
const makeBackgroundColorInput = () => {
const backgroundColorRow = document.createElement('div');
const backgroundColorLabel = document.createElement('label');
backgroundColorLabel.innerText = this.localizationTable.backgroundColor;
const { input: colorInput, container: backgroundColorInputContainer, setValue: setBgColorInputValue, registerWithHelpTextDisplay: registerHelpForInputs, } = makeColorInput(this.editor, (color) => {
if (!color.eq(this.getBackgroundColor())) {
this.setBackgroundColor(color);
}
});
colorInput.id = `${toolbarCSSPrefix}docPropertiesColorInput-${DocumentPropertiesWidget.idCounter++}`;
backgroundColorLabel.htmlFor = colorInput.id;
backgroundColorRow.replaceChildren(backgroundColorLabel, backgroundColorInputContainer);
const registerWithHelp = (helpDisplay) => {
if (!helpDisplay) {
return;
}
helpDisplay?.registerTextHelpForElement(backgroundColorRow, this.localizationTable.pageDropdown__backgroundColorHelpText);
registerHelpForInputs(helpDisplay);
};
return { setBgColorInputValue, backgroundColorRow, registerWithHelp };
};
const { backgroundColorRow, setBgColorInputValue, registerWithHelp: registerBackgroundRowWithHelp, } = makeBackgroundColorInput();
const makeCheckboxRow = (labelText, onChange) => {
const rowContainer = document.createElement('div');
const labelElement = document.createElement('label');
const checkboxElement = document.createElement('input');
checkboxElement.id = `${toolbarCSSPrefix}docPropertiesCheckbox-${DocumentPropertiesWidget.idCounter++}`;
labelElement.htmlFor = checkboxElement.id;
checkboxElement.type = 'checkbox';
labelElement.innerText = labelText;
checkboxElement.oninput = () => {
onChange(checkboxElement.checked);
};
rowContainer.replaceChildren(labelElement, checkboxElement);
return { container: rowContainer, checkbox: checkboxElement };
};
// Background style selector
const { container: useGridRow, checkbox: useGridCheckbox } = makeCheckboxRow(this.localizationTable.useGridOption, (checked) => {
const prevBackgroundType = this.getBackgroundType();
const wasGrid = prevBackgroundType === BackgroundType.Grid;
if (wasGrid === checked) {
// Already the requested background type.
return;
}
let newBackgroundType = BackgroundType.SolidColor;
if (checked) {
newBackgroundType = BackgroundType.Grid;
}
this.editor.dispatch(this.setBackgroundType(newBackgroundType));
});
// Adds a width/height input
const addDimensionRow = (labelContent, onChange) => {
const row = document.createElement('div');
const label = document.createElement('label');
const input = document.createElement('input');
label.innerText = labelContent;
input.type = 'number';
input.min = '0';
input.id = `${toolbarCSSPrefix}docPropertiesDimensionRow-${DocumentPropertiesWidget.idCounter++}`;
label.htmlFor = input.id;
input.style.flexGrow = '2';
input.style.width = '25px';
input.oninput = () => {
onChange(parseFloat(input.value));
};
row.classList.add('js-draw-size-input-row');
row.replaceChildren(label, input);
return {
setValue: (value) => {
// Slightly improve the case where the user tries to change the
// first digit of a dimension like 600.
//
// As changing the value also gives the image zero size (which is unsupported,
// .setValue is called immediately). We work around this by trying to select
// the added/changed digits.
//
// See https://github.com/personalizedrefrigerator/js-draw/issues/58.
if (document.activeElement === input && input.value.match(/^0*$/)) {
// We need to switch to type="text" and back to type="number" because
// number inputs don't support selection.
//
// See https://stackoverflow.com/q/22381837
const originalValue = input.value;
input.type = 'text';
input.value = value.toString();
// Select the added digits
const lengthToSelect = Math.max(1, input.value.length - originalValue.length);
input.setSelectionRange(0, lengthToSelect);
input.type = 'number';
}
else {
input.value = value.toString();
}
},
setIsAutomaticSize: (automatic) => {
input.disabled = automatic;
const automaticSizeClass = 'size-input-row--automatic-size';
if (automatic) {
row.classList.add(automaticSizeClass);
}
else {
row.classList.remove(automaticSizeClass);
}
},
element: row,
};
};
const imageWidthRow = addDimensionRow(this.localizationTable.imageWidthOption, (value) => {
this.updateImportExportRectSize({ width: value });
});
const imageHeightRow = addDimensionRow(this.localizationTable.imageHeightOption, (value) => {
this.updateImportExportRectSize({ height: value });
});
// The autoresize checkbox
const { container: auroresizeRow, checkbox: autoresizeCheckbox } = makeCheckboxRow(this.localizationTable.enableAutoresizeOption, (checked) => {
const image = this.editor.image;
this.editor.dispatch(image.setAutoresizeEnabled(checked));
});
// The "About..." button
const aboutButton = document.createElement('button');
aboutButton.classList.add('about-button');
aboutButton.innerText = this.localizationTable.about;
aboutButton.onclick = () => {
this.editor.showAboutDialog();
};
// Add help text
registerBackgroundRowWithHelp(helpDisplay);
helpDisplay?.registerTextHelpForElement(useGridRow, this.localizationTable.pageDropdown__gridCheckboxHelpText);
helpDisplay?.registerTextHelpForElement(auroresizeRow, this.localizationTable.pageDropdown__autoresizeCheckboxHelpText);
helpDisplay?.registerTextHelpForElement(aboutButton, this.localizationTable.pageDropdown__aboutButtonHelpText);
this.updateDropdownContent = () => {
setBgColorInputValue(this.getBackgroundColor());
const autoresize = this.editor.image.getAutoresizeEnabled();
const importExportRect = this.editor.getImportExportRect();
imageWidthRow.setValue(importExportRect.width);
imageHeightRow.setValue(importExportRect.height);
autoresizeCheckbox.checked = autoresize;
imageWidthRow.setIsAutomaticSize(autoresize);
imageHeightRow.setIsAutomaticSize(autoresize);
useGridCheckbox.checked = this.getBackgroundType() === BackgroundType.Grid;
};
this.updateDropdownContent();
container.replaceChildren(backgroundColorRow, useGridRow, imageWidthRow.element, imageHeightRow.element, auroresizeRow, aboutButton);
dropdown.replaceChildren(container);
return true;
}
}
DocumentPropertiesWidget.idCounter = 0;
export default DocumentPropertiesWidget;