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 = `
+
+
+
+
+
+`
- /**
- * @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`
-
-
- `.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 @@
-
-
-
-
-
-
+
+
+
+
+
+
- "
+