UNPKG

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
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;