How to Implement Auto Scroll in Perspective Component

Hello,
I am using the Accordion Perspective Component to show my new data from the table. As shown in the image below.

I require that whenever I add a new country from Add Country, the accordion should auto-scroll and focus on the newly added country. Is there any possibility to configure Auto Scroll?

Most likely, but not without JavaScript. You can either inject it using the Markdown component, or use Ben Musson's Periscope module that exposes a function to call JavaScript in a less hacky way.. I’ll update with some more concrete info when I'm back on my laptop if someone else doesn’t in the meantime. However there’s a forum post where victordcq documents how to do this with a table component. It will need tweaking for the accordion

You can invoke .focus() on the newly added item in its onStartup event handler. You just need to make sure that the accordion itself first gains focus.

Here’s an example view with an accordion:

{
  "custom": {},
  "params": {},
  "props": {
    "defaultSize": {
      "height": 237
    }
  },
  "root": {
    "children": [
      {
        "children": [
          {
            "children": [
              {
                "meta": {
                  "name": "Accordion"
                },
                "position": {
                  "grow": 1
                },
                "props": {
                  "items": [
                    {
                      "body": {
                        "height": "auto",
                        "style": {
                          "classes": "",
                          "margin": "16px"
                        },
                        "useDefaultViewHeight": false,
                        "useDefaultViewWidth": false,
                        "viewParams": {},
                        "viewPath": "Item"
                      },
                      "expanded": true,
                      "header": {
                        "content": {
                          "style": {
                            "classes": ""
                          },
                          "text": "",
                          "type": "text",
                          "useDefaultViewHeight": false,
                          "useDefaultViewWidth": false,
                          "viewParams": {},
                          "viewPath": ""
                        },
                        "height": "40px",
                        "reverse": false,
                        "style": {
                          "classes": ""
                        },
                        "toggle": {
                          "collapsedIcon": {
                            "color": "",
                            "path": "material/expand_more",
                            "style": {
                              "classes": ""
                            }
                          },
                          "enabled": true,
                          "expandedIcon": {
                            "color": "",
                            "path": "material/expand_less",
                            "style": {
                              "classes": ""
                            }
                          }
                        }
                      }
                    }
                  ]
                },
                "scripts": {
                  "customMethods": [],
                  "extensionFunctions": null,
                  "messageHandlers": [
                    {
                      "messageType": "add-item",
                      "pageScope": true,
                      "script": "\tself.parent.focus()\n\tself.focus()\n\tself.props.items.append({\n\t  \"expanded\": True,\n\t  \"header\": {\n\t    \"toggle\": {\n\t      \"enabled\": True,\n\t      \"expandedIcon\": {\n\t        \"path\": \"material/expand_less\",\n\t        \"color\": \"\",\n\t        \"style\": {\n\t          \"classes\": \"\"\n\t        }\n\t      },\n\t      \"collapsedIcon\": {\n\t        \"path\": \"material/expand_more\",\n\t        \"color\": \"\",\n\t        \"style\": {\n\t          \"classes\": \"\"\n\t        }\n\t      }\n\t    },\n\t    \"content\": {\n\t      \"type\": \"text\",\n\t      \"text\": \"\",\n\t      \"useDefaultViewWidth\": False,\n\t      \"useDefaultViewHeight\": False,\n\t      \"viewPath\": \"\",\n\t      \"viewParams\": {},\n\t      \"style\": {\n\t        \"classes\": \"\"\n\t      }\n\t    },\n\t    \"height\": \"40px\",\n\t    \"reverse\": False,\n\t    \"style\": {\n\t      \"classes\": \"\"\n\t    }\n\t  },\n\t  \"body\": {\n\t    \"viewPath\": \"Item\",\n\t    \"viewParams\": {\n\t      \"index\": len(self.props.items) + 1\n\t    },\n\t    \"useDefaultViewWidth\": False,\n\t    \"useDefaultViewHeight\": False,\n\t    \"height\": \"auto\",\n\t    \"style\": {\n\t      \"classes\": \"\",\n\t      \"margin\": \"16px\"\n\t    }\n\t  }\n\t})",
                      "sessionScope": false,
                      "viewScope": false
                    }
                  ]
                },
                "type": "ia.display.accordion"
              }
            ],
            "meta": {
              "name": "FlexContainer"
            },
            "position": {
              "grow": 1
            },
            "props": {
              "direction": "column"
            },
            "type": "ia.container.flex"
          },
          {
            "events": {
              "component": {
                "onActionPerformed": {
                  "config": {
                    "script": "\tsystem.perspective.sendMessage(\u0027add-item\u0027)"
                  },
                  "scope": "G",
                  "type": "script"
                }
              }
            },
            "meta": {
              "name": "Button"
            },
            "position": {
              "basis": "80px"
            },
            "props": {
              "text": "Add Item"
            },
            "type": "ia.input.button"
          }
        ],
        "meta": {
          "name": "FlexContainer"
        },
        "position": {
          "basis": "200px",
          "shrink": 0
        },
        "type": "ia.container.flex"
      }
    ],
    "meta": {
      "name": "root"
    },
    "props": {
      "direction": "column"
    },
    "type": "ia.container.flex"
  }
}

