Repository: SVG-Edit/svgedit (language: python)

Issue:
Rewrite the seList and seListItem components
NOTE:
This issue is funded on Bountysource (https://app.bountysource.com/issues/109641309-rewrite-the-selist-and-selistitem-components)
However, this is a FULL REWRITE of the list/listitem selector web component without using the elix library.
The component source is: src/editor/components/seList.js (and seListItem.js)
Native JS, CSS preferred but another library is possible if it is popular and actively maintained.

Obviously, the fix needs to work consistently on Safari, Firefox and Chrome.



Discussion / hints:
there is anyone working on this? I'm interested.
not to my knowledge

Hello, is this still available or is marcos still working on it?
> Hello, is this still available or is marcos still working on it?

Hey, I'm not working on it.

Fix patch:
diff --git a/src/editor/components/seList.js b/src/editor/components/seList.js
index f3932e9d7..e36202198 100644
--- a/src/editor/components/seList.js
+++ b/src/editor/components/seList.js
@@ -1,34 +1,46 @@
 /* globals svgEditor */
-import 'elix/define/DropdownList.js'
 import { t } from '../locale.js'
 
 const template = document.createElement('template')
 template.innerHTML = `
 <style>
-elix-dropdown-list {
+#select-container {
   margin-top: 10px;
+  display: inline-block;
 }
 
-elix-dropdown-list:hover {
+#select-container:hover {
   background-color: var(--icon-bg-color-hover);
 }
 
-elix-dropdown-list::part(value) {
+#select-container::part(value) {
   background-color: var(--main-bg-color);
 }
 
-elix-dropdown-list::part(popup-toggle) {
+#select-container::part(popup-toggle) {
   display: none;
 }
 ::slotted(*) {
   padding:0;
   width:100%;
 }
+
+.closed {
+  display: none;
+}
+
+#options-container {
+  position: fixed;
+}
+
 </style>
   <label>Label</label>
-  <elix-dropdown-list>
-    <slot></slot>
-  </elix-dropdown-list>
+  <div id="select-container" tabindex="0">
+    <div id="selected-value"></div>
+    <div id="options-container">
+      <slot></slot>
+    </div>
+  </div>
 
 `
 /**
@@ -43,11 +55,47 @@ export class SeList extends HTMLElement {
     // create the shadowDom and insert the template
     this._shadowRoot = this.attachShadow({ mode: 'open' })
     this._shadowRoot.append(template.content.cloneNode(true))
-    this.$dropdown = this._shadowRoot.querySelector('elix-dropdown-list')
+    this.$dropdown = this._shadowRoot.querySelector('#select-container')
     this.$label = this._shadowRoot.querySelector('label')
-    this.$selection = this.$dropdown.shadowRoot.querySelector('#value')
+    this.$selection = this.$dropdown.querySelector('#selected-value')
     this.items = this.querySelectorAll('se-list-item')
     this.imgPath = svgEditor.configObj.curConfig.imgPath
+    this.$optionsContainer = this._shadowRoot.querySelector('#options-container')
+    this.$optionsContainer.classList.add('closed')
+    this.$selection.addEventListener('click', this.toggleList)
+    this.updateSelectedValue(this.items[0].getAttribute('value'))
+    this.isDropdownOpen = false
+  }
+
+  toggleList = (e) => {
+    if (!this.isDropdownOpen) {
+      this.openDropdown()
+      this.setDropdownListPosition()
+    } else {
+      this.closeDropdown()
+    }
+  }
+
+  updateSelectedValue = (newValue) => {
+    Array.from(this.items).forEach((element) => {
+      if (element.getAttribute('value') === newValue) {
+        element.setAttribute('selected', true)
+        if (element.hasAttribute('src')) {
+        // empty current selection children
+          while (this.$selection.firstChild) { this.$selection.removeChild(this.$selection.firstChild) }
+          // replace selection child with image of new value
+          const img = document.createElement('img')
+          img.src = this.imgPath + '/' + element.getAttribute('src')
+          img.style.height = element.getAttribute('img-height')
+          img.setAttribute('title', t(element.getAttribute('title')))
+          this.$selection.append(img)
+        } else {
+          this.$selection.textContent = t(element.getAttribute('option'))
+        }
+      } else {
+        element.setAttribute('selected', false)
+      }
+    })
   }
 
   /**
@@ -66,7 +114,6 @@ export class SeList extends HTMLElement {
    * @returns {void}
    */
   attributeChangedCallback (name, oldValue, newValue) {
-    const currentObj = this
     if (oldValue === newValue) return
     switch (name) {
       case 'title':
@@ -82,22 +129,7 @@ export class SeList extends HTMLElement {
         this.$dropdown.style.width = newValue
         break
       case 'value':
-        Array.from(this.items).forEach(function (element) {
-          if (element.getAttribute('value') === newValue) {
-            if (element.hasAttribute('src')) {
-            // empty current selection children
-              while (currentObj.$selection.firstChild) { currentObj.$selection.removeChild(currentObj.$selection.firstChild) }
-              // replace selection child with image of new value
-              const img = document.createElement('img')
-              img.src = currentObj.imgPath + '/' + element.getAttribute('src')
-              img.style.height = element.getAttribute('img-height')
-              img.setAttribute('title', t(element.getAttribute('title')))
-              currentObj.$selection.append(img)
-            } else {
-              currentObj.$selection.textContent = t(element.getAttribute('option'))
-            }
-          }
-        })
+        this.updateSelectedValue(newValue)
         break
       default:
         console.error(`unknown attribute: ${name}`)
@@ -169,6 +201,30 @@ export class SeList extends HTMLElement {
     this.setAttribute('height', value)
   }
 
+  openDropdown = () => {
+    this.isDropdownOpen = true
+    this.$optionsContainer.classList.remove('closed')
+  }
+
+  closeDropdown = () => {
+    this.isDropdownOpen = false
+    this.$optionsContainer.classList.add('closed')
+  }
+
+  setDropdownListPosition = () => {
+    const windowHeight = window.innerHeight
+    const selectedContainerPosition = this.$selection.getBoundingClientRect()
+    const optionsContainerPosition = this.$optionsContainer.getBoundingClientRect()
+    // list is bottom of frame - needs to open from above
+    if (selectedContainerPosition.bottom + optionsContainerPosition.height > windowHeight) {
+      this.$optionsContainer.style.top = selectedContainerPosition.top - optionsContainerPosition.height + 'px'
+      this.$optionsContainer.style.left = selectedContainerPosition.left + 'px'
+    } else {
+      this.$optionsContainer.style.top = selectedContainerPosition.bottom + 'px'
+      this.$optionsContainer.style.left = selectedContainerPosition.left + 'px'
+    }
+  }
+
   /**
    * @function connectedCallback
    * @returns {void}
@@ -176,20 +232,28 @@ export class SeList extends HTMLElement {
   connectedCallback () {
     const currentObj = this
     this.$dropdown.addEventListener('selectedindexchange', (e) => {
-      if (e?.detail?.selectedIndex !== undefined) {
-        const value = this.$dropdown.selectedItem.getAttribute('value')
+      if (e?.detail?.selectedItem !== undefined) {
+        const value = e.detail.selectedItem
         const closeEvent = new CustomEvent('change', { detail: { value } })
         currentObj.dispatchEvent(closeEvent)
         currentObj.value = value
         currentObj.setAttribute('value', value)
       }
     })
-    this.$dropdown.addEventListener('close', (_e) => {
-      /** with Chrome, selectedindexchange does not fire consistently
-      * unless you forec change in this close event
-      */
-      this.$dropdown.selectedIndex = this.$dropdown.currentIndex
+
+    this.$dropdown.addEventListener('focusout', (e) => {
+      this.closeDropdown()
     })
+
+    window.addEventListener('mousedown', e => {
+      // When we click on the canvas and if the dropdown is open, then just close the dropdown and stop the event
+      if (this.isDropdownOpen) {
+        if (!e.target.closest('se-list')) {
+          e.stopPropagation()
+          this.closeDropdown()
+        }
+      }
+    }, { capture: true })
   }
 }
 
diff --git a/src/editor/components/seListItem.js b/src/editor/components/seListItem.js
index 63216b504..4a9b2556b 100644
--- a/src/editor/components/seListItem.js
+++ b/src/editor/components/seListItem.js
@@ -1,22 +1,26 @@
 /* globals svgEditor */
-import 'elix/define/Option.js'
 import { t } from '../locale.js'
 
 const template = document.createElement('template')
 template.innerHTML = `
   <style>
-  elix-option{
+  [aria-label="option"]{
     padding:0.25rem 0.125rem !important;
     background-color: var(--icon-bg-color);
   }
-  elix-option:hover{
+  [aria-label="option"]:hover{
     background-color: var(--icon-bg-color-hover);
   }
+
+  .selected {
+    background-color: var(--icon-bg-color-hover);
+  }
+
   </style>
-  <elix-option aria-label="option">
+  <div aria-label="option">
     <img alt="icon" />
     <slot></slot>
-  </elix-option>
+  </div>
 `
 /**
  * @class SeMenu
@@ -30,12 +34,19 @@ export class SeListItem extends HTMLElement {
     // create the shadowDom and insert the template
     this._shadowRoot = this.attachShadow({ mode: 'open' })
     this._shadowRoot.append(template.content.cloneNode(true))
-    this.$menuitem = this._shadowRoot.querySelector('elix-option')
-    this.$svg = this.$menuitem.shadowRoot.querySelector('#checkmark')
-    this.$svg.setAttribute('style', 'display: none;')
+    this.$menuitem = this._shadowRoot.querySelector('[aria-label=option]')
+    // this.$svg = this.$menuitem.shadowRoot.querySelector('#checkmark')
+    // this.$svg.setAttribute('style', 'display: none;')
     this.$img = this._shadowRoot.querySelector('img')
     this.$img.setAttribute('style', 'display: none;')
     this.imgPath = svgEditor.configObj.curConfig.imgPath
+    this.$menuitem.addEventListener('mousedown', e => {
+      this.$menuitem.dispatchEvent(new CustomEvent('selectedindexchange', {
+        bubbles: true,
+        composed: true,
+        detail: { selectedItem: this.getAttribute('value') }
+      }))
+    })
   }
 
   /**
@@ -43,7 +54,7 @@ export class SeListItem extends HTMLElement {
    * @returns {any} observed
    */
   static get observedAttributes () {
-    return ['option', 'src', 'title', 'img-height']
+    return ['option', 'src', 'title', 'img-height', 'selected']
   }
 
   /**
@@ -70,6 +81,13 @@ export class SeListItem extends HTMLElement {
       case 'img-height':
         this.$img.setAttribute('height', newValue)
         break
+      case 'selected':
+        if (newValue === 'true') {
+          this.$menuitem.classList.add('selected')
+        } else {
+          this.$menuitem.classList.remove('selected')
+        }
+        break
       default:
         console.error(`unknown attribute: ${name}`)
         break


Test patch:
diff --git a/cypress/e2e/ui/__snapshots__/scenario.cy.js.snap b/cypress/e2e/ui/__snapshots__/scenario.cy.js.snap
index 951917f1e..a902aed75 100644
--- a/cypress/e2e/ui/__snapshots__/scenario.cy.js.snap
+++ b/cypress/e2e/ui/__snapshots__/scenario.cy.js.snap
@@ -497,7 +497,7 @@ exports[`use various parts of svg-edit > check tool_text_align_to_page #0`] = `
         stroke="#000000"
         stroke-width="0"
         text-anchor="middle"
-        x="145"
+        x="623.33"
         xml:space="preserve"
         y="145"
       >
diff --git a/cypress/e2e/ui/__snapshots__/scenario5.cy.js.snap b/cypress/e2e/ui/__snapshots__/scenario5.cy.js.snap
index eec8bfd2d..b10805051 100644
--- a/cypress/e2e/ui/__snapshots__/scenario5.cy.js.snap
+++ b/cypress/e2e/ui/__snapshots__/scenario5.cy.js.snap
@@ -457,8 +457,8 @@ exports[`use all parts of svg-edit > check tool_line_align_to_page #0`] = `
         opacity="0.25"
         stroke="#bf5f00"
         stroke-width="5"
-        x1="220"
-        x2="470"
+        x1="387.5"
+        x2="637.5"
         y1="220"
         y2="470"
       >
diff --git a/cypress/e2e/ui/__snapshots__/scenario6.cy.js.snap b/cypress/e2e/ui/__snapshots__/scenario6.cy.js.snap
index 92ed50051..e76904c24 100644
--- a/cypress/e2e/ui/__snapshots__/scenario6.cy.js.snap
+++ b/cypress/e2e/ui/__snapshots__/scenario6.cy.js.snap
@@ -377,7 +377,7 @@ exports[`use all parts of svg-edit > check tool_polygon_align_to_page #0`] = `
         fill="#FF0000"
         id="svg_1"
         orient="x"
-        points="378.87455119562924,250 341.6481518837726,301.23774297708 281.41457251841285,281.6666666666667 281.4145725184128,218.33333333333334 341.6481518837726,198.76225702291998 378.87455119562924,250 "
+        points="637.5,250 600.2736206054688,301.23773193359375 540.0400390625,281.6666564941406 540.0400390625,218.3333282470703 600.2736206054688,198.7622528076172 637.5,250 "
         shape="regularPoly"
         sides="5"
         stroke="#000000"
@@ -405,7 +405,7 @@ exports[`use all parts of svg-edit > check tool_polygon_change_stroke_width #0`]
         fill="#FF0000"
         id="svg_1"
         orient="x"
-        points="378.87455119562924,250 341.6481518837726,301.23774297708 281.41457251841285,281.6666666666667 281.4145725184128,218.33333333333334 341.6481518837726,198.76225702291998 378.87455119562924,250 "
+        points="637.5,250 600.2736206054688,301.23773193359375 540.0400390625,281.6666564941406 540.0400390625,218.3333282470703 600.2736206054688,198.7622528076172 637.5,250 "
         shape="regularPoly"
         sides="5"
         stroke="#000000"
@@ -433,7 +433,7 @@ exports[`use all parts of svg-edit > check tool_polygon_change_stoke_fill_color
         fill="#bf0000"
         id="svg_1"
         orient="x"
-        points="378.87455119562924,250 341.6481518837726,301.23774297708 281.41457251841285,281.6666666666667 281.4145725184128,218.33333333333334 341.6481518837726,198.76225702291998 378.87455119562924,250 "
+        points="637.5,250 600.2736206054688,301.23773193359375 540.0400390625,281.6666564941406 540.0400390625,218.3333282470703 600.2736206054688,198.7622528076172 637.5,250 "
         shape="regularPoly"
         sides="5"
         stroke="#0000bf"
@@ -461,7 +461,7 @@ exports[`use all parts of svg-edit > check tool_polygon_change_sides #0`] = `
         fill="#bf0000"
         id="svg_1"
         orient="x"
-        points="406.36004929315476,249.999995640346 374.69338262648813,304.8482712133604 311.3600492931548,304.8482712133604 279.69338262648813,249.999995640346 311.36004929315476,195.15172006733155 374.69338262648813,195.15172006733155 406.36004929315476,249.99999564034596 "
+        points="664.9855026971727,249.999995640346 633.318836030506,304.8482712133604 569.9855026971727,304.8482712133604 538.318836030506,249.999995640346 569.9855026971726,195.15172006733155 633.318836030506,195.15172006733155 664.9855026971727,249.99999564034596 "
         shape="regularPoly"
         sides="6"
         stroke="#0000bf"
diff --git a/cypress/e2e/ui/__snapshots__/scenario7.cy.js.snap b/cypress/e2e/ui/__snapshots__/scenario7.cy.js.snap
index 43ad9460d..68f17a88b 100644
--- a/cypress/e2e/ui/__snapshots__/scenario7.cy.js.snap
+++ b/cypress/e2e/ui/__snapshots__/scenario7.cy.js.snap
@@ -419,7 +419,7 @@ exports[`use all parts of svg-edit > check tool_star_align_to_page #0`] = `
         id="svg_1"
         orient="point"
         point="5"
-        points="300,83.33333333333333 313.0618944953883,132.02184456944562 363.40376775301024,129.39886704167017 321.13458925100343,156.86704431944327 339.18568348616486,203.93446629166317 300,172.22222222222223 260.81431651383514,203.93446629166317 278.86541074899657,156.86704431944327 236.59623224698976,129.39886704167017 286.9381055046117,132.02184456944562 300,83.33333333333333 313.0618944953883,132.02184456944562 "
+        points="574.0962219238281,83.33333587646484 587.1581115722656,132.0218505859375 637.5,129.39886474609375 595.2308044433594,156.86705017089844 613.2818908691406,203.93446350097656 574.0962219238281,172.22222900390625 534.9105529785156,203.93446350097656 552.9616394042969,156.86705017089844 510.6924591064453,129.39886474609375 561.0343322753906,132.0218505859375 574.0962219238281,83.33333587646484 587.1581115722656,132.0218505859375 "
         r="66.67"
         r2="22.22"
         radialshift="0"
@@ -450,7 +450,7 @@ exports[`use all parts of svg-edit > check tool_star_change_stroke_width #0`] =
         id="svg_1"
         orient="point"
         point="5"
-        points="300,83.33333333333333 313.0618944953883,132.02184456944562 363.40376775301024,129.39886704167017 321.13458925100343,156.86704431944327 339.18568348616486,203.93446629166317 300,172.22222222222223 260.81431651383514,203.93446629166317 278.86541074899657,156.86704431944327 236.59623224698976,129.39886704167017 286.9381055046117,132.02184456944562 300,83.33333333333333 313.0618944953883,132.02184456944562 "
+        points="574.0962219238281,83.33333587646484 587.1581115722656,132.0218505859375 637.5,129.39886474609375 595.2308044433594,156.86705017089844 613.2818908691406,203.93446350097656 574.0962219238281,172.22222900390625 534.9105529785156,203.93446350097656 552.9616394042969,156.86705017089844 510.6924591064453,129.39886474609375 561.0343322753906,132.0218505859375 574.0962219238281,83.33333587646484 587.1581115722656,132.0218505859375 "
         r="66.67"
         r2="22.22"
         radialshift="0"
@@ -481,7 +481,7 @@ exports[`use all parts of svg-edit > check tool_star_change_stoke_fill_color #0`
         id="svg_1"
         orient="point"
         point="5"
-        points="300,83.33333333333333 313.0618944953883,132.02184456944562 363.40376775301024,129.39886704167017 321.13458925100343,156.86704431944327 339.18568348616486,203.93446629166317 300,172.22222222222223 260.81431651383514,203.93446629166317 278.86541074899657,156.86704431944327 236.59623224698976,129.39886704167017 286.9381055046117,132.02184456944562 300,83.33333333333333 313.0618944953883,132.02184456944562 "
+        points="574.0962219238281,83.33333587646484 587.1581115722656,132.0218505859375 637.5,129.39886474609375 595.2308044433594,156.86705017089844 613.2818908691406,203.93446350097656 574.0962219238281,172.22222900390625 534.9105529785156,203.93446350097656 552.9616394042969,156.86705017089844 510.6924591064453,129.39886474609375 561.0343322753906,132.0218505859375 574.0962219238281,83.33333587646484 587.1581115722656,132.0218505859375 "
         r="66.67"
         r2="22.22"
         radialshift="0"
@@ -512,7 +512,7 @@ exports[`use all parts of svg-edit > check tool_star_change_sides #0`] = `
         id="svg_1"
         orient="point"
         point="6"
-        points="301.8821476527623,70.14305132911319 312.99325876387337,117.56470902279233 359.61717457172483,103.47638466244652 324.1043698749845,136.80971799577986 359.6171745717249,170.14305132911318 312.9932587638734,156.0547269687674 301.8821476527623,203.47638466244655 290.7710365416512,156.0547269687674 244.1471207337997,170.14305132911323 279.65992543054006,136.80971799577986 244.1471207337997,103.47638466244652 290.7710365416512,117.56470902279233 301.8821476527623,70.14305132911319 312.9932587638734,117.56470902279233 "
+        points="575.9783673967634,70.14305132911319 587.0894785078744,117.56470902279233 633.713394315726,103.47638466244652 598.2005896189855,136.80971799577986 633.713394315726,170.14305132911318 587.0894785078744,156.0547269687674 575.9783673967634,203.47638466244655 564.8672562856523,156.0547269687674 518.2433404778008,170.14305132911323 553.7561451745412,136.80971799577986 518.2433404778008,103.47638466244652 564.8672562856523,117.56470902279233 575.9783673967634,70.14305132911319 587.0894785078744,117.56470902279233 "
         r="66.67"
         r2="22.22"
         radialshift="0"
diff --git a/cypress/e2e/ui/scenario.cy.js b/cypress/e2e/ui/scenario.cy.js
index 1e69b7654..92450468d 100644
--- a/cypress/e2e/ui/scenario.cy.js
+++ b/cypress/e2e/ui/scenario.cy.js
@@ -125,8 +125,8 @@ describe('use various parts of svg-edit', function () {
   })
   it('check tool_text_align_to_page', function () {
     cy.get('#svg_2').click({ force: true })
-    cy.get('#tool_position').shadow().find('elix-dropdown-list').eq(0).invoke('attr', 'opened', 'opened')
-    cy.get('#tool_position').find('se-list-item').eq(2).shadow().find('elix-option').eq(0)
+    cy.get('#tool_position').shadow().find('#select-container').eq(0).click({ force: true })
+    cy.get('#tool_position').find('se-list-item').eq(2).shadow().find('[aria-label="option"]').eq(0)
       .click({ force: true })
     testSnapshot()
   })
diff --git a/cypress/e2e/ui/scenario5.cy.js b/cypress/e2e/ui/scenario5.cy.js
index 3ec70183e..f68ccb8f5 100644
--- a/cypress/e2e/ui/scenario5.cy.js
+++ b/cypress/e2e/ui/scenario5.cy.js
@@ -141,8 +141,8 @@ describe('use all parts of svg-edit', function () {
   })
   it('check tool_line_align_to_page', function () {
     cy.get('#svg_3').click({ force: true })
-    cy.get('#tool_position').shadow().find('elix-dropdown-list').eq(0).invoke('attr', 'opened', 'opened')
-    cy.get('#tool_position').find('se-list-item').eq(2).shadow().find('elix-option').eq(0)
+    cy.get('#tool_position').shadow().find('#select-container').eq(0).click({ force: true })
+    cy.get('#tool_position').find('se-list-item').eq(2).shadow().find('[aria-label="option"]').eq(0)
       .click({ force: true })
     testSnapshot()
   })
diff --git a/cypress/e2e/ui/scenario6.cy.js b/cypress/e2e/ui/scenario6.cy.js
index 53e4c1d99..acba6044a 100644
--- a/cypress/e2e/ui/scenario6.cy.js
+++ b/cypress/e2e/ui/scenario6.cy.js
@@ -94,8 +94,8 @@ describe('use all parts of svg-edit', function () {
   })
   it('check tool_polygon_align_to_page', function () {
     cy.get('#svg_1').click({ force: true })
-    cy.get('#tool_position').shadow().find('elix-dropdown-list').eq(0).invoke('attr', 'opened', 'opened')
-    cy.get('#tool_position').find('se-list-item').eq(0).shadow().find('elix-option').eq(0)
+    cy.get('#tool_position').shadow().find('#select-container').eq(0).click({ force: true })
+    cy.get('#tool_position').find('se-list-item').eq(2).shadow().find('[aria-label="option"]').eq(0)
       .click({ force: true })
     testSnapshot()
   })
diff --git a/cypress/e2e/ui/scenario7.cy.js b/cypress/e2e/ui/scenario7.cy.js
index e5f273eb8..8485c97f8 100644
--- a/cypress/e2e/ui/scenario7.cy.js
+++ b/cypress/e2e/ui/scenario7.cy.js
@@ -94,8 +94,8 @@ describe('use all parts of svg-edit', function () {
   })
   it('check tool_star_align_to_page', function () {
     cy.get('#svg_1').click({ force: true })
-    cy.get('#tool_position').shadow().find('elix-dropdown-list').eq(0).invoke('attr', 'opened', 'opened')
-    cy.get('#tool_position').find('se-list-item').eq(0).shadow().find('elix-option').eq(0)
+    cy.get('#tool_position').shadow().find('#select-container').eq(0).click({ force: true })
+    cy.get('#tool_position').find('se-list-item').eq(2).shadow().find('[aria-label="option"]').eq(0)
       .click({ force: true })
     testSnapshot()
   })