diff --git a/src/editor/components/seDropdown.js b/src/editor/components/seDropdown.js index 91accd1f..50061ff4 100644 --- a/src/editor/components/seDropdown.js +++ b/src/editor/components/seDropdown.js @@ -1,10 +1,9 @@ /* eslint-disable node/no-unpublished-import */ import ListComboBox from 'elix/define/ListComboBox.js'; -import NumberSpinBox from 'elix/define/NumberSpinBox.js'; -// import Input from 'elix/src/base/Input.js'; import {defaultState} from 'elix/src/base/internal.js'; import {templateFrom, fragmentFrom} from 'elix/src/core/htmlLiterals.js'; import {internal} from 'elix'; +import NumberSpinBox from '../dialogs/se-elix/define/NumberSpinBox.js'; /** * @class Dropdown diff --git a/src/editor/components/seSpinInput.js b/src/editor/components/seSpinInput.js index 19cbc6bb..c4722270 100644 --- a/src/editor/components/seSpinInput.js +++ b/src/editor/components/seSpinInput.js @@ -1,5 +1,5 @@ /* eslint-disable node/no-unpublished-import */ -import 'elix/define/NumberSpinBox.js'; +import '../dialogs/se-elix/define/NumberSpinBox.js'; const template = document.createElement('template'); template.innerHTML = ` diff --git a/src/editor/components/seZoom.js b/src/editor/components/seZoom.js index 0f95a2ab..0a403030 100644 --- a/src/editor/components/seZoom.js +++ b/src/editor/components/seZoom.js @@ -1,9 +1,8 @@ /* eslint-disable node/no-unpublished-import */ import ListComboBox from 'elix/define/ListComboBox.js'; -import NumberSpinBox from 'elix/define/NumberSpinBox.js'; -// import Input from 'elix/src/base/Input.js'; import * as internal from 'elix/src/base/internal.js'; import {templateFrom, fragmentFrom} from 'elix/src/core/htmlLiterals.js'; +import NumberSpinBox from '../dialogs/se-elix/define/NumberSpinBox.js'; /** * @class Dropdown diff --git a/src/editor/dialogs/exportDialog.js b/src/editor/dialogs/exportDialog.js index efd54619..ff5a52a4 100644 --- a/src/editor/dialogs/exportDialog.js +++ b/src/editor/dialogs/exportDialog.js @@ -1,7 +1,6 @@ -/* eslint-disable max-len */ /* eslint-disable node/no-unpublished-import */ import 'elix/define/Dialog.js'; -import 'elix/define/NumberSpinBox.js'; +import './se-elix/define/NumberSpinBox.js'; const template = document.createElement('template'); template.innerHTML = ` diff --git a/src/editor/dialogs/se-elix/define/NumberSpinBox.js b/src/editor/dialogs/se-elix/define/NumberSpinBox.js new file mode 100644 index 00000000..034eafbc --- /dev/null +++ b/src/editor/dialogs/se-elix/define/NumberSpinBox.js @@ -0,0 +1,7 @@ +import PlainNumberSpinBox from '../src/plain/PlainNumberSpinBox.js'; +/** + * @class ElixNumberSpinBox + */ +export default class ElixNumberSpinBox extends PlainNumberSpinBox {} + +customElements.define('elix-number-spin-box', ElixNumberSpinBox); diff --git a/src/editor/dialogs/se-elix/src/base/NumberSpinBox.js b/src/editor/dialogs/se-elix/src/base/NumberSpinBox.js new file mode 100644 index 00000000..7e2b9512 --- /dev/null +++ b/src/editor/dialogs/se-elix/src/base/NumberSpinBox.js @@ -0,0 +1,250 @@ +/* eslint-disable node/no-unpublished-import */ +import { + defaultState, + setState, + state, + stateEffects +} from 'elix/src/base/internal.js'; +import { + SpinBox +} from 'elix/src/base/SpinBox.js'; + +/** + * @class NumberSpinBox + */ +class NumberSpinBox extends SpinBox { + /** + * @function attributeChangedCallback + * @param {string} name + * @param {string} oldValue + * @param {string} newValue + * @returns {void} + */ + attributeChangedCallback (name, oldValue, newValue) { + if (name === 'max') { + this.max = parseFloat(newValue); + } else if (name === 'min') { + this.min = parseFloat(newValue); + } else if (name === 'step') { + this.step = parseFloat(newValue); + } else { + super.attributeChangedCallback(name, oldValue, newValue); + } + } + /** + * @function observedAttributes + * @returns {any} observed + */ + get [defaultState] () { + return Object.assign(super[defaultState], { + max: null, + min: null, + step: 1 + }); + } + + /** + * Format the numeric value as a string. + * + * This is used after incrementing/decrementing the value to reformat the + * value as a string. + * + * @param {number} value + * @param {number} precision + */ + formatValue (value, precision) { + return Number(value).toFixed(precision); + } + + /** + * The maximum allowable value of the `value` property. + * + * @type {number|null} + * @default 1 + */ + get max () { + return this[state].max; + } + /** + * The maximum allowable value of the `value` property. + * + * @type {number|null} + * @default 1 + */ + set max (max) { + this[setState]({ + max + }); + } + + /** + * The minimum allowable value of the `value` property. + * + * @type {number|null} + * @default 1 + */ + get min () { + return this[state].min; + } + /** + * @function set + * @returns {void} + */ + set min (min) { + this[setState]({ + min + }); + } + + /** + * Parse the given string as a number. + * + * This is used to parse the current value before incrementing/decrementing + * it. + * + * @param {string} value + * @param {number} precision + */ + parseValue(value, precision) { + const parsed = precision === 0 ? parseInt(value) : parseFloat(value); + return isNaN(parsed) ? 0 : parsed; + } + + [stateEffects] (state, changed) { + const effects = super[stateEffects]; + // If step changed, calculate its precision (number of digits after + // the decimal). + if (changed.step) { + const { + step + } = state; + const decimalRegex = /\.(\d)+$/; + const match = decimalRegex.exec(String(step)); + const precision = match && match[1] ? match[1].length : 0; + Object.assign(effects, { + precision + }); + } + + if (changed.max || changed.min || changed.value) { + // The value is valid if it falls between the min and max. + // TODO: We need a way to let other classes/mixins on the prototype chain + // contribute to validity -- if someone else thinks the value is invalid, + // we should respect that, even if the value falls within the min/max + // bounds. + const { + max, + min, + precision, + value + } = state; + const parsed = parseInt(value, precision); + if (value !== '' && isNaN(parsed)) { + Object.assign(effects, { + valid: false, + validationMessage: 'Value must be a number' + }); + } else if (!(max === null || parsed <= max)) { + Object.assign(effects, { + valid: false, + validationMessage: `Value must be less than or equal to ${max}.` + }); + } else if (!(min === null || parsed >= min)) { + Object.assign(effects, { + valid: false, + validationMessage: `Value must be greater than or equal to ${min}.` + }); + } else { + Object.assign(effects, { + valid: true, + validationMessage: '' + }); + } + // We can only go up if we're below max. + Object.assign(effects, { + canGoUp: isNaN(parsed) || state.max === null || parsed <= state.max + }); + + // We can only go down if we're above min. + Object.assign(effects, { + canGoDown: isNaN(parsed) || state.min === null || parsed >= state.min + }); + } + + return effects; + } + + /** + * The amount by which the `value` will be incremented or decremented. + * + * The precision of the step (the number of digits after any decimal) + * determines how the spin box will format the number. The default `step` + * value of 1 has no decimals, so the `value` will be formatted as an integer. + * A `step` of 0.1 will format the `value` as a number with one decimal place. + * + * @type {number} + * @default 1 + */ + get step () { + return this[state].step; + } + set step (step) { + if (!isNaN(step)) { + this[setState]({ + step + }); + } + } + + /** + * Decrements the `value` by the amount of the `step`. + * + * If the result is still greater than the `max` value, this will force + * `value` to `max`. + */ + stepDown () { + super.stepDown(); + const { + max, + precision, + value + } = this[state]; + let result = this.parseValue(value, precision) - this.step; + if (max !== null) { + result = Math.min(result, max); + } + const { + min + } = this[state]; + if (min === null || result >= min) { + this.value = this.formatValue(result, precision); + } + } + + /** + * Increments the `value` by the amount of the `step`. + * + * If the result is still smaller than the `min` value, this will force + * `value` to `min`. + */ + stepUp () { + super.stepUp(); + const { + min, + precision, + value + } = this[state]; + let result = this.parseValue(value, precision) + this.step; + if (min !== null) { + result = Math.max(result, min); + } + const { + max + } = this[state]; + if (max === null || result <= max) { + this.value = this.formatValue(result, precision); + } + } +} + +export default NumberSpinBox; diff --git a/src/editor/dialogs/se-elix/src/plain/PlainNumberSpinBox.js b/src/editor/dialogs/se-elix/src/plain/PlainNumberSpinBox.js new file mode 100644 index 00000000..a86af29e --- /dev/null +++ b/src/editor/dialogs/se-elix/src/plain/PlainNumberSpinBox.js @@ -0,0 +1,10 @@ +/* eslint-disable node/no-unpublished-import */ +import PlainSpinBoxMixin from 'elix/src/plain/PlainSpinBoxMixin.js'; +import NumberSpinBox from '../base/NumberSpinBox.js'; + +/** + * @class PlainNumberSpinBox + */ +class PlainNumberSpinBox extends PlainSpinBoxMixin(NumberSpinBox) {} + +export default PlainNumberSpinBox; diff --git a/src/editor/index.html b/src/editor/index.html index 8d14f2d9..a12f52ac 100644 --- a/src/editor/index.html +++ b/src/editor/index.html @@ -349,7 +349,7 @@ - +