…and the embedded item view:

{
  "custom": {},
  "params": {
    "index": 1
  },
  "propConfig": {
    "params.index": {
      "paramDirection": "input",
      "persistent": true
    }
  },
  "props": {
    "defaultSize": {
      "height": 43
    }
  },
  "root": {
    "children": [
      {
        "meta": {
          "name": "Label"
        },
        "position": {
          "grow": 1
        },
        "propConfig": {
          "props.text": {
            "binding": {
              "config": {
                "expression": "\u0027Item \u0027 + {view.params.index}"
              },
              "type": "expr"
            }
          }
        },
        "type": "ia.display.label"
      }
    ],
    "events": {
      "system": {
        "onStartup": {
          "config": {
            "script": "\tself.focus()\n"
          },
          "scope": "G",
          "type": "script"
        }
      }
    },
    "meta": {
      "name": "root"
    },
    "props": {
      "direction": "column"
    },
    "type": "ia.container.flex"
  }
}
1 Like

Hi Ryan,
I appreciate your help. Currently, I need to implement auto-scroll.
For example, when you click on the button ‘Add Item’, a new Item is added to the screen and already in focus. I want the scroller to automatically scroll down to that item.

How do you know which item is added? do you know its index in the accordian items?

this Javascript “works”, but it’ll need to be hacked in..

function scrollToAccordionItem(index) {
  const accordion = document.querySelector('.ia_accordionComponent');
  const wrapper = accordion.querySelector('.ia_accordionComponent__wrapper');
  const header = wrapper.querySelector(`.ia_accordionComponent__header[data-index="${index}"]`);

  if (!header) {
    console.warn(`Accordion item with index ${index} not found.`);
    return;
  }

  const accordionTop = accordion.getBoundingClientRect().top;
  const headerTop = header.getBoundingClientRect().top;
  const offset = headerTop - accordionTop;

  accordion.scrollTo({
    top: accordion.scrollTop + offset,
    behavior: 'smooth'
  });
}

if you paste that into the browser dev tools console though and press enter, then type:

scrollToAccordionItem(2);

for example, it will scroll to item 2

Focusing the item does cause the accordion to scroll to it. Did you look at my example?

1 Like

Yes, I have tried that example, but unfortunately, the focus is just highlighting the new item; the scroll is still not working automatically.

I just tried @Ryan_Deardorff ‘s Views and it kinda does both. It focuses it as well as scrolling to it, but the scrolling part doesn’t work quite as well as, at least I, would like – it scrolls only most of the way and shows half the full item:

instead of:

These two Markdown components work with the little testing I’ve done.

“JSInject_Accordian_ScrollToIndexBase” injects the JS function scrollToAccordionItem

“JSInject_Accordian_ScrollToIndex” calls the function with the domID of the accordian to scroll, and the index to scroll to, based on custom props on the component.

Steps

  1. Copy the JSON below and paste it into your View with the accordian.
  2. To your accordian component, add the meta.domId key and set it to something unique like “Accordian”
  3. Inside the “JSInject_Accordian_ScrollToIndex” component, set the “accordianDomId” to what you set it to. Then you can scroll to a specific item index by setting the custom.index to the index number. You can bind to this or set its value via script
