From ad1b9df6a5dc026739963ecb6b073d4a5386f133 Mon Sep 17 00:00:00 2001 From: radasam <45148557+radasam@users.noreply.github.com> Date: Mon, 8 Aug 2022 00:09:13 +0100 Subject: [PATCH] Fix: Zoom selector rewrite (#826) --- cypress/e2e/unit/zoom.cy.js | 217 +++++++++++++ package-lock.json | 2 +- src/editor/components/seZoom.js | 473 ++++++++++++++++++++--------- src/editor/panels/BottomPanel.html | 14 +- 4 files changed, 555 insertions(+), 151 deletions(-) create mode 100644 cypress/e2e/unit/zoom.cy.js diff --git a/cypress/e2e/unit/zoom.cy.js b/cypress/e2e/unit/zoom.cy.js new file mode 100644 index 00000000..50b9d349 --- /dev/null +++ b/cypress/e2e/unit/zoom.cy.js @@ -0,0 +1,217 @@ +import { visitAndApproveStorage } from '../../support/ui-test-helper.js' + +describe('UI - Zoom tool', function () { + beforeEach(() => { + visitAndApproveStorage() + }) + + it('should be able to open', function () { + cy.get('#zoom') + .click() + .shadow() + .find('#options-container') + .should('have.css', 'display', 'flex') + }) + + it('should be able to close', function () { + cy.get('#zoom') + .click() + .shadow() + .find('#options-container') + .should('have.css', 'display', 'flex') + + cy.get('#tool_select') + .click({ force: true }) + .get('#zoom') + .shadow() + .find('#options-container') + .should('have.css', 'display', 'none') + }) + + it('should be able to input zoom level', function () { + cy.get('#canvasBackground') + .invoke('attr', 'width') + .then(width => { + cy.get('#zoom') + .shadow() + .find('input') + .type('200') + cy.get('#tool_select') + .click({ force: true }) + cy.get('#canvasBackground') + .invoke('attr', 'width') + .should('equal', (width * 2).toString()) + }) + }) + + it('should be able to increment zoom level', function () { + cy.get('#canvasBackground') + .invoke('attr', 'width') + .then(width => { + cy.get('#zoom') + .shadow() + .find('#arrow-up') + .click() + cy.get('#canvasBackground') + .invoke('attr', 'width') + .should('equal', (width * 1.1).toString()) + }) + }) + + it('should be able to decrement zoom level', function () { + cy.get('#canvasBackground') + .invoke('attr', 'width') + .then(width => { + cy.get('#zoom') + .shadow() + .find('#arrow-down') + .click() + cy.get('#canvasBackground') + .invoke('attr', 'width') + .should('equal', (width * 0.9).toString()) + }) + }) + + it('should be able to select from popup', function () { + cy.get('#canvasBackground') + .invoke('attr', 'width') + .then(width => { + cy.get('#zoom') + .click() + .find('se-text') + .first() + .click({ force: true }) + .invoke('attr', 'value') + .then(value => { + cy.get('#canvasBackground') + .invoke('attr', 'width') + .should('equal', (width * (value / 100)).toString()) + .toString() + }) + }) + }) + + it('should be able to resize to fit the current selection', function () { + cy.get('#tool_path').click({ force: true }) + cy.get('#svgcontent') + .trigger('mousedown', 50, 50, { force: true }) + .trigger('mouseup', { force: true }) + .trigger('mousemove', 100, 50, { force: true }) + .trigger('mousedown', 100, 50, { force: true }) + .trigger('mouseup', { force: true }) + .trigger('mousemove', 75, 150, { force: true }) + .trigger('mousedown', 75, 150, { force: true }) + .trigger('mouseup', { force: true }) + .trigger('mousemove', 0, 0, { force: true }) + .trigger('mousedown', 0, 0, { force: true }) + .trigger('mouseup', { force: true }) + + cy.get('#tool_select') + .click({ force: true }) + .trigger('mousedown', 50, 50, { force: true }) + .trigger('mousemove', 100, 50, { force: true }) + .trigger('mouseup', { force: true }) + + cy.get('#canvasBackground') + .invoke('attr', 'width') + .then(width => { + cy.get('#zoom') + .click() + .find("se-text[value='layer']") + .click({ force: true }) + cy.get('#zoom') + .invoke('attr', 'value') + .then(value => { + cy.get('#canvasBackground') + .invoke('attr', 'width') + .should('not.equal', '100') + .toString() + }) + }) + }) + + it('should be able to resize to fit the canvas', function () { + cy.get('#canvasBackground') + .invoke('attr', 'width') + .then(width => { + cy.get('#zoom') + .click() + .find("se-text[value='canvas']") + .click({ force: true }) + cy.get('#zoom') + .invoke('attr', 'value') + .then(value => { + cy.get('#canvasBackground') + .invoke('attr', 'width') + .should('not.equal', '100') + .toString() + }) + }) + }) + + it('should be able to resize to fit the current layer', function () { + cy.get('#tool_path').click({ force: true }) + cy.get('#svgcontent') + .trigger('mousedown', 50, 50, { force: true }) + .trigger('mouseup', { force: true }) + .trigger('mousemove', 100, 50, { force: true }) + .trigger('mousedown', 100, 50, { force: true }) + .trigger('mouseup', { force: true }) + .trigger('mousemove', 75, 150, { force: true }) + .trigger('mousedown', 75, 150, { force: true }) + .trigger('mouseup', { force: true }) + .trigger('mousemove', 0, 0, { force: true }) + .trigger('mousedown', 0, 0, { force: true }) + .trigger('mouseup', { force: true }) + + cy.get('#canvasBackground') + .invoke('attr', 'width') + .then(width => { + cy.get('#zoom') + .click() + .find("se-text[value='layer']") + .click({ force: true }) + cy.get('#zoom') + .invoke('attr', 'value') + .then(value => { + cy.get('#canvasBackground') + .invoke('attr', 'width') + .should('not.equal', '100') + .toString() + }) + }) + }) + + it('should be able to resize to fit the current content', function () { + cy.get('#tool_path').click({ force: true }) + cy.get('#svgcontent') + .trigger('mousedown', 50, 50, { force: true }) + .trigger('mouseup', { force: true }) + .trigger('mousemove', 100, 50, { force: true }) + .trigger('mousedown', 100, 50, { force: true }) + .trigger('mouseup', { force: true }) + .trigger('mousemove', 75, 150, { force: true }) + .trigger('mousedown', 75, 150, { force: true }) + .trigger('mouseup', { force: true }) + .trigger('mousemove', 0, 0, { force: true }) + .trigger('mousedown', 0, 0, { force: true }) + .trigger('mouseup', { force: true }) + + cy.get('#canvasBackground') + .invoke('attr', 'width') + .then(width => { + cy.get('#zoom') + .click() + .find("se-text[value='content']") + .click({ force: true }) + cy.get('#zoom') + .invoke('attr', 'value') + .then(value => { + cy.get('#canvasBackground') + .invoke('attr', 'width') + .should('not.equal', '100') + .toString() + }) + }) + }) +}) diff --git a/package-lock.json b/package-lock.json index 6da9dcbc..d1ab2893 100644 --- a/package-lock.json +++ b/package-lock.json @@ -41676,4 +41676,4 @@ "dev": true } } -} +} \ No newline at end of file diff --git a/src/editor/components/seZoom.js b/src/editor/components/seZoom.js index b238d24f..b619c474 100644 --- a/src/editor/components/seZoom.js +++ b/src/editor/components/seZoom.js @@ -1,73 +1,180 @@ /* globals svgEditor */ -import ListComboBox from 'elix/define/ListComboBox.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 - */ -class Zoom extends ListComboBox { - /** - * @function get - * @returns {PlainObject} - */ - get [internal.defaultState] () { - return Object.assign(super[internal.defaultState], { - inputPartType: NumberSpinBox, - src: 'logo.svg', - inputsize: '100%' - }) +const template = document.createElement('template') +template.innerHTML = ` + +
+ icon + +
+
+
+
+
+ +
+
+ +` - /** - * @function get - * @returns {PlainObject} - */ - get [internal.template] () { - const result = super[internal.template] - const source = result.content.getElementById('source') - // add a icon before our dropdown - source.prepend(fragmentFrom.html` - icon - - `.cloneNode(true)) - // change the style so it fits in our toolbar - result.content.append( - templateFrom.html` - - `.content + this.handleMouseDown = this.handleMouseDown.bind(this) + this.handleMouseUp = this.handleMouseUp.bind(this) + this.handleKeyDown = this.handleKeyDown.bind(this) + this.initPopup = this.initPopup.bind(this) + this.handleInput = this.handleInput.bind(this) + + // create the shadowDom and insert the template + this._shadowRoot = this.attachShadow({ mode: 'open' }) + // locate the component + this._shadowRoot.append(template.content.cloneNode(true)) + + // prepare the slot element + this.slotElement = this._shadowRoot.querySelector('slot') + this.slotElement.addEventListener( + 'slotchange', + this.handleOptionsChange.bind(this) ) - return result + + // hookup events for the input box + this.inputElement = this._shadowRoot.querySelector('input') + this.inputElement.addEventListener('click', this.handleClick.bind(this)) + this.inputElement.addEventListener('change', this.handleInput) + this.inputElement.addEventListener('keydown', this.handleKeyDown) + + this.clickArea = this._shadowRoot.querySelector('#down') + this.clickArea.addEventListener('click', this.handleClick.bind(this)) + + // set src for imageElement + this.imageElement = this._shadowRoot.querySelector('img') + this.imageElement.setAttribute( + 'src', + (this.imgPath = + svgEditor.configObj.curConfig.imgPath + '/' + this.getAttribute('src')) + ) + + // hookup events for arrow buttons + this.arrowUp = this._shadowRoot.querySelector('#arrow-up') + this.arrowUp.addEventListener('click', this.increment.bind(this)) + this.arrowUp.addEventListener('mousedown', e => + this.handleMouseDown('up', true) + ) + this.arrowUp.addEventListener('mouseleave', e => this.handleMouseUp('up')) + this.arrowUp.addEventListener('mouseup', e => this.handleMouseUp('up')) + + this.arrowDown = this._shadowRoot.querySelector('#arrow-down') + this.arrowDown.addEventListener('click', this.decrement.bind(this)) + this.arrowDown.addEventListener('mousedown', e => + this.handleMouseDown('down', true) + ) + this.arrowDown.addEventListener('mouseleave', e => + this.handleMouseUp('down') + ) + this.arrowDown.addEventListener('mouseup', e => this.handleMouseUp('down')) + + this.optionsContainer = this._shadowRoot.querySelector( + '#options-container' + ) + + // add an event listener to close the popup + document.addEventListener('click', e => this.handleClose(e)) + this.changedTimeout = null + } + + static get observedAttributes () { + return ['value'] } /** - * @function observedAttributes - * @returns {any} observed + * @function get + * @returns {any} */ - static get observedAttributes () { - return ['title', 'src', 'inputsize', 'value'] + get value () { + return this.getAttribute('value') + } + + /** + * @function set + * @returns {void} + */ + set value (value) { + this.setAttribute('value', value) } /** @@ -78,116 +185,196 @@ class Zoom extends ListComboBox { * @returns {void} */ attributeChangedCallback (name, oldValue, newValue) { - if (oldValue === newValue && name !== 'src') return + if (oldValue === newValue) { + switch (name) { + case 'value': + if (parseInt(this.inputElement.value) !== newValue) { + this.inputElement.value = newValue + } + break + } + + return + } + switch (name) { - case 'title': - // this.$span.setAttribute('title', `${newValue} ${shortcut ? `[${shortcut}]` : ''}`); - break - case 'src': - { - const { imgPath } = svgEditor.configObj.curConfig - this.src = imgPath + '/' + newValue - } - break - case 'inputsize': - this.inputsize = newValue - break - default: - super.attributeChangedCallback(name, oldValue, newValue) + case 'value': + this.inputElement.value = newValue + this.dispatchEvent( + new CustomEvent('change', { detail: { value: newValue } }) + ) break } } /** - * @function [internal.render] - * @param {PlainObject} changed - * @returns {void} - */ - [internal.render] (changed) { - super[internal.render](changed) - if (this[internal.firstRender]) { - this.$img = this.shadowRoot.querySelector('img') - this.$input = this.shadowRoot.getElementById('input') - } - if (changed.src) { - this.$img.setAttribute('src', this[internal.state].src) - } - if (changed.inputsize) { - this.$input.shadowRoot.querySelector('[part~="input"]').style.width = this[internal.state].inputsize - } - if (changed.inputPartType) { - const self = this - this.$input.setAttribute('step', '10') - this.$input.setAttribute('min', '0') - // Handle NumberSpinBox input. - this.$input.addEventListener('change', function (e) { - e.preventDefault() - const value = e.detail?.value - if (value) { - const changeEvent = new CustomEvent('change', { detail: { value } }) - self.dispatchEvent(changeEvent) - } - }) - // Wire up handler on new input. - this.addEventListener('close', (e) => { - e.preventDefault() - const value = e.detail?.closeResult?.getAttribute('value') - if (value) { - const closeEvent = new CustomEvent('change', { detail: { value } }) - this.dispatchEvent(closeEvent) - } + * @function handleOptionsChange + * @returns {void} + */ + handleOptionsChange () { + if (this.slotElement.assignedElements().length > 0) { + this.options = this.slotElement.assignedElements() + this.selectedValue = this.options[0].textContent + + this.initPopup() + + this.options.forEach(option => { + option.addEventListener('click', e => this.handleSelect(e)) }) } } /** - * @function src - * @returns {string} src - */ - get src () { - return this[internal.state].src - } - - /** - * @function src + * @function handleClick * @returns {void} */ - set src (src) { - this[internal.setState]({ src }) + handleClick () { + this.optionsContainer.style.display = 'flex' + this.inputElement.select() + this.initPopup() } /** - * @function inputsize - * @returns {string} src - */ - get inputsize () { - return this[internal.state].inputsize - } - - /** - * @function src + * @function handleSelect + * @param {Event} e * @returns {void} */ - set inputsize (inputsize) { - this[internal.setState]({ inputsize }) + handleSelect (e) { + this.value = e.target.getAttribute('value') + this.title = e.target.getAttribute('text') } /** - * @function value - * @returns {string} src + * @function handleShow + * @returns {void} + * initialises the popup menu position */ - get value () { - return this[internal.state].value + initPopup () { + const zoomPos = this.getBoundingClientRect() + const popupPos = this.optionsContainer.getBoundingClientRect() + const top = zoomPos.top - popupPos.height + const left = zoomPos.left + + this.optionsContainer.style.position = 'fixed' + this.optionsContainer.style.top = `${top}px` + this.optionsContainer.style.left = `${left}px` } /** - * @function value + * @function handleClose + * @param {Event} e + * @returns {void} + * Close the popup menu + */ + handleClose (e) { + if (e.target !== this) { + this.optionsContainer.style.display = 'none' + this.inputElement.blur() + } + } + + /** + * @function handleInput * @returns {void} */ - set value (value) { - this[internal.setState]({ value }) + handleInput () { + if (this.changedTimeout) { + clearTimeout(this.changedTimeout) + } + + this.changedTimeout = setTimeout(this.triggerInputChanged.bind(this), 500) + } + + /** + * @function triggerInputChanged + * @returns {void} + */ + triggerInputChanged () { + const newValue = this.inputElement.value + this.value = newValue + } + + /** + * @function increment + * @returns {void} + */ + increment () { + this.value = parseInt(this.value) + 10 + } + + /** + * @function decrement + * @returns {void} + */ + decrement () { + if (this.value - 10 <= 0) { + this.value = 10 + } else { + this.value = parseInt(this.value) - 10 + } + } + + /** + * @function handleMouseDown + * @param {string} dir + * @param {boolean} isFirst + * @returns {void} + * Increment/Decrement on mouse held down, if its the first call add a delay before starting + */ + handleMouseDown (dir, isFirst) { + if (dir === 'up') { + this.incrementHold = true + !isFirst && this.increment() + + setTimeout( + () => { + if (this.incrementHold) { + this.handleMouseDown(dir, false) + } + }, + isFirst ? 500 : 50 + ) + } else if (dir === 'down') { + this.decrementHold = true + !isFirst && this.decrement() + + setTimeout( + () => { + if (this.decrementHold) { + this.handleMouseDown(dir, false) + } + }, + isFirst ? 500 : 50 + ) + } + } + + /** + * @function handleMouseUp + * @param {string} dir + * @returns {void} + */ + handleMouseUp (dir) { + if (dir === 'up') { + this.incrementHold = false + } else { + this.decrementHold = false + } + } + + /** + * @function handleKeyDown + * @param {Event} e + * @returns {void} + */ + handleKeyDown (e) { + if (e.key === 'ArrowUp') { + this.increment() + } else if (e.key === 'ArrowDown') { + this.decrement() + } } } // Register -customElements.define('se-zoom', Zoom) +customElements.define('se-zoom', SeZoom) diff --git a/src/editor/panels/BottomPanel.html b/src/editor/panels/BottomPanel.html index 9867334d..8d50e736 100644 --- a/src/editor/panels/BottomPanel.html +++ b/src/editor/panels/BottomPanel.html @@ -1,15 +1,15 @@
- - - - - - + + + + + + - " +