[
  {
    "type": "ia.display.markdown",
    "version": 0,
    "props": {
      "style": {
        "position": "absolute"
      },
      "markdown": {
        "escapeHtml": false
      }
    },
    "meta": {
      "name": "JSInject_Accordian_ScrollToIndexBase"
    },
    "position": {
      "shrink": 0,
      "basis": 0
    },
    "custom": {
      "inlineJavascript": "window.scrollToAccordionItem = function(id, index) {\n            const accordion = document.querySelector('#' + id + '.ia_accordionComponent');\n            if (!accordion) {\n                console.warn('Accordion component not found for id: ' + id);\n                return;\n            }\n\n            const wrapper = accordion.querySelector('.ia_accordionComponent__wrapper');\n            const header = wrapper.querySelector('.ia_accordionComponent__header[data-index="' + index + '"]');\n\n            if (!header) {\n                console.warn('Accordion item with index ' + index + ' not found.');\n                return;\n            }\n\n            const accordionTop = accordion.getBoundingClientRect().top;\n            const headerTop = header.getBoundingClientRect().top;\n            const offset = headerTop - accordionTop;\n\n            accordion.scrollTo({\n                top: accordion.scrollTop + offset,\n                behavior: 'smooth'\n            });\n        };"
    },
    "propConfig": {
      "custom.inlineJavascript": {
        "access": "PRIVATE"
      },
      "props.source": {
        "binding": {
          "config": {
            "struct": {
              "script": "{this.custom.inlineJavascript}"
            },
            "waitOnAll": true
          },
          "transforms": [
            {
              "code": "#\tcode =  \"<img style='display:none' src='/favicon.ico' onload=\\\"\" + value.script.replace('\"', '&quot;') + '\\\"></img>'\n#\t\n#\tcode = code.replace(\"\\n\", \"\").replace(\"\\t\", \"\").replace(\"\\n\", \"\").replace(\"\\r\", \"\").replace(\"\\r\", \"\").replace(\"  \", \" \").replace(\"  \", \" \").replace(\"  \", \" \").replace(\"  \", \" \")\n#\t\n#\treturn code\n\tscript = value.script\n\tscript = script.replace('\"', '&quot;')\n\tscript = script.replace('\\n', '').replace('\\t', '').replace('\\r', '')\n\t\n\t# Collapse multiple spaces - repeat until stable\n\twhile '  ' in script:\n\t    script = script.replace('  ', ' ')\n\t\n\tcode = '<img style=\"display:none\" src=\"/favicon.ico\" onload=\"{}\"></img>'.format(script)\n\treturn code",
              "type": "script"
            }
          ],
          "type": "expr-struct"
        }
      }
    }
  },
  {
    "type": "ia.display.markdown",
    "version": 0,
    "props": {
      "style": {
        "position": "absolute"
      },
      "markdown": {
        "escapeHtml": false
      }
    },
    "meta": {
      "name": "JSInject_Accordian_ScrollToIndex"
    },
    "position": {
      "shrink": 0,
      "basis": 0
    },
    "custom": {
      "accordianDomId": "ME",
      "index": 2
    },
    "propConfig": {
      "custom.accordianDomId": {
        "access": "PRIVATE"
      },
      "custom.index": {
        "access": "PRIVATE"
      },
      "custom.inlineJavascript": {
        "binding": {
          "config": {
            "expression": "if({this.custom.index} != \"\"\r\n\t,\"(function() {scrollToAccordionItem('\" + {this.custom.accordianDomId} + \"',  \" + {this.custom.index} + \");})();\"\r\n \t,\"\"\r\n)"
          },
          "type": "expr"
        },
        "access": "PRIVATE"
      },
      "props.source": {
        "binding": {
          "config": {
            "struct": {
              "script": "{this.custom.inlineJavascript}"
            },
            "waitOnAll": true
          },
          "transforms": [
            {
              "code": "#\tcode =  \"<img style='display:none' src='/favicon.ico' onload=\\\"\" + value.script.replace('\"', '&quot;') + '\\\"></img>'\n#\t\n#\tcode = code.replace(\"\\n\", \"\").replace(\"\\t\", \"\").replace(\"\\n\", \"\").replace(\"\\r\", \"\").replace(\"\\r\", \"\").replace(\"  \", \" \").replace(\"  \", \" \").replace(\"  \", \" \").replace(\"  \", \" \")\n#\t\n#\treturn code\n\tscript = value.script\n\tscript = script.replace('\"', '&quot;')\n\tscript = script.replace('\\n', '').replace('\\t', '').replace('\\r', '')\n\t\n\t# Collapse multiple spaces - repeat until stable\n\twhile '  ' in script:\n\t    script = script.replace('  ', ' ')\n\t\n\tcode = '<img style=\"display:none\" src=\"/favicon.ico\" onload=\"{}\"></img>'.format(script)\n\treturn code",
              "type": "script"
            }
          ],
          "type": "expr-struct"
        }
      }
    }
  }
]
2 Likes

Adding a slight sleep() before the focus seems to work to make it gain full focus:

	import time
	
	time.sleep(0.2)
	self.focus()

@ssrivastava I don’t know why it wouldn’t work for you. What browser are you using? It works in the Designer…

If you mean it isn’t working when you try to incorporate it into your design, then I’m guessing the reason is the focusing hierarchy. In order for .focus() to work, the component you are calling it on must be a descendant or sibling of whichever component currently has focus. That’s the reason for the first two lines in my example, as they ensure that the accordion itself gains it before the new item requests it:

image

When I have tested in local it worked properly, but when I am implementing the script to the client server. Its not working.

Developer Hierarchy.
New Item Template → Flex Repeater → Accordion.
Because each expansion will multiple items.

No we are not getting Item index because each expansion is having multiple item so they added Flex Repeater.

So you have multiple accordians now? do they all have unique domIds?

Screen looks like the image I have shared.