Merge pull request #2415 from LenAnderson/parser-followup-2 Parser followup 2
Signed| @@ -0,0 +1,149 @@ | ||
| 1 | +<?xml version="1.0" encoding="UTF-8" standalone="no"?> | |
| 2 | +<!-- Created with Inkscape (http://www.inkscape.org/) --> | |
| 3 | + | |
| 4 | +<svg | |
| 5 | + width="48" | |
| 6 | + height="48" | |
| 7 | + viewBox="0 0 48 48" | |
| 8 | + version="1.1" | |
| 9 | + id="svg2120" | |
| 10 | + xml:space="preserve" | |
| 11 | + inkscape:version="1.2.2 (732a01da63, 2022-12-09)" | |
| 12 | + sodipodi:docname="step-into.svg" | |
| 13 | + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" | |
| 14 | + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" | |
| 15 | + xmlns:xlink="http://www.w3.org/1999/xlink" | |
| 16 | + xmlns="http://www.w3.org/2000/svg" | |
| 17 | + xmlns:svg="http://www.w3.org/2000/svg"><sodipodi:namedview | |
| 18 | + id="namedview2122" | |
| 19 | + pagecolor="#ffffff" | |
| 20 | + bordercolor="#000000" | |
| 21 | + borderopacity="0.25" | |
| 22 | + inkscape:showpageshadow="2" | |
| 23 | + inkscape:pageopacity="0.0" | |
| 24 | + inkscape:pagecheckerboard="0" | |
| 25 | + inkscape:deskcolor="#d1d1d1" | |
| 26 | + inkscape:document-units="px" | |
| 27 | + showgrid="false" | |
| 28 | + inkscape:zoom="11.313708" | |
| 29 | + inkscape:cx="58.910834" | |
| 30 | + inkscape:cy="25.323262" | |
| 31 | + inkscape:window-width="1920" | |
| 32 | + inkscape:window-height="992" | |
| 33 | + inkscape:window-x="-8" | |
| 34 | + inkscape:window-y="-8" | |
| 35 | + inkscape:window-maximized="1" | |
| 36 | + inkscape:current-layer="g2714" /><defs | |
| 37 | + id="defs2117"><inkscape:path-effect | |
| 38 | + effect="spiro" | |
| 39 | + id="path-effect2144" | |
| 40 | + is_visible="true" | |
| 41 | + lpeversion="1" /><inkscape:path-effect | |
| 42 | + effect="bspline" | |
| 43 | + id="path-effect2138" | |
| 44 | + is_visible="true" | |
| 45 | + lpeversion="1" | |
| 46 | + weight="33.333333" | |
| 47 | + steps="2" | |
| 48 | + helper_size="0" | |
| 49 | + apply_no_weight="true" | |
| 50 | + apply_with_weight="true" | |
| 51 | + only_selected="false" /></defs><g | |
| 52 | + inkscape:groupmode="layer" | |
| 53 | + id="layer4" | |
| 54 | + inkscape:label="img" | |
| 55 | + style="display:none"><image | |
| 56 | + width="305.68866" | |
| 57 | + height="70.374367" | |
| 58 | + preserveAspectRatio="none" | |
| 59 | + xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAARYAAABACAYAAADf7VgRAAAABHNCSVQICAgIfAhkiAAABdxJREFU | |
| 60 | +eJzt3T9vo0gYBvB3by+SsZRiKCI5SCkyJS6dzny3+26UpoSSFJFMOaTyUETaK3zDAQYbzGBgeH7S | |
| 61 | +SutN1kKj8cM7fxj/2u/3fwgAQKO/xr4AADAPggUAtEOwAIB2CBYA0A7BAgDaIVgAQDsECwBoh2AB | |
| 62 | +AO0QLACgHYIFALRDsACAdn+PfQFw5nkeWZZFRERSSvJ9f+Qrmg7XdSnLMorjeOxLgZYQLDBpruuS | |
| 63 | +4zj5a4TLPGAoBJNVDRXOOXHOR7wiaAvBApNUDRUF4TIPCBaYnKZQURAu04dggUm5FSoKwmXaECww | |
| 64 | +GW1DRUG4TNfDV4VUx1mtViSEICKiLMtIStn4f4qdJ8syOh6Pw14kPFxdqAghyLKs0jI8EeWvif7v | |
| 65 | +G1gtmpbBg8V1XbJtm4jKHYKoHBhCCIrjuDZkqr+HYDFLU6gEQUCe55X+3ff90p4fIoTLFA0WLI7j | |
| 66 | +kOu6rX/ftm2ybZuEEBRF0dUKBsxxLVSaIFymT/scC2OMdrtdp1Apsm2bPM8jzvlFhQPmybKs9PpW | |
| 67 | +qCi+71/cfKrvZQrXdYkxNvZldKK1YmGM0cfHR+3PhBAkpaTv728iIjqdTmTbNq1WK7Jtu3aYxBij | |
| 68 | +KIp0XuLoXNelz8/PzhWZZVnkuq5x1ZyqMDjnrUNFKVYuURQZOUTe7XZ5NR+GIaVpOvYltaItWCzL | |
| 69 | +qg0VNXdS1yDFf2OM0Xa7LQWMbdt3Vz5TVOwkQRC0DgjLsvK5BlPD5d5Jed/3yXEcI0OlOj+53W5n | |
| 70 | +Ey5ahkLFjq9IKelwOFAQBK0aIk1T8n3/ooOohp27aifZ7XathnrVtjUtbJU+wWBiqBDRRWWrwqXv | |
| 71 | +sKi40jYULRVLtaP3eTo3iiLKssy4/QlJkpSGfCpcrlUudYFNhAnKpZBSUhAEpZvQvZWLGkoXb9RS | |
| 72 | +SkqSZJD+1Lti4ZxfVBVhGPZ6zziOjfvwpGlKYRhe3IGaKpemUDkcDrMohUEPFS59KhfOOXmed/E5 | |
| 73 | +tSwr/5nuCqZ3xfL6+lp63bXjq0naJVDhUpxLqgsXhAoU9alcGGM3q3/VB3WeAdSrYqmO1aSUnTs+ | |
| 74 | +Yyyf0Kz+MVFT5XILQmXZ7q1cmlZpqyzL6vQ4xS29KpZqtZIkSa+LWYq6yuUahIo56irRLqr95Vrl | |
| 75 | +0nUksNlstE2EawsWKaVx8yJDahsuCBWzDLEa0xQu6/W68/vo0itYdIzJumyIMs2tcEGoQFt1/We1 | |
| 76 | +WvV+j3vhzNuRNYULQsVMfTc2Nn346/qLEKLTtg2d+4FGOzbhdDrlz3aYtIv0HtVwQaiYq0+V33W1 | |
| 77 | +UJ0U0LYS0fms1cOCpelQHjzNfKbCRf0dzqSUed9Ych+5ZwuClJLCMGy1MqQevdHl136//6Pt3Rqo | |
| 78 | +Z2SuMfUhMoC++u5runXSngognTe0wSuWup25dd7f3/MnoAHgTMdmyTiOKUmSh27pHzRY1Jbhtr/7 | |
| 79 | ++vqKJWuA/+jcga022FWP+RzKpFaFlrK1vw5jjNbrdX4W8K1zgJdiye1S9xR734n9R7XdoMFS3Zl7 | |
| 80 | +i6nb+K+pOxyLcz7IuHdO0C7necfi8GVOq4W/397e/hnqzdfrNb28vLT+fSklfX19DXU5k8M5p+12 | |
| 81 | +W/uzp6enfGl+Lp1JF7TL2c/PD6VpSs/Pz7ML00G/V0h9vUdbSylxidrPPy3t7F+0S5maG5lTqBAN | |
| 82 | +HCzFPQhtLGnitsspcCaeGNcE7WKGwb8Jse3ZrkKI2aVyH13mk+oOGzcV2sUMgweLmmy7Fi5dT2ef | |
| 83 | +O3wY6qFdzPGQ5WZ1ULY6LU4dEHU8HilJkkVVKvda0vxTF2iXaXroPpYlzaFco+ae2t6hl/LhQbuY | |
| 84 | +Y/ChENTrctrekk7mQ7uYAcEykjiOW91xl3YyH9rFDAiWEd1aMRNC9P4qlTlCu8zfQ45NgOscx6HN | |
| 85 | +ZlOa1M6ybPF3ZLTLfCFYAEA7DIUAQDsECwBoh2ABAO0QLACgHYIFALRDsACAdggWANAOwQIA2iFY | |
| 86 | +AEA7BAsAaIdgAQDt/gUoDXNStc/rMQAAAABJRU5ErkJggg== | |
| 87 | +" | |
| 88 | + id="image2132" | |
| 89 | + x="-82" | |
| 90 | + y="-11.9" /></g><g | |
| 91 | + inkscape:groupmode="layer" | |
| 92 | + id="layer5" | |
| 93 | + inkscape:label="dot" /><g | |
| 94 | + inkscape:label="over" | |
| 95 | + inkscape:groupmode="layer" | |
| 96 | + id="layer1" | |
| 97 | + style="display:none"><ellipse | |
| 98 | + style="fill:#000000;stroke:#000000;stroke-width:5.27982;stroke-dasharray:none" | |
| 99 | + id="path2637" | |
| 100 | + cx="24" | |
| 101 | + cy="33" | |
| 102 | + rx="4.3600898" | |
| 103 | + ry="4.3600893" /><path | |
| 104 | + style="fill:none;stroke:#000000;stroke-width:5;stroke-dasharray:none" | |
| 105 | + d="M 9,24 C 9.7590836,20.626295 11.695032,17.529367 14.395314,15.369142 17.095595,13.208917 20.541953,12 24,12 c 3.458047,0 6.904405,1.208917 9.604686,3.369142 C 36.304968,17.529367 38.240916,20.626295 39,24" | |
| 106 | + id="path2142" | |
| 107 | + inkscape:path-effect="#path-effect2144" | |
| 108 | + inkscape:original-d="m 9,24 c 4.959859,-3.824406 10.021901,-8.173595 15,-12 4.978099,-3.8264055 10.285024,8.398237 15,12" | |
| 109 | + sodipodi:nodetypes="csc" /><path | |
| 110 | + style="fill:none;stroke:#000000;stroke-width:5;stroke-dasharray:none" | |
| 111 | + d="M 26,22 H 39 V 9" | |
| 112 | + id="path2626" | |
| 113 | + sodipodi:nodetypes="ccc" /></g><g | |
| 114 | + inkscape:groupmode="layer" | |
| 115 | + id="layer6" | |
| 116 | + inkscape:label="into" | |
| 117 | + style="display:inline"><ellipse | |
| 118 | + style="fill:#000000;stroke:#000000;stroke-width:5.27982;stroke-dasharray:none" | |
| 119 | + id="path2637-3" | |
| 120 | + cx="24" | |
| 121 | + cy="38.5" | |
| 122 | + rx="4.3600898" | |
| 123 | + ry="4.3600893" /><path | |
| 124 | + style="fill:#000000;stroke:#000000;stroke-width:5;stroke-dasharray:none" | |
| 125 | + d="M 24,2 V 24" | |
| 126 | + id="path2668" | |
| 127 | + sodipodi:nodetypes="cc" /><path | |
| 128 | + style="fill:none;stroke:#000000;stroke-width:5;stroke-dasharray:none" | |
| 129 | + d="M 14.807612,16.994936 24,26.187324 33.192388,16.994936" | |
| 130 | + id="path2626-8" | |
| 131 | + sodipodi:nodetypes="ccc" /></g><g | |
| 132 | + inkscape:groupmode="layer" | |
| 133 | + id="g2714" | |
| 134 | + inkscape:label="out" | |
| 135 | + style="display:none"><ellipse | |
| 136 | + style="fill:#000000;stroke:#000000;stroke-width:5.27982;stroke-dasharray:none" | |
| 137 | + id="ellipse2708" | |
| 138 | + cx="24" | |
| 139 | + cy="38.5" | |
| 140 | + rx="4.3600898" | |
| 141 | + ry="4.3600893" /><path | |
| 142 | + style="fill:#000000;stroke:#000000;stroke-width:5;stroke-dasharray:none" | |
| 143 | + d="M 24,29.722858 V 7.7228579" | |
| 144 | + id="path2710" | |
| 145 | + sodipodi:nodetypes="cc" /><path | |
| 146 | + style="display:inline;fill:none;stroke:#000000;stroke-width:5;stroke-dasharray:none" | |
| 147 | + d="M 33.192388,14.727922 24,5.5355339 14.807612,14.727922" | |
| 148 | + id="path2712" | |
| 149 | + sodipodi:nodetypes="ccc" /></g></svg> | |
| @@ -0,0 +1,149 @@ | ||
| 1 | +<?xml version="1.0" encoding="UTF-8" standalone="no"?> | |
| 2 | +<!-- Created with Inkscape (http://www.inkscape.org/) --> | |
| 3 | + | |
| 4 | +<svg | |
| 5 | + width="48" | |
| 6 | + height="48" | |
| 7 | + viewBox="0 0 48 48" | |
| 8 | + version="1.1" | |
| 9 | + id="svg2120" | |
| 10 | + xml:space="preserve" | |
| 11 | + inkscape:version="1.2.2 (732a01da63, 2022-12-09)" | |
| 12 | + sodipodi:docname="step-out.svg" | |
| 13 | + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" | |
| 14 | + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" | |
| 15 | + xmlns:xlink="http://www.w3.org/1999/xlink" | |
| 16 | + xmlns="http://www.w3.org/2000/svg" | |
| 17 | + xmlns:svg="http://www.w3.org/2000/svg"><sodipodi:namedview | |
| 18 | + id="namedview2122" | |
| 19 | + pagecolor="#ffffff" | |
| 20 | + bordercolor="#000000" | |
| 21 | + borderopacity="0.25" | |
| 22 | + inkscape:showpageshadow="2" | |
| 23 | + inkscape:pageopacity="0.0" | |
| 24 | + inkscape:pagecheckerboard="0" | |
| 25 | + inkscape:deskcolor="#d1d1d1" | |
| 26 | + inkscape:document-units="px" | |
| 27 | + showgrid="false" | |
| 28 | + inkscape:zoom="11.313708" | |
| 29 | + inkscape:cx="58.910834" | |
| 30 | + inkscape:cy="25.323262" | |
| 31 | + inkscape:window-width="1920" | |
| 32 | + inkscape:window-height="992" | |
| 33 | + inkscape:window-x="-8" | |
| 34 | + inkscape:window-y="-8" | |
| 35 | + inkscape:window-maximized="1" | |
| 36 | + inkscape:current-layer="g2714" /><defs | |
| 37 | + id="defs2117"><inkscape:path-effect | |
| 38 | + effect="spiro" | |
| 39 | + id="path-effect2144" | |
| 40 | + is_visible="true" | |
| 41 | + lpeversion="1" /><inkscape:path-effect | |
| 42 | + effect="bspline" | |
| 43 | + id="path-effect2138" | |
| 44 | + is_visible="true" | |
| 45 | + lpeversion="1" | |
| 46 | + weight="33.333333" | |
| 47 | + steps="2" | |
| 48 | + helper_size="0" | |
| 49 | + apply_no_weight="true" | |
| 50 | + apply_with_weight="true" | |
| 51 | + only_selected="false" /></defs><g | |
| 52 | + inkscape:groupmode="layer" | |
| 53 | + id="layer4" | |
| 54 | + inkscape:label="img" | |
| 55 | + style="display:none"><image | |
| 56 | + width="305.68866" | |
| 57 | + height="70.374367" | |
| 58 | + preserveAspectRatio="none" | |
| 59 | + xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAARYAAABACAYAAADf7VgRAAAABHNCSVQICAgIfAhkiAAABdxJREFU | |
| 60 | +eJzt3T9vo0gYBvB3by+SsZRiKCI5SCkyJS6dzny3+26UpoSSFJFMOaTyUETaK3zDAQYbzGBgeH7S | |
| 61 | +SutN1kKj8cM7fxj/2u/3fwgAQKO/xr4AADAPggUAtEOwAIB2CBYA0A7BAgDaIVgAQDsECwBoh2AB | |
| 62 | +AO0QLACgHYIFALRDsACAdn+PfQFw5nkeWZZFRERSSvJ9f+Qrmg7XdSnLMorjeOxLgZYQLDBpruuS | |
| 63 | +4zj5a4TLPGAoBJNVDRXOOXHOR7wiaAvBApNUDRUF4TIPCBaYnKZQURAu04dggUm5FSoKwmXaECww | |
| 64 | +GW1DRUG4TNfDV4VUx1mtViSEICKiLMtIStn4f4qdJ8syOh6Pw14kPFxdqAghyLKs0jI8EeWvif7v | |
| 65 | +G1gtmpbBg8V1XbJtm4jKHYKoHBhCCIrjuDZkqr+HYDFLU6gEQUCe55X+3ff90p4fIoTLFA0WLI7j | |
| 66 | +kOu6rX/ftm2ybZuEEBRF0dUKBsxxLVSaIFymT/scC2OMdrtdp1Apsm2bPM8jzvlFhQPmybKs9PpW | |
| 67 | +qCi+71/cfKrvZQrXdYkxNvZldKK1YmGM0cfHR+3PhBAkpaTv728iIjqdTmTbNq1WK7Jtu3aYxBij | |
| 68 | +KIp0XuLoXNelz8/PzhWZZVnkuq5x1ZyqMDjnrUNFKVYuURQZOUTe7XZ5NR+GIaVpOvYltaItWCzL | |
| 69 | +qg0VNXdS1yDFf2OM0Xa7LQWMbdt3Vz5TVOwkQRC0DgjLsvK5BlPD5d5Jed/3yXEcI0OlOj+53W5n | |
| 70 | +Ey5ahkLFjq9IKelwOFAQBK0aIk1T8n3/ooOohp27aifZ7XathnrVtjUtbJU+wWBiqBDRRWWrwqXv | |
| 71 | +sKi40jYULRVLtaP3eTo3iiLKssy4/QlJkpSGfCpcrlUudYFNhAnKpZBSUhAEpZvQvZWLGkoXb9RS | |
| 72 | +SkqSZJD+1Lti4ZxfVBVhGPZ6zziOjfvwpGlKYRhe3IGaKpemUDkcDrMohUEPFS59KhfOOXmed/E5 | |
| 73 | +tSwr/5nuCqZ3xfL6+lp63bXjq0naJVDhUpxLqgsXhAoU9alcGGM3q3/VB3WeAdSrYqmO1aSUnTs+ | |
| 74 | +Yyyf0Kz+MVFT5XILQmXZ7q1cmlZpqyzL6vQ4xS29KpZqtZIkSa+LWYq6yuUahIo56irRLqr95Vrl | |
| 75 | +0nUksNlstE2EawsWKaVx8yJDahsuCBWzDLEa0xQu6/W68/vo0itYdIzJumyIMs2tcEGoQFt1/We1 | |
| 76 | +WvV+j3vhzNuRNYULQsVMfTc2Nn346/qLEKLTtg2d+4FGOzbhdDrlz3aYtIv0HtVwQaiYq0+V33W1 | |
| 77 | +UJ0U0LYS0fms1cOCpelQHjzNfKbCRf0dzqSUed9Ych+5ZwuClJLCMGy1MqQevdHl136//6Pt3Rqo | |
| 78 | +Z2SuMfUhMoC++u5runXSngognTe0wSuWup25dd7f3/MnoAHgTMdmyTiOKUmSh27pHzRY1Jbhtr/7 | |
| 79 | ++vqKJWuA/+jcga022FWP+RzKpFaFlrK1vw5jjNbrdX4W8K1zgJdiye1S9xR734n9R7XdoMFS3Zl7 | |
| 80 | +i6nb+K+pOxyLcz7IuHdO0C7necfi8GVOq4W/397e/hnqzdfrNb28vLT+fSklfX19DXU5k8M5p+12 | |
| 81 | +W/uzp6enfGl+Lp1JF7TL2c/PD6VpSs/Pz7ML00G/V0h9vUdbSylxidrPPy3t7F+0S5maG5lTqBAN | |
| 82 | +HCzFPQhtLGnitsspcCaeGNcE7WKGwb8Jse3ZrkKI2aVyH13mk+oOGzcV2sUMgweLmmy7Fi5dT2ef | |
| 83 | +O3wY6qFdzPGQ5WZ1ULY6LU4dEHU8HilJkkVVKvda0vxTF2iXaXroPpYlzaFco+ae2t6hl/LhQbuY | |
| 84 | +Y/ChENTrctrekk7mQ7uYAcEykjiOW91xl3YyH9rFDAiWEd1aMRNC9P4qlTlCu8zfQ45NgOscx6HN | |
| 85 | +ZlOa1M6ybPF3ZLTLfCFYAEA7DIUAQDsECwBoh2ABAO0QLACgHYIFALRDsACAdggWANAOwQIA2iFY | |
| 86 | +AEA7BAsAaIdgAQDt/gUoDXNStc/rMQAAAABJRU5ErkJggg== | |
| 87 | +" | |
| 88 | + id="image2132" | |
| 89 | + x="-82" | |
| 90 | + y="-11.9" /></g><g | |
| 91 | + inkscape:groupmode="layer" | |
| 92 | + id="layer5" | |
| 93 | + inkscape:label="dot" /><g | |
| 94 | + inkscape:label="over" | |
| 95 | + inkscape:groupmode="layer" | |
| 96 | + id="layer1" | |
| 97 | + style="display:none"><ellipse | |
| 98 | + style="fill:#000000;stroke:#000000;stroke-width:5.27982;stroke-dasharray:none" | |
| 99 | + id="path2637" | |
| 100 | + cx="24" | |
| 101 | + cy="33" | |
| 102 | + rx="4.3600898" | |
| 103 | + ry="4.3600893" /><path | |
| 104 | + style="fill:none;stroke:#000000;stroke-width:5;stroke-dasharray:none" | |
| 105 | + d="M 9,24 C 9.7590836,20.626295 11.695032,17.529367 14.395314,15.369142 17.095595,13.208917 20.541953,12 24,12 c 3.458047,0 6.904405,1.208917 9.604686,3.369142 C 36.304968,17.529367 38.240916,20.626295 39,24" | |
| 106 | + id="path2142" | |
| 107 | + inkscape:path-effect="#path-effect2144" | |
| 108 | + inkscape:original-d="m 9,24 c 4.959859,-3.824406 10.021901,-8.173595 15,-12 4.978099,-3.8264055 10.285024,8.398237 15,12" | |
| 109 | + sodipodi:nodetypes="csc" /><path | |
| 110 | + style="fill:none;stroke:#000000;stroke-width:5;stroke-dasharray:none" | |
| 111 | + d="M 26,22 H 39 V 9" | |
| 112 | + id="path2626" | |
| 113 | + sodipodi:nodetypes="ccc" /></g><g | |
| 114 | + inkscape:groupmode="layer" | |
| 115 | + id="layer6" | |
| 116 | + inkscape:label="into" | |
| 117 | + style="display:none"><ellipse | |
| 118 | + style="fill:#000000;stroke:#000000;stroke-width:5.27982;stroke-dasharray:none" | |
| 119 | + id="path2637-3" | |
| 120 | + cx="24" | |
| 121 | + cy="38.5" | |
| 122 | + rx="4.3600898" | |
| 123 | + ry="4.3600893" /><path | |
| 124 | + style="fill:#000000;stroke:#000000;stroke-width:5;stroke-dasharray:none" | |
| 125 | + d="M 24,2 V 24" | |
| 126 | + id="path2668" | |
| 127 | + sodipodi:nodetypes="cc" /><path | |
| 128 | + style="fill:none;stroke:#000000;stroke-width:5;stroke-dasharray:none" | |
| 129 | + d="M 14.807612,16.994936 24,26.187324 33.192388,16.994936" | |
| 130 | + id="path2626-8" | |
| 131 | + sodipodi:nodetypes="ccc" /></g><g | |
| 132 | + inkscape:groupmode="layer" | |
| 133 | + id="g2714" | |
| 134 | + inkscape:label="out" | |
| 135 | + style="display:inline"><ellipse | |
| 136 | + style="fill:#000000;stroke:#000000;stroke-width:5.27982;stroke-dasharray:none" | |
| 137 | + id="ellipse2708" | |
| 138 | + cx="24" | |
| 139 | + cy="38.5" | |
| 140 | + rx="4.3600898" | |
| 141 | + ry="4.3600893" /><path | |
| 142 | + style="fill:#000000;stroke:#000000;stroke-width:5;stroke-dasharray:none" | |
| 143 | + d="M 24,29.722858 V 7.7228579" | |
| 144 | + id="path2710" | |
| 145 | + sodipodi:nodetypes="cc" /><path | |
| 146 | + style="display:inline;fill:none;stroke:#000000;stroke-width:5;stroke-dasharray:none" | |
| 147 | + d="M 33.192388,14.727922 24,5.5355339 14.807612,14.727922" | |
| 148 | + id="path2712" | |
| 149 | + sodipodi:nodetypes="ccc" /></g></svg> | |
| @@ -0,0 +1,149 @@ | ||
| 1 | +<?xml version="1.0" encoding="UTF-8" standalone="no"?> | |
| 2 | +<!-- Created with Inkscape (http://www.inkscape.org/) --> | |
| 3 | + | |
| 4 | +<svg | |
| 5 | + width="48" | |
| 6 | + height="48" | |
| 7 | + viewBox="0 0 48 48" | |
| 8 | + version="1.1" | |
| 9 | + id="svg2120" | |
| 10 | + xml:space="preserve" | |
| 11 | + inkscape:version="1.2.2 (732a01da63, 2022-12-09)" | |
| 12 | + sodipodi:docname="step-over.svg" | |
| 13 | + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" | |
| 14 | + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" | |
| 15 | + xmlns:xlink="http://www.w3.org/1999/xlink" | |
| 16 | + xmlns="http://www.w3.org/2000/svg" | |
| 17 | + xmlns:svg="http://www.w3.org/2000/svg"><sodipodi:namedview | |
| 18 | + id="namedview2122" | |
| 19 | + pagecolor="#ffffff" | |
| 20 | + bordercolor="#000000" | |
| 21 | + borderopacity="0.25" | |
| 22 | + inkscape:showpageshadow="2" | |
| 23 | + inkscape:pageopacity="0.0" | |
| 24 | + inkscape:pagecheckerboard="0" | |
| 25 | + inkscape:deskcolor="#d1d1d1" | |
| 26 | + inkscape:document-units="px" | |
| 27 | + showgrid="false" | |
| 28 | + inkscape:zoom="11.313708" | |
| 29 | + inkscape:cx="58.910834" | |
| 30 | + inkscape:cy="25.323262" | |
| 31 | + inkscape:window-width="1920" | |
| 32 | + inkscape:window-height="992" | |
| 33 | + inkscape:window-x="-8" | |
| 34 | + inkscape:window-y="-8" | |
| 35 | + inkscape:window-maximized="1" | |
| 36 | + inkscape:current-layer="g2714" /><defs | |
| 37 | + id="defs2117"><inkscape:path-effect | |
| 38 | + effect="spiro" | |
| 39 | + id="path-effect2144" | |
| 40 | + is_visible="true" | |
| 41 | + lpeversion="1" /><inkscape:path-effect | |
| 42 | + effect="bspline" | |
| 43 | + id="path-effect2138" | |
| 44 | + is_visible="true" | |
| 45 | + lpeversion="1" | |
| 46 | + weight="33.333333" | |
| 47 | + steps="2" | |
| 48 | + helper_size="0" | |
| 49 | + apply_no_weight="true" | |
| 50 | + apply_with_weight="true" | |
| 51 | + only_selected="false" /></defs><g | |
| 52 | + inkscape:groupmode="layer" | |
| 53 | + id="layer4" | |
| 54 | + inkscape:label="img" | |
| 55 | + style="display:none"><image | |
| 56 | + width="305.68866" | |
| 57 | + height="70.374367" | |
| 58 | + preserveAspectRatio="none" | |
| 59 | + xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAARYAAABACAYAAADf7VgRAAAABHNCSVQICAgIfAhkiAAABdxJREFU | |
| 60 | +eJzt3T9vo0gYBvB3by+SsZRiKCI5SCkyJS6dzny3+26UpoSSFJFMOaTyUETaK3zDAQYbzGBgeH7S | |
| 61 | +SutN1kKj8cM7fxj/2u/3fwgAQKO/xr4AADAPggUAtEOwAIB2CBYA0A7BAgDaIVgAQDsECwBoh2AB | |
| 62 | +AO0QLACgHYIFALRDsACAdn+PfQFw5nkeWZZFRERSSvJ9f+Qrmg7XdSnLMorjeOxLgZYQLDBpruuS | |
| 63 | +4zj5a4TLPGAoBJNVDRXOOXHOR7wiaAvBApNUDRUF4TIPCBaYnKZQURAu04dggUm5FSoKwmXaECww | |
| 64 | +GW1DRUG4TNfDV4VUx1mtViSEICKiLMtIStn4f4qdJ8syOh6Pw14kPFxdqAghyLKs0jI8EeWvif7v | |
| 65 | +G1gtmpbBg8V1XbJtm4jKHYKoHBhCCIrjuDZkqr+HYDFLU6gEQUCe55X+3ff90p4fIoTLFA0WLI7j | |
| 66 | +kOu6rX/ftm2ybZuEEBRF0dUKBsxxLVSaIFymT/scC2OMdrtdp1Apsm2bPM8jzvlFhQPmybKs9PpW | |
| 67 | +qCi+71/cfKrvZQrXdYkxNvZldKK1YmGM0cfHR+3PhBAkpaTv728iIjqdTmTbNq1WK7Jtu3aYxBij | |
| 68 | +KIp0XuLoXNelz8/PzhWZZVnkuq5x1ZyqMDjnrUNFKVYuURQZOUTe7XZ5NR+GIaVpOvYltaItWCzL | |
| 69 | +qg0VNXdS1yDFf2OM0Xa7LQWMbdt3Vz5TVOwkQRC0DgjLsvK5BlPD5d5Jed/3yXEcI0OlOj+53W5n | |
| 70 | +Ey5ahkLFjq9IKelwOFAQBK0aIk1T8n3/ooOohp27aifZ7XathnrVtjUtbJU+wWBiqBDRRWWrwqXv | |
| 71 | +sKi40jYULRVLtaP3eTo3iiLKssy4/QlJkpSGfCpcrlUudYFNhAnKpZBSUhAEpZvQvZWLGkoXb9RS | |
| 72 | +SkqSZJD+1Lti4ZxfVBVhGPZ6zziOjfvwpGlKYRhe3IGaKpemUDkcDrMohUEPFS59KhfOOXmed/E5 | |
| 73 | +tSwr/5nuCqZ3xfL6+lp63bXjq0naJVDhUpxLqgsXhAoU9alcGGM3q3/VB3WeAdSrYqmO1aSUnTs+ | |
| 74 | +Yyyf0Kz+MVFT5XILQmXZ7q1cmlZpqyzL6vQ4xS29KpZqtZIkSa+LWYq6yuUahIo56irRLqr95Vrl | |
| 75 | +0nUksNlstE2EawsWKaVx8yJDahsuCBWzDLEa0xQu6/W68/vo0itYdIzJumyIMs2tcEGoQFt1/We1 | |
| 76 | +WvV+j3vhzNuRNYULQsVMfTc2Nn346/qLEKLTtg2d+4FGOzbhdDrlz3aYtIv0HtVwQaiYq0+V33W1 | |
| 77 | +UJ0U0LYS0fms1cOCpelQHjzNfKbCRf0dzqSUed9Ych+5ZwuClJLCMGy1MqQevdHl136//6Pt3Rqo | |
| 78 | +Z2SuMfUhMoC++u5runXSngognTe0wSuWup25dd7f3/MnoAHgTMdmyTiOKUmSh27pHzRY1Jbhtr/7 | |
| 79 | ++vqKJWuA/+jcga022FWP+RzKpFaFlrK1vw5jjNbrdX4W8K1zgJdiye1S9xR734n9R7XdoMFS3Zl7 | |
| 80 | +i6nb+K+pOxyLcz7IuHdO0C7necfi8GVOq4W/397e/hnqzdfrNb28vLT+fSklfX19DXU5k8M5p+12 | |
| 81 | +W/uzp6enfGl+Lp1JF7TL2c/PD6VpSs/Pz7ML00G/V0h9vUdbSylxidrPPy3t7F+0S5maG5lTqBAN | |
| 82 | +HCzFPQhtLGnitsspcCaeGNcE7WKGwb8Jse3ZrkKI2aVyH13mk+oOGzcV2sUMgweLmmy7Fi5dT2ef | |
| 83 | +O3wY6qFdzPGQ5WZ1ULY6LU4dEHU8HilJkkVVKvda0vxTF2iXaXroPpYlzaFco+ae2t6hl/LhQbuY | |
| 84 | +Y/ChENTrctrekk7mQ7uYAcEykjiOW91xl3YyH9rFDAiWEd1aMRNC9P4qlTlCu8zfQ45NgOscx6HN | |
| 85 | +ZlOa1M6ybPF3ZLTLfCFYAEA7DIUAQDsECwBoh2ABAO0QLACgHYIFALRDsACAdggWANAOwQIA2iFY | |
| 86 | +AEA7BAsAaIdgAQDt/gUoDXNStc/rMQAAAABJRU5ErkJggg== | |
| 87 | +" | |
| 88 | + id="image2132" | |
| 89 | + x="-82" | |
| 90 | + y="-11.9" /></g><g | |
| 91 | + inkscape:groupmode="layer" | |
| 92 | + id="layer5" | |
| 93 | + inkscape:label="dot" /><g | |
| 94 | + inkscape:label="over" | |
| 95 | + inkscape:groupmode="layer" | |
| 96 | + id="layer1" | |
| 97 | + style="display:inline"><ellipse | |
| 98 | + style="fill:#000000;stroke:#000000;stroke-width:5.27982;stroke-dasharray:none" | |
| 99 | + id="path2637" | |
| 100 | + cx="24" | |
| 101 | + cy="33" | |
| 102 | + rx="4.3600898" | |
| 103 | + ry="4.3600893" /><path | |
| 104 | + style="fill:none;stroke:#000000;stroke-width:5;stroke-dasharray:none" | |
| 105 | + d="M 9,24 C 9.7590836,20.626295 11.695032,17.529367 14.395314,15.369142 17.095595,13.208917 20.541953,12 24,12 c 3.458047,0 6.904405,1.208917 9.604686,3.369142 C 36.304968,17.529367 38.240916,20.626295 39,24" | |
| 106 | + id="path2142" | |
| 107 | + inkscape:path-effect="#path-effect2144" | |
| 108 | + inkscape:original-d="m 9,24 c 4.959859,-3.824406 10.021901,-8.173595 15,-12 4.978099,-3.8264055 10.285024,8.398237 15,12" | |
| 109 | + sodipodi:nodetypes="csc" /><path | |
| 110 | + style="fill:none;stroke:#000000;stroke-width:5;stroke-dasharray:none" | |
| 111 | + d="M 26,22 H 39 V 9" | |
| 112 | + id="path2626" | |
| 113 | + sodipodi:nodetypes="ccc" /></g><g | |
| 114 | + inkscape:groupmode="layer" | |
| 115 | + id="layer6" | |
| 116 | + inkscape:label="into" | |
| 117 | + style="display:none"><ellipse | |
| 118 | + style="fill:#000000;stroke:#000000;stroke-width:5.27982;stroke-dasharray:none" | |
| 119 | + id="path2637-3" | |
| 120 | + cx="24" | |
| 121 | + cy="38.5" | |
| 122 | + rx="4.3600898" | |
| 123 | + ry="4.3600893" /><path | |
| 124 | + style="fill:#000000;stroke:#000000;stroke-width:5;stroke-dasharray:none" | |
| 125 | + d="M 24,2 V 24" | |
| 126 | + id="path2668" | |
| 127 | + sodipodi:nodetypes="cc" /><path | |
| 128 | + style="fill:none;stroke:#000000;stroke-width:5;stroke-dasharray:none" | |
| 129 | + d="M 14.807612,16.994936 24,26.187324 33.192388,16.994936" | |
| 130 | + id="path2626-8" | |
| 131 | + sodipodi:nodetypes="ccc" /></g><g | |
| 132 | + inkscape:groupmode="layer" | |
| 133 | + id="g2714" | |
| 134 | + inkscape:label="out" | |
| 135 | + style="display:none"><ellipse | |
| 136 | + style="fill:#000000;stroke:#000000;stroke-width:5.27982;stroke-dasharray:none" | |
| 137 | + id="ellipse2708" | |
| 138 | + cx="24" | |
| 139 | + cy="38.5" | |
| 140 | + rx="4.3600898" | |
| 141 | + ry="4.3600893" /><path | |
| 142 | + style="fill:#000000;stroke:#000000;stroke-width:5;stroke-dasharray:none" | |
| 143 | + d="M 24,29.722858 V 7.7228579" | |
| 144 | + id="path2710" | |
| 145 | + sodipodi:nodetypes="cc" /><path | |
| 146 | + style="display:inline;fill:none;stroke:#000000;stroke-width:5;stroke-dasharray:none" | |
| 147 | + d="M 33.192388,14.727922 24,5.5355339 14.807612,14.727922" | |
| 148 | + id="path2712" | |
| 149 | + sodipodi:nodetypes="ccc" /></g></svg> | |
| @@ -0,0 +1,218 @@ | ||
| 1 | +<?xml version="1.0" encoding="UTF-8" standalone="no"?> | |
| 2 | +<!-- Created with Inkscape (http://www.inkscape.org/) --> | |
| 3 | + | |
| 4 | +<svg | |
| 5 | + width="48" | |
| 6 | + height="48" | |
| 7 | + viewBox="0 0 48 48" | |
| 8 | + version="1.1" | |
| 9 | + id="svg2120" | |
| 10 | + xml:space="preserve" | |
| 11 | + inkscape:version="1.2.2 (732a01da63, 2022-12-09)" | |
| 12 | + sodipodi:docname="step-resume.svg" | |
| 13 | + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" | |
| 14 | + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" | |
| 15 | + xmlns:xlink="http://www.w3.org/1999/xlink" | |
| 16 | + xmlns="http://www.w3.org/2000/svg" | |
| 17 | + xmlns:svg="http://www.w3.org/2000/svg"><sodipodi:namedview | |
| 18 | + id="namedview2122" | |
| 19 | + pagecolor="#ffffff" | |
| 20 | + bordercolor="#000000" | |
| 21 | + borderopacity="0.25" | |
| 22 | + inkscape:showpageshadow="2" | |
| 23 | + inkscape:pageopacity="0.0" | |
| 24 | + inkscape:pagecheckerboard="0" | |
| 25 | + inkscape:deskcolor="#d1d1d1" | |
| 26 | + inkscape:document-units="px" | |
| 27 | + showgrid="false" | |
| 28 | + inkscape:zoom="11.315" | |
| 29 | + inkscape:cx="4.7282369" | |
| 30 | + inkscape:cy="24.259832" | |
| 31 | + inkscape:window-width="1920" | |
| 32 | + inkscape:window-height="992" | |
| 33 | + inkscape:window-x="-8" | |
| 34 | + inkscape:window-y="-8" | |
| 35 | + inkscape:window-maximized="1" | |
| 36 | + inkscape:current-layer="layer8" /><defs | |
| 37 | + id="defs2117"><inkscape:path-effect | |
| 38 | + effect="spiro" | |
| 39 | + id="path-effect2144" | |
| 40 | + is_visible="true" | |
| 41 | + lpeversion="1" /><inkscape:path-effect | |
| 42 | + effect="bspline" | |
| 43 | + id="path-effect2138" | |
| 44 | + is_visible="true" | |
| 45 | + lpeversion="1" | |
| 46 | + weight="33.333333" | |
| 47 | + steps="2" | |
| 48 | + helper_size="0" | |
| 49 | + apply_no_weight="true" | |
| 50 | + apply_with_weight="true" | |
| 51 | + only_selected="false" /></defs><g | |
| 52 | + inkscape:groupmode="layer" | |
| 53 | + id="layer4" | |
| 54 | + inkscape:label="img" | |
| 55 | + style="display:none"><image | |
| 56 | + width="305.68866" | |
| 57 | + height="70.374367" | |
| 58 | + preserveAspectRatio="none" | |
| 59 | + xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAARYAAABACAYAAADf7VgRAAAABHNCSVQICAgIfAhkiAAABdxJREFU | |
| 60 | +eJzt3T9vo0gYBvB3by+SsZRiKCI5SCkyJS6dzny3+26UpoSSFJFMOaTyUETaK3zDAQYbzGBgeH7S | |
| 61 | +SutN1kKj8cM7fxj/2u/3fwgAQKO/xr4AADAPggUAtEOwAIB2CBYA0A7BAgDaIVgAQDsECwBoh2AB | |
| 62 | +AO0QLACgHYIFALRDsACAdn+PfQFw5nkeWZZFRERSSvJ9f+Qrmg7XdSnLMorjeOxLgZYQLDBpruuS | |
| 63 | +4zj5a4TLPGAoBJNVDRXOOXHOR7wiaAvBApNUDRUF4TIPCBaYnKZQURAu04dggUm5FSoKwmXaECww | |
| 64 | +GW1DRUG4TNfDV4VUx1mtViSEICKiLMtIStn4f4qdJ8syOh6Pw14kPFxdqAghyLKs0jI8EeWvif7v | |
| 65 | +G1gtmpbBg8V1XbJtm4jKHYKoHBhCCIrjuDZkqr+HYDFLU6gEQUCe55X+3ff90p4fIoTLFA0WLI7j | |
| 66 | +kOu6rX/ftm2ybZuEEBRF0dUKBsxxLVSaIFymT/scC2OMdrtdp1Apsm2bPM8jzvlFhQPmybKs9PpW | |
| 67 | +qCi+71/cfKrvZQrXdYkxNvZldKK1YmGM0cfHR+3PhBAkpaTv728iIjqdTmTbNq1WK7Jtu3aYxBij | |
| 68 | +KIp0XuLoXNelz8/PzhWZZVnkuq5x1ZyqMDjnrUNFKVYuURQZOUTe7XZ5NR+GIaVpOvYltaItWCzL | |
| 69 | +qg0VNXdS1yDFf2OM0Xa7LQWMbdt3Vz5TVOwkQRC0DgjLsvK5BlPD5d5Jed/3yXEcI0OlOj+53W5n | |
| 70 | +Ey5ahkLFjq9IKelwOFAQBK0aIk1T8n3/ooOohp27aifZ7XathnrVtjUtbJU+wWBiqBDRRWWrwqXv | |
| 71 | +sKi40jYULRVLtaP3eTo3iiLKssy4/QlJkpSGfCpcrlUudYFNhAnKpZBSUhAEpZvQvZWLGkoXb9RS | |
| 72 | +SkqSZJD+1Lti4ZxfVBVhGPZ6zziOjfvwpGlKYRhe3IGaKpemUDkcDrMohUEPFS59KhfOOXmed/E5 | |
| 73 | +tSwr/5nuCqZ3xfL6+lp63bXjq0naJVDhUpxLqgsXhAoU9alcGGM3q3/VB3WeAdSrYqmO1aSUnTs+ | |
| 74 | +Yyyf0Kz+MVFT5XILQmXZ7q1cmlZpqyzL6vQ4xS29KpZqtZIkSa+LWYq6yuUahIo56irRLqr95Vrl | |
| 75 | +0nUksNlstE2EawsWKaVx8yJDahsuCBWzDLEa0xQu6/W68/vo0itYdIzJumyIMs2tcEGoQFt1/We1 | |
| 76 | +WvV+j3vhzNuRNYULQsVMfTc2Nn346/qLEKLTtg2d+4FGOzbhdDrlz3aYtIv0HtVwQaiYq0+V33W1 | |
| 77 | +UJ0U0LYS0fms1cOCpelQHjzNfKbCRf0dzqSUed9Ych+5ZwuClJLCMGy1MqQevdHl136//6Pt3Rqo | |
| 78 | +Z2SuMfUhMoC++u5runXSngognTe0wSuWup25dd7f3/MnoAHgTMdmyTiOKUmSh27pHzRY1Jbhtr/7 | |
| 79 | ++vqKJWuA/+jcga022FWP+RzKpFaFlrK1vw5jjNbrdX4W8K1zgJdiye1S9xR734n9R7XdoMFS3Zl7 | |
| 80 | +i6nb+K+pOxyLcz7IuHdO0C7necfi8GVOq4W/397e/hnqzdfrNb28vLT+fSklfX19DXU5k8M5p+12 | |
| 81 | +W/uzp6enfGl+Lp1JF7TL2c/PD6VpSs/Pz7ML00G/V0h9vUdbSylxidrPPy3t7F+0S5maG5lTqBAN | |
| 82 | +HCzFPQhtLGnitsspcCaeGNcE7WKGwb8Jse3ZrkKI2aVyH13mk+oOGzcV2sUMgweLmmy7Fi5dT2ef | |
| 83 | +O3wY6qFdzPGQ5WZ1ULY6LU4dEHU8HilJkkVVKvda0vxTF2iXaXroPpYlzaFco+ae2t6hl/LhQbuY | |
| 84 | +Y/ChENTrctrekk7mQ7uYAcEykjiOW91xl3YyH9rFDAiWEd1aMRNC9P4qlTlCu8zfQ45NgOscx6HN | |
| 85 | +ZlOa1M6ybPF3ZLTLfCFYAEA7DIUAQDsECwBoh2ABAO0QLACgHYIFALRDsACAdggWANAOwQIA2iFY | |
| 86 | +AEA7BAsAaIdgAQDt/gUoDXNStc/rMQAAAABJRU5ErkJggg== | |
| 87 | +" | |
| 88 | + id="image2132" | |
| 89 | + x="-82" | |
| 90 | + y="-11.9" /><image | |
| 91 | + width="460" | |
| 92 | + height="71" | |
| 93 | + preserveAspectRatio="none" | |
| 94 | + xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAcwAAABHCAYAAACK23cpAAAABHNCSVQICAgIfAhkiAAACuZJREFU | |
| 95 | +eJzt3T9o3FgeB/Cv77aZBHInYZPD+JLF4iDxDl44ZNiAvVfNhC2vCEqxWQJpJn3MwlRXTeP0drNF | |
| 96 | +NtfIKa5cYnVxkQMLDoztbU6GgBkwDiPOEHuOK/aK7FM0Gs3M01gzkp6+HzBknPFEkSV93z/9NLO6 | |
| 97 | +uvoLiIiIaKjfZL0BRERERcDAJCIiksDAJCIiksDAJCIiksDAJCIiksDAJCIiksDAJCIiksDAJCIi | |
| 98 | +ksDAJCIiksDAJCIiksDAJCIiksDAJCIikvBZ1htAlIZmswlN0wAAvu+j1WplvEX5YVkWOp0OHMfJ | |
| 99 | +elMoBYuLi8Gfj4+PM9yS8mFgEinMsiyYphm8ZmgSjY9DskSKioZlvV5HrVbLcIuoyEzTDEZxyoqB | |
| 100 | +SaSgaFgKDE0aR6PRCI6pMocmA5NIMYPCUmBoUhKNRgOGYQD4eOyUOTQZmEQKGRWWAkOTZPm+3/O6 | |
| 101 | +zKHJwCRShGxYCgxNkmHbNlzX7fleWUNzqqtk5xaWMbuwHLx+d+Tg4vw0eH3txs2e19RPXBA1TQuW | |
| 102 | +lHc6nb5WYFj4ouj7ft/BT8UXF5ae50HX9Z7bbQD0XOTq9ToArp6l4WzbBoC+RWQA4Lru0OuPSqYa | |
| 103 | +mLMLy1i69yh4/f5kvycgv3nyI47evuwL0jKzLCuYPxjWmvM8D47jxIanOLDF+xiYahkUlltbW2g2 | |
| 104 | +mz3fb7VaPfesAgxNksPQzOGQ7NK9R/jLgw3MhXqiZWSaJjY2NoJhj1FDH4ZhBCvZyjZMUmbDwnKQ | |
| 105 | +VqsV26ji8CyNUvbh2VwWLrh24ya+frCBs5N9uK+fl6q3aRgGarVa0Ksc5+ebzSZ2dnZK0+ors06n | |
| 106 | +0/N6VFgKcT1NVY8Vy7Lgui48z8t6Uwpr1HVpUE9T0zQ8ffp0rDAVo2F5GhHLXQ8zbG5hGd88+RG3 | |
| 107 | +l2q4duNm1pszcaKXGHdQioPHtm3Yto2tra2hoViv15XsbY77f9I0DY1GQ7n94TgOdnZ2AMiHpRDu | |
| 108 | +acb1HFTQaDRgmmbP1AYlI86dUfsvrqfp+z42NzfHaowZhgHLsnI18pHLHmaUef+Z8r1NcVBGibnJ | |
| 109 | +uNZx+Hvi4AofrOJ7qhAnrWEYiU5CTdOCuTzLsmDbtlK9Kcdxxl7M1Wq1YJqmkmEZnf8Xv3v2NJMJ | |
| 110 | +r4GQfW+4IS9Cc9yeZp7m2H9769atv03rH5tbWMbcH78MXkcX94QXBEVdv3ETf/rzXwEAF+en+N9/ | |
| 111 | +P0xuQ6csfEEXfN/HixcvgovhKL7vY3d3F7quY35+Pvi+rut97yvixdGyLFSrVQBApVJBtVrFwcEB | |
| 112 | +ut0uAGBtbQ2VSgUA0O12sbu7C6B/34r9U8R9MEy73Y79/qD9IvOzRddut1GtVoP/f6VSgWEYaLfb | |
| 113 | +V2owaZqGSqUSHHvTNu1h9MePH/e89jwPvu/3fIWvM4Zh4PLyEr7vB/uo2+3i4OAA8/PzfT876Cv6 | |
| 114 | +mb7vZ36s5raHeXF+ig/np32Lf5buPcLnX9Thvn6Os5P9jLYuXdFe4FWetmHbNjqdTqJWYRG4rgvD | |
| 115 | +MIKLhZgbGdbTjGuIAPloqdLkxfVsxu1pip8LD0v6vo+9vb1SHU+Dhv3j6hYD/T3NJFMG0Tl2y7KC | |
| 116 | +sM5KbucwP5yf4s2r9dhhWLEo6OsHG4Wf24ybSBfLt8cVntdShed5fUOpwxYUDArLra0tDsmVSNwc | |
| 117 | +Wlz4DVOr1dBsNvver2ka6vV634W9jCaxejauMTzusG5acjske3F+indHDv5zdox//+sfuP67P+D3 | |
| 118 | +c70HrBimFUO0RRymffjwYTBkBCS/oNdqteDADH/put43HAsUd0gWQDAkYxhGzzBbtVrtOYkqlQrW | |
| 119 | +1tb6fr6MYSkzJKs6MRw4zvCszDoAcQxOa99Oe0g2PFo17PpxeHjYNyUUNzwra9DvLTodM0257WFG | |
| 120 | +ua+f482r9dhFP+b9ZzDvPytcbzN6f6Xv+4kv6GIRTNyXigb1NEcpY1jSJ+P2NOMW4sXRNC1RWUJV | |
| 121 | +pd3THPZ7y6Knmds5zDhnJ/v46YfvcPerb/H5F/WegBS3oBSpUlD0BNvb28toS4pFhKbsScOwVEfc | |
| 122 | +MHsS0eNl2Jxm0kanqquNk0q7IpCYK15ZWQl+f4ZhBPt7mnOahelhhv38z78PXPRTpEpBKysrwZ99 | |
| 123 | +3y/V4oGriutpxmFYqkWMyoz7Negz43qaSXswcVMgZZV2T9NxnL4Ohfi8aSpUDzPsQwF6kKOMuxI2 | |
| 124 | +LMmqM9WM6mkyLEnWoIVjV/2MMptETzNq2o2UQgbm3a++HXjP5rsjB+7r51PeIsrKoNBkWKrpqsNv | |
| 125 | +g0It7ngRTwOSxeHYfmmFZtziK3HuT1OhAnNuYRl37z2KHW49O9nHz29fKnNv5iDiwPN9P6gjqlLV | |
| 126 | +mnFEQ5Nhqa6rjMokvdVIPPlHtucYretLH101NEXJ0LCkZSDTUpjANO8/w+2l+JqC7uvneHek9vyf | |
| 127 | +uH0kSnYuT3Xh1ibD8pNOp8OGFca7L9f3fdi2LbVSVpSwpHjjhmZcydCkBRDSlPvAvL1Ug3n/Wezf | |
| 128 | +qV5fVhhW+Fg8nUTV4tlJMCj7lXmOW7hKEQvP87CzszO0chYX7MlJGppiMVaYaMRkJbeBef3Xaj5x | |
| 129 | +w68X56dKlcYbRvZRX/V6PfOyUUR5k0bFJ8dx4LouS+OlIGloRhf1ZF08P7eBee3GzdhCBGVa1CNK | |
| 130 | +b8m+1zRNnrxEv0qzPKIYBhTzmWVqmIbncXVdly7mkITMU07ysDYht4EZVZZFPVdhGEZpA1MUZtc0 | |
| 131 | +DcfHx8GCjbIr836JK2l31YtuWfZdWKfT6SleP6nbZ4aFpmEYmYclUJDALMOinjhJb8pVtRzeMHEr | |
| 132 | +6IBPcx15OMmywP2CYOW0OC/y0EMpItu2p1b0fFBo5mV9Rm6LrwMfe5VvXq3j/QR6lbOzs7i4uEj9 | |
| 133 | +c9Ok63rwDEgZ4pmYZVGr1QYWxq5UKkGDI+n9dEXH/fJRt9uF53mYn5/H9va2MmE57eLr3W4X7XYb | |
| 134 | +mqZhZmam52ERk3CVgu2Tlsse5qQX9czOzuL777/H+vr6RD4/LUlP8DLdByY7v1uv16debzJL3C+9 | |
| 135 | +srwFQSWe5020wSHzPM08yF0t2XdHDn764buJhuWTJ08m8tlpE08el1Wm+ctRj1wa971Fx/1CRTSJ | |
| 136 | +52lOwlR7mO9P9nH09mXwOloP9s2r9Yku6hFhOTs7O7F/I23RJ8YPMukWYN4kma8VC1/y1FKdFO4X | |
| 137 | +Kqq0a89OwlQD8+xkf2ggMiz7iUUawx5llVWZqKzkqcWZJ9wvVHR5D81czmGmrahhKXieh1arFRQx | |
| 138 | +0HUdmqbBdV24rluqnuW4sj7R8or7hfImz6GpfGAWPSzDHMcp1TzlIGJuV7ZHVZZQ4H4hVeQ1NHO3 | |
| 139 | +6CdNKoUl9Yo+TDat9xYd9wupIo8LgZQNTIal2hzHkWpllq0wNvcLqWRQaGZVpEXJwGRYlsPm5ubQ | |
| 140 | +cMjiAbN5wP1CKomGpud5mVX+mWqln2lIEpZsYRdbt9vF7u4ufN9HpVIJqpC4rouDgwNsb2+Xcp6O | |
| 141 | ++0Vt0670kweHh4fQdR2Xl5eZ3hEws7q6+ktm/3rKkvYs817ph4iI8kOZVbKi3B0REdEkKNXDJCIi | |
| 142 | +mhQlF/0QERGljYFJREQkgYFJREQkgYFJREQkgYFJREQkgYFJREQkgYFJREQkgYFJREQkgYFJREQk | |
| 143 | +4bPFxcWst4GIiCj32MMkIiKSMHPnzh3WkiUiIhqBPUwiIiIJDEwiIiIJDEwiIiIJ/weH8+Ed/xCz | |
| 144 | +fAAAAABJRU5ErkJggg== | |
| 145 | +" | |
| 146 | + id="image2845" | |
| 147 | + x="-15.489594" | |
| 148 | + y="-8.2945251" /></g><g | |
| 149 | + inkscape:groupmode="layer" | |
| 150 | + id="layer5" | |
| 151 | + inkscape:label="dot" /><g | |
| 152 | + inkscape:label="over" | |
| 153 | + inkscape:groupmode="layer" | |
| 154 | + id="layer1" | |
| 155 | + style="display:none"><ellipse | |
| 156 | + style="fill:#000000;stroke:#000000;stroke-width:5.27982;stroke-dasharray:none" | |
| 157 | + id="path2637" | |
| 158 | + cx="24" | |
| 159 | + cy="33" | |
| 160 | + rx="4.3600898" | |
| 161 | + ry="4.3600893" /><path | |
| 162 | + style="fill:none;stroke:#000000;stroke-width:5;stroke-dasharray:none" | |
| 163 | + d="M 9,24 C 9.7590836,20.626295 11.695032,17.529367 14.395314,15.369142 17.095595,13.208917 20.541953,12 24,12 c 3.458047,0 6.904405,1.208917 9.604686,3.369142 C 36.304968,17.529367 38.240916,20.626295 39,24" | |
| 164 | + id="path2142" | |
| 165 | + inkscape:path-effect="#path-effect2144" | |
| 166 | + inkscape:original-d="m 9,24 c 4.959859,-3.824406 10.021901,-8.173595 15,-12 4.978099,-3.8264055 10.285024,8.398237 15,12" | |
| 167 | + sodipodi:nodetypes="csc" /><path | |
| 168 | + style="fill:none;stroke:#000000;stroke-width:5;stroke-dasharray:none" | |
| 169 | + d="M 26,22 H 39 V 9" | |
| 170 | + id="path2626" | |
| 171 | + sodipodi:nodetypes="ccc" /></g><g | |
| 172 | + inkscape:groupmode="layer" | |
| 173 | + id="layer6" | |
| 174 | + inkscape:label="into" | |
| 175 | + style="display:none"><ellipse | |
| 176 | + style="fill:#000000;stroke:#000000;stroke-width:5.27982;stroke-dasharray:none" | |
| 177 | + id="path2637-3" | |
| 178 | + cx="24" | |
| 179 | + cy="38.5" | |
| 180 | + rx="4.3600898" | |
| 181 | + ry="4.3600893" /><path | |
| 182 | + style="fill:#000000;stroke:#000000;stroke-width:5;stroke-dasharray:none" | |
| 183 | + d="M 24,2 V 24" | |
| 184 | + id="path2668" | |
| 185 | + sodipodi:nodetypes="cc" /><path | |
| 186 | + style="fill:none;stroke:#000000;stroke-width:5;stroke-dasharray:none" | |
| 187 | + d="M 14.807612,16.994936 24,26.187324 33.192388,16.994936" | |
| 188 | + id="path2626-8" | |
| 189 | + sodipodi:nodetypes="ccc" /></g><g | |
| 190 | + inkscape:groupmode="layer" | |
| 191 | + id="g2714" | |
| 192 | + inkscape:label="out" | |
| 193 | + style="display:none"><ellipse | |
| 194 | + style="fill:#000000;stroke:#000000;stroke-width:5.27982;stroke-dasharray:none" | |
| 195 | + id="ellipse2708" | |
| 196 | + cx="24" | |
| 197 | + cy="38.5" | |
| 198 | + rx="4.3600898" | |
| 199 | + ry="4.3600893" /><path | |
| 200 | + style="fill:#000000;stroke:#000000;stroke-width:5;stroke-dasharray:none" | |
| 201 | + d="M 24,29.722858 V 7.7228579" | |
| 202 | + id="path2710" | |
| 203 | + sodipodi:nodetypes="cc" /><path | |
| 204 | + style="display:inline;fill:none;stroke:#000000;stroke-width:5;stroke-dasharray:none" | |
| 205 | + d="M 33.192388,14.727922 24,5.5355339 14.807612,14.727922" | |
| 206 | + id="path2712" | |
| 207 | + sodipodi:nodetypes="ccc" /></g><g | |
| 208 | + inkscape:groupmode="layer" | |
| 209 | + id="layer8" | |
| 210 | + inkscape:label="resume"><path | |
| 211 | + style="fill:#000000;stroke:#000000;stroke-width:5;stroke-dasharray:none" | |
| 212 | + d="M 13,12 V 38" | |
| 213 | + id="path2850" | |
| 214 | + sodipodi:nodetypes="cc" /><path | |
| 215 | + style="fill:none;stroke:#000000;stroke-width:5;stroke-dasharray:none" | |
| 216 | + d="M 21,16 V 34 L 36.5,25 Z" | |
| 217 | + id="path2852" | |
| 218 | + sodipodi:nodetypes="cccc" /></g></svg> | |
| @@ -4268,6 +4268,16 @@ | ||
| 4268 | 4268 | </div> |
| 4269 | 4269 | </div> |
| 4270 | 4270 | </div> |
| 4271 | + <div title="Determines which keys select an item from the AutoComplete suggestions"> | |
| 4272 | + <label data-i18n="Keyboard"> | |
| 4273 | + <small>Keyboard:</small> | |
| 4274 | + </label> | |
| 4275 | + <select id="stscript_autocomplete_select"> | |
| 4276 | + <option value="3" data-i18n="Select with Tab or Enter">Select with Tab or Enter</option> | |
| 4277 | + <option value="1" data-i18n="Select with Tab">Select with Tab</option> | |
| 4278 | + <option value="2" data-i18n="Select with Enter">Select with Enter</option> | |
| 4279 | + </select> | |
| 4280 | + </div> | |
| 4271 | 4281 | <div class="flex-container flexFlowColumn gap0" title="Sets the font size of the autocomplete." data-i18n="[title]Sets the font size of the autocomplete."> |
| 4272 | 4282 | <label for="stscript_autocomplete_font_scale"><small>Font Scale</small></label> |
| 4273 | 4283 | <input class="neo-range-slider" type="range" id="stscript_autocomplete_font_scale" min="0.5" max="2" step="0.01"> |
| @@ -16,8 +16,15 @@ export const AUTOCOMPLETE_WIDTH = { | ||
| 16 | 16 | 'FULL': 2, |
| 17 | 17 | }; |
| 18 | 18 | |
| 19 | +/**@readonly*/ | |
| 20 | +/**@enum {Number}*/ | |
| 21 | +export const AUTOCOMPLETE_SELECT_KEY = { | |
| 22 | + 'TAB': 1, // 2^0 | |
| 23 | + 'ENTER': 2, // 2^1 | |
| 24 | +}; | |
| 25 | + | |
| 19 | 26 | export class AutoComplete { |
| 20 | 27 | /**@type {HTMLTextAreaElement|HTMLInputElement}*/ textarea; |
| 21 | 28 | /**@type {boolean}*/ isFloating = false; |
| 22 | 29 | /**@type {()=>boolean}*/ checkIfActivate; |
| 23 | 30 | /**@type {(text:string, index:number) => Promise<AutoCompleteNameResult>}*/ getNameAt; |
| @@ -56,6 +63,8 @@ export class AutoComplete { | ||
| 56 | 63 | /**@type {function}*/ updateDetailsPositionDebounced; |
| 57 | 64 | /**@type {function}*/ updateFloatingPositionDebounced; |
| 58 | 65 | |
| 66 | + /**@type {(item:AutoCompleteOption)=>any}*/ onSelect; | |
| 67 | + | |
| 59 | 68 | get matchType() { |
| 60 | 69 | return power_user.stscript.matching ?? 'fuzzy'; |
| 61 | 70 | } |
| @@ -68,7 +77,7 @@ export class AutoComplete { | ||
| 68 | 77 | |
| 69 | 78 | |
| 70 | 79 | /** |
| 71 | 80 | * @param {HTMLTextAreaElement|HTMLInputElement} textarea The textarea to receive autocomplete. |
| 72 | 81 | * @param {() => boolean} checkIfActivate Function should return true only if under the current conditions, autocomplete should display (e.g., for slash commands: autoComplete.text[0] == '/') |
| 73 | 82 | * @param {(text: string, index: number) => Promise<AutoCompleteNameResult>} getNameAt Function should return (unfiltered, matching against input is done in AutoComplete) information about name options at index in text. |
| 74 | 83 | * @param {boolean} isFloating Whether autocomplete should float at the keyboard cursor. |
| @@ -102,10 +111,15 @@ export class AutoComplete { | ||
| 102 | 111 | this.updateDetailsPositionDebounced = debounce(this.updateDetailsPosition.bind(this), 10); |
| 103 | 112 | this.updateFloatingPositionDebounced = debounce(this.updateFloatingPosition.bind(this), 10); |
| 104 | 113 | |
| 105 | - textarea.addEventListener('input', ()=>this.text != this.textarea.value && this.show(true, this.wasForced)); | |
| 114 | + textarea.addEventListener('input', ()=>{ | |
| 115 | + this.selectionStart = this.textarea.selectionStart; | |
| 116 | + if (this.text != this.textarea.value) this.show(true, this.wasForced); | |
| 117 | + }); | |
| 106 | 118 | textarea.addEventListener('keydown', (evt)=>this.handleKeyDown(evt)); |
| 107 | 119 | textarea.addEventListener('click', ()=>this.isActive ? this.show() : null);{ |
| 108 | - textarea.addEventListener('selectionchange', ()=>this.show()); | |
| 120 | + this.selectionStart = this.textarea.selectionStart; | |
| 121 | + if (this.isActive) this.show(); | |
| 122 | + }); | |
| 109 | 123 | textarea.addEventListener('blur', ()=>this.hide()); |
| 110 | 124 | if (isFloating) { |
| 111 | 125 | textarea.addEventListener('scroll', ()=>this.updateFloatingPositionDebounced()); |
| @@ -189,6 +203,11 @@ export class AutoComplete { | ||
| 189 | 203 | * @returns The option. |
| 190 | 204 | */ |
| 191 | 205 | fuzzyScore(option) { |
| 206 | + // might have been matched by the options matchProvider function instead | |
| 207 | + if (!this.fuzzyRegex.test(option.name)) { | |
| 208 | + option.score = new AutoCompleteFuzzyScore(Number.MAX_SAFE_INTEGER, -1); | |
| 209 | + return option; | |
| 210 | + } | |
| 192 | 211 | const parts = this.fuzzyRegex.exec(option.name).slice(1, -1); |
| 193 | 212 | let start = null; |
| 194 | 213 | let consecutive = []; |
| @@ -339,7 +358,7 @@ export class AutoComplete { | ||
| 339 | 358 | |
| 340 | 359 | this.result = this.effectiveParserResult.optionList |
| 341 | 360 | // filter the list of options by the partial name according to the matching type |
| 342 | 361 | .filter(it => this.isReplaceable || it.name == '' ? (it.matchProvider ? it.matchProvider(this.name) : matchers[this.matchType](it.name)) : it.name.toLowerCase() == this.name) |
| 343 | 362 | // remove aliases |
| 344 | 363 | .filter((it,idx,list) => list.findIndex(opt=>opt.value == it.value) == idx); |
| 345 | 364 | |
| @@ -357,10 +376,11 @@ export class AutoComplete { | ||
| 357 | 376 | // build element |
| 358 | 377 | option.dom = this.makeItem(option); |
| 359 | 378 | // update replacer and add quotes if necessary |
| 379 | + const optionName = option.valueProvider ? option.valueProvider(this.name) : option.name; | |
| 360 | 380 | if (this.effectiveParserResult.canBeQuoted) { |
| 361 | 381 | option.replacer = option.nameoptionName.includes(' ') || this.startQuote || this.endQuote ? `"${optionoptionName.namereplace(/"/g, '\\"')}"` : `${option.nameoptionName}`; |
| 362 | 382 | } else { |
| 363 | 383 | option.replacer = option.nameoptionName; |
| 364 | 384 | } |
| 365 | 385 | // calculate fuzzy score if matching is fuzzy |
| 366 | 386 | if (this.matchType == 'fuzzy') this.fuzzyScore(option); |
| @@ -399,7 +419,7 @@ export class AutoComplete { | ||
| 399 | 419 | , |
| 400 | 420 | ); |
| 401 | 421 | this.result.push(option); |
| 402 | 422 | } else if (this.result.length == 1 && this.effectiveParserResult && this.effectiveParserResult != this.secondaryParserResult && this.result[0].name == this.effectiveParserResult.name) { |
| 403 | 423 | // only one result that is exactly the current value? just show hint, no autocomplete |
| 404 | 424 | this.isReplaceable = false; |
| 405 | 425 | this.isShowingDetails = false; |
| @@ -439,11 +459,14 @@ export class AutoComplete { | ||
| 439 | 459 | } else { |
| 440 | 460 | item.dom.classList.remove('selected'); |
| 441 | 461 | } |
| 462 | + if (!item.isSelectable) { | |
| 463 | + item.dom.classList.add('not-selectable'); | |
| 464 | + } | |
| 442 | 465 | frag.append(item.dom); |
| 443 | 466 | } |
| 444 | 467 | this.dom.append(frag); |
| 445 | 468 | this.updatePosition(); |
| 446 | 469 | getTopmostModalLayerthis.getLayer().append(this.domWrap); |
| 447 | 470 | } else { |
| 448 | 471 | this.domWrap.remove(); |
| 449 | 472 | } |
| @@ -458,10 +481,17 @@ export class AutoComplete { | ||
| 458 | 481 | if (!this.isShowingDetails && this.isReplaceable) return this.detailsWrap.remove(); |
| 459 | 482 | this.detailsDom.innerHTML = ''; |
| 460 | 483 | this.detailsDom.append(this.selectedItem?.renderDetails() ?? 'NO ITEM'); |
| 461 | 484 | getTopmostModalLayerthis.getLayer().append(this.detailsWrap); |
| 462 | 485 | this.updateDetailsPositionDebounced(); |
| 463 | 486 | } |
| 464 | 487 | |
| 488 | + /** | |
| 489 | + * @returns {HTMLElement} closest ancestor dialog or body | |
| 490 | + */ | |
| 491 | + getLayer() { | |
| 492 | + return this.textarea.closest('dialog, body'); | |
| 493 | + } | |
| 494 | + | |
| 465 | 495 | |
| 466 | 496 | |
| 467 | 497 | /** |
| @@ -474,7 +504,7 @@ export class AutoComplete { | ||
| 474 | 504 | const rect = {}; |
| 475 | 505 | rect[AUTOCOMPLETE_WIDTH.INPUT] = this.textarea.getBoundingClientRect(); |
| 476 | 506 | rect[AUTOCOMPLETE_WIDTH.CHAT] = document.querySelector('#sheld').getBoundingClientRect(); |
| 477 | 507 | rect[AUTOCOMPLETE_WIDTH.FULL] = getTopmostModalLayerthis.getLayer().getBoundingClientRect(); |
| 478 | 508 | this.domWrap.style.setProperty('--bottom', `${window.innerHeight - rect[AUTOCOMPLETE_WIDTH.INPUT].top}px`); |
| 479 | 509 | this.dom.style.setProperty('--bottom', `${window.innerHeight - rect[AUTOCOMPLETE_WIDTH.INPUT].top}px`); |
| 480 | 510 | this.domWrap.style.bottom = `${window.innerHeight - rect[AUTOCOMPLETE_WIDTH.INPUT].top}px`; |
| @@ -501,7 +531,7 @@ export class AutoComplete { | ||
| 501 | 531 | const rect = {}; |
| 502 | 532 | rect[AUTOCOMPLETE_WIDTH.INPUT] = this.textarea.getBoundingClientRect(); |
| 503 | 533 | rect[AUTOCOMPLETE_WIDTH.CHAT] = document.querySelector('#sheld').getBoundingClientRect(); |
| 504 | 534 | rect[AUTOCOMPLETE_WIDTH.FULL] = getTopmostModalLayerthis.getLayer().getBoundingClientRect(); |
| 505 | 535 | if (this.isReplaceable) { |
| 506 | 536 | this.detailsWrap.classList.remove('full'); |
| 507 | 537 | const selRect = this.selectedItem.dom.children[0].getBoundingClientRect(); |
| @@ -527,32 +557,34 @@ export class AutoComplete { | ||
| 527 | 557 | updateFloatingPosition() { |
| 528 | 558 | const location = this.getCursorPosition(); |
| 529 | 559 | const rect = this.textarea.getBoundingClientRect(); |
| 560 | + const layerRect = this.getLayer().getBoundingClientRect(); | |
| 530 | 561 | // cursor is out of view -> hide |
| 531 | 562 | if (location.bottom < rect.top || location.top > rect.bottom || location.left < rect.left || location.left > rect.right) { |
| 532 | 563 | return this.hide(); |
| 533 | 564 | } |
| 534 | 565 | const left = Math.max(rect.left, location.left) - layerRect.left; |
| 535 | 566 | this.domWrap.style.setProperty('--targetOffset', `${left}`); |
| 536 | 567 | if (location.top <= window.innerHeight / 2) { |
| 537 | 568 | // if cursor is in lower half of window, show list above line |
| 538 | 569 | this.domWrap.style.top = `${location.bottom - layerRect.top}px`; |
| 539 | 570 | this.domWrap.style.bottom = 'auto'; |
| 540 | 571 | this.domWrap.style.maxHeight = `calc(${location.bottom - layerRect.top}px - ${this.textarea.closest('dialog') ? '0' : '1vh'})`; |
| 541 | 572 | } else { |
| 542 | 573 | // if cursor is in upper half of window, show list below line |
| 543 | 574 | this.domWrap.style.top = 'auto'; |
| 544 | 575 | this.domWrap.style.bottom = `calc(100vh${layerRect.height}px - ${location.top - layerRect.top}px)`; |
| 545 | 576 | this.domWrap.style.maxHeight = `calc(${location.top - layerRect.top}px - ${this.textarea.closest('dialog') ? '0' : '1vh'})`; |
| 546 | 577 | } |
| 547 | 578 | } |
| 548 | 579 | |
| 549 | 580 | updateFloatingDetailsPosition(location = null) { |
| 550 | 581 | if (!location) location = this.getCursorPosition(); |
| 551 | 582 | const rect = this.textarea.getBoundingClientRect(); |
| 583 | + const layerRect = this.getLayer().getBoundingClientRect(); | |
| 552 | 584 | if (location.bottom < rect.top || location.top > rect.bottom || location.left < rect.left || location.left > rect.right) { |
| 553 | 585 | return this.hide(); |
| 554 | 586 | } |
| 555 | 587 | const left = Math.max(rect.left, location.left) - layerRect.left; |
| 556 | 588 | this.detailsWrap.style.setProperty('--targetOffset', `${left}`); |
| 557 | 589 | if (this.isReplaceable) { |
| 558 | 590 | this.detailsWrap.classList.remove('full'); |
| @@ -572,14 +604,14 @@ export class AutoComplete { | ||
| 572 | 604 | } |
| 573 | 605 | if (location.top <= window.innerHeight / 2) { |
| 574 | 606 | // if cursor is in lower half of window, show list above line |
| 575 | 607 | this.detailsWrap.style.top = `${location.bottom - layerRect.top}px`; |
| 576 | 608 | this.detailsWrap.style.bottom = 'auto'; |
| 577 | 609 | this.detailsWrap.style.maxHeight = `calc(${location.bottom - layerRect.top}px - ${this.textarea.closest('dialog') ? '0' : '1vh'})`; |
| 578 | 610 | } else { |
| 579 | 611 | // if cursor is in upper half of window, show list below line |
| 580 | 612 | this.detailsWrap.style.top = 'auto'; |
| 581 | 613 | this.detailsWrap.style.bottom = `calc(100vh${layerRect.height}px - ${location.top - layerRect.top}px)`; |
| 582 | 614 | this.detailsWrap.style.maxHeight = `calc(${location.top - layerRect.top}px - ${this.textarea.closest('dialog') ? '0' : '1vh'})`; |
| 583 | 615 | } |
| 584 | 616 | } |
| 585 | 617 | |
| @@ -597,7 +629,7 @@ export class AutoComplete { | ||
| 597 | 629 | } |
| 598 | 630 | this.clone.style.position = 'fixed'; |
| 599 | 631 | this.clone.style.visibility = 'hidden'; |
| 600 | 632 | getTopmostModalLayer()document.body.append(this.clone); |
| 601 | 633 | const mo = new MutationObserver(muts=>{ |
| 602 | 634 | if (muts.find(it=>Array.from(it.removedNodes).includes(this.textarea))) { |
| 603 | 635 | this.clone.remove(); |
| @@ -656,6 +688,7 @@ export class AutoComplete { | ||
| 656 | 688 | } |
| 657 | 689 | this.wasForced = false; |
| 658 | 690 | this.textarea.dispatchEvent(new Event('input', { bubbles:true })); |
| 691 | + this.onSelect?.(this.selectedItem); | |
| 659 | 692 | } |
| 660 | 693 | |
| 661 | 694 | |
| @@ -708,8 +741,10 @@ export class AutoComplete { | ||
| 708 | 741 | } |
| 709 | 742 | case 'Enter': { |
| 710 | 743 | // pick the selected item to autocomplete |
| 744 | + if ((power_user.stscript.autocomplete.select & AUTOCOMPLETE_SELECT_KEY.ENTER) != AUTOCOMPLETE_SELECT_KEY.ENTER) break; | |
| 711 | 745 | if (evt.ctrlKey || evt.altKey || evt.shiftKey || this.selectedItem.value == '') break; |
| 712 | 746 | if (this.selectedItem.name == this.name) break; |
| 747 | + if (!this.selectedItem.isSelectable) break; | |
| 713 | 748 | evt.preventDefault(); |
| 714 | 749 | evt.stopImmediatePropagation(); |
| 715 | 750 | this.select(); |
| @@ -717,9 +752,11 @@ export class AutoComplete { | ||
| 717 | 752 | } |
| 718 | 753 | case 'Tab': { |
| 719 | 754 | // pick the selected item to autocomplete |
| 755 | + if ((power_user.stscript.autocomplete.select & AUTOCOMPLETE_SELECT_KEY.TAB) != AUTOCOMPLETE_SELECT_KEY.TAB) break; | |
| 720 | 756 | if (evt.ctrlKey || evt.altKey || evt.shiftKey || this.selectedItem.value == '') break; |
| 721 | 757 | evt.preventDefault(); |
| 722 | 758 | evt.stopImmediatePropagation(); |
| 759 | + if (!this.selectedItem.isSelectable) break; | |
| 723 | 760 | this.select(); |
| 724 | 761 | return; |
| 725 | 762 | } |
| @@ -772,13 +809,7 @@ export class AutoComplete { | ||
| 772 | 809 | // ignore keydown on modifier keys |
| 773 | 810 | return; |
| 774 | 811 | } |
| 775 | - switch (evt.key) { | |
| 812 | + // await keyup to see if cursor position or text has changed | |
| 776 | - case 'ArrowUp': | |
| 777 | - case 'ArrowDown': | |
| 778 | - case 'ArrowRight': | |
| 779 | - case 'ArrowLeft': { | |
| 780 | - if (this.isActive) { | |
| 781 | - // keyboard navigation, wait for keyup to complete cursor move | |
| 782 | 813 | const oldText = this.textarea.value; |
| 783 | 814 | await new Promise(resolve=>{ |
| 784 | 815 | window.addEventListener('keyup', resolve, { once:true }); |
| @@ -786,16 +817,8 @@ export class AutoComplete { | ||
| 786 | 817 | if (this.selectionStart != this.textarea.selectionStart) { |
| 787 | 818 | this.selectionStart = this.textarea.selectionStart; |
| 788 | 819 | this.show(this.isReplaceable || oldText != this.textarea.value); |
| 789 | - } | |
| 820 | + } else if (this.isActive) { | |
| 790 | - } | |
| 791 | - break; | |
| 792 | - } | |
| 793 | - default: { | |
| 794 | - if (this.isActive) { | |
| 795 | 821 | this.text != this.textarea.value && this.show(this.isReplaceable); |
| 796 | 822 | } |
| 797 | - break; | |
| 798 | - } | |
| 799 | - } | |
| 800 | 823 | } |
| 801 | 824 | } |
| @@ -1,36 +1,9 @@ | ||
| 1 | 1 | import { SlashCommandNamedArgumentAutoCompleteOptionAutoCompleteNameResultBase } from '../slash-commands/SlashCommandNamedArgumentAutoCompleteOptionAutoCompleteNameResultBase.js'; |
| 2 | 2 | import { AutoCompleteOptionAutoCompleteSecondaryNameResult } from './AutoCompleteOptionAutoCompleteSecondaryNameResult.js'; |
| 3 | -// import { AutoCompleteSecondaryNameResult } from './AutoCompleteSecondaryNameResult.js'; | |
| 4 | 3 | |
| 5 | 4 | |
| 6 | 5 | |
| 7 | 6 | export class AutoCompleteNameResult extends AutoCompleteNameResultBase { |
| 8 | - /**@type {string} */ name; | |
| 9 | - /**@type {number} */ start; | |
| 10 | - /**@type {AutoCompleteOption[]} */ optionList = []; | |
| 11 | - /**@type {boolean} */ canBeQuoted = false; | |
| 12 | - /**@type {()=>string} */ makeNoMatchText = ()=>`No matches found for "${this.name}"`; | |
| 13 | - /**@type {()=>string} */ makeNoOptionsText = ()=>'No options'; | |
| 14 | - | |
| 15 | - | |
| 16 | - /** | |
| 17 | - * @param {string} name Name (potentially partial) of the name at the requested index. | |
| 18 | - * @param {number} start Index where the name starts. | |
| 19 | - * @param {AutoCompleteOption[]} optionList A list of autocomplete options found in the current scope. | |
| 20 | - * @param {boolean} canBeQuoted Whether the name can be inside quotes. | |
| 21 | - * @param {()=>string} makeNoMatchText Function that returns text to show when no matches where found. | |
| 22 | - * @param {()=>string} makeNoOptionsText Function that returns text to show when no options are available to match against. | |
| 23 | - */ | |
| 24 | - constructor(name, start, optionList = [], canBeQuoted = false, makeNoMatchText = null, makeNoOptionsText = null) { | |
| 25 | - this.name = name; | |
| 26 | - this.start = start; | |
| 27 | - this.optionList = optionList; | |
| 28 | - this.canBeQuoted = canBeQuoted; | |
| 29 | - this.noMatchText = makeNoMatchText ?? this.makeNoMatchText; | |
| 30 | - this.noOptionstext = makeNoOptionsText ?? this.makeNoOptionsText; | |
| 31 | - } | |
| 32 | - | |
| 33 | - | |
| 34 | 7 | /** |
| 35 | 8 | * |
| 36 | 9 | * @param {string} text The whole text |
| @@ -0,0 +1,31 @@ | ||
| 1 | +import { SlashCommandNamedArgumentAutoCompleteOption } from '../slash-commands/SlashCommandNamedArgumentAutoCompleteOption.js'; | |
| 2 | +import { AutoCompleteOption } from './AutoCompleteOption.js'; | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | +export class AutoCompleteNameResultBase { | |
| 7 | + /**@type {string} */ name; | |
| 8 | + /**@type {number} */ start; | |
| 9 | + /**@type {AutoCompleteOption[]} */ optionList = []; | |
| 10 | + /**@type {boolean} */ canBeQuoted = false; | |
| 11 | + /**@type {()=>string} */ makeNoMatchText = ()=>`No matches found for "${this.name}"`; | |
| 12 | + /**@type {()=>string} */ makeNoOptionsText = ()=>'No options'; | |
| 13 | + | |
| 14 | + | |
| 15 | + /** | |
| 16 | + * @param {string} name Name (potentially partial) of the name at the requested index. | |
| 17 | + * @param {number} start Index where the name starts. | |
| 18 | + * @param {AutoCompleteOption[]} optionList A list of autocomplete options found in the current scope. | |
| 19 | + * @param {boolean} canBeQuoted Whether the name can be inside quotes. | |
| 20 | + * @param {()=>string} makeNoMatchText Function that returns text to show when no matches where found. | |
| 21 | + * @param {()=>string} makeNoOptionsText Function that returns text to show when no options are available to match against. | |
| 22 | + */ | |
| 23 | + constructor(name, start, optionList = [], canBeQuoted = false, makeNoMatchText = null, makeNoOptionsText = null) { | |
| 24 | + this.name = name; | |
| 25 | + this.start = start; | |
| 26 | + this.optionList = optionList; | |
| 27 | + this.canBeQuoted = canBeQuoted; | |
| 28 | + this.noMatchText = makeNoMatchText ?? this.makeNoMatchText; | |
| 29 | + this.noOptionstext = makeNoOptionsText ?? this.makeNoOptionsText; | |
| 30 | + } | |
| 31 | +} | |
| @@ -11,6 +11,9 @@ export class AutoCompleteOption { | ||
| 11 | 11 | /**@type {AutoCompleteFuzzyScore}*/ score; |
| 12 | 12 | /**@type {string}*/ replacer; |
| 13 | 13 | /**@type {HTMLElement}*/ dom; |
| 14 | + /**@type {(input:string)=>boolean}*/ matchProvider; | |
| 15 | + /**@type {(input:string)=>string}*/ valueProvider; | |
| 16 | + /**@type {boolean}*/ makeSelectable = false; | |
| 14 | 17 | |
| 15 | 18 | |
| 16 | 19 | /** |
| @@ -21,14 +24,21 @@ export class AutoCompleteOption { | ||
| 21 | 24 | return this.name; |
| 22 | 25 | } |
| 23 | 26 | |
| 27 | + get isSelectable() { | |
| 28 | + return this.makeSelectable || !this.valueProvider; | |
| 29 | + } | |
| 30 | + | |
| 24 | 31 | |
| 25 | 32 | /** |
| 26 | 33 | * @param {string} name |
| 27 | 34 | */ |
| 28 | 35 | constructor(name, typeIcon = ' ', type = '', matchProvider = null, valueProvider = null, makeSelectable = false) { |
| 29 | 36 | this.name = name; |
| 30 | 37 | this.typeIcon = typeIcon; |
| 31 | 38 | this.type = type; |
| 39 | + this.matchProvider = matchProvider; | |
| 40 | + this.valueProvider = valueProvider; | |
| 41 | + this.makeSelectable = makeSelectable; | |
| 32 | 42 | } |
| 33 | 43 | |
| 34 | 44 | |
| @@ -1,6 +1,6 @@ | ||
| 1 | 1 | import { AutoCompleteNameResultAutoCompleteNameResultBase } from './AutoCompleteNameResultAutoCompleteNameResultBase.js'; |
| 2 | 2 | |
| 3 | 3 | export class AutoCompleteSecondaryNameResult extends AutoCompleteNameResultAutoCompleteNameResultBase { |
| 4 | 4 | /**@type {boolean}*/ isRequired = false; |
| 5 | 5 | /**@type {boolean}*/ forceMatch = true; |
| 6 | 6 | } |
| @@ -24,9 +24,17 @@ export class QuickReplyApi { | ||
| 24 | 24 | |
| 25 | 25 | |
| 26 | 26 | /** |
| 27 | + * @param {QuickReply} qr | |
| 28 | + * @returns {QuickReplySet} | |
| 29 | + */ | |
| 30 | + getSetByQr(qr) { | |
| 31 | + return QuickReplySet.list.find(it=>it.qrList.includes(qr)); | |
| 32 | + } | |
| 33 | + | |
| 34 | + /** | |
| 27 | 35 | * Finds and returns an existing Quick Reply Set by its name. |
| 28 | 36 | * |
| 29 | 37 | * @param {Stringstring} name name of the quick reply set |
| 30 | 38 | * @returns the quick reply set, or undefined if not found |
| 31 | 39 | */ |
| 32 | 40 | getSetByName(name) { |
| @@ -36,13 +44,14 @@ export class QuickReplyApi { | ||
| 36 | 44 | /** |
| 37 | 45 | * Finds and returns an existing Quick Reply by its set's name and its label. |
| 38 | 46 | * |
| 39 | 47 | * @param {Stringstring} setName name of the quick reply set |
| 40 | 48 | * @param {Stringstring|number} label label or numeric ID of the quick reply |
| 41 | 49 | * @returns the quick reply, or undefined if not found |
| 42 | 50 | */ |
| 43 | 51 | getQrByLabel(setName, label) { |
| 44 | 52 | const set = this.getSetByName(setName); |
| 45 | 53 | if (!set) return; |
| 54 | + if (Number.isInteger(label)) return set.qrList.find(it=>it.id == label); | |
| 46 | 55 | return set.qrList.find(it=>it.label == label); |
| 47 | 56 | } |
| 48 | 57 | |
| @@ -70,24 +79,25 @@ export class QuickReplyApi { | ||
| 70 | 79 | /** |
| 71 | 80 | * Executes an existing quick reply. |
| 72 | 81 | * |
| 73 | 82 | * @param {Stringstring} setName name of the existing quick reply set |
| 74 | 83 | * @param {Stringstring|number} label label of the existing quick reply (text on the button) or its numeric ID |
| 75 | 84 | * @param {Objectobject} [args] optional arguments |
| 85 | + * @param {import('../../../slash-commands.js').ExecuteSlashCommandsOptions} [options] optional execution options | |
| 76 | 86 | */ |
| 77 | 87 | async executeQuickReply(setName, label, args = {}, options = {}) { |
| 78 | 88 | const qr = this.getQrByLabel(setName, label); |
| 79 | 89 | if (!qr) { |
| 80 | 90 | throw new Error(`No quick reply with label "${label}" in set "${setName}" found.`); |
| 81 | 91 | } |
| 82 | 92 | return await qr.execute(args, false, false, options); |
| 83 | 93 | } |
| 84 | 94 | |
| 85 | 95 | |
| 86 | 96 | /** |
| 87 | 97 | * Adds or removes a quick reply set to the list of globally active quick reply sets. |
| 88 | 98 | * |
| 89 | 99 | * @param {Stringstring} name the name of the set |
| 90 | 100 | * @param {Booleanboolean} isVisible whether to show the set's buttons or not |
| 91 | 101 | */ |
| 92 | 102 | toggleGlobalSet(name, isVisible = true) { |
| 93 | 103 | const set = this.getSetByName(name); |
| @@ -104,8 +114,8 @@ export class QuickReplyApi { | ||
| 104 | 114 | /** |
| 105 | 115 | * Adds a quick reply set to the list of globally active quick reply sets. |
| 106 | 116 | * |
| 107 | 117 | * @param {Stringstring} name the name of the set |
| 108 | 118 | * @param {Booleanboolean} isVisible whether to show the set's buttons or not |
| 109 | 119 | */ |
| 110 | 120 | addGlobalSet(name, isVisible = true) { |
| 111 | 121 | const set = this.getSetByName(name); |
| @@ -118,7 +128,7 @@ export class QuickReplyApi { | ||
| 118 | 128 | /** |
| 119 | 129 | * Removes a quick reply set from the list of globally active quick reply sets. |
| 120 | 130 | * |
| 121 | 131 | * @param {Stringstring} name the name of the set |
| 122 | 132 | */ |
| 123 | 133 | removeGlobalSet(name) { |
| 124 | 134 | const set = this.getSetByName(name); |
| @@ -132,8 +142,8 @@ export class QuickReplyApi { | ||
| 132 | 142 | /** |
| 133 | 143 | * Adds or removes a quick reply set to the list of the current chat's active quick reply sets. |
| 134 | 144 | * |
| 135 | 145 | * @param {Stringstring} name the name of the set |
| 136 | 146 | * @param {Booleanboolean} isVisible whether to show the set's buttons or not |
| 137 | 147 | */ |
| 138 | 148 | toggleChatSet(name, isVisible = true) { |
| 139 | 149 | if (!this.settings.chatConfig) return; |
| @@ -151,8 +161,8 @@ export class QuickReplyApi { | ||
| 151 | 161 | /** |
| 152 | 162 | * Adds a quick reply set to the list of the current chat's active quick reply sets. |
| 153 | 163 | * |
| 154 | 164 | * @param {Stringstring} name the name of the set |
| 155 | 165 | * @param {Booleanboolean} isVisible whether to show the set's buttons or not |
| 156 | 166 | */ |
| 157 | 167 | addChatSet(name, isVisible = true) { |
| 158 | 168 | if (!this.settings.chatConfig) return; |
| @@ -166,7 +176,7 @@ export class QuickReplyApi { | ||
| 166 | 176 | /** |
| 167 | 177 | * Removes a quick reply set from the list of the current chat's active quick reply sets. |
| 168 | 178 | * |
| 169 | 179 | * @param {Stringstring} name the name of the set |
| 170 | 180 | */ |
| 171 | 181 | removeChatSet(name) { |
| 172 | 182 | if (!this.settings.chatConfig) return; |
| @@ -181,21 +191,25 @@ export class QuickReplyApi { | ||
| 181 | 191 | /** |
| 182 | 192 | * Creates a new quick reply in an existing quick reply set. |
| 183 | 193 | * |
| 184 | 194 | * @param {Stringstring} setName name of the quick reply set to insert the new quick reply into |
| 185 | 195 | * @param {Stringstring} label label for the new quick reply (text on the button) |
| 186 | 196 | * @param {Objectobject} [props] |
| 187 | 197 | * @param {Stringstring} [props.messageicon] the message to be sent or slash commandicon to be executedshow byon the new quickQR replybutton |
| 188 | 198 | * @param {Stringboolean} [props.titleshowLabel] the titlewhether /to tooltipshow tothe belabel showneven onwhen thean quickicon replyis buttonassigned |
| 189 | 199 | * @param {Booleanstring} [props.isHiddenmessage] whetherthe message to hidebe sent or showslash command to be executed by the buttonnew quick reply |
| 190 | 200 | * @param {Booleanstring} [props.executeOnStartuptitle] whetherthe title / tooltip to executebe shown on the quick reply when SillyTavern startsbutton |
| 191 | 201 | * @param {Booleanboolean} [props.executeOnUserisHidden] whether to execute the quick reply after a userhide hasor sentshow athe messagebutton |
| 192 | 202 | * @param {Booleanboolean} [props.executeOnAiexecuteOnStartup] whether to execute the quick reply after the AI has sentwhen aSillyTavern messagestarts |
| 193 | 203 | * @param {Booleanboolean} [props.executeOnChatChangeexecuteOnUser] whether to execute the quick reply whenafter a newuser chathas issent loadeda message |
| 194 | 204 | * @param {Booleanboolean} [props.executeOnGroupMemberDraftexecuteOnAi] whether to execute the quick reply whenafter athe groupAI memberhas issent selecteda message |
| 195 | 205 | * @param {Stringboolean} [props.automationIdexecuteOnChatChange] whenwhether notto empty,execute the quick reply will be executed when the WI with the givena automationnew IDchat is activatedloaded |
| 206 | + * @param {boolean} [props.executeOnGroupMemberDraft] whether to execute the quick reply when a group member is selected | |
| 207 | + * @param {string} [props.automationId] when not empty, the quick reply will be executed when the WI with the given automation ID is activated | |
| 196 | 208 | * @returns {QuickReply} the new quick reply |
| 197 | 209 | */ |
| 198 | 210 | createQuickReply(setName, label, { |
| 211 | + icon, | |
| 212 | + showLabel, | |
| 199 | 213 | message, |
| 200 | 214 | title, |
| 201 | 215 | isHidden, |
| @@ -212,6 +226,8 @@ export class QuickReplyApi { | ||
| 212 | 226 | } |
| 213 | 227 | const qr = set.addQuickReply(); |
| 214 | 228 | qr.label = label ?? ''; |
| 229 | + qr.icon = icon ?? ''; | |
| 230 | + qr.showLabel = showLabel ?? false; | |
| 215 | 231 | qr.message = message ?? ''; |
| 216 | 232 | qr.title = title ?? ''; |
| 217 | 233 | qr.isHidden = isHidden ?? false; |
| @@ -228,22 +244,26 @@ export class QuickReplyApi { | ||
| 228 | 244 | /** |
| 229 | 245 | * Updates an existing quick reply. |
| 230 | 246 | * |
| 231 | 247 | * @param {Stringstring} setName name of the existing quick reply set |
| 232 | 248 | * @param {Stringstring|number} label label of the existing quick reply (text on the button) or its numeric ID |
| 233 | 249 | * @param {Objectobject} [props] |
| 234 | 250 | * @param {Stringstring} [props.newLabelicon] new label forthe quickicon replyto (textshow on the QR button) |
| 235 | 251 | * @param {Stringboolean} [props.messageshowLabel] the messagewhether to be sent or slashshow commandthe tolabel beeven executedwhen byan theicon quickis replyassigned |
| 236 | 252 | * @param {Stringstring} [props.titlenewLabel] the titlenew /label tooltipfor toquick bereply shown(text on the quick reply button) |
| 237 | 253 | * @param {Booleanstring} [props.isHiddenmessage] whetherthe message to hidebe sent or showslash command to be executed by the buttonquick reply |
| 238 | 254 | * @param {Booleanstring} [props.executeOnStartuptitle] whetherthe title / tooltip to executebe shown on the quick reply when SillyTavern startsbutton |
| 239 | 255 | * @param {Booleanboolean} [props.executeOnUserisHidden] whether to execute the quick reply after a userhide hasor sentshow athe messagebutton |
| 240 | 256 | * @param {Booleanboolean} [props.executeOnAiexecuteOnStartup] whether to execute the quick reply after the AI has sentwhen aSillyTavern messagestarts |
| 241 | 257 | * @param {Booleanboolean} [props.executeOnChatChangeexecuteOnUser] whether to execute the quick reply whenafter a newuser chathas issent loadeda message |
| 242 | 258 | * @param {Booleanboolean} [props.executeOnGroupMemberDraftexecuteOnAi] whether to execute the quick reply whenafter athe groupAI memberhas issent selecteda message |
| 243 | 259 | * @param {Stringboolean} [props.automationIdexecuteOnChatChange] whenwhether notto empty,execute the quick reply will be executed when the WI with the givena automationnew IDchat is activatedloaded |
| 260 | + * @param {boolean} [props.executeOnGroupMemberDraft] whether to execute the quick reply when a group member is selected | |
| 261 | + * @param {string} [props.automationId] when not empty, the quick reply will be executed when the WI with the given automation ID is activated | |
| 244 | 262 | * @returns {QuickReply} the altered quick reply |
| 245 | 263 | */ |
| 246 | 264 | updateQuickReply(setName, label, { |
| 265 | + icon, | |
| 266 | + showLabel, | |
| 247 | 267 | newLabel, |
| 248 | 268 | message, |
| 249 | 269 | title, |
| @@ -259,6 +279,8 @@ export class QuickReplyApi { | ||
| 259 | 279 | if (!qr) { |
| 260 | 280 | throw new Error(`No quick reply with label "${label}" in set "${setName}" found.`); |
| 261 | 281 | } |
| 282 | + qr.updateIcon(icon ?? qr.icon); | |
| 283 | + qr.updateShowLabel(showLabel ?? qr.showLabel); | |
| 262 | 284 | qr.updateLabel(newLabel ?? qr.label); |
| 263 | 285 | qr.updateMessage(message ?? qr.message); |
| 264 | 286 | qr.updateTitle(title ?? qr.title); |
| @@ -276,8 +298,8 @@ export class QuickReplyApi { | ||
| 276 | 298 | /** |
| 277 | 299 | * Deletes an existing quick reply. |
| 278 | 300 | * |
| 279 | 301 | * @param {Stringstring} setName name of the existing quick reply set |
| 280 | 302 | * @param {Stringstring|number} label label of the existing quick reply (text on the button) or its numeric ID |
| 281 | 303 | */ |
| 282 | 304 | deleteQuickReply(setName, label) { |
| 283 | 305 | const qr = this.getQrByLabel(setName, label); |
| @@ -291,10 +313,10 @@ export class QuickReplyApi { | ||
| 291 | 313 | /** |
| 292 | 314 | * Adds an existing quick reply set as a context menu to an existing quick reply. |
| 293 | 315 | * |
| 294 | 316 | * @param {Stringstring} setName name of the existing quick reply set containing the quick reply |
| 295 | 317 | * @param {Stringstring|number} label label of the existing quick reply or its numeric ID |
| 296 | 318 | * @param {Stringstring} contextSetName name of the existing quick reply set to be used as a context menu |
| 297 | 319 | * @param {Booleanboolean} isChained whether or not to chain the context menu quick replies |
| 298 | 320 | */ |
| 299 | 321 | createContextItem(setName, label, contextSetName, isChained = false) { |
| 300 | 322 | const qr = this.getQrByLabel(setName, label); |
| @@ -314,9 +336,9 @@ export class QuickReplyApi { | ||
| 314 | 336 | /** |
| 315 | 337 | * Removes a quick reply set from a quick reply's context menu. |
| 316 | 338 | * |
| 317 | 339 | * @param {Stringstring} setName name of the existing quick reply set containing the quick reply |
| 318 | 340 | * @param {Stringstring|number} label label of the existing quick reply or its numeric ID |
| 319 | 341 | * @param {Stringstring} contextSetName name of the existing quick reply set to be used as a context menu |
| 320 | 342 | */ |
| 321 | 343 | deleteContextItem(setName, label, contextSetName) { |
| 322 | 344 | const qr = this.getQrByLabel(setName, label); |
| @@ -333,8 +355,8 @@ export class QuickReplyApi { | ||
| 333 | 355 | /** |
| 334 | 356 | * Removes all entries from a quick reply's context menu. |
| 335 | 357 | * |
| 336 | 358 | * @param {Stringstring} setName name of the existing quick reply set containing the quick reply |
| 337 | 359 | * @param {Stringstring|number} label label of the existing quick reply or its numeric ID |
| 338 | 360 | */ |
| 339 | 361 | clearContextMenu(setName, label) { |
| 340 | 362 | const qr = this.getQrByLabel(setName, label); |
| @@ -348,11 +370,11 @@ export class QuickReplyApi { | ||
| 348 | 370 | /** |
| 349 | 371 | * Create a new quick reply set. |
| 350 | 372 | * |
| 351 | 373 | * @param {Stringstring} name name of the new quick reply set |
| 352 | 374 | * @param {Objectobject} [props] |
| 353 | 375 | * @param {Booleanboolean} [props.disableSend] whether or not to send the quick replies or put the message or slash command into the char input box |
| 354 | 376 | * @param {Booleanboolean} [props.placeBeforeInput] whether or not to place the quick reply contents before the existing user input |
| 355 | 377 | * @param {Booleanboolean} [props.injectInput] whether or not to automatically inject the user input at the end of the quick reply |
| 356 | 378 | * @returns {Promise<QuickReplySet>} the new quick reply set |
| 357 | 379 | */ |
| 358 | 380 | async createSet(name, { |
| @@ -384,11 +406,11 @@ export class QuickReplyApi { | ||
| 384 | 406 | /** |
| 385 | 407 | * Update an existing quick reply set. |
| 386 | 408 | * |
| 387 | 409 | * @param {Stringstring} name name of the existing quick reply set |
| 388 | 410 | * @param {Objectobject} [props] |
| 389 | 411 | * @param {Booleanboolean} [props.disableSend] whether or not to send the quick replies or put the message or slash command into the char input box |
| 390 | 412 | * @param {Booleanboolean} [props.placeBeforeInput] whether or not to place the quick reply contents before the existing user input |
| 391 | 413 | * @param {Booleanboolean} [props.injectInput] whether or not to automatically inject the user input at the end of the quick reply |
| 392 | 414 | * @returns {Promise<QuickReplySet>} the altered quick reply set |
| 393 | 415 | */ |
| 394 | 416 | async updateSet(name, { |
| @@ -411,7 +433,7 @@ export class QuickReplyApi { | ||
| 411 | 433 | /** |
| 412 | 434 | * Delete an existing quick reply set. |
| 413 | 435 | * |
| 414 | 436 | * @param {Stringstring} name name of the existing quick reply set |
| 415 | 437 | */ |
| 416 | 438 | async deleteSet(name) { |
| 417 | 439 | const set = this.getSetByName(name); |
| @@ -451,7 +473,7 @@ export class QuickReplyApi { | ||
| 451 | 473 | /** |
| 452 | 474 | * Gets a list of all quick replies in the quick reply set. |
| 453 | 475 | * |
| 454 | 476 | * @param {Stringstring} setName name of the existing quick reply set |
| 455 | 477 | * @returns array with the labels of this set's quick replies |
| 456 | 478 | */ |
| 457 | 479 | listQuickReplies(setName) { |
| @@ -2,10 +2,23 @@ | ||
| 2 | 2 | <div id="qr--main"> |
| 3 | 3 | <h3 data-i18n="Labels and Message">Labels and Message</h3> |
| 4 | 4 | <div class="qr--labels"> |
| 5 | - <label> | |
| 5 | + <label class="qr--fit"> | |
| 6 | + <span class="qr--labelText" data-i18n="Label">Icon</span> | |
| 7 | + <small class="qr--labelHint"> </small> | |
| 8 | + <div class="menu_button fa-fw" id="qr--modal-icon" title="Click to change icon"></div> | |
| 9 | + </label> | |
| 10 | + <div class="label"> | |
| 6 | 11 | <span class="qr--labelText" data-i18n="Label">Label</span> |
| 7 | - <input type="text" class="text_pole" id="qr--modal-label"> | |
| 12 | + <small class="qr--labelHint" data-i18n="(label of the button, if no icon is chosen) ">(label of the button, if no icon is chosen)</small> | |
| 13 | + <div class="qr--inputGroup"> | |
| 14 | + <label class="checkbox_label" title="Show label even if an icon is assigned"> | |
| 15 | + <input type="checkbox" id="qr--modal-showLabel"> | |
| 16 | + Show | |
| 8 | 17 | </label> |
| 18 | + <input type="text" class="text_pole" id="qr--modal-label"> | |
| 19 | + <div class="menu_button fa-fw fa-solid fa-chevron-down" id="qr--modal-switcher" title="Switch to another QR"></div> | |
| 20 | + </div> | |
| 21 | + </div> | |
| 9 | 22 | <label> |
| 10 | 23 | <span class="qr--labelText" data-i18n="Title">Title</span> |
| 11 | 24 | <small class="qr--labelHint" data-i18n="(tooltip, leave empty to show message or /command)">(tooltip, leave empty to show message or /command)</small> |
| @@ -33,6 +46,8 @@ | ||
| 33 | 46 | <input type="checkbox" id="qr--modal-syntax"> |
| 34 | 47 | <span>Syntax highlight</span> |
| 35 | 48 | </label> |
| 49 | + <small>Ctrl+Alt+Click (or F9) to set / remove breakpoints</small> | |
| 50 | + <small>Ctrl+<span id="qr--modal-commentKey"></span> to toggle block comments</small> | |
| 36 | 51 | </div> |
| 37 | 52 | <div id="qr--modal-messageHolder"> |
| 38 | 53 | <pre id="qr--modal-messageSyntax"><code id="qr--modal-messageSyntaxInner" class="hljs language-stscript"></code></pre> |
| @@ -43,6 +58,10 @@ | ||
| 43 | 58 | |
| 44 | 59 | |
| 45 | 60 | |
| 61 | + <div id="qr--resizeHandle"></div> | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 46 | 65 | <div id="qr--qrOptions"> |
| 47 | 66 | <h3 data-i18n="Context Menu">Context Menu</h3> |
| 48 | 67 | <div id="qr--ctxEditor"> |
| @@ -64,7 +83,7 @@ | ||
| 64 | 83 | |
| 65 | 84 | |
| 66 | 85 | <h3 data-i18n="Auto-Execute">Auto-Execute</h3> |
| 67 | 86 | <div id="qr--autoExec" class="flex-container flexFlowColumn"> |
| 68 | 87 | <label class="checkbox_label" title="Prevent this quick reply from triggering other auto-executed quick replies while auto-executing (i.e., prevent recursive auto-execution)"> |
| 69 | 88 | <input type="checkbox" id="qr--preventAutoExecute" > |
| 70 | 89 | <span><i class="fa-solid fa-fw fa-plane-slash"></i><span data-i18n="Don't trigger auto-execute">Don't trigger auto-execute</span></span> |
| @@ -117,11 +136,18 @@ | ||
| 117 | 136 | </div> |
| 118 | 137 | </div> |
| 119 | 138 | <div id="qr--modal-executeProgress"></div> |
| 120 | - <label class="checkbox_label"> | |
| 121 | - <input type="checkbox" id="qr--modal-executeHide"> | |
| 122 | - <span title="Hide editor while executing"> Hide editor while executing</span> | |
| 123 | - </label> | |
| 124 | 139 | <div id="qr--modal-executeErrors"></div> |
| 125 | 140 | <div id="qr--modal-executeResult"></div> |
| 141 | + | |
| 142 | + <div id="qr--modal-debugButtons"> | |
| 143 | + <div title="Resume" id="qr--modal-resume" class="qr--modal-debugButton menu_button"></div> | |
| 144 | + <div title="Step Over" id="qr--modal-step" class="qr--modal-debugButton menu_button"></div> | |
| 145 | + <div title="Step Into" id="qr--modal-stepInto" class="qr--modal-debugButton menu_button"></div> | |
| 146 | + <div title="Step Out" id="qr--modal-stepOut" class="qr--modal-debugButton menu_button"></div> | |
| 147 | + <div title="Minimize" id="qr--modal-minimize" class="qr--modal-debugButton menu_button fa-solid fa-minimize"></div> | |
| 148 | + <div title="Maximize" id="qr--modal-maximize" class="qr--modal-debugButton menu_button fa-solid fa-maximize"></div> | |
| 149 | + </div> | |
| 150 | + <textarea rows="1" id="qr--modal-send_textarea" placeholder="Chat input for use with {{input}}" title="Chat input for use with {{input}}"></textarea> | |
| 151 | + <div id="qr--modal-debugState"></div> | |
| 126 | 152 | </div> |
| 127 | 153 | </div> |
| @@ -60,10 +60,20 @@ | ||
| 60 | 60 | <label class="flex-container" id="qr--injectInputContainer"> |
| 61 | 61 | <input type="checkbox" id="qr--injectInput"> <span><span data-i18n="Inject user input automatically">Inject user input automatically</span> <small><span data-i18n="(if disabled, use ">(if disabled, use</span><code>{{input}}</code> <span data-i18n="macro for manual injection)">macro for manual injection)</span></small></span> |
| 62 | 62 | </label> |
| 63 | + <div class="flex-container alignItemsCenter"> | |
| 64 | + <toolcool-color-picker id="qr--color"></toolcool-color-picker> | |
| 65 | + <div class="menu_button" id="qr--colorClear">Clear</div> | |
| 66 | + <span data-i18n="Color">Color</span> | |
| 67 | + </div> | |
| 68 | + <label class="flex-container" id="qr--onlyBorderColorContainer"> | |
| 69 | + <input type="checkbox" id="qr--onlyBorderColor"> <span data-i18n="Only apply color as accent">Only apply color as accent</span> | |
| 70 | + </label> | |
| 63 | 71 | </div> |
| 64 | 72 | <div id="qr--set-qrList" class="qr--qrList"></div> |
| 65 | 73 | <div class="qr--set-qrListActions"> |
| 66 | 74 | <div class="qr--add menu_button menu_button_icon fa-solid fa-plus" id="qr--set-add" title="Add quick reply"></div> |
| 75 | + <div class="qr--paste menu_button menu_button_icon fa-solid fa-paste" id="qr--set-paste" title="Paste quick reply from clipboard"></div> | |
| 76 | + <div class="qr--import menu_button menu_button_icon fa-solid fa-file-import" id="qr--set-importQr" title="Import quick reply from file"></div> | |
| 67 | 77 | </div> |
| 68 | 78 | </div> |
| 69 | 79 | </div> |
| @@ -176,7 +176,7 @@ const init = async () => { | ||
| 176 | 176 | buttons.show(); |
| 177 | 177 | settings.onSave = ()=>buttons.refresh(); |
| 178 | 178 | |
| 179 | 179 | window['executeQuickReplyByName'] = async(name, args = {}, options = {}) => { |
| 180 | 180 | let qr = [...settings.config.setList, ...(settings.chatConfig?.setList ?? [])] |
| 181 | 181 | .map(it=>it.set.qrList) |
| 182 | 182 | .flat() |
| @@ -191,7 +191,7 @@ const init = async () => { | ||
| 191 | 191 | } |
| 192 | 192 | } |
| 193 | 193 | if (qr && qr.onExecute) { |
| 194 | 194 | return await qr.execute(args, false, true, options); |
| 195 | 195 | } else { |
| 196 | 196 | throw new Error(`No Quick Reply found for "${name}".`); |
| 197 | 197 | } |
| @@ -0,0 +1,769 @@ | ||
| 1 | +var DOCUMENT_FRAGMENT_NODE = 11; | |
| 2 | + | |
| 3 | +function morphAttrs(fromNode, toNode) { | |
| 4 | + var toNodeAttrs = toNode.attributes; | |
| 5 | + var attr; | |
| 6 | + var attrName; | |
| 7 | + var attrNamespaceURI; | |
| 8 | + var attrValue; | |
| 9 | + var fromValue; | |
| 10 | + | |
| 11 | + // document-fragments dont have attributes so lets not do anything | |
| 12 | + if (toNode.nodeType === DOCUMENT_FRAGMENT_NODE || fromNode.nodeType === DOCUMENT_FRAGMENT_NODE) { | |
| 13 | + return; | |
| 14 | + } | |
| 15 | + | |
| 16 | + // update attributes on original DOM element | |
| 17 | + for (var i = toNodeAttrs.length - 1; i >= 0; i--) { | |
| 18 | + attr = toNodeAttrs[i]; | |
| 19 | + attrName = attr.name; | |
| 20 | + attrNamespaceURI = attr.namespaceURI; | |
| 21 | + attrValue = attr.value; | |
| 22 | + | |
| 23 | + if (attrNamespaceURI) { | |
| 24 | + attrName = attr.localName || attrName; | |
| 25 | + fromValue = fromNode.getAttributeNS(attrNamespaceURI, attrName); | |
| 26 | + | |
| 27 | + if (fromValue !== attrValue) { | |
| 28 | + if (attr.prefix === 'xmlns'){ | |
| 29 | + attrName = attr.name; // It's not allowed to set an attribute with the XMLNS namespace without specifying the `xmlns` prefix | |
| 30 | + } | |
| 31 | + fromNode.setAttributeNS(attrNamespaceURI, attrName, attrValue); | |
| 32 | + } | |
| 33 | + } else { | |
| 34 | + fromValue = fromNode.getAttribute(attrName); | |
| 35 | + | |
| 36 | + if (fromValue !== attrValue) { | |
| 37 | + fromNode.setAttribute(attrName, attrValue); | |
| 38 | + } | |
| 39 | + } | |
| 40 | + } | |
| 41 | + | |
| 42 | + // Remove any extra attributes found on the original DOM element that | |
| 43 | + // weren't found on the target element. | |
| 44 | + var fromNodeAttrs = fromNode.attributes; | |
| 45 | + | |
| 46 | + for (var d = fromNodeAttrs.length - 1; d >= 0; d--) { | |
| 47 | + attr = fromNodeAttrs[d]; | |
| 48 | + attrName = attr.name; | |
| 49 | + attrNamespaceURI = attr.namespaceURI; | |
| 50 | + | |
| 51 | + if (attrNamespaceURI) { | |
| 52 | + attrName = attr.localName || attrName; | |
| 53 | + | |
| 54 | + if (!toNode.hasAttributeNS(attrNamespaceURI, attrName)) { | |
| 55 | + fromNode.removeAttributeNS(attrNamespaceURI, attrName); | |
| 56 | + } | |
| 57 | + } else { | |
| 58 | + if (!toNode.hasAttribute(attrName)) { | |
| 59 | + fromNode.removeAttribute(attrName); | |
| 60 | + } | |
| 61 | + } | |
| 62 | + } | |
| 63 | +} | |
| 64 | + | |
| 65 | +var range; // Create a range object for efficently rendering strings to elements. | |
| 66 | +var NS_XHTML = 'http://www.w3.org/1999/xhtml'; | |
| 67 | + | |
| 68 | +var doc = typeof document === 'undefined' ? undefined : document; | |
| 69 | +var HAS_TEMPLATE_SUPPORT = !!doc && 'content' in doc.createElement('template'); | |
| 70 | +var HAS_RANGE_SUPPORT = !!doc && doc.createRange && 'createContextualFragment' in doc.createRange(); | |
| 71 | + | |
| 72 | +function createFragmentFromTemplate(str) { | |
| 73 | + var template = doc.createElement('template'); | |
| 74 | + template.innerHTML = str; | |
| 75 | + return template.content.childNodes[0]; | |
| 76 | +} | |
| 77 | + | |
| 78 | +function createFragmentFromRange(str) { | |
| 79 | + if (!range) { | |
| 80 | + range = doc.createRange(); | |
| 81 | + range.selectNode(doc.body); | |
| 82 | + } | |
| 83 | + | |
| 84 | + var fragment = range.createContextualFragment(str); | |
| 85 | + return fragment.childNodes[0]; | |
| 86 | +} | |
| 87 | + | |
| 88 | +function createFragmentFromWrap(str) { | |
| 89 | + var fragment = doc.createElement('body'); | |
| 90 | + fragment.innerHTML = str; | |
| 91 | + return fragment.childNodes[0]; | |
| 92 | +} | |
| 93 | + | |
| 94 | +/** | |
| 95 | + * This is about the same | |
| 96 | + * var html = new DOMParser().parseFromString(str, 'text/html'); | |
| 97 | + * return html.body.firstChild; | |
| 98 | + * | |
| 99 | + * @method toElement | |
| 100 | + * @param {String} str | |
| 101 | + */ | |
| 102 | +function toElement(str) { | |
| 103 | + str = str.trim(); | |
| 104 | + if (HAS_TEMPLATE_SUPPORT) { | |
| 105 | + // avoid restrictions on content for things like `<tr><th>Hi</th></tr>` which | |
| 106 | + // createContextualFragment doesn't support | |
| 107 | + // <template> support not available in IE | |
| 108 | + return createFragmentFromTemplate(str); | |
| 109 | + } else if (HAS_RANGE_SUPPORT) { | |
| 110 | + return createFragmentFromRange(str); | |
| 111 | + } | |
| 112 | + | |
| 113 | + return createFragmentFromWrap(str); | |
| 114 | +} | |
| 115 | + | |
| 116 | +/** | |
| 117 | + * Returns true if two node's names are the same. | |
| 118 | + * | |
| 119 | + * NOTE: We don't bother checking `namespaceURI` because you will never find two HTML elements with the same | |
| 120 | + * nodeName and different namespace URIs. | |
| 121 | + * | |
| 122 | + * @param {Element} a | |
| 123 | + * @param {Element} b The target element | |
| 124 | + * @return {boolean} | |
| 125 | + */ | |
| 126 | +function compareNodeNames(fromEl, toEl) { | |
| 127 | + var fromNodeName = fromEl.nodeName; | |
| 128 | + var toNodeName = toEl.nodeName; | |
| 129 | + var fromCodeStart, toCodeStart; | |
| 130 | + | |
| 131 | + if (fromNodeName === toNodeName) { | |
| 132 | + return true; | |
| 133 | + } | |
| 134 | + | |
| 135 | + fromCodeStart = fromNodeName.charCodeAt(0); | |
| 136 | + toCodeStart = toNodeName.charCodeAt(0); | |
| 137 | + | |
| 138 | + // If the target element is a virtual DOM node or SVG node then we may | |
| 139 | + // need to normalize the tag name before comparing. Normal HTML elements that are | |
| 140 | + // in the "http://www.w3.org/1999/xhtml" | |
| 141 | + // are converted to upper case | |
| 142 | + if (fromCodeStart <= 90 && toCodeStart >= 97) { // from is upper and to is lower | |
| 143 | + return fromNodeName === toNodeName.toUpperCase(); | |
| 144 | + } else if (toCodeStart <= 90 && fromCodeStart >= 97) { // to is upper and from is lower | |
| 145 | + return toNodeName === fromNodeName.toUpperCase(); | |
| 146 | + } else { | |
| 147 | + return false; | |
| 148 | + } | |
| 149 | +} | |
| 150 | + | |
| 151 | +/** | |
| 152 | + * Create an element, optionally with a known namespace URI. | |
| 153 | + * | |
| 154 | + * @param {string} name the element name, e.g. 'div' or 'svg' | |
| 155 | + * @param {string} [namespaceURI] the element's namespace URI, i.e. the value of | |
| 156 | + * its `xmlns` attribute or its inferred namespace. | |
| 157 | + * | |
| 158 | + * @return {Element} | |
| 159 | + */ | |
| 160 | +function createElementNS(name, namespaceURI) { | |
| 161 | + return !namespaceURI || namespaceURI === NS_XHTML ? | |
| 162 | + doc.createElement(name) : | |
| 163 | + doc.createElementNS(namespaceURI, name); | |
| 164 | +} | |
| 165 | + | |
| 166 | +/** | |
| 167 | + * Copies the children of one DOM element to another DOM element | |
| 168 | + */ | |
| 169 | +function moveChildren(fromEl, toEl) { | |
| 170 | + var curChild = fromEl.firstChild; | |
| 171 | + while (curChild) { | |
| 172 | + var nextChild = curChild.nextSibling; | |
| 173 | + toEl.appendChild(curChild); | |
| 174 | + curChild = nextChild; | |
| 175 | + } | |
| 176 | + return toEl; | |
| 177 | +} | |
| 178 | + | |
| 179 | +function syncBooleanAttrProp(fromEl, toEl, name) { | |
| 180 | + if (fromEl[name] !== toEl[name]) { | |
| 181 | + fromEl[name] = toEl[name]; | |
| 182 | + if (fromEl[name]) { | |
| 183 | + fromEl.setAttribute(name, ''); | |
| 184 | + } else { | |
| 185 | + fromEl.removeAttribute(name); | |
| 186 | + } | |
| 187 | + } | |
| 188 | +} | |
| 189 | + | |
| 190 | +var specialElHandlers = { | |
| 191 | + OPTION: function(fromEl, toEl) { | |
| 192 | + var parentNode = fromEl.parentNode; | |
| 193 | + if (parentNode) { | |
| 194 | + var parentName = parentNode.nodeName.toUpperCase(); | |
| 195 | + if (parentName === 'OPTGROUP') { | |
| 196 | + parentNode = parentNode.parentNode; | |
| 197 | + parentName = parentNode && parentNode.nodeName.toUpperCase(); | |
| 198 | + } | |
| 199 | + if (parentName === 'SELECT' && !parentNode.hasAttribute('multiple')) { | |
| 200 | + if (fromEl.hasAttribute('selected') && !toEl.selected) { | |
| 201 | + // Workaround for MS Edge bug where the 'selected' attribute can only be | |
| 202 | + // removed if set to a non-empty value: | |
| 203 | + // https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/12087679/ | |
| 204 | + fromEl.setAttribute('selected', 'selected'); | |
| 205 | + fromEl.removeAttribute('selected'); | |
| 206 | + } | |
| 207 | + // We have to reset select element's selectedIndex to -1, otherwise setting | |
| 208 | + // fromEl.selected using the syncBooleanAttrProp below has no effect. | |
| 209 | + // The correct selectedIndex will be set in the SELECT special handler below. | |
| 210 | + parentNode.selectedIndex = -1; | |
| 211 | + } | |
| 212 | + } | |
| 213 | + syncBooleanAttrProp(fromEl, toEl, 'selected'); | |
| 214 | + }, | |
| 215 | + /** | |
| 216 | + * The "value" attribute is special for the <input> element since it sets | |
| 217 | + * the initial value. Changing the "value" attribute without changing the | |
| 218 | + * "value" property will have no effect since it is only used to the set the | |
| 219 | + * initial value. Similar for the "checked" attribute, and "disabled". | |
| 220 | + */ | |
| 221 | + INPUT: function(fromEl, toEl) { | |
| 222 | + syncBooleanAttrProp(fromEl, toEl, 'checked'); | |
| 223 | + syncBooleanAttrProp(fromEl, toEl, 'disabled'); | |
| 224 | + | |
| 225 | + if (fromEl.value !== toEl.value) { | |
| 226 | + fromEl.value = toEl.value; | |
| 227 | + } | |
| 228 | + | |
| 229 | + if (!toEl.hasAttribute('value')) { | |
| 230 | + fromEl.removeAttribute('value'); | |
| 231 | + } | |
| 232 | + }, | |
| 233 | + | |
| 234 | + TEXTAREA: function(fromEl, toEl) { | |
| 235 | + var newValue = toEl.value; | |
| 236 | + if (fromEl.value !== newValue) { | |
| 237 | + fromEl.value = newValue; | |
| 238 | + } | |
| 239 | + | |
| 240 | + var firstChild = fromEl.firstChild; | |
| 241 | + if (firstChild) { | |
| 242 | + // Needed for IE. Apparently IE sets the placeholder as the | |
| 243 | + // node value and vise versa. This ignores an empty update. | |
| 244 | + var oldValue = firstChild.nodeValue; | |
| 245 | + | |
| 246 | + if (oldValue == newValue || (!newValue && oldValue == fromEl.placeholder)) { | |
| 247 | + return; | |
| 248 | + } | |
| 249 | + | |
| 250 | + firstChild.nodeValue = newValue; | |
| 251 | + } | |
| 252 | + }, | |
| 253 | + SELECT: function(fromEl, toEl) { | |
| 254 | + if (!toEl.hasAttribute('multiple')) { | |
| 255 | + var selectedIndex = -1; | |
| 256 | + var i = 0; | |
| 257 | + // We have to loop through children of fromEl, not toEl since nodes can be moved | |
| 258 | + // from toEl to fromEl directly when morphing. | |
| 259 | + // At the time this special handler is invoked, all children have already been morphed | |
| 260 | + // and appended to / removed from fromEl, so using fromEl here is safe and correct. | |
| 261 | + var curChild = fromEl.firstChild; | |
| 262 | + var optgroup; | |
| 263 | + var nodeName; | |
| 264 | + while(curChild) { | |
| 265 | + nodeName = curChild.nodeName && curChild.nodeName.toUpperCase(); | |
| 266 | + if (nodeName === 'OPTGROUP') { | |
| 267 | + optgroup = curChild; | |
| 268 | + curChild = optgroup.firstChild; | |
| 269 | + } else { | |
| 270 | + if (nodeName === 'OPTION') { | |
| 271 | + if (curChild.hasAttribute('selected')) { | |
| 272 | + selectedIndex = i; | |
| 273 | + break; | |
| 274 | + } | |
| 275 | + i++; | |
| 276 | + } | |
| 277 | + curChild = curChild.nextSibling; | |
| 278 | + if (!curChild && optgroup) { | |
| 279 | + curChild = optgroup.nextSibling; | |
| 280 | + optgroup = null; | |
| 281 | + } | |
| 282 | + } | |
| 283 | + } | |
| 284 | + | |
| 285 | + fromEl.selectedIndex = selectedIndex; | |
| 286 | + } | |
| 287 | + } | |
| 288 | +}; | |
| 289 | + | |
| 290 | +var ELEMENT_NODE = 1; | |
| 291 | +var DOCUMENT_FRAGMENT_NODE$1 = 11; | |
| 292 | +var TEXT_NODE = 3; | |
| 293 | +var COMMENT_NODE = 8; | |
| 294 | + | |
| 295 | +function noop() {} | |
| 296 | + | |
| 297 | +function defaultGetNodeKey(node) { | |
| 298 | + if (node) { | |
| 299 | + return (node.getAttribute && node.getAttribute('id')) || node.id; | |
| 300 | + } | |
| 301 | +} | |
| 302 | + | |
| 303 | +function morphdomFactory(morphAttrs) { | |
| 304 | + | |
| 305 | + return function morphdom(fromNode, toNode, options) { | |
| 306 | + if (!options) { | |
| 307 | + options = {}; | |
| 308 | + } | |
| 309 | + | |
| 310 | + if (typeof toNode === 'string') { | |
| 311 | + if (fromNode.nodeName === '#document' || fromNode.nodeName === 'HTML' || fromNode.nodeName === 'BODY') { | |
| 312 | + var toNodeHtml = toNode; | |
| 313 | + toNode = doc.createElement('html'); | |
| 314 | + toNode.innerHTML = toNodeHtml; | |
| 315 | + } else { | |
| 316 | + toNode = toElement(toNode); | |
| 317 | + } | |
| 318 | + } else if (toNode.nodeType === DOCUMENT_FRAGMENT_NODE$1) { | |
| 319 | + toNode = toNode.firstElementChild; | |
| 320 | + } | |
| 321 | + | |
| 322 | + var getNodeKey = options.getNodeKey || defaultGetNodeKey; | |
| 323 | + var onBeforeNodeAdded = options.onBeforeNodeAdded || noop; | |
| 324 | + var onNodeAdded = options.onNodeAdded || noop; | |
| 325 | + var onBeforeElUpdated = options.onBeforeElUpdated || noop; | |
| 326 | + var onElUpdated = options.onElUpdated || noop; | |
| 327 | + var onBeforeNodeDiscarded = options.onBeforeNodeDiscarded || noop; | |
| 328 | + var onNodeDiscarded = options.onNodeDiscarded || noop; | |
| 329 | + var onBeforeElChildrenUpdated = options.onBeforeElChildrenUpdated || noop; | |
| 330 | + var skipFromChildren = options.skipFromChildren || noop; | |
| 331 | + var addChild = options.addChild || function(parent, child){ return parent.appendChild(child); }; | |
| 332 | + var childrenOnly = options.childrenOnly === true; | |
| 333 | + | |
| 334 | + // This object is used as a lookup to quickly find all keyed elements in the original DOM tree. | |
| 335 | + var fromNodesLookup = Object.create(null); | |
| 336 | + var keyedRemovalList = []; | |
| 337 | + | |
| 338 | + function addKeyedRemoval(key) { | |
| 339 | + keyedRemovalList.push(key); | |
| 340 | + } | |
| 341 | + | |
| 342 | + function walkDiscardedChildNodes(node, skipKeyedNodes) { | |
| 343 | + if (node.nodeType === ELEMENT_NODE) { | |
| 344 | + var curChild = node.firstChild; | |
| 345 | + while (curChild) { | |
| 346 | + | |
| 347 | + var key = undefined; | |
| 348 | + | |
| 349 | + if (skipKeyedNodes && (key = getNodeKey(curChild))) { | |
| 350 | + // If we are skipping keyed nodes then we add the key | |
| 351 | + // to a list so that it can be handled at the very end. | |
| 352 | + addKeyedRemoval(key); | |
| 353 | + } else { | |
| 354 | + // Only report the node as discarded if it is not keyed. We do this because | |
| 355 | + // at the end we loop through all keyed elements that were unmatched | |
| 356 | + // and then discard them in one final pass. | |
| 357 | + onNodeDiscarded(curChild); | |
| 358 | + if (curChild.firstChild) { | |
| 359 | + walkDiscardedChildNodes(curChild, skipKeyedNodes); | |
| 360 | + } | |
| 361 | + } | |
| 362 | + | |
| 363 | + curChild = curChild.nextSibling; | |
| 364 | + } | |
| 365 | + } | |
| 366 | + } | |
| 367 | + | |
| 368 | + /** | |
| 369 | + * Removes a DOM node out of the original DOM | |
| 370 | + * | |
| 371 | + * @param {Node} node The node to remove | |
| 372 | + * @param {Node} parentNode The nodes parent | |
| 373 | + * @param {Boolean} skipKeyedNodes If true then elements with keys will be skipped and not discarded. | |
| 374 | + * @return {undefined} | |
| 375 | + */ | |
| 376 | + function removeNode(node, parentNode, skipKeyedNodes) { | |
| 377 | + if (onBeforeNodeDiscarded(node) === false) { | |
| 378 | + return; | |
| 379 | + } | |
| 380 | + | |
| 381 | + if (parentNode) { | |
| 382 | + parentNode.removeChild(node); | |
| 383 | + } | |
| 384 | + | |
| 385 | + onNodeDiscarded(node); | |
| 386 | + walkDiscardedChildNodes(node, skipKeyedNodes); | |
| 387 | + } | |
| 388 | + | |
| 389 | + // // TreeWalker implementation is no faster, but keeping this around in case this changes in the future | |
| 390 | + // function indexTree(root) { | |
| 391 | + // var treeWalker = document.createTreeWalker( | |
| 392 | + // root, | |
| 393 | + // NodeFilter.SHOW_ELEMENT); | |
| 394 | + // | |
| 395 | + // var el; | |
| 396 | + // while((el = treeWalker.nextNode())) { | |
| 397 | + // var key = getNodeKey(el); | |
| 398 | + // if (key) { | |
| 399 | + // fromNodesLookup[key] = el; | |
| 400 | + // } | |
| 401 | + // } | |
| 402 | + // } | |
| 403 | + | |
| 404 | + // // NodeIterator implementation is no faster, but keeping this around in case this changes in the future | |
| 405 | + // | |
| 406 | + // function indexTree(node) { | |
| 407 | + // var nodeIterator = document.createNodeIterator(node, NodeFilter.SHOW_ELEMENT); | |
| 408 | + // var el; | |
| 409 | + // while((el = nodeIterator.nextNode())) { | |
| 410 | + // var key = getNodeKey(el); | |
| 411 | + // if (key) { | |
| 412 | + // fromNodesLookup[key] = el; | |
| 413 | + // } | |
| 414 | + // } | |
| 415 | + // } | |
| 416 | + | |
| 417 | + function indexTree(node) { | |
| 418 | + if (node.nodeType === ELEMENT_NODE || node.nodeType === DOCUMENT_FRAGMENT_NODE$1) { | |
| 419 | + var curChild = node.firstChild; | |
| 420 | + while (curChild) { | |
| 421 | + var key = getNodeKey(curChild); | |
| 422 | + if (key) { | |
| 423 | + fromNodesLookup[key] = curChild; | |
| 424 | + } | |
| 425 | + | |
| 426 | + // Walk recursively | |
| 427 | + indexTree(curChild); | |
| 428 | + | |
| 429 | + curChild = curChild.nextSibling; | |
| 430 | + } | |
| 431 | + } | |
| 432 | + } | |
| 433 | + | |
| 434 | + indexTree(fromNode); | |
| 435 | + | |
| 436 | + function handleNodeAdded(el) { | |
| 437 | + onNodeAdded(el); | |
| 438 | + | |
| 439 | + var curChild = el.firstChild; | |
| 440 | + while (curChild) { | |
| 441 | + var nextSibling = curChild.nextSibling; | |
| 442 | + | |
| 443 | + var key = getNodeKey(curChild); | |
| 444 | + if (key) { | |
| 445 | + var unmatchedFromEl = fromNodesLookup[key]; | |
| 446 | + // if we find a duplicate #id node in cache, replace `el` with cache value | |
| 447 | + // and morph it to the child node. | |
| 448 | + if (unmatchedFromEl && compareNodeNames(curChild, unmatchedFromEl)) { | |
| 449 | + curChild.parentNode.replaceChild(unmatchedFromEl, curChild); | |
| 450 | + morphEl(unmatchedFromEl, curChild); | |
| 451 | + } else { | |
| 452 | + handleNodeAdded(curChild); | |
| 453 | + } | |
| 454 | + } else { | |
| 455 | + // recursively call for curChild and it's children to see if we find something in | |
| 456 | + // fromNodesLookup | |
| 457 | + handleNodeAdded(curChild); | |
| 458 | + } | |
| 459 | + | |
| 460 | + curChild = nextSibling; | |
| 461 | + } | |
| 462 | + } | |
| 463 | + | |
| 464 | + function cleanupFromEl(fromEl, curFromNodeChild, curFromNodeKey) { | |
| 465 | + // We have processed all of the "to nodes". If curFromNodeChild is | |
| 466 | + // non-null then we still have some from nodes left over that need | |
| 467 | + // to be removed | |
| 468 | + while (curFromNodeChild) { | |
| 469 | + var fromNextSibling = curFromNodeChild.nextSibling; | |
| 470 | + if ((curFromNodeKey = getNodeKey(curFromNodeChild))) { | |
| 471 | + // Since the node is keyed it might be matched up later so we defer | |
| 472 | + // the actual removal to later | |
| 473 | + addKeyedRemoval(curFromNodeKey); | |
| 474 | + } else { | |
| 475 | + // NOTE: we skip nested keyed nodes from being removed since there is | |
| 476 | + // still a chance they will be matched up later | |
| 477 | + removeNode(curFromNodeChild, fromEl, true /* skip keyed nodes */); | |
| 478 | + } | |
| 479 | + curFromNodeChild = fromNextSibling; | |
| 480 | + } | |
| 481 | + } | |
| 482 | + | |
| 483 | + function morphEl(fromEl, toEl, childrenOnly) { | |
| 484 | + var toElKey = getNodeKey(toEl); | |
| 485 | + | |
| 486 | + if (toElKey) { | |
| 487 | + // If an element with an ID is being morphed then it will be in the final | |
| 488 | + // DOM so clear it out of the saved elements collection | |
| 489 | + delete fromNodesLookup[toElKey]; | |
| 490 | + } | |
| 491 | + | |
| 492 | + if (!childrenOnly) { | |
| 493 | + // optional | |
| 494 | + var beforeUpdateResult = onBeforeElUpdated(fromEl, toEl); | |
| 495 | + if (beforeUpdateResult === false) { | |
| 496 | + return; | |
| 497 | + } else if (beforeUpdateResult instanceof HTMLElement) { | |
| 498 | + fromEl = beforeUpdateResult; | |
| 499 | + // reindex the new fromEl in case it's not in the same | |
| 500 | + // tree as the original fromEl | |
| 501 | + // (Phoenix LiveView sometimes returns a cloned tree, | |
| 502 | + // but keyed lookups would still point to the original tree) | |
| 503 | + indexTree(fromEl); | |
| 504 | + } | |
| 505 | + | |
| 506 | + // update attributes on original DOM element first | |
| 507 | + morphAttrs(fromEl, toEl); | |
| 508 | + // optional | |
| 509 | + onElUpdated(fromEl); | |
| 510 | + | |
| 511 | + if (onBeforeElChildrenUpdated(fromEl, toEl) === false) { | |
| 512 | + return; | |
| 513 | + } | |
| 514 | + } | |
| 515 | + | |
| 516 | + if (fromEl.nodeName !== 'TEXTAREA') { | |
| 517 | + morphChildren(fromEl, toEl); | |
| 518 | + } else { | |
| 519 | + specialElHandlers.TEXTAREA(fromEl, toEl); | |
| 520 | + } | |
| 521 | + } | |
| 522 | + | |
| 523 | + function morphChildren(fromEl, toEl) { | |
| 524 | + var skipFrom = skipFromChildren(fromEl, toEl); | |
| 525 | + var curToNodeChild = toEl.firstChild; | |
| 526 | + var curFromNodeChild = fromEl.firstChild; | |
| 527 | + var curToNodeKey; | |
| 528 | + var curFromNodeKey; | |
| 529 | + | |
| 530 | + var fromNextSibling; | |
| 531 | + var toNextSibling; | |
| 532 | + var matchingFromEl; | |
| 533 | + | |
| 534 | + // walk the children | |
| 535 | + outer: while (curToNodeChild) { | |
| 536 | + toNextSibling = curToNodeChild.nextSibling; | |
| 537 | + curToNodeKey = getNodeKey(curToNodeChild); | |
| 538 | + | |
| 539 | + // walk the fromNode children all the way through | |
| 540 | + while (!skipFrom && curFromNodeChild) { | |
| 541 | + fromNextSibling = curFromNodeChild.nextSibling; | |
| 542 | + | |
| 543 | + if (curToNodeChild.isSameNode && curToNodeChild.isSameNode(curFromNodeChild)) { | |
| 544 | + curToNodeChild = toNextSibling; | |
| 545 | + curFromNodeChild = fromNextSibling; | |
| 546 | + continue outer; | |
| 547 | + } | |
| 548 | + | |
| 549 | + curFromNodeKey = getNodeKey(curFromNodeChild); | |
| 550 | + | |
| 551 | + var curFromNodeType = curFromNodeChild.nodeType; | |
| 552 | + | |
| 553 | + // this means if the curFromNodeChild doesnt have a match with the curToNodeChild | |
| 554 | + var isCompatible = undefined; | |
| 555 | + | |
| 556 | + if (curFromNodeType === curToNodeChild.nodeType) { | |
| 557 | + if (curFromNodeType === ELEMENT_NODE) { | |
| 558 | + // Both nodes being compared are Element nodes | |
| 559 | + | |
| 560 | + if (curToNodeKey) { | |
| 561 | + // The target node has a key so we want to match it up with the correct element | |
| 562 | + // in the original DOM tree | |
| 563 | + if (curToNodeKey !== curFromNodeKey) { | |
| 564 | + // The current element in the original DOM tree does not have a matching key so | |
| 565 | + // let's check our lookup to see if there is a matching element in the original | |
| 566 | + // DOM tree | |
| 567 | + if ((matchingFromEl = fromNodesLookup[curToNodeKey])) { | |
| 568 | + if (fromNextSibling === matchingFromEl) { | |
| 569 | + // Special case for single element removals. To avoid removing the original | |
| 570 | + // DOM node out of the tree (since that can break CSS transitions, etc.), | |
| 571 | + // we will instead discard the current node and wait until the next | |
| 572 | + // iteration to properly match up the keyed target element with its matching | |
| 573 | + // element in the original tree | |
| 574 | + isCompatible = false; | |
| 575 | + } else { | |
| 576 | + // We found a matching keyed element somewhere in the original DOM tree. | |
| 577 | + // Let's move the original DOM node into the current position and morph | |
| 578 | + // it. | |
| 579 | + | |
| 580 | + // NOTE: We use insertBefore instead of replaceChild because we want to go through | |
| 581 | + // the `removeNode()` function for the node that is being discarded so that | |
| 582 | + // all lifecycle hooks are correctly invoked | |
| 583 | + fromEl.insertBefore(matchingFromEl, curFromNodeChild); | |
| 584 | + | |
| 585 | + // fromNextSibling = curFromNodeChild.nextSibling; | |
| 586 | + | |
| 587 | + if (curFromNodeKey) { | |
| 588 | + // Since the node is keyed it might be matched up later so we defer | |
| 589 | + // the actual removal to later | |
| 590 | + addKeyedRemoval(curFromNodeKey); | |
| 591 | + } else { | |
| 592 | + // NOTE: we skip nested keyed nodes from being removed since there is | |
| 593 | + // still a chance they will be matched up later | |
| 594 | + removeNode(curFromNodeChild, fromEl, true /* skip keyed nodes */); | |
| 595 | + } | |
| 596 | + | |
| 597 | + curFromNodeChild = matchingFromEl; | |
| 598 | + curFromNodeKey = getNodeKey(curFromNodeChild); | |
| 599 | + } | |
| 600 | + } else { | |
| 601 | + // The nodes are not compatible since the "to" node has a key and there | |
| 602 | + // is no matching keyed node in the source tree | |
| 603 | + isCompatible = false; | |
| 604 | + } | |
| 605 | + } | |
| 606 | + } else if (curFromNodeKey) { | |
| 607 | + // The original has a key | |
| 608 | + isCompatible = false; | |
| 609 | + } | |
| 610 | + | |
| 611 | + isCompatible = isCompatible !== false && compareNodeNames(curFromNodeChild, curToNodeChild); | |
| 612 | + if (isCompatible) { | |
| 613 | + // We found compatible DOM elements so transform | |
| 614 | + // the current "from" node to match the current | |
| 615 | + // target DOM node. | |
| 616 | + // MORPH | |
| 617 | + morphEl(curFromNodeChild, curToNodeChild); | |
| 618 | + } | |
| 619 | + | |
| 620 | + } else if (curFromNodeType === TEXT_NODE || curFromNodeType == COMMENT_NODE) { | |
| 621 | + // Both nodes being compared are Text or Comment nodes | |
| 622 | + isCompatible = true; | |
| 623 | + // Simply update nodeValue on the original node to | |
| 624 | + // change the text value | |
| 625 | + if (curFromNodeChild.nodeValue !== curToNodeChild.nodeValue) { | |
| 626 | + curFromNodeChild.nodeValue = curToNodeChild.nodeValue; | |
| 627 | + } | |
| 628 | + | |
| 629 | + } | |
| 630 | + } | |
| 631 | + | |
| 632 | + if (isCompatible) { | |
| 633 | + // Advance both the "to" child and the "from" child since we found a match | |
| 634 | + // Nothing else to do as we already recursively called morphChildren above | |
| 635 | + curToNodeChild = toNextSibling; | |
| 636 | + curFromNodeChild = fromNextSibling; | |
| 637 | + continue outer; | |
| 638 | + } | |
| 639 | + | |
| 640 | + // No compatible match so remove the old node from the DOM and continue trying to find a | |
| 641 | + // match in the original DOM. However, we only do this if the from node is not keyed | |
| 642 | + // since it is possible that a keyed node might match up with a node somewhere else in the | |
| 643 | + // target tree and we don't want to discard it just yet since it still might find a | |
| 644 | + // home in the final DOM tree. After everything is done we will remove any keyed nodes | |
| 645 | + // that didn't find a home | |
| 646 | + if (curFromNodeKey) { | |
| 647 | + // Since the node is keyed it might be matched up later so we defer | |
| 648 | + // the actual removal to later | |
| 649 | + addKeyedRemoval(curFromNodeKey); | |
| 650 | + } else { | |
| 651 | + // NOTE: we skip nested keyed nodes from being removed since there is | |
| 652 | + // still a chance they will be matched up later | |
| 653 | + removeNode(curFromNodeChild, fromEl, true /* skip keyed nodes */); | |
| 654 | + } | |
| 655 | + | |
| 656 | + curFromNodeChild = fromNextSibling; | |
| 657 | + } // END: while(curFromNodeChild) {} | |
| 658 | + | |
| 659 | + // If we got this far then we did not find a candidate match for | |
| 660 | + // our "to node" and we exhausted all of the children "from" | |
| 661 | + // nodes. Therefore, we will just append the current "to" node | |
| 662 | + // to the end | |
| 663 | + if (curToNodeKey && (matchingFromEl = fromNodesLookup[curToNodeKey]) && compareNodeNames(matchingFromEl, curToNodeChild)) { | |
| 664 | + // MORPH | |
| 665 | + if(!skipFrom){ addChild(fromEl, matchingFromEl); } | |
| 666 | + morphEl(matchingFromEl, curToNodeChild); | |
| 667 | + } else { | |
| 668 | + var onBeforeNodeAddedResult = onBeforeNodeAdded(curToNodeChild); | |
| 669 | + if (onBeforeNodeAddedResult !== false) { | |
| 670 | + if (onBeforeNodeAddedResult) { | |
| 671 | + curToNodeChild = onBeforeNodeAddedResult; | |
| 672 | + } | |
| 673 | + | |
| 674 | + if (curToNodeChild.actualize) { | |
| 675 | + curToNodeChild = curToNodeChild.actualize(fromEl.ownerDocument || doc); | |
| 676 | + } | |
| 677 | + addChild(fromEl, curToNodeChild); | |
| 678 | + handleNodeAdded(curToNodeChild); | |
| 679 | + } | |
| 680 | + } | |
| 681 | + | |
| 682 | + curToNodeChild = toNextSibling; | |
| 683 | + curFromNodeChild = fromNextSibling; | |
| 684 | + } | |
| 685 | + | |
| 686 | + cleanupFromEl(fromEl, curFromNodeChild, curFromNodeKey); | |
| 687 | + | |
| 688 | + var specialElHandler = specialElHandlers[fromEl.nodeName]; | |
| 689 | + if (specialElHandler) { | |
| 690 | + specialElHandler(fromEl, toEl); | |
| 691 | + } | |
| 692 | + } // END: morphChildren(...) | |
| 693 | + | |
| 694 | + var morphedNode = fromNode; | |
| 695 | + var morphedNodeType = morphedNode.nodeType; | |
| 696 | + var toNodeType = toNode.nodeType; | |
| 697 | + | |
| 698 | + if (!childrenOnly) { | |
| 699 | + // Handle the case where we are given two DOM nodes that are not | |
| 700 | + // compatible (e.g. <div> --> <span> or <div> --> TEXT) | |
| 701 | + if (morphedNodeType === ELEMENT_NODE) { | |
| 702 | + if (toNodeType === ELEMENT_NODE) { | |
| 703 | + if (!compareNodeNames(fromNode, toNode)) { | |
| 704 | + onNodeDiscarded(fromNode); | |
| 705 | + morphedNode = moveChildren(fromNode, createElementNS(toNode.nodeName, toNode.namespaceURI)); | |
| 706 | + } | |
| 707 | + } else { | |
| 708 | + // Going from an element node to a text node | |
| 709 | + morphedNode = toNode; | |
| 710 | + } | |
| 711 | + } else if (morphedNodeType === TEXT_NODE || morphedNodeType === COMMENT_NODE) { // Text or comment node | |
| 712 | + if (toNodeType === morphedNodeType) { | |
| 713 | + if (morphedNode.nodeValue !== toNode.nodeValue) { | |
| 714 | + morphedNode.nodeValue = toNode.nodeValue; | |
| 715 | + } | |
| 716 | + | |
| 717 | + return morphedNode; | |
| 718 | + } else { | |
| 719 | + // Text node to something else | |
| 720 | + morphedNode = toNode; | |
| 721 | + } | |
| 722 | + } | |
| 723 | + } | |
| 724 | + | |
| 725 | + if (morphedNode === toNode) { | |
| 726 | + // The "to node" was not compatible with the "from node" so we had to | |
| 727 | + // toss out the "from node" and use the "to node" | |
| 728 | + onNodeDiscarded(fromNode); | |
| 729 | + } else { | |
| 730 | + if (toNode.isSameNode && toNode.isSameNode(morphedNode)) { | |
| 731 | + return; | |
| 732 | + } | |
| 733 | + | |
| 734 | + morphEl(morphedNode, toNode, childrenOnly); | |
| 735 | + | |
| 736 | + // We now need to loop over any keyed nodes that might need to be | |
| 737 | + // removed. We only do the removal if we know that the keyed node | |
| 738 | + // never found a match. When a keyed node is matched up we remove | |
| 739 | + // it out of fromNodesLookup and we use fromNodesLookup to determine | |
| 740 | + // if a keyed node has been matched up or not | |
| 741 | + if (keyedRemovalList) { | |
| 742 | + for (var i=0, len=keyedRemovalList.length; i<len; i++) { | |
| 743 | + var elToRemove = fromNodesLookup[keyedRemovalList[i]]; | |
| 744 | + if (elToRemove) { | |
| 745 | + removeNode(elToRemove, elToRemove.parentNode, false); | |
| 746 | + } | |
| 747 | + } | |
| 748 | + } | |
| 749 | + } | |
| 750 | + | |
| 751 | + if (!childrenOnly && morphedNode !== fromNode && fromNode.parentNode) { | |
| 752 | + if (morphedNode.actualize) { | |
| 753 | + morphedNode = morphedNode.actualize(fromNode.ownerDocument || doc); | |
| 754 | + } | |
| 755 | + // If we had to swap out the from node with a new node because the old | |
| 756 | + // node was not compatible with the target node then we need to | |
| 757 | + // replace the old DOM node in the original DOM tree. This is only | |
| 758 | + // possible if the original DOM node was part of a DOM tree which | |
| 759 | + // we know is the case if it has a parent node. | |
| 760 | + fromNode.parentNode.replaceChild(morphedNode, fromNode); | |
| 761 | + } | |
| 762 | + | |
| 763 | + return morphedNode; | |
| 764 | + }; | |
| 765 | +} | |
| 766 | + | |
| 767 | +var morphdom = morphdomFactory(morphAttrs); | |
| 768 | + | |
| 769 | +export default morphdom; | |
| @@ -0,0 +1,21 @@ | ||
| 1 | +The MIT License (MIT) | |
| 2 | + | |
| 3 | +Copyright (c) Patrick Steele-Idem <pnidem@gmail.com> (psteeleidem.com) | |
| 4 | + | |
| 5 | +Permission is hereby granted, free of charge, to any person obtaining a copy | |
| 6 | +of this software and associated documentation files (the "Software"), to deal | |
| 7 | +in the Software without restriction, including without limitation the rights | |
| 8 | +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | |
| 9 | +copies of the Software, and to permit persons to whom the Software is | |
| 10 | +furnished to do so, subject to the following conditions: | |
| 11 | + | |
| 12 | +The above copyright notice and this permission notice shall be included in | |
| 13 | +all copies or substantial portions of the Software. | |
| 14 | + | |
| 15 | +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | |
| 16 | +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |
| 17 | +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | |
| 18 | +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |
| 19 | +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | |
| 20 | +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN | |
| 21 | +THE SOFTWARE. | |
| 21 | \ No newline at end of file | |
| @@ -1,10 +1,17 @@ | ||
| 1 | 1 | import { POPUP_RESULT, POPUP_TYPE, Popup } from '../../../popup.js'; |
| 2 | 2 | import { setSlashCommandAutoComplete } from '../../../slash-commands.js'; |
| 3 | 3 | import { SlashCommandAbortController } from '../../../slash-commands/SlashCommandAbortController.js'; |
| 4 | +import { SlashCommandBreakPoint } from '../../../slash-commands/SlashCommandBreakPoint.js'; | |
| 5 | +import { SlashCommandClosure } from '../../../slash-commands/SlashCommandClosure.js'; | |
| 6 | +import { SlashCommandClosureResult } from '../../../slash-commands/SlashCommandClosureResult.js'; | |
| 7 | +import { SlashCommandDebugController } from '../../../slash-commands/SlashCommandDebugController.js'; | |
| 8 | +import { SlashCommandExecutor } from '../../../slash-commands/SlashCommandExecutor.js'; | |
| 9 | +import { SlashCommandParser } from '../../../slash-commands/SlashCommandParser.js'; | |
| 4 | 10 | import { SlashCommandParserError } from '../../../slash-commands/SlashCommandParserError.js'; |
| 5 | 11 | import { SlashCommandScope } from '../../../slash-commands/SlashCommandScope.js'; |
| 6 | 12 | import { debounce, delay, getSortableDelay, showFontAwesomePicker } from '../../../utils.js'; |
| 7 | 13 | import { log, quickReplyApi, warn } from '../index.js'; |
| 14 | +import morphdom from '../lib/morphdom-esm.js'; | |
| 8 | 15 | import { QuickReplyContextLink } from './QuickReplyContextLink.js'; |
| 9 | 16 | import { QuickReplySet } from './QuickReplySet.js'; |
| 10 | 17 | import { ContextMenu } from './ui/ctx/ContextMenu.js'; |
| @@ -21,48 +28,61 @@ export class QuickReply { | ||
| 21 | 28 | |
| 22 | 29 | |
| 23 | 30 | |
| 24 | 31 | /**@type {Numbernumber}*/ id; |
| 25 | 32 | /**@type {Stringstring}*/ label = ''icon; |
| 26 | 33 | /**@type {Stringstring}*/ titlelabel = ''; |
| 27 | 34 | /**@type {Stringboolean}*/ messageshowLabel = ''false; |
| 35 | + /**@type {string}*/ title = ''; | |
| 36 | + /**@type {string}*/ message = ''; | |
| 28 | 37 | |
| 29 | 38 | /**@type {QuickReplyContextLink[]}*/ contextList; |
| 30 | 39 | |
| 31 | 40 | /**@type {Booleanboolean}*/ preventAutoExecute = true; |
| 32 | 41 | /**@type {Booleanboolean}*/ isHidden = false; |
| 33 | 42 | /**@type {Booleanboolean}*/ executeOnStartup = false; |
| 34 | 43 | /**@type {Booleanboolean}*/ executeOnUser = false; |
| 35 | 44 | /**@type {Booleanboolean}*/ executeOnAi = false; |
| 36 | 45 | /**@type {Booleanboolean}*/ executeOnChatChange = false; |
| 37 | 46 | /**@type {Booleanboolean}*/ executeOnGroupMemberDraft = false; |
| 38 | 47 | /**@type {Stringstring}*/ automationId = ''; |
| 39 | 48 | |
| 40 | 49 | /**@type {Functionfunction}*/ onExecute; |
| 41 | - /**@type {Function}*/ onDelete; | |
| 50 | + /**@type {(qr:QuickReply)=>AsyncGenerator<SlashCommandClosureResult|{closure:SlashCommandClosure, executor:SlashCommandExecutor|SlashCommandClosureResult}, SlashCommandClosureResult, boolean>}*/ onDebug; | |
| 42 | 51 | /**@type {Functionfunction}*/ onUpdateonDelete; |
| 52 | + /**@type {function}*/ onUpdate; | |
| 53 | + /**@type {function}*/ onInsertBefore; | |
| 54 | + /**@type {function}*/ onTransfer; | |
| 43 | 55 | |
| 44 | 56 | |
| 45 | 57 | /**@type {HTMLElement}*/ dom; |
| 58 | + /**@type {HTMLElement}*/ domIcon; | |
| 46 | 59 | /**@type {HTMLElement}*/ domLabel; |
| 47 | 60 | /**@type {HTMLElement}*/ settingsDom; |
| 61 | + /**@type {HTMLElement}*/ settingsDomIcon; | |
| 48 | 62 | /**@type {HTMLInputElement}*/ settingsDomLabel; |
| 49 | 63 | /**@type {HTMLTextAreaElement}*/ settingsDomMessage; |
| 50 | 64 | |
| 51 | 65 | /**@type {Popup}*/ editorPopup; |
| 66 | + /**@type {HTMLElement}*/ editorDom; | |
| 52 | 67 | |
| 68 | + /**@type {HTMLTextAreaElement}*/ editorMessage; | |
| 69 | + /**@type {HTMLTextAreaElement}*/ editorMessageLabel; | |
| 70 | + /**@type {HTMLElement}*/ editorSyntax; | |
| 53 | 71 | /**@type {HTMLElement}*/ editorExecuteBtn; |
| 54 | 72 | /**@type {HTMLElement}*/ editorExecuteBtnPause; |
| 55 | 73 | /**@type {HTMLElement}*/ editorExecuteBtnStop; |
| 56 | 74 | /**@type {HTMLElement}*/ editorExecuteProgress; |
| 57 | 75 | /**@type {HTMLElement}*/ editorExecuteErrors; |
| 58 | 76 | /**@type {HTMLElement}*/ editorExecuteResult; |
| 59 | 77 | /**@type {HTMLInputElementHTMLElement}*/ editorExecuteHideeditorDebugState; |
| 60 | 78 | /**@type {Promise}*/ editorExecutePromise; |
| 79 | + /**@type {boolean}*/ isExecuting; | |
| 61 | 80 | /**@type {SlashCommandAbortController}*/ abortController; |
| 81 | + /**@type {SlashCommandDebugController}*/ debugController; | |
| 62 | 82 | |
| 63 | 83 | |
| 64 | 84 | get hasContext() { |
| 65 | 85 | return this.contextList && this.contextList.filter(it => it.set).length > 0; |
| 66 | 86 | } |
| 67 | 87 | |
| 68 | 88 | |
| @@ -75,6 +95,14 @@ export class QuickReply { | ||
| 75 | 95 | updateRender() { |
| 76 | 96 | if (!this.dom) return; |
| 77 | 97 | this.dom.title = this.title || this.message; |
| 98 | + if (this.icon) { | |
| 99 | + this.domIcon.classList.remove('qr--hidden'); | |
| 100 | + if (this.showLabel) this.domLabel.classList.remove('qr--hidden'); | |
| 101 | + else this.domLabel.classList.add('qr--hidden'); | |
| 102 | + } else { | |
| 103 | + this.domIcon.classList.add('qr--hidden'); | |
| 104 | + this.domLabel.classList.remove('qr--hidden'); | |
| 105 | + } | |
| 78 | 106 | this.domLabel.textContent = this.label; |
| 79 | 107 | this.dom.classList[this.hasContext ? 'add' : 'remove']('qr--hasCtx'); |
| 80 | 108 | } |
| @@ -105,9 +133,18 @@ export class QuickReply { | ||
| 105 | 133 | } |
| 106 | 134 | this.execute(); |
| 107 | 135 | }); |
| 136 | + const icon = document.createElement('div'); { | |
| 137 | + this.domIcon = icon; | |
| 138 | + icon.classList.add('qr--button-icon'); | |
| 139 | + icon.classList.add('fa-solid'); | |
| 140 | + if (!this.icon) icon.classList.add('qr--hidden'); | |
| 141 | + else icon.classList.add(this.icon); | |
| 142 | + root.append(icon); | |
| 143 | + } | |
| 108 | 144 | const lbl = document.createElement('div'); { |
| 109 | 145 | this.domLabel = lbl; |
| 110 | 146 | lbl.classList.add('qr--button-label'); |
| 147 | + if (this.icon && !this.showLabel) lbl.classList.add('qr--hidden'); | |
| 111 | 148 | lbl.textContent = this.label; |
| 112 | 149 | root.append(lbl); |
| 113 | 150 | } |
| @@ -138,14 +175,90 @@ export class QuickReply { | ||
| 138 | 175 | item.classList.add('qr--set-item'); |
| 139 | 176 | item.setAttribute('data-order', String(idx)); |
| 140 | 177 | item.setAttribute('data-id', String(this.id)); |
| 178 | + const adder = document.createElement('div'); { | |
| 179 | + adder.classList.add('qr--set-itemAdder'); | |
| 180 | + const actions = document.createElement('div'); { | |
| 181 | + actions.classList.add('qr--actions'); | |
| 182 | + const addNew = document.createElement('div'); { | |
| 183 | + addNew.classList.add('qr--action'); | |
| 184 | + addNew.classList.add('qr--add'); | |
| 185 | + addNew.classList.add('menu_button'); | |
| 186 | + addNew.classList.add('menu_button_icon'); | |
| 187 | + addNew.classList.add('fa-solid'); | |
| 188 | + addNew.classList.add('fa-plus'); | |
| 189 | + addNew.title = 'Add quick reply'; | |
| 190 | + addNew.addEventListener('click', ()=>this.onInsertBefore()); | |
| 191 | + actions.append(addNew); | |
| 192 | + } | |
| 193 | + const paste = document.createElement('div'); { | |
| 194 | + paste.classList.add('qr--action'); | |
| 195 | + paste.classList.add('qr--paste'); | |
| 196 | + paste.classList.add('menu_button'); | |
| 197 | + paste.classList.add('menu_button_icon'); | |
| 198 | + paste.classList.add('fa-solid'); | |
| 199 | + paste.classList.add('fa-paste'); | |
| 200 | + paste.title = 'Add quick reply from clipboard'; | |
| 201 | + paste.addEventListener('click', async()=>{ | |
| 202 | + const text = await navigator.clipboard.readText(); | |
| 203 | + this.onInsertBefore(text); | |
| 204 | + }); | |
| 205 | + actions.append(paste); | |
| 206 | + } | |
| 207 | + const importFile = document.createElement('div'); { | |
| 208 | + importFile.classList.add('qr--action'); | |
| 209 | + importFile.classList.add('qr--importFile'); | |
| 210 | + importFile.classList.add('menu_button'); | |
| 211 | + importFile.classList.add('menu_button_icon'); | |
| 212 | + importFile.classList.add('fa-solid'); | |
| 213 | + importFile.classList.add('fa-file-import'); | |
| 214 | + importFile.title = 'Add quick reply from JSON file'; | |
| 215 | + importFile.addEventListener('click', async()=>{ | |
| 216 | + const inp = document.createElement('input'); { | |
| 217 | + inp.type = 'file'; | |
| 218 | + inp.accept = '.json'; | |
| 219 | + inp.addEventListener('change', async()=>{ | |
| 220 | + if (inp.files.length > 0) { | |
| 221 | + for (const file of inp.files) { | |
| 222 | + const text = await file.text(); | |
| 223 | + this.onInsertBefore(text); | |
| 224 | + } | |
| 225 | + } | |
| 226 | + }); | |
| 227 | + inp.click(); | |
| 228 | + } | |
| 229 | + }); | |
| 230 | + actions.append(importFile); | |
| 231 | + } | |
| 232 | + adder.append(actions); | |
| 233 | + } | |
| 234 | + item.append(adder); | |
| 235 | + } | |
| 236 | + const itemContent = document.createElement('div'); { | |
| 237 | + itemContent.classList.add('qr--content'); | |
| 141 | 238 | const drag = document.createElement('div'); { |
| 142 | 239 | drag.classList.add('drag-handle'); |
| 143 | 240 | drag.classList.add('ui-sortable-handle'); |
| 144 | 241 | drag.textContent = '☰'; |
| 145 | 242 | item itemContent.append(drag); |
| 146 | 243 | } |
| 147 | 244 | const lblContainer = document.createElement('div'); { |
| 148 | 245 | lblContainer.classList.add('qr--set-itemLabelContainer'); |
| 246 | + const icon = document.createElement('div'); { | |
| 247 | + this.settingsDomIcon = icon; | |
| 248 | + icon.title = 'Click to change icon'; | |
| 249 | + icon.classList.add('qr--set-itemIcon'); | |
| 250 | + icon.classList.add('menu_button'); | |
| 251 | + icon.classList.add('fa-fw'); | |
| 252 | + if (this.icon) { | |
| 253 | + icon.classList.add('fa-solid'); | |
| 254 | + icon.classList.add(this.icon); | |
| 255 | + } | |
| 256 | + icon.addEventListener('click', async()=>{ | |
| 257 | + let value = await showFontAwesomePicker(); | |
| 258 | + this.updateIcon(value); | |
| 259 | + }); | |
| 260 | + lblContainer.append(icon); | |
| 261 | + } | |
| 149 | 262 | const lbl = document.createElement('input'); { |
| 150 | 263 | this.settingsDomLabel = lbl; |
| 151 | 264 | lbl.classList.add('qr--set-itemLabel'); |
| @@ -154,20 +267,23 @@ export class QuickReply { | ||
| 154 | 267 | lbl.addEventListener('input', ()=>this.updateLabel(lbl.value)); |
| 155 | 268 | lblContainer.append(lbl); |
| 156 | 269 | } |
| 157 | 270 | item itemContent.append(lblContainer); |
| 271 | + } | |
| 272 | + item.append(itemContent); | |
| 158 | 273 | } |
| 159 | 274 | const optContainer = document.createElement('div'); { |
| 160 | 275 | optContainer.classList.add('qr--set-optionsContainer'); |
| 161 | 276 | const opt = document.createElement('div'); { |
| 162 | 277 | opt.classList.add('qr--action'); |
| 163 | 278 | opt.classList.add('menu_button'); |
| 279 | + opt.classList.add('fa-fw'); | |
| 164 | 280 | opt.classList.add('fa-solid'); |
| 165 | 281 | opt.textContent = '⁝'; |
| 166 | 282 | opt.title = 'Additional options:\n - large editor\n - context menu\n - auto-execution\n - tooltip'; |
| 167 | 283 | opt.addEventListener('click', ()=>this.showEditor()); |
| 168 | 284 | optContainer.append(opt); |
| 169 | 285 | } |
| 170 | 286 | itemitemContent.append(optContainer); |
| 171 | 287 | } |
| 172 | 288 | const mes = document.createElement('textarea'); { |
| 173 | 289 | this.settingsDomMessage = mes; |
| @@ -176,22 +292,89 @@ export class QuickReply { | ||
| 176 | 292 | mes.value = this.message; |
| 177 | 293 | //HACK need to use jQuery to catch the triggered event from the expanded editor |
| 178 | 294 | $(mes).on('input', ()=>this.updateMessage(mes.value)); |
| 179 | 295 | itemitemContent.append(mes); |
| 180 | 296 | } |
| 181 | 297 | const actions = document.createElement('div'); { |
| 182 | 298 | actions.classList.add('qr--actions'); |
| 299 | + const move = document.createElement('div'); { | |
| 300 | + move.classList.add('qr--action'); | |
| 301 | + move.classList.add('menu_button'); | |
| 302 | + move.classList.add('fa-fw'); | |
| 303 | + move.classList.add('fa-solid'); | |
| 304 | + move.classList.add('fa-truck-arrow-right'); | |
| 305 | + move.title = 'Move quick reply to other set'; | |
| 306 | + move.addEventListener('click', ()=>this.onTransfer(this)); | |
| 307 | + actions.append(move); | |
| 308 | + } | |
| 309 | + const copy = document.createElement('div'); { | |
| 310 | + copy.classList.add('qr--action'); | |
| 311 | + copy.classList.add('menu_button'); | |
| 312 | + copy.classList.add('fa-fw'); | |
| 313 | + copy.classList.add('fa-solid'); | |
| 314 | + copy.classList.add('fa-copy'); | |
| 315 | + copy.title = 'Copy quick reply to clipboard'; | |
| 316 | + copy.addEventListener('click', async()=>{ | |
| 317 | + await navigator.clipboard.writeText(JSON.stringify(this)); | |
| 318 | + copy.classList.add('qr--success'); | |
| 319 | + await delay(3010); | |
| 320 | + copy.classList.remove('qr--success'); | |
| 321 | + }); | |
| 322 | + actions.append(copy); | |
| 323 | + } | |
| 324 | + const cut = document.createElement('div'); { | |
| 325 | + cut.classList.add('qr--action'); | |
| 326 | + cut.classList.add('menu_button'); | |
| 327 | + cut.classList.add('fa-fw'); | |
| 328 | + cut.classList.add('fa-solid'); | |
| 329 | + cut.classList.add('fa-cut'); | |
| 330 | + cut.title = 'Cut quick reply to clipboard (copy and remove)'; | |
| 331 | + cut.addEventListener('click', async()=>{ | |
| 332 | + await navigator.clipboard.writeText(JSON.stringify(this)); | |
| 333 | + this.delete(); | |
| 334 | + }); | |
| 335 | + actions.append(cut); | |
| 336 | + } | |
| 337 | + const exp = document.createElement('div'); { | |
| 338 | + exp.classList.add('qr--action'); | |
| 339 | + exp.classList.add('menu_button'); | |
| 340 | + exp.classList.add('fa-fw'); | |
| 341 | + exp.classList.add('fa-solid'); | |
| 342 | + exp.classList.add('fa-file-export'); | |
| 343 | + exp.title = 'Export quick reply as file'; | |
| 344 | + exp.addEventListener('click', ()=>{ | |
| 345 | + const blob = new Blob([JSON.stringify(this)], { type:'text' }); | |
| 346 | + const url = URL.createObjectURL(blob); | |
| 347 | + const a = document.createElement('a'); { | |
| 348 | + a.href = url; | |
| 349 | + a.download = `${this.label}.qr.json`; | |
| 350 | + a.click(); | |
| 351 | + } | |
| 352 | + }); | |
| 353 | + actions.append(exp); | |
| 354 | + } | |
| 183 | 355 | const del = document.createElement('div'); { |
| 184 | 356 | del.classList.add('qr--action'); |
| 185 | 357 | del.classList.add('menu_button'); |
| 186 | 358 | del.classList.add('menu_button_iconfa-fw'); |
| 187 | 359 | del.classList.add('fa-solid'); |
| 188 | 360 | del.classList.add('fa-trash-can'); |
| 189 | 361 | del.classList.add('redWarningBG'); |
| 190 | 362 | del.title = 'Remove quickQuick replyReply\n---\nShit+Click to skip confirmation'; |
| 191 | 363 | del.addEventListener('click', async(evt)=>this.delete());{ |
| 364 | + if (!evt.shiftKey) { | |
| 365 | + const result = await Popup.show.confirm( | |
| 366 | + 'Remove Quick Reply', | |
| 367 | + 'Are you sure you want to remove this Quick Reply?', | |
| 368 | + ); | |
| 369 | + if (result != POPUP_RESULT.AFFIRMATIVE) { | |
| 370 | + return; | |
| 371 | + } | |
| 372 | + } | |
| 373 | + this.delete(); | |
| 374 | + }); | |
| 192 | 375 | actions.append(del); |
| 193 | 376 | } |
| 194 | 377 | itemitemContent.append(actions); |
| 195 | 378 | } |
| 196 | 379 | } |
| 197 | 380 | } |
| @@ -208,22 +391,156 @@ export class QuickReply { | ||
| 208 | 391 | /**@type {HTMLElement} */ |
| 209 | 392 | // @ts-ignore |
| 210 | 393 | const dom = this.template.cloneNode(true); |
| 394 | + this.editorDom = dom; | |
| 211 | 395 | this.editorPopup = new Popup(dom, POPUP_TYPE.TEXT, undefined, { okButton: 'OK', wide: true, large: true, rows: 1 }); |
| 212 | 396 | const popupResult = this.editorPopup.show(); |
| 213 | 397 | |
| 214 | 398 | // basics |
| 399 | + /**@type {HTMLElement}*/ | |
| 400 | + const icon = dom.querySelector('#qr--modal-icon'); | |
| 401 | + if (this.icon) { | |
| 402 | + icon.classList.add('fa-solid'); | |
| 403 | + icon.classList.add(this.icon); | |
| 404 | + } | |
| 405 | + else { | |
| 406 | + icon.textContent = '…'; | |
| 407 | + } | |
| 408 | + icon.addEventListener('click', async()=>{ | |
| 409 | + let value = await showFontAwesomePicker(); | |
| 410 | + if (value === null) return; | |
| 411 | + if (this.icon) icon.classList.remove(this.icon); | |
| 412 | + if (value == '') { | |
| 413 | + icon.classList.remove('fa-solid'); | |
| 414 | + icon.textContent = '…'; | |
| 415 | + } else { | |
| 416 | + icon.textContent = ''; | |
| 417 | + icon.classList.add('fa-solid'); | |
| 418 | + icon.classList.add(value); | |
| 419 | + } | |
| 420 | + this.updateIcon(value); | |
| 421 | + }); | |
| 422 | + /**@type {HTMLInputElement}*/ | |
| 423 | + const showLabel = dom.querySelector('#qr--modal-showLabel'); | |
| 424 | + showLabel.checked = this.showLabel; | |
| 425 | + showLabel.addEventListener('click', ()=>{ | |
| 426 | + this.updateShowLabel(showLabel.checked); | |
| 427 | + }); | |
| 215 | 428 | /**@type {HTMLInputElement}*/ |
| 216 | 429 | const label = dom.querySelector('#qr--modal-label'); |
| 217 | 430 | label.value = this.label; |
| 218 | 431 | label.addEventListener('input', ()=>{ |
| 219 | 432 | this.updateLabel(label.value); |
| 220 | 433 | }); |
| 434 | + let switcherList; | |
| 435 | + dom.querySelector('#qr--modal-switcher').addEventListener('click', (evt)=>{ | |
| 436 | + if (switcherList) { | |
| 437 | + switcherList.remove(); | |
| 438 | + switcherList = null; | |
| 439 | + return; | |
| 440 | + } | |
| 441 | + const list = document.createElement('ul'); { | |
| 442 | + switcherList = list; | |
| 443 | + list.classList.add('qr--modal-switcherList'); | |
| 444 | + const makeList = (qrs)=>{ | |
| 445 | + const setItem = document.createElement('li'); { | |
| 446 | + setItem.classList.add('qr--modal-switcherItem'); | |
| 447 | + setItem.addEventListener('click', ()=>{ | |
| 448 | + list.innerHTML = ''; | |
| 449 | + for (const qrs of quickReplyApi.listSets()) { | |
| 450 | + const item = document.createElement('li'); { | |
| 451 | + item.classList.add('qr--modal-switcherItem'); | |
| 452 | + item.addEventListener('click', ()=>{ | |
| 453 | + list.innerHTML = ''; | |
| 454 | + makeList(quickReplyApi.getSetByName(qrs)); | |
| 455 | + }); | |
| 456 | + const lbl = document.createElement('div'); { | |
| 457 | + lbl.classList.add('qr--label'); | |
| 458 | + lbl.textContent = qrs; | |
| 459 | + item.append(lbl); | |
| 460 | + } | |
| 461 | + list.append(item); | |
| 462 | + } | |
| 463 | + } | |
| 464 | + }); | |
| 465 | + const lbl = document.createElement('div'); { | |
| 466 | + lbl.classList.add('qr--label'); | |
| 467 | + const icon = document.createElement('i'); { | |
| 468 | + icon.classList.add('fa-solid'); | |
| 469 | + icon.classList.add('fa-arrow-alt-circle-right'); | |
| 470 | + icon.classList.add('menu_button'); | |
| 471 | + lbl.append(icon); | |
| 472 | + } | |
| 473 | + const text = document.createElement('span'); { | |
| 474 | + text.textContent = 'Switch QR Sets...'; | |
| 475 | + lbl.append(text); | |
| 476 | + } | |
| 477 | + setItem.append(lbl); | |
| 478 | + } | |
| 479 | + list.append(setItem); | |
| 480 | + } | |
| 481 | + const addItem = document.createElement('li'); { | |
| 482 | + addItem.classList.add('qr--modal-switcherItem'); | |
| 483 | + addItem.addEventListener('click', ()=>{ | |
| 484 | + const qr = quickReplyApi.getSetByQr(this).addQuickReply(); | |
| 485 | + this.editorPopup.completeAffirmative(); | |
| 486 | + qr.showEditor(); | |
| 487 | + }); | |
| 488 | + const lbl = document.createElement('div'); { | |
| 489 | + lbl.classList.add('qr--label'); | |
| 490 | + const icon = document.createElement('i'); { | |
| 491 | + icon.classList.add('fa-solid'); | |
| 492 | + icon.classList.add('fa-plus'); | |
| 493 | + icon.classList.add('menu_button'); | |
| 494 | + lbl.append(icon); | |
| 495 | + } | |
| 496 | + const text = document.createElement('span'); { | |
| 497 | + text.textContent = 'Add QR'; | |
| 498 | + lbl.append(text); | |
| 499 | + } | |
| 500 | + addItem.append(lbl); | |
| 501 | + } | |
| 502 | + list.append(addItem); | |
| 503 | + } | |
| 504 | + for (const qr of qrs.qrList.toSorted((a,b)=>a.label.toLowerCase().localeCompare(b.label.toLowerCase()))) { | |
| 505 | + const item = document.createElement('li'); { | |
| 506 | + item.classList.add('qr--modal-switcherItem'); | |
| 507 | + if (qr == this) item.classList.add('qr--current'); | |
| 508 | + else item.addEventListener('click', ()=>{ | |
| 509 | + this.editorPopup.completeAffirmative(); | |
| 510 | + qr.showEditor(); | |
| 511 | + }); | |
| 512 | + const lbl = document.createElement('div'); { | |
| 513 | + lbl.classList.add('qr--label'); | |
| 514 | + lbl.textContent = qr.label; | |
| 515 | + item.append(lbl); | |
| 516 | + } | |
| 517 | + const id = document.createElement('div'); { | |
| 518 | + id.classList.add('qr--id'); | |
| 519 | + id.textContent = qr.id.toString(); | |
| 520 | + item.append(id); | |
| 521 | + } | |
| 522 | + const mes = document.createElement('div'); { | |
| 523 | + mes.classList.add('qr--message'); | |
| 524 | + mes.textContent = qr.message; | |
| 525 | + item.append(mes); | |
| 526 | + } | |
| 527 | + list.append(item); | |
| 528 | + } | |
| 529 | + } | |
| 530 | + }; | |
| 531 | + makeList(quickReplyApi.getSetByQr(this)); | |
| 532 | + } | |
| 533 | + label.parentElement.append(list); | |
| 534 | + }); | |
| 221 | 535 | /**@type {HTMLInputElement}*/ |
| 222 | 536 | const title = dom.querySelector('#qr--modal-title'); |
| 223 | 537 | title.value = this.title; |
| 224 | 538 | title.addEventListener('input', () => { |
| 225 | 539 | this.updateTitle(title.value); |
| 226 | 540 | }); |
| 541 | + /**@type {HTMLElement}*/ | |
| 542 | + const messageSyntaxInner = dom.querySelector('#qr--modal-messageSyntaxInner'); | |
| 543 | + this.editorSyntax = messageSyntaxInner; | |
| 227 | 544 | /**@type {HTMLInputElement}*/ |
| 228 | 545 | const wrap = dom.querySelector('#qr--modal-wrap'); |
| 229 | 546 | wrap.checked = JSON.parse(localStorage.getItem('qr--wrap') ?? 'false'); |
| @@ -235,9 +552,15 @@ export class QuickReply { | ||
| 235 | 552 | if (wrap.checked) { |
| 236 | 553 | message.style.whiteSpace = 'pre-wrap'; |
| 237 | 554 | messageSyntaxInner.style.whiteSpace = 'pre-wrap'; |
| 555 | + if (this.clone) { | |
| 556 | + this.clone.style.whiteSpace = 'pre-wrap'; | |
| 557 | + } | |
| 238 | 558 | } else { |
| 239 | 559 | message.style.whiteSpace = 'pre'; |
| 240 | 560 | messageSyntaxInner.style.whiteSpace = 'pre'; |
| 561 | + if (this.clone) { | |
| 562 | + this.clone.style.whiteSpace = 'pre'; | |
| 563 | + } | |
| 241 | 564 | } |
| 242 | 565 | updateScrollDebounced(); |
| 243 | 566 | }; |
| @@ -261,11 +584,8 @@ export class QuickReply { | ||
| 261 | 584 | }); |
| 262 | 585 | }; |
| 263 | 586 | const updateScrollDebounced = updateScroll; |
| 264 | - const updateSyntax = ()=>{ | |
| 265 | - messageSyntaxInner.innerHTML = hljs.highlight(`${message.value}${message.value.slice(-1) == '\n' ? ' ' : ''}`, { language:'stscript', ignoreIllegals:true })?.value; | |
| 266 | - }; | |
| 267 | 587 | const updateSyntaxEnabled = ()=>{ |
| 268 | - if (JSON.parse(localStorage.getItem('qr--syntax'))) { | |
| 588 | + if (syntax.checked) { | |
| 269 | 589 | dom.querySelector('#qr--modal-messageHolder').classList.remove('qr--noSyntax'); |
| 270 | 590 | } else { |
| 271 | 591 | dom.querySelector('#qr--modal-messageHolder').classList.add('qr--noSyntax'); |
| @@ -296,48 +616,108 @@ export class QuickReply { | ||
| 296 | 616 | localStorage.setItem('qr--syntax', JSON.stringify(syntax.checked)); |
| 297 | 617 | updateSyntaxEnabled(); |
| 298 | 618 | }); |
| 619 | + if (navigator.keyboard) { | |
| 620 | + navigator.keyboard.getLayoutMap().then(it=>dom.querySelector('#qr--modal-commentKey').textContent = it.get('Backslash')); | |
| 621 | + } else { | |
| 622 | + dom.querySelector('#qr--modal-commentKey').closest('small').remove(); | |
| 623 | + } | |
| 624 | + this.editorMessageLabel = dom.querySelector('label[for="qr--modal-message"]'); | |
| 299 | 625 | /**@type {HTMLTextAreaElement}*/ |
| 300 | 626 | const message = dom.querySelector('#qr--modal-message'); |
| 627 | + this.editorMessage = message; | |
| 301 | 628 | message.value = this.message; |
| 629 | + const updateMessageDebounced = debounce((value)=>this.updateMessage(value), 10); | |
| 302 | 630 | message.addEventListener('input', () => { |
| 303 | 631 | updateSyntaxupdateMessageDebounced(message.value); |
| 304 | - this.updateMessage(message.value); | |
| 305 | 632 | updateScrollDebounced(); |
| 306 | - }); | |
| 633 | + }, { passive:true }); | |
| 307 | - setSlashCommandAutoComplete(message, true); | |
| 634 | + const getLineStart = ()=>{ | |
| 308 | - //TODO move tab support for textarea into its own helper(?) and use for both this and .editor_maximize | |
| 635 | + const start = message.selectionStart; | |
| 636 | + const end = message.selectionEnd; | |
| 637 | + let lineStart; | |
| 638 | + if (start == 0 || message.value[start - 1] == '\n') { | |
| 639 | + // cursor is already at beginning of line | |
| 640 | + // -> keep start | |
| 641 | + lineStart = start; | |
| 642 | + } else { | |
| 643 | + // cursor is at end of line or somewhere in the line | |
| 644 | + // -> find last newline before cursor and start after that | |
| 645 | + lineStart = message.value.lastIndexOf('\n', start - 1) + 1; | |
| 646 | + } | |
| 647 | + return lineStart; | |
| 648 | + }; | |
| 309 | 649 | message.addEventListener('keydown', async(evt) => { |
| 650 | + if (this.isExecuting) return; | |
| 310 | 651 | if (evt.key == 'Tab' && !evt.shiftKey && !evt.ctrlKey && !evt.altKey) { |
| 652 | + // increase indent | |
| 311 | 653 | evt.preventDefault(); |
| 312 | 654 | const start = message.selectionStart; |
| 313 | 655 | const end = message.selectionEnd; |
| 314 | 656 | if (end - start > 0 && message.value.substring(start, end).includes('\n')) { |
| 315 | - const lineStart = message.value.lastIndexOf('\n', start); | |
| 657 | + evt.stopImmediatePropagation(); | |
| 316 | - const count = message.value.substring(lineStart, end).split('\n').length - 1; | |
| 658 | + evt.stopPropagation(); | |
| 317 | - message.value = `${message.value.substring(0, lineStart)}${message.value.substring(lineStart, end).replace(/\n/g, '\n\t')}${message.value.substring(end)}`; | |
| 659 | + const lineStart = getLineStart(); | |
| 318 | 660 | message.selectionStart = start + 1lineStart; |
| 319 | - message.selectionEnd = end + count; | |
| 661 | + const affectedLines = message.value.substring(lineStart, end).split('\n'); | |
| 320 | - updateSyntax(); | |
| 662 | + // document.execCommand is deprecated (and potentially buggy in some browsers) but the only way to retain undo-history | |
| 321 | - } else { | |
| 663 | + document.execCommand('insertText', false, `\t${affectedLines.join('\n\t')}`); | |
| 322 | - message.value = `${message.value.substring(0, start)}\t${message.value.substring(end)}`; | |
| 323 | 664 | message.selectionStart = start + 1; |
| 324 | 665 | message.selectionEnd = end + 1affectedLines.length; |
| 325 | - updateSyntax(); | |
| 666 | + message.dispatchEvent(new Event('input', { bubbles:true })); | |
| 667 | + } else if (!(ac.isReplaceable && ac.isActive)) { | |
| 668 | + evt.stopImmediatePropagation(); | |
| 669 | + evt.stopPropagation(); | |
| 670 | + // document.execCommand is deprecated (and potentially buggy in some browsers) but the only way to retain undo-history | |
| 671 | + document.execCommand('insertText', false, '\t'); | |
| 672 | + message.dispatchEvent(new Event('input', { bubbles:true })); | |
| 326 | 673 | } |
| 327 | 674 | } else if (evt.key == 'Tab' && evt.shiftKey && !evt.ctrlKey && !evt.altKey) { |
| 675 | + // decrease indent | |
| 328 | 676 | evt.preventDefault(); |
| 677 | + evt.stopImmediatePropagation(); | |
| 678 | + evt.stopPropagation(); | |
| 329 | 679 | const start = message.selectionStart; |
| 330 | 680 | const end = message.selectionEnd; |
| 331 | 681 | const lineStart = message.value.lastIndexOfgetLineStart('\n', start); |
| 332 | - const count = message.value.substring(lineStart, end).split('\n\t').length - 1; | |
| 682 | + message.selectionStart = lineStart; | |
| 333 | - message.value = `${message.value.substring(0, lineStart)}${message.value.substring(lineStart, end).replace(/\n\t/g, '\n')}${message.value.substring(end)}`; | |
| 683 | + const affectedLines = message.value.substring(lineStart, end).split('\n'); | |
| 334 | - message.selectionStart = start - 1; | |
| 684 | + const newText = affectedLines.map(it=>it.replace(/^\t/, '')).join('\n'); | |
| 335 | - message.selectionEnd = end - count; | |
| 685 | + const delta = affectedLines.join('\n').length - newText.length; | |
| 336 | - updateSyntax(); | |
| 686 | + // document.execCommand is deprecated (and potentially buggy in some browsers) but the only way to retain undo-history | |
| 337 | - } else if (evt.key == 'Enter' && evt.ctrlKey && !evt.shiftKey && !evt.altKey) { | |
| 687 | + if (delta > 0) { | |
| 688 | + if (newText == '') { | |
| 689 | + document.execCommand('delete', false); | |
| 690 | + } else { | |
| 691 | + document.execCommand('insertText', false, newText); | |
| 692 | + } | |
| 693 | + message.selectionStart = start - (affectedLines[0].startsWith('\t') ? 1 : 0); | |
| 694 | + message.selectionEnd = end - delta; | |
| 695 | + message.dispatchEvent(new Event('input', { bubbles:true })); | |
| 696 | + } else { | |
| 697 | + message.selectionStart = start; | |
| 698 | + } | |
| 699 | + } else if (evt.key == 'Enter' && !evt.ctrlKey && !evt.shiftKey && !evt.altKey && !(ac.isReplaceable && ac.isActive)) { | |
| 700 | + // new line, keep indent | |
| 701 | + const start = message.selectionStart; | |
| 702 | + const end = message.selectionEnd; | |
| 703 | + let lineStart = getLineStart(); | |
| 704 | + const indent = /^([^\S\n]*)/.exec(message.value.slice(lineStart))[1] ?? ''; | |
| 705 | + if (indent.length) { | |
| 706 | + evt.stopImmediatePropagation(); | |
| 338 | 707 | evt.stopPropagation(); |
| 339 | 708 | evt.preventDefault(); |
| 709 | + // document.execCommand is deprecated (and potentially buggy in some browsers) but the only way to retain undo-history | |
| 710 | + document.execCommand('insertText', false, `\n${indent}`); | |
| 711 | + message.selectionStart = start + 1 + indent.length; | |
| 712 | + message.selectionEnd = message.selectionStart; | |
| 713 | + message.dispatchEvent(new Event('input', { bubbles:true })); | |
| 714 | + } | |
| 715 | + } else if (evt.key == 'Enter' && evt.ctrlKey && !evt.shiftKey && !evt.altKey) { | |
| 340 | 716 | if (executeShortcut.checked) { |
| 717 | + // execute QR | |
| 718 | + evt.stopImmediatePropagation(); | |
| 719 | + evt.stopPropagation(); | |
| 720 | + evt.preventDefault(); | |
| 341 | 721 | const selectionStart = message.selectionStart; |
| 342 | 722 | const selectionEnd = message.selectionEnd; |
| 343 | 723 | message.blur(); |
| @@ -348,17 +728,175 @@ export class QuickReply { | ||
| 348 | 728 | message.selectionEnd = selectionEnd; |
| 349 | 729 | } |
| 350 | 730 | } |
| 731 | + } else if (evt.key == 'F9' && !evt.ctrlKey && !evt.shiftKey && !evt.altKey) { | |
| 732 | + // toggle breakpoint | |
| 733 | + evt.stopImmediatePropagation(); | |
| 734 | + evt.stopPropagation(); | |
| 735 | + evt.preventDefault(); | |
| 736 | + preBreakPointStart = message.selectionStart; | |
| 737 | + preBreakPointEnd = message.selectionEnd; | |
| 738 | + toggleBreakpoint(); | |
| 739 | + } else if (evt.code == 'Backslash' && evt.ctrlKey && !evt.shiftKey && !evt.altKey) { | |
| 740 | + // toggle block comment | |
| 741 | + // (evt.code will use the same physical key on the keyboard across different keyboard layouts) | |
| 742 | + evt.stopImmediatePropagation(); | |
| 743 | + evt.stopPropagation(); | |
| 744 | + evt.preventDefault(); | |
| 745 | + // check if we are inside a comment -> uncomment | |
| 746 | + const parser = new SlashCommandParser(); | |
| 747 | + parser.parse(message.value, false); | |
| 748 | + const start = message.selectionStart; | |
| 749 | + const end = message.selectionEnd; | |
| 750 | + const comment = parser.commandIndex.findLast(it=>it.name == '*' && (it.start <= start && it.end >= start || it.start <= end && it.end >= end)); | |
| 751 | + if (comment) { | |
| 752 | + // uncomment | |
| 753 | + let content = message.value.slice(comment.start + 1, comment.end - 1); | |
| 754 | + let len = content.length; | |
| 755 | + content = content.replace(/^ /, ''); | |
| 756 | + const offsetStart = len - content.length; | |
| 757 | + len = content.length; | |
| 758 | + content = content.replace(/ $/, ''); | |
| 759 | + const offsetEnd = len - content.length; | |
| 760 | + message.selectionStart = comment.start - 1; | |
| 761 | + message.selectionEnd = comment.end + 1; | |
| 762 | + // document.execCommand is deprecated (and potentially buggy in some browsers) but the only way to retain undo-history | |
| 763 | + document.execCommand('insertText', false, content); | |
| 764 | + message.selectionStart = start - (start >= comment.start ? 2 + offsetStart : 0); | |
| 765 | + message.selectionEnd = end - 2 - offsetStart - (end >= comment.end ? 2 + offsetEnd : 0); | |
| 766 | + } else { | |
| 767 | + // comment | |
| 768 | + const lineStart = getLineStart(); | |
| 769 | + const lineEnd = message.value.indexOf('\n', end); | |
| 770 | + message.selectionStart = lineStart; | |
| 771 | + message.selectionEnd = lineEnd; | |
| 772 | + // document.execCommand is deprecated (and potentially buggy in some browsers) but the only way to retain undo-history | |
| 773 | + document.execCommand('insertText', false, `/* ${message.value.slice(lineStart, lineEnd)} *|`); | |
| 774 | + message.selectionStart = start + 3; | |
| 775 | + message.selectionEnd = end + 3; | |
| 776 | + } | |
| 777 | + message.dispatchEvent(new Event('input', { bubbles:true })); | |
| 351 | 778 | } |
| 352 | 779 | }); |
| 780 | + const ac = await setSlashCommandAutoComplete(message, true); | |
| 353 | 781 | message.addEventListener('wheel', (evt)=>{ |
| 354 | 782 | updateScrollDebounced(evt); |
| 355 | 783 | }); |
| 356 | 784 | message.addEventListener('scroll', (evt)=>{ |
| 357 | 785 | updateScrollDebounced(); |
| 358 | 786 | }); |
| 787 | + let preBreakPointStart; | |
| 788 | + let preBreakPointEnd; | |
| 789 | + /** | |
| 790 | + * @param {SlashCommandBreakPoint} bp | |
| 791 | + */ | |
| 792 | + const removeBreakpoint = (bp)=>{ | |
| 793 | + // start at -1 because "/" is not included in start-end | |
| 794 | + let start = bp.start - 1; | |
| 795 | + // step left until forward slash "/" | |
| 796 | + while (message.value[start] != '/') start--; | |
| 797 | + // step left while whitespace (except newline) before start | |
| 798 | + while (/[^\S\n]/.test(message.value[start - 1])) start--; | |
| 799 | + // if newline before indent, include the newline for removal | |
| 800 | + if (message.value[start - 1] == '\n') start--; | |
| 801 | + let end = bp.end; | |
| 802 | + // step right while whitespace | |
| 803 | + while (/\s/.test(message.value[end])) end++; | |
| 804 | + // if pipe after whitepace, include pipe for removal | |
| 805 | + if (message.value[end] == '|') end++; | |
| 806 | + message.selectionStart = start; | |
| 807 | + message.selectionEnd = end; | |
| 808 | + // document.execCommand is deprecated (and potentially buggy in some browsers) but the only way to retain undo-history | |
| 809 | + document.execCommand('insertText', false, ''); | |
| 810 | + message.dispatchEvent(new Event('input', { bubbles:true })); | |
| 811 | + let postStart = preBreakPointStart; | |
| 812 | + let postEnd = preBreakPointEnd; | |
| 813 | + // set caret back to where it was | |
| 814 | + if (preBreakPointStart <= start) { | |
| 815 | + // selection start was before breakpoint: do nothing | |
| 816 | + } else if (preBreakPointStart > start && preBreakPointEnd < end) { | |
| 817 | + // selection start was inside breakpoint: move to index before breakpoint | |
| 818 | + postStart = start; | |
| 819 | + } else if (preBreakPointStart >= end) { | |
| 820 | + // selection start was behind breakpoint: move back by length of removed string | |
| 821 | + postStart = preBreakPointStart - (end - start); | |
| 822 | + } | |
| 823 | + if (preBreakPointEnd <= start) { | |
| 824 | + // do nothing | |
| 825 | + } else if (preBreakPointEnd > start && preBreakPointEnd < end) { | |
| 826 | + // selection end was inside breakpoint: move to index before breakpoint | |
| 827 | + postEnd = start; | |
| 828 | + } else if (preBreakPointEnd >= end) { | |
| 829 | + // selection end was behind breakpoint: move back by length of removed string | |
| 830 | + postEnd = preBreakPointEnd - (end - start); | |
| 831 | + } | |
| 832 | + return { start:postStart, end:postEnd }; | |
| 833 | + }; | |
| 834 | + /** | |
| 835 | + * @param {SlashCommandExecutor} cmd | |
| 836 | + */ | |
| 837 | + const addBreakpoint = (cmd)=>{ | |
| 838 | + // start at -1 because "/" is not included in start-end | |
| 839 | + let start = cmd.start - 1; | |
| 840 | + let indent = ''; | |
| 841 | + // step left until forward slash "/" | |
| 842 | + while (message.value[start] != '/') start--; | |
| 843 | + // step left while whitespace (except newline) before start, collect the whitespace to help build indentation | |
| 844 | + while (/[^\S\n]/.test(message.value[start - 1])) { | |
| 845 | + start--; | |
| 846 | + indent += message.value[start]; | |
| 847 | + } | |
| 848 | + // if newline before indent, include the newline | |
| 849 | + if (message.value[start - 1] == '\n') { | |
| 850 | + start--; | |
| 851 | + indent = `\n${indent}`; | |
| 852 | + } | |
| 853 | + const breakpointText = `${indent}/breakpoint |`; | |
| 854 | + message.selectionStart = start; | |
| 855 | + message.selectionEnd = start; | |
| 856 | + // document.execCommand is deprecated (and potentially buggy in some browsers) but the only way to retain undo-history | |
| 857 | + document.execCommand('insertText', false, breakpointText); | |
| 858 | + message.dispatchEvent(new Event('input', { bubbles:true })); | |
| 859 | + return breakpointText.length; | |
| 860 | + }; | |
| 861 | + const toggleBreakpoint = ()=>{ | |
| 862 | + const idx = message.selectionStart; | |
| 863 | + let postStart = preBreakPointStart; | |
| 864 | + let postEnd = preBreakPointEnd; | |
| 865 | + const parser = new SlashCommandParser(); | |
| 866 | + parser.parse(message.value, false); | |
| 867 | + const cmdIdx = parser.commandIndex.findLastIndex(it=>it.start <= idx); | |
| 868 | + if (cmdIdx > -1) { | |
| 869 | + const cmd = parser.commandIndex[cmdIdx]; | |
| 870 | + if (cmd instanceof SlashCommandBreakPoint) { | |
| 871 | + const bp = cmd; | |
| 872 | + const { start, end } = removeBreakpoint(bp); | |
| 873 | + postStart = start; | |
| 874 | + postEnd = end; | |
| 875 | + } else if (parser.commandIndex[cmdIdx - 1] instanceof SlashCommandBreakPoint) { | |
| 876 | + const bp = parser.commandIndex[cmdIdx - 1]; | |
| 877 | + const { start, end } = removeBreakpoint(bp); | |
| 878 | + postStart = start; | |
| 879 | + postEnd = end; | |
| 880 | + } else { | |
| 881 | + const len = addBreakpoint(cmd); | |
| 882 | + postStart += len; | |
| 883 | + postEnd += len; | |
| 884 | + } | |
| 885 | + message.selectionStart = postStart; | |
| 886 | + message.selectionEnd = postEnd; | |
| 887 | + } | |
| 888 | + }; | |
| 889 | + message.addEventListener('pointerdown', (evt)=>{ | |
| 890 | + if (!evt.ctrlKey || !evt.altKey) return; | |
| 891 | + preBreakPointStart = message.selectionStart; | |
| 892 | + preBreakPointEnd = message.selectionEnd; | |
| 893 | + }); | |
| 894 | + message.addEventListener('pointerup', async(evt)=>{ | |
| 895 | + if (!evt.ctrlKey || !evt.altKey || message.selectionStart != message.selectionEnd) return; | |
| 896 | + toggleBreakpoint(); | |
| 897 | + }); | |
| 359 | 898 | /** @type {any} */ |
| 360 | 899 | const resizeListener = debounce((evt) => { |
| 361 | - updateSyntax(); | |
| 362 | 900 | updateScrollDebounced(evt); |
| 363 | 901 | if (document.activeElement == message) { |
| 364 | 902 | message.blur(); |
| @@ -367,10 +905,43 @@ export class QuickReply { | ||
| 367 | 905 | }); |
| 368 | 906 | window.addEventListener('resize', resizeListener); |
| 369 | 907 | updateSyntaxEnabled(); |
| 370 | - message.style.setProperty('text-shadow', 'none', 'important'); | |
| 908 | + const updateSyntax = ()=>{ | |
| 371 | - /**@type {HTMLElement}*/ | |
| 909 | + if (messageSyntaxInner && syntax.checked) { | |
| 372 | - const messageSyntaxInner = dom.querySelector('#qr--modal-messageSyntaxInner'); | |
| 910 | + morphdom( | |
| 911 | + messageSyntaxInner, | |
| 912 | + `<div>${hljs.highlight(`${message.value}${message.value.slice(-1) == '\n' ? ' ' : ''}`, { language:'stscript', ignoreIllegals:true })?.value}</div>`, | |
| 913 | + { childrenOnly: true }, | |
| 914 | + ); | |
| 915 | + updateScrollDebounced(); | |
| 916 | + } | |
| 917 | + }; | |
| 918 | + let lastSyntaxUpdate = 0; | |
| 919 | + const fpsTime = 1000 / 30; | |
| 920 | + let lastMessageValue = null; | |
| 921 | + let wasSyntax = null; | |
| 922 | + const updateSyntaxLoop = ()=>{ | |
| 923 | + const now = Date.now(); | |
| 924 | + // fps limit | |
| 925 | + if (now - lastSyntaxUpdate < fpsTime) return requestAnimationFrame(updateSyntaxLoop); | |
| 926 | + // elements don't exist (yet?) | |
| 927 | + if (!messageSyntaxInner || !message) return requestAnimationFrame(updateSyntaxLoop); | |
| 928 | + // elements no longer part of the document | |
| 929 | + if (!messageSyntaxInner.closest('body')) return; | |
| 930 | + // debugger is running | |
| 931 | + if (this.isExecuting) { | |
| 932 | + lastMessageValue = null; | |
| 933 | + return requestAnimationFrame(updateSyntaxLoop); | |
| 934 | + } | |
| 935 | + // value hasn't changed | |
| 936 | + if (wasSyntax == syntax.checked && lastMessageValue == message.value) return requestAnimationFrame(updateSyntaxLoop); | |
| 937 | + wasSyntax = syntax.checked; | |
| 938 | + lastSyntaxUpdate = now; | |
| 939 | + lastMessageValue = message.value; | |
| 373 | 940 | updateSyntax(); |
| 941 | + requestAnimationFrame(updateSyntaxLoop); | |
| 942 | + }; | |
| 943 | + requestAnimationFrame(()=>updateSyntaxLoop()); | |
| 944 | + message.style.setProperty('text-shadow', 'none', 'important'); | |
| 374 | 945 | updateWrap(); |
| 375 | 946 | updateTabSize(); |
| 376 | 947 | |
| @@ -379,7 +950,7 @@ export class QuickReply { | ||
| 379 | 950 | const tpl = dom.querySelector('#qr--ctxItem'); |
| 380 | 951 | const linkList = dom.querySelector('#qr--ctxEditor'); |
| 381 | 952 | const fillQrSetSelect = (/**@type {HTMLSelectElement}*/select, /**@type {QuickReplyContextLink}*/ link) => { |
| 382 | 953 | [{ name: 'Select a QR set' }, ...QuickReplySet.list.toSorted((a,b)=>a.name.toLowerCase().localeCompare(b.name.toLowerCase()))].forEach(qrs => { |
| 383 | 954 | const opt = document.createElement('option'); { |
| 384 | 955 | opt.value = qrs.name; |
| 385 | 956 | opt.textContent = qrs.name; |
| @@ -388,7 +959,7 @@ export class QuickReply { | ||
| 388 | 959 | } |
| 389 | 960 | }); |
| 390 | 961 | }; |
| 391 | 962 | const addCtxItem = (/**@type {QuickReplyContextLink}*/link, /**@type {Numbernumber}*/idx) => { |
| 392 | 963 | /**@type {HTMLElement} */ |
| 393 | 964 | // @ts-ignore |
| 394 | 965 | const itemDom = tpl.content.querySelector('.qr--ctxItem').cloneNode(true); { |
| @@ -506,9 +1077,9 @@ export class QuickReply { | ||
| 506 | 1077 | /**@type {HTMLElement}*/ |
| 507 | 1078 | const executeResult = dom.querySelector('#qr--modal-executeResult'); |
| 508 | 1079 | this.editorExecuteResult = executeResult; |
| 509 | 1080 | /**@type {HTMLInputElementHTMLElement}*/ |
| 510 | 1081 | const executeHidedebugState = dom.querySelector('#qr--modal-executeHidedebugState'); |
| 511 | 1082 | this.editorExecuteHideeditorDebugState = executeHidedebugState; |
| 512 | 1083 | /**@type {HTMLElement}*/ |
| 513 | 1084 | const executeBtn = dom.querySelector('#qr--modal-execute'); |
| 514 | 1085 | this.editorExecuteBtn = executeBtn; |
| @@ -536,6 +1107,76 @@ export class QuickReply { | ||
| 536 | 1107 | this.abortController?.abort('Stop button clicked'); |
| 537 | 1108 | }); |
| 538 | 1109 | |
| 1110 | + /**@type {HTMLTextAreaElement} */ | |
| 1111 | + const inputOg = document.querySelector('#send_textarea'); | |
| 1112 | + const inputMirror = dom.querySelector('#qr--modal-send_textarea'); | |
| 1113 | + inputMirror.value = inputOg.value; | |
| 1114 | + const inputOgMo = new MutationObserver(muts=>{ | |
| 1115 | + if (muts.find(it=>[...it.removedNodes].includes(inputMirror) || [...it.removedNodes].find(n=>n.contains(inputMirror)))) { | |
| 1116 | + inputOg.removeEventListener('input', inputOgListener); | |
| 1117 | + } | |
| 1118 | + }); | |
| 1119 | + inputOgMo.observe(document.body, { childList:true }); | |
| 1120 | + const inputOgListener = ()=>{ | |
| 1121 | + inputMirror.value = inputOg.value; | |
| 1122 | + }; | |
| 1123 | + inputOg.addEventListener('input', inputOgListener); | |
| 1124 | + inputMirror.addEventListener('input', ()=>{ | |
| 1125 | + inputOg.value = inputMirror.value; | |
| 1126 | + }); | |
| 1127 | + | |
| 1128 | + /**@type {HTMLElement}*/ | |
| 1129 | + const resumeBtn = dom.querySelector('#qr--modal-resume'); | |
| 1130 | + resumeBtn.addEventListener('click', ()=>{ | |
| 1131 | + this.debugController?.resume(); | |
| 1132 | + }); | |
| 1133 | + /**@type {HTMLElement}*/ | |
| 1134 | + const stepBtn = dom.querySelector('#qr--modal-step'); | |
| 1135 | + stepBtn.addEventListener('click', ()=>{ | |
| 1136 | + this.debugController?.step(); | |
| 1137 | + }); | |
| 1138 | + /**@type {HTMLElement}*/ | |
| 1139 | + const stepIntoBtn = dom.querySelector('#qr--modal-stepInto'); | |
| 1140 | + stepIntoBtn.addEventListener('click', ()=>{ | |
| 1141 | + this.debugController?.stepInto(); | |
| 1142 | + }); | |
| 1143 | + /**@type {HTMLElement}*/ | |
| 1144 | + const stepOutBtn = dom.querySelector('#qr--modal-stepOut'); | |
| 1145 | + stepOutBtn.addEventListener('click', ()=>{ | |
| 1146 | + this.debugController?.stepOut(); | |
| 1147 | + }); | |
| 1148 | + /**@type {HTMLElement}*/ | |
| 1149 | + const minimizeBtn = dom.querySelector('#qr--modal-minimize'); | |
| 1150 | + minimizeBtn.addEventListener('click', ()=>{ | |
| 1151 | + this.editorDom.classList.add('qr--minimized'); | |
| 1152 | + }); | |
| 1153 | + const maximizeBtn = dom.querySelector('#qr--modal-maximize'); | |
| 1154 | + maximizeBtn.addEventListener('click', ()=>{ | |
| 1155 | + this.editorDom.classList.remove('qr--minimized'); | |
| 1156 | + }); | |
| 1157 | + /**@type {boolean}*/ | |
| 1158 | + let isResizing = false; | |
| 1159 | + let resizeStart; | |
| 1160 | + let wStart; | |
| 1161 | + /**@type {HTMLElement}*/ | |
| 1162 | + const resizeHandle = dom.querySelector('#qr--resizeHandle'); | |
| 1163 | + resizeHandle.addEventListener('pointerdown', (evt)=>{ | |
| 1164 | + if (isResizing) return; | |
| 1165 | + isResizing = true; | |
| 1166 | + evt.preventDefault(); | |
| 1167 | + resizeStart = evt.x; | |
| 1168 | + wStart = dom.querySelector('#qr--qrOptions').offsetWidth; | |
| 1169 | + const dragListener = debounce((evt)=>{ | |
| 1170 | + const w = wStart + resizeStart - evt.x; | |
| 1171 | + dom.querySelector('#qr--qrOptions').style.setProperty('--width', `${w}px`); | |
| 1172 | + }, 5); | |
| 1173 | + window.addEventListener('pointerup', ()=>{ | |
| 1174 | + window.removeEventListener('pointermove', dragListener); | |
| 1175 | + isResizing = false; | |
| 1176 | + }, { once:true }); | |
| 1177 | + window.addEventListener('pointermove', dragListener); | |
| 1178 | + }); | |
| 1179 | + | |
| 539 | 1180 | await popupResult; |
| 540 | 1181 | |
| 541 | 1182 | window.removeEventListener('resize', resizeListener); |
| @@ -544,8 +1185,62 @@ export class QuickReply { | ||
| 544 | 1185 | } |
| 545 | 1186 | } |
| 546 | 1187 | |
| 1188 | + getEditorPosition(start, end, message = null) { | |
| 1189 | + const inputRect = this.editorMessage.getBoundingClientRect(); | |
| 1190 | + const style = window.getComputedStyle(this.editorMessage); | |
| 1191 | + if (!this.clone) { | |
| 1192 | + this.clone = document.createElement('div'); | |
| 1193 | + for (const key of style) { | |
| 1194 | + this.clone.style[key] = style[key]; | |
| 1195 | + } | |
| 1196 | + this.clone.style.position = 'fixed'; | |
| 1197 | + this.clone.style.visibility = 'hidden'; | |
| 1198 | + const mo = new MutationObserver(muts=>{ | |
| 1199 | + if (muts.find(it=>[...it.removedNodes].includes(this.editorMessage) || [...it.removedNodes].find(n=>n.contains(this.editorMessage)))) { | |
| 1200 | + this.clone?.remove(); | |
| 1201 | + this.clone = null; | |
| 1202 | + } | |
| 1203 | + }); | |
| 1204 | + mo.observe(document.body, { childList:true }); | |
| 1205 | + } | |
| 1206 | + document.body.append(this.clone); | |
| 1207 | + this.clone.style.width = `${inputRect.width}px`; | |
| 1208 | + this.clone.style.height = `${inputRect.height}px`; | |
| 1209 | + this.clone.style.left = `${inputRect.left}px`; | |
| 1210 | + this.clone.style.top = `${inputRect.top}px`; | |
| 1211 | + this.clone.style.whiteSpace = style.whiteSpace; | |
| 1212 | + this.clone.style.tabSize = style.tabSize; | |
| 1213 | + const text = message ?? this.editorMessage.value; | |
| 1214 | + const before = text.slice(0, start); | |
| 1215 | + this.clone.textContent = before; | |
| 1216 | + const locator = document.createElement('span'); | |
| 1217 | + locator.textContent = text.slice(start, end); | |
| 1218 | + this.clone.append(locator); | |
| 1219 | + this.clone.append(text.slice(end)); | |
| 1220 | + this.clone.scrollTop = this.editorSyntax.scrollTop; | |
| 1221 | + this.clone.scrollLeft = this.editorSyntax.scrollLeft; | |
| 1222 | + const locatorRect = locator.getBoundingClientRect(); | |
| 1223 | + const bodyRect = document.body.getBoundingClientRect(); | |
| 1224 | + const location = { | |
| 1225 | + left: locatorRect.left - bodyRect.left, | |
| 1226 | + right: locatorRect.right - bodyRect.left, | |
| 1227 | + top: locatorRect.top - bodyRect.top, | |
| 1228 | + bottom: locatorRect.bottom - bodyRect.top, | |
| 1229 | + }; | |
| 1230 | + // this.clone.remove(); | |
| 1231 | + return location; | |
| 1232 | + } | |
| 547 | 1233 | async executeFromEditor() { |
| 548 | 1234 | if (this.editorExecutePromiseisExecuting) return; |
| 1235 | + this.editorPopup.onClosing = ()=>false; | |
| 1236 | + const uuidCheck = /^[0-9a-z]{8}(-[0-9a-z]{4}){3}-[0-9a-z]{12}$/; | |
| 1237 | + const oText = this.message; | |
| 1238 | + this.isExecuting = true; | |
| 1239 | + this.editorDom.classList.add('qr--isExecuting'); | |
| 1240 | + const noSyntax = this.editorDom.querySelector('#qr--modal-messageHolder').classList.contains('qr--noSyntax'); | |
| 1241 | + if (noSyntax) { | |
| 1242 | + this.editorDom.querySelector('#qr--modal-messageHolder').classList.remove('qr--noSyntax'); | |
| 1243 | + } | |
| 549 | 1244 | this.editorExecuteBtn.classList.add('qr--busy'); |
| 550 | 1245 | this.editorExecuteProgress.style.setProperty('--prog', '0'); |
| 551 | 1246 | this.editorExecuteErrors.classList.remove('qr--hasErrors'); |
| @@ -556,12 +1251,451 @@ export class QuickReply { | ||
| 556 | 1251 | this.editorExecuteProgress.classList.remove('qr--aborted'); |
| 557 | 1252 | this.editorExecuteErrors.innerHTML = ''; |
| 558 | 1253 | this.editorExecuteResult.innerHTML = ''; |
| 559 | - if (this.editorExecuteHide.checked) { | |
| 1254 | + const syntax = this.editorDom.querySelector('#qr--modal-messageSyntaxInner'); | |
| 560 | - this.editorPopup.dlg.classList.add('qr--hide'); | |
| 1255 | + const updateScroll = (evt) => { | |
| 1256 | + let left = syntax.scrollLeft; | |
| 1257 | + let top = syntax.scrollTop; | |
| 1258 | + if (evt) { | |
| 1259 | + evt.preventDefault(); | |
| 1260 | + left = syntax.scrollLeft + evt.deltaX; | |
| 1261 | + top = syntax.scrollTop + evt.deltaY; | |
| 1262 | + syntax.scrollTo({ | |
| 1263 | + behavior: 'instant', | |
| 1264 | + left, | |
| 1265 | + top, | |
| 1266 | + }); | |
| 561 | 1267 | } |
| 1268 | + this.editorMessage.scrollTo({ | |
| 1269 | + behavior: 'instant', | |
| 1270 | + left, | |
| 1271 | + top, | |
| 1272 | + }); | |
| 1273 | + }; | |
| 1274 | + const updateScrollDebounced = updateScroll; | |
| 1275 | + syntax.addEventListener('wheel', (evt)=>{ | |
| 1276 | + updateScrollDebounced(evt); | |
| 1277 | + }); | |
| 1278 | + syntax.addEventListener('scroll', (evt)=>{ | |
| 1279 | + updateScrollDebounced(); | |
| 1280 | + }); | |
| 562 | 1281 | try { |
| 563 | 1282 | this.editorExecutePromiseabortController = this.execute({},new trueSlashCommandAbortController(); |
| 564 | - const result = await this.editorExecutePromise; | |
| 1283 | + this.debugController = new SlashCommandDebugController(); | |
| 1284 | + this.debugController.onBreakPoint = async(closure, executor)=>{ | |
| 1285 | + this.editorDom.classList.add('qr--isPaused'); | |
| 1286 | + syntax.innerHTML = hljs.highlight(`${closure.fullText}${closure.fullText.slice(-1) == '\n' ? ' ' : ''}`, { language:'stscript', ignoreIllegals:true })?.value; | |
| 1287 | + this.editorMessageLabel.innerHTML = ''; | |
| 1288 | + if (uuidCheck.test(closure.source)) { | |
| 1289 | + const p0 = document.createElement('span'); { | |
| 1290 | + p0.textContent = 'anonymous: '; | |
| 1291 | + this.editorMessageLabel.append(p0); | |
| 1292 | + } | |
| 1293 | + const p1 = document.createElement('strong'); { | |
| 1294 | + p1.textContent = executor.source.slice(0,5); | |
| 1295 | + this.editorMessageLabel.append(p1); | |
| 1296 | + } | |
| 1297 | + const p2 = document.createElement('span'); { | |
| 1298 | + p2.textContent = executor.source.slice(5, -5); | |
| 1299 | + this.editorMessageLabel.append(p2); | |
| 1300 | + } | |
| 1301 | + const p3 = document.createElement('strong'); { | |
| 1302 | + p3.textContent = executor.source.slice(-5); | |
| 1303 | + this.editorMessageLabel.append(p3); | |
| 1304 | + } | |
| 1305 | + } else { | |
| 1306 | + this.editorMessageLabel.textContent = executor.source; | |
| 1307 | + } | |
| 1308 | + const source = closure.source; | |
| 1309 | + this.editorDebugState.innerHTML = ''; | |
| 1310 | + let ci = -1; | |
| 1311 | + const varNames = []; | |
| 1312 | + const macroNames = []; | |
| 1313 | + /** | |
| 1314 | + * @param {SlashCommandScope} scope | |
| 1315 | + */ | |
| 1316 | + const buildVars = (scope, isCurrent = false)=>{ | |
| 1317 | + if (!isCurrent) { | |
| 1318 | + ci--; | |
| 1319 | + } | |
| 1320 | + const c = this.debugController.stack.slice(ci)[0]; | |
| 1321 | + const wrap = document.createElement('div'); { | |
| 1322 | + wrap.classList.add('qr--scope'); | |
| 1323 | + if (isCurrent) { | |
| 1324 | + const executor = this.debugController.cmdStack.slice(-1)[0]; | |
| 1325 | + { // named args | |
| 1326 | + const namedTitle = document.createElement('div'); { | |
| 1327 | + namedTitle.classList.add('qr--title'); | |
| 1328 | + namedTitle.textContent = `Named Args - /${executor.name}`; | |
| 1329 | + if (executor.command.name == 'run') { | |
| 1330 | + namedTitle.textContent += `${(executor.name == ':' ? '' : ' ')}${executor.unnamedArgumentList[0]?.value}`; | |
| 1331 | + } | |
| 1332 | + wrap.append(namedTitle); | |
| 1333 | + } | |
| 1334 | + const keys = new Set([...Object.keys(this.debugController.namedArguments ?? {}), ...(executor.namedArgumentList ?? []).map(it=>it.name)]); | |
| 1335 | + for (const key of keys) { | |
| 1336 | + if (key[0] == '_') continue; | |
| 1337 | + const item = document.createElement('div'); { | |
| 1338 | + item.classList.add('qr--var'); | |
| 1339 | + const k = document.createElement('div'); { | |
| 1340 | + k.classList.add('qr--key'); | |
| 1341 | + k.textContent = key; | |
| 1342 | + item.append(k); | |
| 1343 | + } | |
| 1344 | + const vUnresolved = document.createElement('div'); { | |
| 1345 | + vUnresolved.classList.add('qr--val'); | |
| 1346 | + vUnresolved.classList.add('qr--singleCol'); | |
| 1347 | + const val = executor.namedArgumentList.find(it=>it.name == key)?.value; | |
| 1348 | + if (val instanceof SlashCommandClosure) { | |
| 1349 | + vUnresolved.classList.add('qr--closure'); | |
| 1350 | + vUnresolved.title = val.rawText; | |
| 1351 | + vUnresolved.textContent = val.toString(); | |
| 1352 | + } else if (val === undefined) { | |
| 1353 | + vUnresolved.classList.add('qr--undefined'); | |
| 1354 | + vUnresolved.textContent = 'undefined'; | |
| 1355 | + } else { | |
| 1356 | + let jsonVal; | |
| 1357 | + try { jsonVal = JSON.parse(val); } catch { /* empty */ } | |
| 1358 | + if (jsonVal && typeof jsonVal == 'object') { | |
| 1359 | + vUnresolved.textContent = JSON.stringify(jsonVal, null, 2); | |
| 1360 | + } else { | |
| 1361 | + vUnresolved.textContent = val; | |
| 1362 | + vUnresolved.classList.add('qr--simple'); | |
| 1363 | + } | |
| 1364 | + } | |
| 1365 | + item.append(vUnresolved); | |
| 1366 | + } | |
| 1367 | + const vResolved = document.createElement('div'); { | |
| 1368 | + vResolved.classList.add('qr--val'); | |
| 1369 | + vResolved.classList.add('qr--singleCol'); | |
| 1370 | + if (this.debugController.namedArguments === undefined) { | |
| 1371 | + vResolved.classList.add('qr--unresolved'); | |
| 1372 | + } else { | |
| 1373 | + const val = this.debugController.namedArguments?.[key]; | |
| 1374 | + if (val instanceof SlashCommandClosure) { | |
| 1375 | + vResolved.classList.add('qr--closure'); | |
| 1376 | + vResolved.title = val.rawText; | |
| 1377 | + vResolved.textContent = val.toString(); | |
| 1378 | + } else if (val === undefined) { | |
| 1379 | + vResolved.classList.add('qr--undefined'); | |
| 1380 | + vResolved.textContent = 'undefined'; | |
| 1381 | + } else { | |
| 1382 | + let jsonVal; | |
| 1383 | + try { jsonVal = JSON.parse(val); } catch { /* empty */ } | |
| 1384 | + if (jsonVal && typeof jsonVal == 'object') { | |
| 1385 | + vResolved.textContent = JSON.stringify(jsonVal, null, 2); | |
| 1386 | + } else { | |
| 1387 | + vResolved.textContent = val; | |
| 1388 | + vResolved.classList.add('qr--simple'); | |
| 1389 | + } | |
| 1390 | + } | |
| 1391 | + } | |
| 1392 | + item.append(vResolved); | |
| 1393 | + } | |
| 1394 | + wrap.append(item); | |
| 1395 | + } | |
| 1396 | + } | |
| 1397 | + } | |
| 1398 | + { // unnamed args | |
| 1399 | + const unnamedTitle = document.createElement('div'); { | |
| 1400 | + unnamedTitle.classList.add('qr--title'); | |
| 1401 | + unnamedTitle.textContent = `Unnamed Args - /${executor.name}`; | |
| 1402 | + if (executor.command.name == 'run') { | |
| 1403 | + unnamedTitle.textContent += `${(executor.name == ':' ? '' : ' ')}${executor.unnamedArgumentList[0]?.value}`; | |
| 1404 | + } | |
| 1405 | + wrap.append(unnamedTitle); | |
| 1406 | + } | |
| 1407 | + let i = 0; | |
| 1408 | + let unnamed = this.debugController.unnamedArguments ?? []; | |
| 1409 | + if (!Array.isArray(unnamed)) unnamed = [unnamed]; | |
| 1410 | + while (unnamed.length < executor.unnamedArgumentList?.length ?? 0) unnamed.push(undefined); | |
| 1411 | + unnamed = unnamed.map((it,idx)=>[executor.unnamedArgumentList?.[idx], it]); | |
| 1412 | + for (const arg of unnamed) { | |
| 1413 | + i++; | |
| 1414 | + const item = document.createElement('div'); { | |
| 1415 | + item.classList.add('qr--var'); | |
| 1416 | + const k = document.createElement('div'); { | |
| 1417 | + k.classList.add('qr--key'); | |
| 1418 | + k.textContent = i.toString(); | |
| 1419 | + item.append(k); | |
| 1420 | + } | |
| 1421 | + const vUnresolved = document.createElement('div'); { | |
| 1422 | + vUnresolved.classList.add('qr--val'); | |
| 1423 | + vUnresolved.classList.add('qr--singleCol'); | |
| 1424 | + const val = arg[0]?.value; | |
| 1425 | + if (val instanceof SlashCommandClosure) { | |
| 1426 | + vUnresolved.classList.add('qr--closure'); | |
| 1427 | + vUnresolved.title = val.rawText; | |
| 1428 | + vUnresolved.textContent = val.toString(); | |
| 1429 | + } else if (val === undefined) { | |
| 1430 | + vUnresolved.classList.add('qr--undefined'); | |
| 1431 | + vUnresolved.textContent = 'undefined'; | |
| 1432 | + } else { | |
| 1433 | + let jsonVal; | |
| 1434 | + try { jsonVal = JSON.parse(val); } catch { /* empty */ } | |
| 1435 | + if (jsonVal && typeof jsonVal == 'object') { | |
| 1436 | + vUnresolved.textContent = JSON.stringify(jsonVal, null, 2); | |
| 1437 | + } else { | |
| 1438 | + vUnresolved.textContent = val; | |
| 1439 | + vUnresolved.classList.add('qr--simple'); | |
| 1440 | + } | |
| 1441 | + } | |
| 1442 | + item.append(vUnresolved); | |
| 1443 | + } | |
| 1444 | + const vResolved = document.createElement('div'); { | |
| 1445 | + vResolved.classList.add('qr--val'); | |
| 1446 | + vResolved.classList.add('qr--singleCol'); | |
| 1447 | + if (this.debugController.unnamedArguments === undefined) { | |
| 1448 | + vResolved.classList.add('qr--unresolved'); | |
| 1449 | + } else if ((Array.isArray(this.debugController.unnamedArguments) ? this.debugController.unnamedArguments : [this.debugController.unnamedArguments]).length < i) { | |
| 1450 | + // do nothing | |
| 1451 | + } else { | |
| 1452 | + const val = arg[1]; | |
| 1453 | + if (val instanceof SlashCommandClosure) { | |
| 1454 | + vResolved.classList.add('qr--closure'); | |
| 1455 | + vResolved.title = val.rawText; | |
| 1456 | + vResolved.textContent = val.toString(); | |
| 1457 | + } else if (val === undefined) { | |
| 1458 | + vResolved.classList.add('qr--undefined'); | |
| 1459 | + vResolved.textContent = 'undefined'; | |
| 1460 | + } else { | |
| 1461 | + let jsonVal; | |
| 1462 | + try { jsonVal = JSON.parse(val); } catch { /* empty */ } | |
| 1463 | + if (jsonVal && typeof jsonVal == 'object') { | |
| 1464 | + vResolved.textContent = JSON.stringify(jsonVal, null, 2); | |
| 1465 | + } else { | |
| 1466 | + vResolved.textContent = val; | |
| 1467 | + vResolved.classList.add('qr--simple'); | |
| 1468 | + } | |
| 1469 | + } | |
| 1470 | + } | |
| 1471 | + item.append(vResolved); | |
| 1472 | + } | |
| 1473 | + wrap.append(item); | |
| 1474 | + } | |
| 1475 | + } | |
| 1476 | + } | |
| 1477 | + } | |
| 1478 | + // current scope | |
| 1479 | + const title = document.createElement('div'); { | |
| 1480 | + title.classList.add('qr--title'); | |
| 1481 | + title.textContent = isCurrent ? 'Current Scope' : 'Parent Scope'; | |
| 1482 | + if (c.source == source) { | |
| 1483 | + let hi; | |
| 1484 | + title.addEventListener('pointerenter', ()=>{ | |
| 1485 | + const loc = this.getEditorPosition(Math.max(0, c.executorList[0].start - 1), c.executorList.slice(-1)[0].end, c.fullText); | |
| 1486 | + const layer = syntax.getBoundingClientRect(); | |
| 1487 | + hi = document.createElement('div'); | |
| 1488 | + hi.classList.add('qr--highlight-secondary'); | |
| 1489 | + hi.style.left = `${loc.left - layer.left}px`; | |
| 1490 | + hi.style.width = `${loc.right - loc.left}px`; | |
| 1491 | + hi.style.top = `${loc.top - layer.top + syntax.scrollTop}px`; | |
| 1492 | + hi.style.height = `${loc.bottom - loc.top}px`; | |
| 1493 | + syntax.append(hi); | |
| 1494 | + }); | |
| 1495 | + title.addEventListener('pointerleave', ()=>hi?.remove()); | |
| 1496 | + } | |
| 1497 | + wrap.append(title); | |
| 1498 | + } | |
| 1499 | + for (const key of Object.keys(scope.variables)) { | |
| 1500 | + const isHidden = varNames.includes(key); | |
| 1501 | + if (!isHidden) varNames.push(key); | |
| 1502 | + const item = document.createElement('div'); { | |
| 1503 | + item.classList.add('qr--var'); | |
| 1504 | + if (isHidden) item.classList.add('qr--isHidden'); | |
| 1505 | + const k = document.createElement('div'); { | |
| 1506 | + k.classList.add('qr--key'); | |
| 1507 | + k.textContent = key; | |
| 1508 | + item.append(k); | |
| 1509 | + } | |
| 1510 | + const v = document.createElement('div'); { | |
| 1511 | + v.classList.add('qr--val'); | |
| 1512 | + const val = scope.variables[key]; | |
| 1513 | + if (val instanceof SlashCommandClosure) { | |
| 1514 | + v.classList.add('qr--closure'); | |
| 1515 | + v.title = val.rawText; | |
| 1516 | + v.textContent = val.toString(); | |
| 1517 | + } else if (val === undefined) { | |
| 1518 | + v.classList.add('qr--undefined'); | |
| 1519 | + v.textContent = 'undefined'; | |
| 1520 | + } else { | |
| 1521 | + let jsonVal; | |
| 1522 | + try { jsonVal = JSON.parse(val); } catch { /* empty */ } | |
| 1523 | + if (jsonVal && typeof jsonVal == 'object') { | |
| 1524 | + v.textContent = JSON.stringify(jsonVal, null, 2); | |
| 1525 | + } else { | |
| 1526 | + v.textContent = val; | |
| 1527 | + v.classList.add('qr--simple'); | |
| 1528 | + } | |
| 1529 | + } | |
| 1530 | + item.append(v); | |
| 1531 | + } | |
| 1532 | + wrap.append(item); | |
| 1533 | + } | |
| 1534 | + } | |
| 1535 | + for (const key of Object.keys(scope.macros)) { | |
| 1536 | + const isHidden = macroNames.includes(key); | |
| 1537 | + if (!isHidden) macroNames.push(key); | |
| 1538 | + const item = document.createElement('div'); { | |
| 1539 | + item.classList.add('qr--macro'); | |
| 1540 | + if (isHidden) item.classList.add('qr--isHidden'); | |
| 1541 | + const k = document.createElement('div'); { | |
| 1542 | + k.classList.add('qr--key'); | |
| 1543 | + k.textContent = key; | |
| 1544 | + item.append(k); | |
| 1545 | + } | |
| 1546 | + const v = document.createElement('div'); { | |
| 1547 | + v.classList.add('qr--val'); | |
| 1548 | + const val = scope.macros[key]; | |
| 1549 | + if (val instanceof SlashCommandClosure) { | |
| 1550 | + v.classList.add('qr--closure'); | |
| 1551 | + v.title = val.rawText; | |
| 1552 | + v.textContent = val.toString(); | |
| 1553 | + } else if (val === undefined) { | |
| 1554 | + v.classList.add('qr--undefined'); | |
| 1555 | + v.textContent = 'undefined'; | |
| 1556 | + } else { | |
| 1557 | + let jsonVal; | |
| 1558 | + try { jsonVal = JSON.parse(val); } catch { /* empty */ } | |
| 1559 | + if (jsonVal && typeof jsonVal == 'object') { | |
| 1560 | + v.textContent = JSON.stringify(jsonVal, null, 2); | |
| 1561 | + } else { | |
| 1562 | + v.textContent = val; | |
| 1563 | + v.classList.add('qr--simple'); | |
| 1564 | + } | |
| 1565 | + } | |
| 1566 | + item.append(v); | |
| 1567 | + } | |
| 1568 | + wrap.append(item); | |
| 1569 | + } | |
| 1570 | + } | |
| 1571 | + const pipeItem = document.createElement('div'); { | |
| 1572 | + pipeItem.classList.add('qr--pipe'); | |
| 1573 | + const k = document.createElement('div'); { | |
| 1574 | + k.classList.add('qr--key'); | |
| 1575 | + k.textContent = 'pipe'; | |
| 1576 | + pipeItem.append(k); | |
| 1577 | + } | |
| 1578 | + const v = document.createElement('div'); { | |
| 1579 | + v.classList.add('qr--val'); | |
| 1580 | + const val = scope.pipe; | |
| 1581 | + if (val instanceof SlashCommandClosure) { | |
| 1582 | + v.classList.add('qr--closure'); | |
| 1583 | + v.title = val.rawText; | |
| 1584 | + v.textContent = val.toString(); | |
| 1585 | + } else if (val === undefined) { | |
| 1586 | + v.classList.add('qr--undefined'); | |
| 1587 | + v.textContent = 'undefined'; | |
| 1588 | + } else { | |
| 1589 | + let jsonVal; | |
| 1590 | + try { jsonVal = JSON.parse(val); } catch { /* empty */ } | |
| 1591 | + if (jsonVal && typeof jsonVal == 'object') { | |
| 1592 | + v.textContent = JSON.stringify(jsonVal, null, 2); | |
| 1593 | + } else { | |
| 1594 | + v.textContent = val; | |
| 1595 | + v.classList.add('qr--simple'); | |
| 1596 | + } | |
| 1597 | + } | |
| 1598 | + pipeItem.append(v); | |
| 1599 | + } | |
| 1600 | + wrap.append(pipeItem); | |
| 1601 | + } | |
| 1602 | + if (scope.parent) { | |
| 1603 | + wrap.append(buildVars(scope.parent)); | |
| 1604 | + } | |
| 1605 | + } | |
| 1606 | + return wrap; | |
| 1607 | + }; | |
| 1608 | + const buildStack = ()=>{ | |
| 1609 | + const wrap = document.createElement('div'); { | |
| 1610 | + wrap.classList.add('qr--stack'); | |
| 1611 | + const title = document.createElement('div'); { | |
| 1612 | + title.classList.add('qr--title'); | |
| 1613 | + title.textContent = 'Call Stack'; | |
| 1614 | + wrap.append(title); | |
| 1615 | + } | |
| 1616 | + let ei = -1; | |
| 1617 | + for (const executor of this.debugController.cmdStack.toReversed()) { | |
| 1618 | + ei++; | |
| 1619 | + const c = this.debugController.stack.toReversed()[ei]; | |
| 1620 | + const item = document.createElement('div'); { | |
| 1621 | + item.classList.add('qr--item'); | |
| 1622 | + if (executor.source == source) { | |
| 1623 | + let hi; | |
| 1624 | + item.addEventListener('pointerenter', ()=>{ | |
| 1625 | + const loc = this.getEditorPosition(Math.max(0, executor.start - 1), executor.end, c.fullText); | |
| 1626 | + const layer = syntax.getBoundingClientRect(); | |
| 1627 | + hi = document.createElement('div'); | |
| 1628 | + hi.classList.add('qr--highlight-secondary'); | |
| 1629 | + hi.style.left = `${loc.left - layer.left}px`; | |
| 1630 | + hi.style.width = `${loc.right - loc.left}px`; | |
| 1631 | + hi.style.top = `${loc.top - layer.top + syntax.scrollTop}px`; | |
| 1632 | + hi.style.height = `${loc.bottom - loc.top}px`; | |
| 1633 | + syntax.append(hi); | |
| 1634 | + }); | |
| 1635 | + item.addEventListener('pointerleave', ()=>hi?.remove()); | |
| 1636 | + } | |
| 1637 | + const cmd = document.createElement('div'); { | |
| 1638 | + cmd.classList.add('qr--cmd'); | |
| 1639 | + cmd.textContent = `/${executor.name}`; | |
| 1640 | + if (executor.command.name == 'run') { | |
| 1641 | + cmd.textContent += `${(executor.name == ':' ? '' : ' ')}${executor.unnamedArgumentList[0]?.value}`; | |
| 1642 | + } | |
| 1643 | + item.append(cmd); | |
| 1644 | + } | |
| 1645 | + const src = document.createElement('div'); { | |
| 1646 | + src.classList.add('qr--source'); | |
| 1647 | + const line = closure.fullText.slice(0, executor.start).split('\n').length; | |
| 1648 | + if (uuidCheck.test(executor.source)) { | |
| 1649 | + const p1 = document.createElement('span'); { | |
| 1650 | + p1.classList.add('qr--fixed'); | |
| 1651 | + p1.textContent = executor.source.slice(0,5); | |
| 1652 | + src.append(p1); | |
| 1653 | + } | |
| 1654 | + const p2 = document.createElement('span'); { | |
| 1655 | + p2.classList.add('qr--truncated'); | |
| 1656 | + p2.textContent = '…'; | |
| 1657 | + src.append(p2); | |
| 1658 | + } | |
| 1659 | + const p3 = document.createElement('span'); { | |
| 1660 | + p3.classList.add('qr--fixed'); | |
| 1661 | + p3.textContent = `${executor.source.slice(-5)}:${line}`; | |
| 1662 | + src.append(p3); | |
| 1663 | + } | |
| 1664 | + src.title = `anonymous: ${executor.source}`; | |
| 1665 | + } else { | |
| 1666 | + src.textContent = `${executor.source}:${line}`; | |
| 1667 | + } | |
| 1668 | + item.append(src); | |
| 1669 | + } | |
| 1670 | + wrap.append(item); | |
| 1671 | + } | |
| 1672 | + } | |
| 1673 | + } | |
| 1674 | + return wrap; | |
| 1675 | + }; | |
| 1676 | + this.editorDebugState.append(buildVars(closure.scope, true)); | |
| 1677 | + this.editorDebugState.append(buildStack()); | |
| 1678 | + this.editorDebugState.classList.add('qr--active'); | |
| 1679 | + const loc = this.getEditorPosition(Math.max(0, executor.start - 1), executor.end, closure.fullText); | |
| 1680 | + const layer = syntax.getBoundingClientRect(); | |
| 1681 | + const hi = document.createElement('div'); | |
| 1682 | + hi.classList.add('qr--highlight'); | |
| 1683 | + if (this.debugController.namedArguments === undefined) { | |
| 1684 | + hi.classList.add('qr--unresolved'); | |
| 1685 | + } | |
| 1686 | + hi.style.left = `${loc.left - layer.left}px`; | |
| 1687 | + hi.style.width = `${loc.right - loc.left}px`; | |
| 1688 | + hi.style.top = `${loc.top - layer.top + syntax.scrollTop}px`; | |
| 1689 | + hi.style.height = `${loc.bottom - loc.top}px`; | |
| 1690 | + syntax.append(hi); | |
| 1691 | + const isStepping = await this.debugController.awaitContinue(); | |
| 1692 | + hi.remove(); | |
| 1693 | + this.editorDebugState.textContent = ''; | |
| 1694 | + this.editorDebugState.classList.remove('qr--active'); | |
| 1695 | + this.editorDom.classList.remove('qr--isPaused'); | |
| 1696 | + return isStepping; | |
| 1697 | + }; | |
| 1698 | + const result = await this.onDebug(this); | |
| 565 | 1699 | if (this.abortController?.signal?.aborted) { |
| 566 | 1700 | this.editorExecuteProgress.classList.add('qr--aborted'); |
| 567 | 1701 | } else { |
| @@ -586,9 +1720,18 @@ export class QuickReply { | ||
| 586 | 1720 | `; |
| 587 | 1721 | } |
| 588 | 1722 | } |
| 1723 | + if (noSyntax) { | |
| 1724 | + this.editorDom.querySelector('#qr--modal-messageHolder').classList.add('qr--noSyntax'); | |
| 1725 | + } | |
| 1726 | + this.editorMessageLabel.innerHTML = ''; | |
| 1727 | + this.editorMessageLabel.textContent = 'Message / Command: '; | |
| 1728 | + this.editorMessage.value = oText; | |
| 1729 | + this.editorMessage.dispatchEvent(new Event('input', { bubbles:true })); | |
| 589 | 1730 | this.editorExecutePromise = null; |
| 590 | 1731 | this.editorExecuteBtn.classList.remove('qr--busy'); |
| 591 | 1732 | this.editorPopup.dlgeditorDom.classList.remove('qr--hideisExecuting'); |
| 1733 | + this.isExecuting = false; | |
| 1734 | + this.editorPopup.onClosing = null; | |
| 592 | 1735 | } |
| 593 | 1736 | |
| 594 | 1737 | updateEditorProgress(done, total) { |
| @@ -623,6 +1766,47 @@ export class QuickReply { | ||
| 623 | 1766 | /** |
| 624 | 1767 | * @param {string} value |
| 625 | 1768 | */ |
| 1769 | + updateIcon(value) { | |
| 1770 | + if (this.onUpdate) { | |
| 1771 | + if (value === null) return; | |
| 1772 | + if (this.settingsDomIcon) { | |
| 1773 | + if (this.icon != value) { | |
| 1774 | + if (value == '') { | |
| 1775 | + if (this.icon) { | |
| 1776 | + this.settingsDomIcon.classList.remove(this.icon); | |
| 1777 | + } | |
| 1778 | + this.settingsDomIcon.textContent = '…'; | |
| 1779 | + this.settingsDomIcon.classList.remove('fa-solid'); | |
| 1780 | + } else { | |
| 1781 | + if (this.icon) { | |
| 1782 | + this.settingsDomIcon.classList.remove(this.icon); | |
| 1783 | + } else { | |
| 1784 | + this.settingsDomIcon.classList.add('fa-solid'); | |
| 1785 | + } | |
| 1786 | + this.settingsDomIcon.classList.add(value); | |
| 1787 | + } | |
| 1788 | + } | |
| 1789 | + } | |
| 1790 | + this.icon = value; | |
| 1791 | + this.updateRender(); | |
| 1792 | + this.onUpdate(this); | |
| 1793 | + } | |
| 1794 | + } | |
| 1795 | + | |
| 1796 | + /** | |
| 1797 | + * @param {boolean} value | |
| 1798 | + */ | |
| 1799 | + updateShowLabel(value) { | |
| 1800 | + if (this.onUpdate) { | |
| 1801 | + this.showLabel = value; | |
| 1802 | + this.updateRender(); | |
| 1803 | + this.onUpdate(this); | |
| 1804 | + } | |
| 1805 | + } | |
| 1806 | + | |
| 1807 | + /** | |
| 1808 | + * @param {string} value | |
| 1809 | + */ | |
| 626 | 1810 | updateLabel(value) { |
| 627 | 1811 | if (this.onUpdate) { |
| 628 | 1812 | if (this.settingsDomLabel && this.settingsDomLabel.value != value) { |
| @@ -670,12 +1854,15 @@ export class QuickReply { | ||
| 670 | 1854 | } |
| 671 | 1855 | |
| 672 | 1856 | |
| 673 | 1857 | async execute(args = {}, isEditor = false, isRun = false, options = {}) { |
| 674 | 1858 | if (this.message?.length > 0 && this.onExecute) { |
| 675 | 1859 | const scope = new SlashCommandScope(); |
| 676 | 1860 | for (const key of Object.keys(args)) { |
| 1861 | + if (key[0] == '_') continue; | |
| 1862 | + if (key == 'isAutoExecute') continue; | |
| 677 | 1863 | scope.setMacro(`arg::${key}`, args[key]); |
| 678 | 1864 | } |
| 1865 | + scope.setMacro('arg::*', ''); | |
| 679 | 1866 | if (isEditor) { |
| 680 | 1867 | this.abortController = new SlashCommandAbortController(); |
| 681 | 1868 | } |
| @@ -685,6 +1872,7 @@ export class QuickReply { | ||
| 685 | 1872 | isEditor, |
| 686 | 1873 | isRun, |
| 687 | 1874 | scope, |
| 1875 | + executionOptions: options, | |
| 688 | 1876 | }); |
| 689 | 1877 | } |
| 690 | 1878 | } |
| @@ -695,6 +1883,8 @@ export class QuickReply { | ||
| 695 | 1883 | toJSON() { |
| 696 | 1884 | return { |
| 697 | 1885 | id: this.id, |
| 1886 | + icon: this.icon, | |
| 1887 | + showLabel: this.showLabel, | |
| 698 | 1888 | label: this.label, |
| 699 | 1889 | title: this.title, |
| 700 | 1890 | message: this.message, |
| @@ -60,7 +60,12 @@ export class QuickReplyConfig { | ||
| 60 | 60 | /**@type {HTMLElement}*/ |
| 61 | 61 | this.setListDom = root.querySelector('.qr--setList'); |
| 62 | 62 | root.querySelector('.qr--setListAdd').addEventListener('click', ()=>{ |
| 63 | - this.addSet(QuickReplySet.list[0]); | |
| 63 | + const newSet = QuickReplySet.list.find(qr=>!this.setList.find(qrl=>qrl.set == qr)); | |
| 64 | + if (newSet) { | |
| 65 | + this.addSet(newSet); | |
| 66 | + } else { | |
| 67 | + toastr.warning('All existing QR Sets have already been added.'); | |
| 68 | + } | |
| 64 | 69 | }); |
| 65 | 70 | this.updateSetListDom(); |
| 66 | 71 | } |
| @@ -1,7 +1,9 @@ | ||
| 1 | 1 | import { getRequestHeaders, substituteParams } from '../../../../script.js'; |
| 2 | +import { Popup, POPUP_RESULT, POPUP_TYPE } from '../../../popup.js'; | |
| 2 | 3 | import { executeSlashCommands, executeSlashCommandsOnChatInput, executeSlashCommandsWithOptions } from '../../../slash-commands.js'; |
| 4 | +import { SlashCommandParser } from '../../../slash-commands/SlashCommandParser.js'; | |
| 3 | 5 | import { SlashCommandScope } from '../../../slash-commands/SlashCommandScope.js'; |
| 4 | 6 | import { debounceAsync, log, warn } from '../index.js'; |
| 5 | 7 | import { QuickReply } from './QuickReply.js'; |
| 6 | 8 | |
| 7 | 9 | export class QuickReplySet { |
| @@ -16,7 +18,7 @@ export class QuickReplySet { | ||
| 16 | 18 | } |
| 17 | 19 | |
| 18 | 20 | /** |
| 19 | 21 | * @param {Stringstring} name - name of the QuickReplySet |
| 20 | 22 | */ |
| 21 | 23 | static get(name) { |
| 22 | 24 | return this.list.find(it=>it.name == name); |
| @@ -25,17 +27,19 @@ export class QuickReplySet { | ||
| 25 | 27 | |
| 26 | 28 | |
| 27 | 29 | |
| 28 | 30 | /**@type {Stringstring}*/ name; |
| 29 | 31 | /**@type {Booleanboolean}*/ disableSend = false; |
| 30 | 32 | /**@type {Booleanboolean}*/ placeBeforeInput = false; |
| 31 | 33 | /**@type {Booleanboolean}*/ injectInput = false; |
| 34 | + /**@type {string}*/ color = 'transparent'; | |
| 35 | + /**@type {boolean}*/ onlyBorderColor = false; | |
| 32 | 36 | /**@type {QuickReply[]}*/ qrList = []; |
| 33 | 37 | |
| 34 | 38 | /**@type {Numbernumber}*/ idIndex = 0; |
| 35 | 39 | |
| 36 | 40 | /**@type {Booleanboolean}*/ isDeleted = false; |
| 37 | 41 | |
| 38 | 42 | /**@type {Functionfunction}*/ save; |
| 39 | 43 | |
| 40 | 44 | /**@type {HTMLElement}*/ dom; |
| 41 | 45 | /**@type {HTMLElement}*/ settingsDom; |
| @@ -64,6 +68,7 @@ export class QuickReplySet { | ||
| 64 | 68 | const root = document.createElement('div'); { |
| 65 | 69 | this.dom = root; |
| 66 | 70 | root.classList.add('qr--buttons'); |
| 71 | + this.updateColor(); | |
| 67 | 72 | this.qrList.filter(qr=>!qr.isHidden).forEach(qr=>{ |
| 68 | 73 | root.append(qr.render()); |
| 69 | 74 | }); |
| @@ -78,6 +83,22 @@ export class QuickReplySet { | ||
| 78 | 83 | this.dom.append(qr.render()); |
| 79 | 84 | }); |
| 80 | 85 | } |
| 86 | + updateColor() { | |
| 87 | + if (!this.dom) return; | |
| 88 | + if (this.color && this.color != 'transparent') { | |
| 89 | + this.dom.style.setProperty('--qr--color', this.color); | |
| 90 | + this.dom.classList.add('qr--color'); | |
| 91 | + if (this.onlyBorderColor) { | |
| 92 | + this.dom.classList.add('qr--borderColor'); | |
| 93 | + } else { | |
| 94 | + this.dom.classList.remove('qr--borderColor'); | |
| 95 | + } | |
| 96 | + } else { | |
| 97 | + this.dom.style.setProperty('--qr--color', 'transparent'); | |
| 98 | + this.dom.classList.remove('qr--color'); | |
| 99 | + this.dom.classList.remove('qr--borderColor'); | |
| 100 | + } | |
| 101 | + } | |
| 81 | 102 | |
| 82 | 103 | |
| 83 | 104 | |
| @@ -93,6 +114,11 @@ export class QuickReplySet { | ||
| 93 | 114 | } |
| 94 | 115 | return this.settingsDom; |
| 95 | 116 | } |
| 117 | + /** | |
| 118 | + * | |
| 119 | + * @param {QuickReply} qr | |
| 120 | + * @param {number} idx | |
| 121 | + */ | |
| 96 | 122 | renderSettingsItem(qr, idx) { |
| 97 | 123 | this.settingsDom.append(qr.renderSettings(idx)); |
| 98 | 124 | } |
| @@ -102,6 +128,18 @@ export class QuickReplySet { | ||
| 102 | 128 | |
| 103 | 129 | /** |
| 104 | 130 | * |
| 131 | + * @param {QuickReply} qr | |
| 132 | + */ | |
| 133 | + async debug(qr) { | |
| 134 | + const parser = new SlashCommandParser(); | |
| 135 | + const closure = parser.parse(qr.message, true, [], qr.abortController, qr.debugController); | |
| 136 | + closure.source = `${this.name}.${qr.label}`; | |
| 137 | + closure.onProgress = (done, total) => qr.updateEditorProgress(done, total); | |
| 138 | + closure.scope.setMacro('arg::*', ''); | |
| 139 | + return (await closure.execute())?.pipe; | |
| 140 | + } | |
| 141 | + /** | |
| 142 | + * | |
| 105 | 143 | * @param {QuickReply} qr The QR to execute. |
| 106 | 144 | * @param {object} options |
| 107 | 145 | * @param {string} [options.message] (null) altered message to be used |
| @@ -109,6 +147,7 @@ export class QuickReplySet { | ||
| 109 | 147 | * @param {boolean} [options.isEditor] (false) whether the execution is triggered by the QR editor |
| 110 | 148 | * @param {boolean} [options.isRun] (false) whether the execution is triggered by /run or /: (window.executeQuickReplyByName) |
| 111 | 149 | * @param {SlashCommandScope} [options.scope] (null) scope to be used when running the command |
| 150 | + * @param {import('../../../slash-commands.js').ExecuteSlashCommandsOptions} [options.executionOptions] ({}) further execution options | |
| 112 | 151 | * @returns |
| 113 | 152 | */ |
| 114 | 153 | async executeWithOptions(qr, options = {}) { |
| @@ -118,7 +157,9 @@ export class QuickReplySet { | ||
| 118 | 157 | isEditor:false, |
| 119 | 158 | isRun:false, |
| 120 | 159 | scope:null, |
| 160 | + executionOptions:{}, | |
| 121 | 161 | }, options); |
| 162 | + const execOptions = options.executionOptions; | |
| 122 | 163 | /**@type {HTMLTextAreaElement}*/ |
| 123 | 164 | const ta = document.querySelector('#send_textarea'); |
| 124 | 165 | const finalMessage = options.message ?? qr.message; |
| @@ -136,21 +177,24 @@ export class QuickReplySet { | ||
| 136 | 177 | if (input[0] == '/' && !this.disableSend) { |
| 137 | 178 | let result; |
| 138 | 179 | if (options.isAutoExecute || options.isRun) { |
| 139 | 180 | result = await executeSlashCommandsWithOptions(input, Object.assign(execOptions, { |
| 140 | 181 | handleParserErrors: true, |
| 141 | 182 | scope: options.scope, |
| 142 | - }); | |
| 183 | + source: `${this.name}.${qr.label}`, | |
| 184 | + })); | |
| 143 | 185 | } else if (options.isEditor) { |
| 144 | 186 | result = await executeSlashCommandsWithOptions(input, Object.assign(execOptions, { |
| 145 | 187 | handleParserErrors: false, |
| 146 | 188 | scope: options.scope, |
| 147 | 189 | abortController: qr.abortController, |
| 190 | + source: `${this.name}.${qr.label}`, | |
| 148 | 191 | onProgress: (done, total) => qr.updateEditorProgress(done, total), |
| 149 | 192 | })); |
| 150 | 193 | } else { |
| 151 | 194 | result = await executeSlashCommandsOnChatInput(input, Object.assign(execOptions, { |
| 152 | 195 | scope: options.scope, |
| 153 | - }); | |
| 196 | + source: `${this.name}.${qr.label}`, | |
| 197 | + })); | |
| 154 | 198 | } |
| 155 | 199 | return typeof result === 'object' ? result?.pipe : ''; |
| 156 | 200 | } |
| @@ -165,7 +209,7 @@ export class QuickReplySet { | ||
| 165 | 209 | } |
| 166 | 210 | /** |
| 167 | 211 | * @param {QuickReply} qr |
| 168 | 212 | * @param {Stringstring} [message] - optional altered message to be used |
| 169 | 213 | * @param {SlashCommandScope} [scope] - optional scope to be used when running the command |
| 170 | 214 | */ |
| 171 | 215 | async execute(qr, message = null, isAutoExecute = false, scope = null) { |
| @@ -179,10 +223,11 @@ export class QuickReplySet { | ||
| 179 | 223 | |
| 180 | 224 | |
| 181 | 225 | |
| 182 | 226 | addQuickReply(data = {}) { |
| 183 | 227 | const id = Math.max(this.idIndex, this.qrList.reduce((max,qr)=>Math.max(max,qr.id),0)) + 1; |
| 228 | + data.id = | |
| 184 | 229 | this.idIndex = id + 1; |
| 185 | 230 | const qr = QuickReply.from({ id }data); |
| 186 | 231 | this.qrList.push(qr); |
| 187 | 232 | this.hookQuickReply(qr); |
| 188 | 233 | if (this.settingsDom) { |
| @@ -194,11 +239,131 @@ export class QuickReplySet { | ||
| 194 | 239 | this.save(); |
| 195 | 240 | return qr; |
| 196 | 241 | } |
| 242 | + addQuickReplyFromText(qrJson) { | |
| 243 | + let data; | |
| 244 | + if (qrJson) { | |
| 245 | + try { | |
| 246 | + data = JSON.parse(qrJson ?? '{}'); | |
| 247 | + delete data.id; | |
| 248 | + } catch { | |
| 249 | + // not JSON data | |
| 250 | + } | |
| 251 | + if (data) { | |
| 252 | + // JSON data | |
| 253 | + if (data.label === undefined || data.message === undefined) { | |
| 254 | + // not a QR | |
| 255 | + toastr.error('Not a QR.'); | |
| 256 | + return; | |
| 257 | + } | |
| 258 | + } else { | |
| 259 | + // no JSON, use plaintext as QR message | |
| 260 | + data = { message: qrJson }; | |
| 261 | + } | |
| 262 | + } else { | |
| 263 | + data = {}; | |
| 264 | + } | |
| 265 | + const newQr = this.addQuickReply(data); | |
| 266 | + return newQr; | |
| 267 | + } | |
| 197 | 268 | |
| 269 | + /** | |
| 270 | + * | |
| 271 | + * @param {QuickReply} qr | |
| 272 | + */ | |
| 198 | 273 | hookQuickReply(qr) { |
| 274 | + qr.onDebug = ()=>this.debug(qr); | |
| 199 | 275 | qr.onExecute = (_, options)=>this.executeWithOptions(qr, options); |
| 200 | 276 | qr.onDelete = ()=>this.removeQuickReply(qr); |
| 201 | 277 | qr.onUpdate = ()=>this.save(); |
| 278 | + qr.onInsertBefore = (qrJson)=>{ | |
| 279 | + this.addQuickReplyFromText(qrJson); | |
| 280 | + const newQr = this.qrList.pop(); | |
| 281 | + this.qrList.splice(this.qrList.indexOf(qr), 0, newQr); | |
| 282 | + if (qr.settingsDom) { | |
| 283 | + qr.settingsDom.insertAdjacentElement('beforebegin', newQr.settingsDom); | |
| 284 | + } | |
| 285 | + this.save(); | |
| 286 | + }; | |
| 287 | + qr.onTransfer = async()=>{ | |
| 288 | + /**@type {HTMLSelectElement} */ | |
| 289 | + let sel; | |
| 290 | + let isCopy = false; | |
| 291 | + const dom = document.createElement('div'); { | |
| 292 | + dom.classList.add('qr--transferModal'); | |
| 293 | + const title = document.createElement('h3'); { | |
| 294 | + title.textContent = 'Transfer Quick Reply'; | |
| 295 | + dom.append(title); | |
| 296 | + } | |
| 297 | + const subTitle = document.createElement('h4'); { | |
| 298 | + const entryName = qr.label; | |
| 299 | + const bookName = this.name; | |
| 300 | + subTitle.textContent = `${bookName}: ${entryName}`; | |
| 301 | + dom.append(subTitle); | |
| 302 | + } | |
| 303 | + sel = document.createElement('select'); { | |
| 304 | + sel.classList.add('qr--transferSelect'); | |
| 305 | + sel.setAttribute('autofocus', '1'); | |
| 306 | + const noOpt = document.createElement('option'); { | |
| 307 | + noOpt.value = ''; | |
| 308 | + noOpt.textContent = '-- Select QR Set --'; | |
| 309 | + sel.append(noOpt); | |
| 310 | + } | |
| 311 | + for (const qrs of QuickReplySet.list) { | |
| 312 | + const opt = document.createElement('option'); { | |
| 313 | + opt.value = qrs.name; | |
| 314 | + opt.textContent = qrs.name; | |
| 315 | + sel.append(opt); | |
| 316 | + } | |
| 317 | + } | |
| 318 | + sel.addEventListener('keyup', (evt)=>{ | |
| 319 | + if (evt.key == 'Shift') { | |
| 320 | + (dlg.dom ?? dlg.dlg).classList.remove('qr--isCopy'); | |
| 321 | + return; | |
| 322 | + } | |
| 323 | + }); | |
| 324 | + sel.addEventListener('keydown', (evt)=>{ | |
| 325 | + if (evt.key == 'Shift') { | |
| 326 | + (dlg.dom ?? dlg.dlg).classList.add('qr--isCopy'); | |
| 327 | + return; | |
| 328 | + } | |
| 329 | + if (!evt.ctrlKey && !evt.altKey && evt.key == 'Enter') { | |
| 330 | + evt.preventDefault(); | |
| 331 | + if (evt.shiftKey) isCopy = true; | |
| 332 | + dlg.completeAffirmative(); | |
| 333 | + } | |
| 334 | + }); | |
| 335 | + dom.append(sel); | |
| 336 | + } | |
| 337 | + const hintP = document.createElement('p'); { | |
| 338 | + const hint = document.createElement('small'); { | |
| 339 | + hint.textContent = 'Type or arrows to select QR Set. Enter to transfer. Shift+Enter to copy.'; | |
| 340 | + hintP.append(hint); | |
| 341 | + } | |
| 342 | + dom.append(hintP); | |
| 343 | + } | |
| 344 | + } | |
| 345 | + const dlg = new Popup(dom, POPUP_TYPE.CONFIRM, null, { okButton:'Transfer', cancelButton:'Cancel' }); | |
| 346 | + const copyBtn = document.createElement('div'); { | |
| 347 | + copyBtn.classList.add('qr--copy'); | |
| 348 | + copyBtn.classList.add('menu_button'); | |
| 349 | + copyBtn.textContent = 'Copy'; | |
| 350 | + copyBtn.addEventListener('click', ()=>{ | |
| 351 | + isCopy = true; | |
| 352 | + dlg.completeAffirmative(); | |
| 353 | + }); | |
| 354 | + (dlg.ok ?? dlg.okButton).insertAdjacentElement('afterend', copyBtn); | |
| 355 | + } | |
| 356 | + const prom = dlg.show(); | |
| 357 | + sel.focus(); | |
| 358 | + await prom; | |
| 359 | + if (dlg.result == POPUP_RESULT.AFFIRMATIVE) { | |
| 360 | + const qrs = QuickReplySet.list.find(it=>it.name == sel.value); | |
| 361 | + qrs.addQuickReply(qr.toJSON()); | |
| 362 | + if (!isCopy) { | |
| 363 | + qr.delete(); | |
| 364 | + } | |
| 365 | + } | |
| 366 | + }; | |
| 202 | 367 | } |
| 203 | 368 | |
| 204 | 369 | removeQuickReply(qr) { |
| @@ -214,6 +379,8 @@ export class QuickReplySet { | ||
| 214 | 379 | disableSend: this.disableSend, |
| 215 | 380 | placeBeforeInput: this.placeBeforeInput, |
| 216 | 381 | injectInput: this.injectInput, |
| 382 | + color: this.color, | |
| 383 | + onlyBorderColor: this.onlyBorderColor, | |
| 217 | 384 | qrList: this.qrList, |
| 218 | 385 | idIndex: this.idIndex, |
| 219 | 386 | }; |
| @@ -245,9 +412,13 @@ export class QuickReplySet { | ||
| 245 | 412 | if (response.ok) { |
| 246 | 413 | this.unrender(); |
| 247 | 414 | const idx = QuickReplySet.list.indexOf(this); |
| 415 | + if (idx > -1) { | |
| 248 | 416 | QuickReplySet.list.splice(idx, 1); |
| 249 | 417 | this.isDeleted = true; |
| 250 | 418 | } else { |
| 419 | + warn(`Deleted Quick Reply Set was not found in the list of sets: ${this.name}`); | |
| 420 | + } | |
| 421 | + } else { | |
| 251 | 422 | warn(`Failed to delete Quick Reply Set: ${this.name}`); |
| 252 | 423 | } |
| 253 | 424 | } |
| @@ -45,7 +45,7 @@ export class QuickReplySetLink { | ||
| 45 | 45 | this.set = QuickReplySet.get(set.value); |
| 46 | 46 | this.update(); |
| 47 | 47 | }); |
| 48 | - QuickReplySet.list.forEach(qrs=>{ | |
| 48 | + QuickReplySet.list.toSorted((a,b)=>a.name.toLowerCase().localeCompare(b.name.toLowerCase())).forEach(qrs=>{ | |
| 49 | 49 | const opt = document.createElement('option'); { |
| 50 | 50 | opt.value = qrs.name; |
| 51 | 51 | opt.textContent = qrs.name; |
| @@ -1,8 +1,12 @@ | ||
| 1 | 1 | import { SlashCommand } from '../../../slash-commands/SlashCommand.js'; |
| 2 | +import { SlashCommandAbortController } from '../../../slash-commands/SlashCommandAbortController.js'; | |
| 2 | 3 | import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from '../../../slash-commands/SlashCommandArgument.js'; |
| 4 | +import { SlashCommandClosure } from '../../../slash-commands/SlashCommandClosure.js'; | |
| 3 | 5 | import { enumIcons } from '../../../slash-commands/SlashCommandCommonEnumsProvider.js'; |
| 6 | +import { SlashCommandDebugController } from '../../../slash-commands/SlashCommandDebugController.js'; | |
| 4 | 7 | import { SlashCommandEnumValue, enumTypes } from '../../../slash-commands/SlashCommandEnumValue.js'; |
| 5 | 8 | import { SlashCommandParser } from '../../../slash-commands/SlashCommandParser.js'; |
| 9 | +import { SlashCommandScope } from '../../../slash-commands/SlashCommandScope.js'; | |
| 6 | 10 | import { isTrueBoolean } from '../../../utils.js'; |
| 7 | 11 | // eslint-disable-next-line no-unused-vars |
| 8 | 12 | import { QuickReplyApi } from '../api/QuickReplyApi.js'; |
| @@ -47,6 +51,13 @@ export class SlashCommandHandler { | ||
| 47 | 51 | return new SlashCommandEnumValue(qr.label, message, enumTypes.enum, enumIcons.qr); |
| 48 | 52 | }) ?? [], |
| 49 | 53 | |
| 54 | + /** All QRs inside a set, utilizing the "set" named argument, returns the QR's ID */ | |
| 55 | + qrIds: (executor) => QuickReplySet.get(String(executor.namedArgumentList.find(x => x.name == 'set')?.value))?.qrList.map(qr => { | |
| 56 | + const icons = getExecutionIcons(qr); | |
| 57 | + const message = `${qr.automationId ? `[${qr.automationId}]` : ''}${icons ? `[auto: ${icons}]` : ''} ${qr.title || qr.message}`.trim(); | |
| 58 | + return new SlashCommandEnumValue(qr.label, message, enumTypes.enum, enumIcons.qr, null, ()=>qr.id.toString(), true); | |
| 59 | + }) ?? [], | |
| 60 | + | |
| 50 | 61 | /** All QRs as a set.name string, to be able to execute, for example via the /run command */ |
| 51 | 62 | qrExecutables: () => { |
| 52 | 63 | const globalSetList = this.api.settings.config.setList; |
| @@ -63,7 +74,7 @@ export class SlashCommandHandler { | ||
| 63 | 74 | ...otherQrs.map(x => new SlashCommandEnumValue(`${x.set.name}.${x.qr.label}`, `${x.qr.title || x.qr.message}`, enumTypes.qr, enumIcons.qr)), |
| 64 | 75 | ]; |
| 65 | 76 | }, |
| 66 | 77 | }; |
| 67 | 78 | |
| 68 | 79 | window['qrEnumProviderExecutables'] = localEnumProviders.qrExecutables; |
| 69 | 80 | |
| @@ -234,8 +245,20 @@ export class SlashCommandHandler { | ||
| 234 | 245 | name: 'label', |
| 235 | 246 | description: 'text on the button, e.g., label=MyButton', |
| 236 | 247 | typeList: [ARGUMENT_TYPE.STRING], |
| 237 | 248 | isRequired: truefalse, |
| 238 | 249 | enumProvider: localEnumProviders.qrLabelsqrEntries, |
| 250 | + }), | |
| 251 | + SlashCommandNamedArgument.fromProps({ | |
| 252 | + name: 'icon', | |
| 253 | + description: 'icon to show on the button, e.g., icon=fa-pencil', | |
| 254 | + typeList: [ARGUMENT_TYPE.STRING], | |
| 255 | + isRequired: false, | |
| 256 | + }), | |
| 257 | + SlashCommandNamedArgument.fromProps({ | |
| 258 | + name: 'showlabel', | |
| 259 | + description: 'whether to show the label even when an icon is assigned, e.g., icon=fa-pencil showlabel=true', | |
| 260 | + typeList: [ARGUMENT_TYPE.BOOLEAN], | |
| 261 | + isRequired: false, | |
| 239 | 262 | }), |
| 240 | 263 | new SlashCommandNamedArgument('hidden', 'whether the button should be hidden, e.g., hidden=true', [ARGUMENT_TYPE.BOOLEAN], false, false, 'false'), |
| 241 | 264 | new SlashCommandNamedArgument('startup', 'auto execute on app startup, e.g., startup=true', [ARGUMENT_TYPE.BOOLEAN], false, false, 'false'), |
| @@ -247,6 +270,13 @@ export class SlashCommandHandler { | ||
| 247 | 270 | ]; |
| 248 | 271 | const qrUpdateArgs = [ |
| 249 | 272 | new SlashCommandNamedArgument('newlabel', 'new text for the button', [ARGUMENT_TYPE.STRING], false), |
| 273 | + SlashCommandNamedArgument.fromProps({ | |
| 274 | + name: 'id', | |
| 275 | + description: 'numeric ID of the QR, e.g., id=42', | |
| 276 | + typeList: [ARGUMENT_TYPE.NUMBER], | |
| 277 | + isRequired: false, | |
| 278 | + enumProvider: localEnumProviders.qrIds, | |
| 279 | + }), | |
| 250 | 280 | ]; |
| 251 | 281 | |
| 252 | 282 | SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'qr-create', |
| @@ -272,13 +302,61 @@ export class SlashCommandHandler { | ||
| 272 | 302 | </div> |
| 273 | 303 | `, |
| 274 | 304 | })); |
| 305 | + SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'qr-get', | |
| 306 | + callback: (args, _) => { | |
| 307 | + return this.getQuickReply(args); | |
| 308 | + }, | |
| 309 | + namedArgumentList: [ | |
| 310 | + SlashCommandNamedArgument.fromProps({ | |
| 311 | + name: 'set', | |
| 312 | + description: 'name of the QR set, e.g., set=PresetName1', | |
| 313 | + typeList: [ARGUMENT_TYPE.STRING], | |
| 314 | + isRequired: true, | |
| 315 | + enumProvider: localEnumProviders.qrSets, | |
| 316 | + }), | |
| 317 | + SlashCommandNamedArgument.fromProps({ | |
| 318 | + name: 'label', | |
| 319 | + description: 'text on the button, e.g., label=MyButton', | |
| 320 | + typeList: [ARGUMENT_TYPE.STRING], | |
| 321 | + isRequired: false, | |
| 322 | + enumProvider: localEnumProviders.qrEntries, | |
| 323 | + }), | |
| 324 | + SlashCommandNamedArgument.fromProps({ | |
| 325 | + name: 'id', | |
| 326 | + description: 'numeric ID of the QR, e.g., id=42', | |
| 327 | + typeList: [ARGUMENT_TYPE.NUMBER], | |
| 328 | + isRequired: false, | |
| 329 | + enumProvider: localEnumProviders.qrIds, | |
| 330 | + }), | |
| 331 | + ], | |
| 332 | + returns: 'a dictionary with all the QR\'s properties', | |
| 333 | + helpString: ` | |
| 334 | + <div>Get a Quick Reply's properties.</div> | |
| 335 | + <div> | |
| 336 | + <strong>Examples:</strong> | |
| 337 | + <ul> | |
| 338 | + <li> | |
| 339 | + <pre><code>/qr-get set=MyPreset label=MyButton | /echo</code></pre> | |
| 340 | + <pre><code>/qr-get set=MyPreset id=42 | /echo</code></pre> | |
| 341 | + </li> | |
| 342 | + </ul> | |
| 343 | + </div> | |
| 344 | + `, | |
| 345 | + })); | |
| 275 | 346 | SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'qr-update', |
| 276 | 347 | callback: (args, message) => { |
| 277 | 348 | this.updateQuickReply(args, message); |
| 278 | 349 | return ''; |
| 279 | 350 | }, |
| 280 | 351 | returns: 'updated quick reply', |
| 281 | 352 | namedArgumentList: [...qrUpdateArgs, ...qrArgs],.map(it=>{ |
| 353 | + if (it.name == 'label') { | |
| 354 | + const clone = SlashCommandNamedArgument.fromProps(it); | |
| 355 | + clone.isRequired = false; | |
| 356 | + return clone; | |
| 357 | + } | |
| 358 | + return it; | |
| 359 | + })], | |
| 282 | 360 | unnamedArgumentList: [ |
| 283 | 361 | new SlashCommandArgument('command', [ARGUMENT_TYPE.STRING]), |
| 284 | 362 | ], |
| @@ -315,6 +393,12 @@ export class SlashCommandHandler { | ||
| 315 | 393 | typeList: [ARGUMENT_TYPE.STRING], |
| 316 | 394 | enumProvider: localEnumProviders.qrEntries, |
| 317 | 395 | }), |
| 396 | + SlashCommandNamedArgument.fromProps({ | |
| 397 | + name: 'id', | |
| 398 | + description: 'numeric ID of the QR, e.g., id=42', | |
| 399 | + typeList: [ARGUMENT_TYPE.NUMBER], | |
| 400 | + enumProvider: localEnumProviders.qrIds, | |
| 401 | + }), | |
| 318 | 402 | ], |
| 319 | 403 | unnamedArgumentList: [ |
| 320 | 404 | SlashCommandArgument.fromProps({ |
| @@ -344,6 +428,12 @@ export class SlashCommandHandler { | ||
| 344 | 428 | typeList: [ARGUMENT_TYPE.STRING], |
| 345 | 429 | enumProvider: localEnumProviders.qrEntries, |
| 346 | 430 | }), |
| 431 | + SlashCommandNamedArgument.fromProps({ | |
| 432 | + name: 'id', | |
| 433 | + description: 'numeric ID of the QR, e.g., id=42', | |
| 434 | + typeList: [ARGUMENT_TYPE.NUMBER], | |
| 435 | + enumProvider: localEnumProviders.qrIds, | |
| 436 | + }), | |
| 347 | 437 | new SlashCommandNamedArgument( |
| 348 | 438 | 'chain', 'boolean', [ARGUMENT_TYPE.BOOLEAN], false, false, 'false', |
| 349 | 439 | ), |
| @@ -389,6 +479,12 @@ export class SlashCommandHandler { | ||
| 389 | 479 | typeList: [ARGUMENT_TYPE.STRING], |
| 390 | 480 | enumProvider: localEnumProviders.qrEntries, |
| 391 | 481 | }), |
| 482 | + SlashCommandNamedArgument.fromProps({ | |
| 483 | + name: 'id', | |
| 484 | + description: 'numeric ID of the QR, e.g., id=42', | |
| 485 | + typeList: [ARGUMENT_TYPE.NUMBER], | |
| 486 | + enumProvider: localEnumProviders.qrIds, | |
| 487 | + }), | |
| 392 | 488 | ], |
| 393 | 489 | unnamedArgumentList: [ |
| 394 | 490 | SlashCommandArgument.fromProps({ |
| @@ -425,6 +521,12 @@ export class SlashCommandHandler { | ||
| 425 | 521 | isRequired: true, |
| 426 | 522 | enumProvider: localEnumProviders.qrSets, |
| 427 | 523 | }), |
| 524 | + SlashCommandNamedArgument.fromProps({ | |
| 525 | + name: 'id', | |
| 526 | + description: 'numeric ID of the QR, e.g., id=42', | |
| 527 | + typeList: [ARGUMENT_TYPE.NUMBER], | |
| 528 | + enumProvider: localEnumProviders.qrIds, | |
| 529 | + }), | |
| 428 | 530 | ], |
| 429 | 531 | unnamedArgumentList: [ |
| 430 | 532 | SlashCommandArgument.fromProps({ |
| @@ -454,8 +556,8 @@ export class SlashCommandHandler { | ||
| 454 | 556 | new SlashCommandNamedArgument('inject', 'inject user input automatically (if disabled use {{input}})', [ARGUMENT_TYPE.BOOLEAN], false), |
| 455 | 557 | ]; |
| 456 | 558 | SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'qr-set-create', |
| 457 | 559 | callback: async (args, name) => { |
| 458 | 560 | await this.createSet(name, args); |
| 459 | 561 | return ''; |
| 460 | 562 | }, |
| 461 | 563 | aliases: ['qr-presetadd'], |
| @@ -485,8 +587,8 @@ export class SlashCommandHandler { | ||
| 485 | 587 | })); |
| 486 | 588 | |
| 487 | 589 | SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'qr-set-update', |
| 488 | 590 | callback: async (args, name) => { |
| 489 | 591 | await this.updateSet(name, args); |
| 490 | 592 | return ''; |
| 491 | 593 | }, |
| 492 | 594 | aliases: ['qr-presetupdate'], |
| @@ -510,8 +612,8 @@ export class SlashCommandHandler { | ||
| 510 | 612 | `, |
| 511 | 613 | })); |
| 512 | 614 | SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'qr-set-delete', |
| 513 | 615 | callback: async (_, name) => { |
| 514 | 616 | await this.deleteSet(name); |
| 515 | 617 | return ''; |
| 516 | 618 | }, |
| 517 | 619 | aliases: ['qr-presetdelete'], |
| @@ -533,6 +635,134 @@ export class SlashCommandHandler { | ||
| 533 | 635 | </div> |
| 534 | 636 | `, |
| 535 | 637 | })); |
| 638 | + | |
| 639 | + SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'qr-arg', | |
| 640 | + callback: ({ _scope }, [key, value]) => { | |
| 641 | + _scope.setMacro(`arg::${key}`, value, key.includes('*')); | |
| 642 | + return ''; | |
| 643 | + }, | |
| 644 | + unnamedArgumentList: [ | |
| 645 | + SlashCommandArgument.fromProps({ description: 'argument name', | |
| 646 | + typeList: ARGUMENT_TYPE.STRING, | |
| 647 | + isRequired: true, | |
| 648 | + }), | |
| 649 | + SlashCommandArgument.fromProps({ description: 'argument value', | |
| 650 | + typeList: [ARGUMENT_TYPE.STRING, ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.BOOLEAN, ARGUMENT_TYPE.LIST, ARGUMENT_TYPE.DICTIONARY], | |
| 651 | + isRequired: true, | |
| 652 | + }), | |
| 653 | + ], | |
| 654 | + splitUnnamedArgument: true, | |
| 655 | + splitUnnamedArgumentCount: 2, | |
| 656 | + helpString: ` | |
| 657 | + <div> | |
| 658 | + Set a fallback value for a Quick Reply argument. | |
| 659 | + </div> | |
| 660 | + <div> | |
| 661 | + <strong>Example:</strong> | |
| 662 | + <pre><code>/qr-arg x foo |\n/echo {{arg::x}}</code></pre> | |
| 663 | + </div> | |
| 664 | + `, | |
| 665 | + })); | |
| 666 | + | |
| 667 | + SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'import', | |
| 668 | + /** | |
| 669 | + * | |
| 670 | + * @param {{_scope:SlashCommandScope, _abortController:SlashCommandAbortController, _debugController:SlashCommandDebugController, from:string}} args | |
| 671 | + * @param {string} value | |
| 672 | + */ | |
| 673 | + callback: (args, value) => { | |
| 674 | + if (!args.from) throw new Error('/import requires from= to be set.'); | |
| 675 | + if (!value) throw new Error('/import requires the unnamed argument to be set.'); | |
| 676 | + let qr = [...this.api.listGlobalSets(), ...this.api.listChatSets()] | |
| 677 | + .map(it=>this.api.getSetByName(it)?.qrList ?? []) | |
| 678 | + .flat() | |
| 679 | + .find(it=>it.label == args.from) | |
| 680 | + ; | |
| 681 | + if (!qr) { | |
| 682 | + let [setName, ...qrNameParts] = args.from.split('.'); | |
| 683 | + let qrName = qrNameParts.join('.'); | |
| 684 | + let qrs = QuickReplySet.get(setName); | |
| 685 | + if (qrs) { | |
| 686 | + qr = qrs.qrList.find(it=>it.label == qrName); | |
| 687 | + } | |
| 688 | + } | |
| 689 | + if (qr) { | |
| 690 | + const parser = new SlashCommandParser(); | |
| 691 | + const closure = parser.parse(qr.message, true, [], args._abortController, args._debugController); | |
| 692 | + if (args._debugController) { | |
| 693 | + closure.source = args.from; | |
| 694 | + } | |
| 695 | + const testCandidates = (executor)=>{ | |
| 696 | + return ( | |
| 697 | + executor.namedArgumentList.find(arg=>arg.name == 'key') | |
| 698 | + && executor.unnamedArgumentList.length > 0 | |
| 699 | + && executor.unnamedArgumentList[0].value instanceof SlashCommandClosure | |
| 700 | + ) || ( | |
| 701 | + !executor.namedArgumentList.find(arg=>arg.name == 'key') | |
| 702 | + && executor.unnamedArgumentList.length > 1 | |
| 703 | + && executor.unnamedArgumentList[1].value instanceof SlashCommandClosure | |
| 704 | + ); | |
| 705 | + }; | |
| 706 | + const candidates = closure.executorList | |
| 707 | + .filter(executor=>['let', 'var'].includes(executor.command.name)) | |
| 708 | + .filter(testCandidates) | |
| 709 | + .map(executor=>({ | |
| 710 | + key: executor.namedArgumentList.find(arg=>arg.name == 'key')?.value ?? executor.unnamedArgumentList[0].value, | |
| 711 | + value: executor.unnamedArgumentList[executor.namedArgumentList.find(arg=>arg.name == 'key') ? 0 : 1].value, | |
| 712 | + })) | |
| 713 | + ; | |
| 714 | + for (let i = 0; i < value.length; i++) { | |
| 715 | + const srcName = value[i]; | |
| 716 | + let dstName = srcName; | |
| 717 | + if (i + 2 < value.length && value[i + 1] == 'as') { | |
| 718 | + dstName = value[i + 2]; | |
| 719 | + i += 2; | |
| 720 | + } | |
| 721 | + const pick = candidates.find(it=>it.key == srcName); | |
| 722 | + if (!pick) throw new Error(`No scoped closure named "${srcName}" found in "${args.from}"`); | |
| 723 | + if (args._scope.existsVariableInScope(dstName)) { | |
| 724 | + args._scope.setVariable(dstName, pick.value); | |
| 725 | + } else { | |
| 726 | + args._scope.letVariable(dstName, pick.value); | |
| 727 | + } | |
| 728 | + } | |
| 729 | + } else { | |
| 730 | + throw new Error(`No Quick Reply found for "${name}".`); | |
| 731 | + } | |
| 732 | + return ''; | |
| 733 | + }, | |
| 734 | + namedArgumentList: [ | |
| 735 | + SlashCommandNamedArgument.fromProps({ name: 'from', | |
| 736 | + description: 'Quick Reply to import from (QRSet.QRLabel)', | |
| 737 | + typeList: ARGUMENT_TYPE.STRING, | |
| 738 | + isRequired: true, | |
| 739 | + }), | |
| 740 | + ], | |
| 741 | + unnamedArgumentList: [ | |
| 742 | + SlashCommandArgument.fromProps({ description: 'what to import (x or x as y)', | |
| 743 | + acceptsMultiple: true, | |
| 744 | + typeList: ARGUMENT_TYPE.STRING, | |
| 745 | + isRequired: true, | |
| 746 | + }), | |
| 747 | + ], | |
| 748 | + splitUnnamedArgument: true, | |
| 749 | + helpString: ` | |
| 750 | + <div> | |
| 751 | + Import one or more closures from another Quick Reply. | |
| 752 | + </div> | |
| 753 | + <div> | |
| 754 | + Only imports closures that are directly assigned a scoped variable via <code>/let</code> or <code>/var</code>. | |
| 755 | + </div> | |
| 756 | + <div> | |
| 757 | + <strong>Examples:</strong> | |
| 758 | + <ul> | |
| 759 | + <li><pre><code>/import from=LibraryQrSet.FooBar foo |\n/:foo</code></pre></li> | |
| 760 | + <li><pre><code>/import from=LibraryQrSet.FooBar\n\tfoo\n\tbar\n|\n/:foo |\n/:bar</code></pre></li> | |
| 761 | + <li><pre><code>/import from=LibraryQrSet.FooBar\n\tfoo as x\n\tbar as y\n|\n/:x |\n/:y</code></pre></li> | |
| 762 | + </ul> | |
| 763 | + </div> | |
| 764 | + `, | |
| 765 | + })); | |
| 536 | 766 | } |
| 537 | 767 | |
| 538 | 768 | |
| @@ -618,6 +848,8 @@ export class SlashCommandHandler { | ||
| 618 | 848 | args.set ?? '', |
| 619 | 849 | args.label ?? '', |
| 620 | 850 | { |
| 851 | + icon: args.icon, | |
| 852 | + showLabel: args.showlabel === undefined ? undefined : isTrueBoolean(args.showlabel), | |
| 621 | 853 | message: message ?? '', |
| 622 | 854 | title: args.title, |
| 623 | 855 | isHidden: isTrueBoolean(args.hidden), |
| @@ -633,12 +865,21 @@ export class SlashCommandHandler { | ||
| 633 | 865 | toastr.error(ex.message); |
| 634 | 866 | } |
| 635 | 867 | } |
| 868 | + getQuickReply(args) { | |
| 869 | + try { | |
| 870 | + return JSON.stringify(this.api.getQrByLabel(args.set, args.id !== undefined ? Number(args.id) : args.label)); | |
| 871 | + } catch (ex) { | |
| 872 | + toastr.error(ex.message); | |
| 873 | + } | |
| 874 | + } | |
| 636 | 875 | updateQuickReply(args, message) { |
| 637 | 876 | try { |
| 638 | 877 | this.api.updateQuickReply( |
| 639 | 878 | args.set ?? '', |
| 640 | - args.label ?? '', | |
| 879 | + args.id !== undefined ? Number(args.id) : (args.label ?? ''), | |
| 641 | 880 | { |
| 881 | + icon: args.icon, | |
| 882 | + showLabel: args.showlabel === undefined ? undefined : isTrueBoolean(args.showlabel), | |
| 642 | 883 | newLabel: args.newlabel, |
| 643 | 884 | message: (message ?? '').trim().length > 0 ? message : undefined, |
| 644 | 885 | title: args.title, |
| @@ -657,7 +898,7 @@ export class SlashCommandHandler { | ||
| 657 | 898 | } |
| 658 | 899 | deleteQuickReply(args, label) { |
| 659 | 900 | try { |
| 660 | 901 | this.api.deleteQuickReply(args.set, args.id !== undefined ? Number(args.id) : (args.label ?? label)); |
| 661 | 902 | } catch (ex) { |
| 662 | 903 | toastr.error(ex.message); |
| 663 | 904 | } |
| @@ -692,9 +933,9 @@ export class SlashCommandHandler { | ||
| 692 | 933 | } |
| 693 | 934 | |
| 694 | 935 | |
| 695 | 936 | async createSet(name, args) { |
| 696 | 937 | try { |
| 697 | 938 | await this.api.createSet( |
| 698 | 939 | args.name ?? name ?? '', |
| 699 | 940 | { |
| 700 | 941 | disableSend: isTrueBoolean(args.nosend), |
| @@ -706,9 +947,9 @@ export class SlashCommandHandler { | ||
| 706 | 947 | toastr.error(ex.message); |
| 707 | 948 | } |
| 708 | 949 | } |
| 709 | 950 | async updateSet(name, args) { |
| 710 | 951 | try { |
| 711 | 952 | await this.api.updateSet( |
| 712 | 953 | args.name ?? name ?? '', |
| 713 | 954 | { |
| 714 | 955 | disableSend: args.nosend !== undefined ? isTrueBoolean(args.nosend) : undefined, |
| @@ -720,9 +961,9 @@ export class SlashCommandHandler { | ||
| 720 | 961 | toastr.error(ex.message); |
| 721 | 962 | } |
| 722 | 963 | } |
| 723 | 964 | async deleteSet(name) { |
| 724 | 965 | try { |
| 725 | 966 | await this.api.deleteSet(name ?? ''); |
| 726 | 967 | } catch (ex) { |
| 727 | 968 | toastr.error(ex.message); |
| 728 | 969 | } |
| @@ -23,6 +23,8 @@ export class SettingsUi { | ||
| 23 | 23 | /**@type {HTMLInputElement}*/ disableSend; |
| 24 | 24 | /**@type {HTMLInputElement}*/ placeBeforeInput; |
| 25 | 25 | /**@type {HTMLInputElement}*/ injectInput; |
| 26 | + /**@type {HTMLInputElement}*/ color; | |
| 27 | + /**@type {HTMLInputElement}*/ onlyBorderColor; | |
| 26 | 28 | /**@type {HTMLSelectElement}*/ currentSet; |
| 27 | 29 | |
| 28 | 30 | |
| @@ -117,10 +119,29 @@ export class SettingsUi { | ||
| 117 | 119 | this.dom.querySelector('#qr--set-add').addEventListener('click', async()=>{ |
| 118 | 120 | this.currentQrSet.addQuickReply(); |
| 119 | 121 | }); |
| 122 | + this.dom.querySelector('#qr--set-paste').addEventListener('click', async()=>{ | |
| 123 | + const text = await navigator.clipboard.readText(); | |
| 124 | + this.currentQrSet.addQuickReplyFromText(text); | |
| 125 | + }); | |
| 126 | + this.dom.querySelector('#qr--set-importQr').addEventListener('click', async()=>{ | |
| 127 | + const inp = document.createElement('input'); { | |
| 128 | + inp.type = 'file'; | |
| 129 | + inp.accept = '.json'; | |
| 130 | + inp.addEventListener('change', async()=>{ | |
| 131 | + if (inp.files.length > 0) { | |
| 132 | + for (const file of inp.files) { | |
| 133 | + const text = await file.text(); | |
| 134 | + this.currentQrSet.addQuickReply(JSON.parse(text)); | |
| 135 | + } | |
| 136 | + } | |
| 137 | + }); | |
| 138 | + inp.click(); | |
| 139 | + } | |
| 140 | + }); | |
| 120 | 141 | this.qrList = this.dom.querySelector('#qr--set-qrList'); |
| 121 | 142 | this.currentSet = this.dom.querySelector('#qr--set'); |
| 122 | 143 | this.currentSet.addEventListener('change', ()=>this.onQrSetChange()); |
| 123 | - QuickReplySet.list.forEach(qrs=>{ | |
| 144 | + QuickReplySet.list.toSorted((a,b)=>a.name.toLowerCase().localeCompare(b.name.toLowerCase())).forEach(qrs=>{ | |
| 124 | 145 | const opt = document.createElement('option'); { |
| 125 | 146 | opt.value = qrs.name; |
| 126 | 147 | opt.textContent = qrs.name; |
| @@ -145,6 +166,34 @@ export class SettingsUi { | ||
| 145 | 166 | qrs.injectInput = this.injectInput.checked; |
| 146 | 167 | qrs.save(); |
| 147 | 168 | }); |
| 169 | + let initialColorChange = true; | |
| 170 | + this.color = this.dom.querySelector('#qr--color'); | |
| 171 | + this.color.color = this.currentQrSet?.color ?? 'transparent'; | |
| 172 | + this.color.addEventListener('change', (evt)=>{ | |
| 173 | + if (!this.dom.closest('body')) return; | |
| 174 | + const qrs = this.currentQrSet; | |
| 175 | + if (initialColorChange) { | |
| 176 | + initialColorChange = false; | |
| 177 | + this.color.color = qrs.color; | |
| 178 | + return; | |
| 179 | + } | |
| 180 | + qrs.color = evt.detail.rgb; | |
| 181 | + qrs.save(); | |
| 182 | + this.currentQrSet.updateColor(); | |
| 183 | + }); | |
| 184 | + this.dom.querySelector('#qr--colorClear').addEventListener('click', (evt)=>{ | |
| 185 | + const qrs = this.currentQrSet; | |
| 186 | + this.color.color = 'transparent'; | |
| 187 | + qrs.save(); | |
| 188 | + this.currentQrSet.updateColor(); | |
| 189 | + }); | |
| 190 | + this.onlyBorderColor = this.dom.querySelector('#qr--onlyBorderColor'); | |
| 191 | + this.onlyBorderColor.addEventListener('click', ()=>{ | |
| 192 | + const qrs = this.currentQrSet; | |
| 193 | + qrs.onlyBorderColor = this.onlyBorderColor.checked; | |
| 194 | + qrs.save(); | |
| 195 | + this.currentQrSet.updateColor(); | |
| 196 | + }); | |
| 148 | 197 | this.onQrSetChange(); |
| 149 | 198 | } |
| 150 | 199 | onQrSetChange() { |
| @@ -152,6 +201,8 @@ export class SettingsUi { | ||
| 152 | 201 | this.disableSend.checked = this.currentQrSet.disableSend; |
| 153 | 202 | this.placeBeforeInput.checked = this.currentQrSet.placeBeforeInput; |
| 154 | 203 | this.injectInput.checked = this.currentQrSet.injectInput; |
| 204 | + this.color.color = this.currentQrSet.color ?? 'transparent'; | |
| 205 | + this.onlyBorderColor.checked = this.currentQrSet.onlyBorderColor; | |
| 155 | 206 | this.qrList.innerHTML = ''; |
| 156 | 207 | const qrsDom = this.currentQrSet.renderSettings(); |
| 157 | 208 | this.qrList.append(qrsDom); |
| @@ -265,7 +316,7 @@ export class SettingsUi { | ||
| 265 | 316 | const qrs = new QuickReplySet(); |
| 266 | 317 | qrs.name = name; |
| 267 | 318 | qrs.addQuickReply(); |
| 268 | 319 | const idx = QuickReplySet.list.findIndex(it=>it.name.toLowerCase().localeCompare(name.toLowerCase()) == 1); |
| 269 | 320 | if (idx > -1) { |
| 270 | 321 | QuickReplySet.list.splice(idx, 0, qrs); |
| 271 | 322 | } else { |
| @@ -321,7 +372,7 @@ export class SettingsUi { | ||
| 321 | 372 | this.prepareChatSetList(); |
| 322 | 373 | } |
| 323 | 374 | } else { |
| 324 | 375 | const idx = QuickReplySet.list.findIndex(it=>it.name.toLowerCase().localeCompare(qrs.name.toLowerCase()) == 1); |
| 325 | 376 | if (idx > -1) { |
| 326 | 377 | QuickReplySet.list.splice(idx, 0, qrs); |
| 327 | 378 | } else { |
| @@ -33,11 +33,14 @@ export class ContextMenu { | ||
| 33 | 33 | */ |
| 34 | 34 | build(qr, chainedMessage = null, hierarchy = [], labelHierarchy = []) { |
| 35 | 35 | const tree = { |
| 36 | + icon: qr.icon, | |
| 37 | + showLabel: qr.showLabel, | |
| 36 | 38 | label: qr.label, |
| 37 | 39 | message: (chainedMessage && qr.message ? `${chainedMessage} | ` : '') + qr.message, |
| 38 | 40 | children: [], |
| 39 | 41 | }; |
| 40 | 42 | qr.contextList.forEach((cl) => { |
| 43 | + if (!cl.set) return; | |
| 41 | 44 | if (!hierarchy.includes(cl.set)) { |
| 42 | 45 | const nextHierarchy = [...hierarchy, cl.set]; |
| 43 | 46 | const nextLabelHierarchy = [...labelHierarchy, tree.label]; |
| @@ -45,6 +48,8 @@ export class ContextMenu { | ||
| 45 | 48 | cl.set.qrList.forEach(subQr => { |
| 46 | 49 | const subTree = this.build(subQr, cl.isChained ? tree.message : null, nextHierarchy, nextLabelHierarchy); |
| 47 | 50 | tree.children.push(new MenuItem( |
| 51 | + subTree.icon, | |
| 52 | + subTree.showLabel, | |
| 48 | 53 | subTree.label, |
| 49 | 54 | subTree.message, |
| 50 | 55 | (evt) => { |
| @@ -2,7 +2,7 @@ import { MenuItem } from './MenuItem.js'; | ||
| 2 | 2 | |
| 3 | 3 | export class MenuHeader extends MenuItem { |
| 4 | 4 | constructor(/**@type {String}*/label) { |
| 5 | 5 | super(null, null, label, null, null); |
| 6 | 6 | } |
| 7 | 7 | |
| 8 | 8 | |
| @@ -1,21 +1,34 @@ | ||
| 1 | 1 | import { SubMenu } from './SubMenu.js'; |
| 2 | 2 | |
| 3 | 3 | export class MenuItem { |
| 4 | 4 | /**@type {Stringstring}*/ labelicon; |
| 5 | 5 | /**@type {Objectboolean}*/ valueshowLabel; |
| 6 | 6 | /**@type {Functionstring}*/ callbacklabel; |
| 7 | + /**@type {object}*/ value; | |
| 8 | + /**@type {function}*/ callback; | |
| 7 | 9 | /**@type {MenuItem[]}*/ childList = []; |
| 8 | 10 | /**@type {SubMenu}*/ subMenu; |
| 9 | 11 | /**@type {Booleanboolean}*/ isForceExpanded = false; |
| 10 | 12 | |
| 11 | 13 | /**@type {HTMLElement}*/ root; |
| 12 | 14 | |
| 13 | 15 | /**@type {Functionfunction}*/ onExpand; |
| 14 | 16 | |
| 15 | 17 | |
| 16 | 18 | |
| 17 | 19 | |
| 18 | - constructor(/**@type {String}*/label, /**@type {Object}*/value, /**@type {function}*/callback, /**@type {MenuItem[]}*/children = []) { | |
| 20 | + /** | |
| 21 | + * | |
| 22 | + * @param {string} icon | |
| 23 | + * @param {boolean} showLabel | |
| 24 | + * @param {string} label | |
| 25 | + * @param {object} value | |
| 26 | + * @param {function} callback | |
| 27 | + * @param {MenuItem[]} children | |
| 28 | + */ | |
| 29 | + constructor(icon, showLabel, label, value, callback, children = []) { | |
| 30 | + this.icon = icon; | |
| 31 | + this.showLabel = showLabel; | |
| 19 | 32 | this.label = label; |
| 20 | 33 | this.value = value; |
| 21 | 34 | this.callback = callback; |
| @@ -33,7 +46,21 @@ export class MenuItem { | ||
| 33 | 46 | if (this.callback) { |
| 34 | 47 | item.addEventListener('click', (evt) => this.callback(evt, this)); |
| 35 | 48 | } |
| 36 | - item.append(this.label); | |
| 49 | + const icon = document.createElement('div'); { | |
| 50 | + this.domIcon = icon; | |
| 51 | + icon.classList.add('qr--button-icon'); | |
| 52 | + icon.classList.add('fa-solid'); | |
| 53 | + if (!this.icon) icon.classList.add('qr--hidden'); | |
| 54 | + else icon.classList.add(this.icon); | |
| 55 | + item.append(icon); | |
| 56 | + } | |
| 57 | + const lbl = document.createElement('div'); { | |
| 58 | + this.domLabel = lbl; | |
| 59 | + lbl.classList.add('qr--button-label'); | |
| 60 | + if (this.icon && !this.showLabel) lbl.classList.add('qr--hidden'); | |
| 61 | + lbl.textContent = this.label; | |
| 62 | + item.append(lbl); | |
| 63 | + } | |
| 37 | 64 | if (this.childList.length > 0) { |
| 38 | 65 | item.classList.add('ctx-has-children'); |
| 39 | 66 | const sub = new SubMenu(this.childList); |
| @@ -1,3 +1,20 @@ | ||
| 1 | +@keyframes qr--success { | |
| 2 | + 0%, | |
| 3 | + 100% { | |
| 4 | + color: var(--SmartThemeBodyColor); | |
| 5 | + } | |
| 6 | + 25%, | |
| 7 | + 75% { | |
| 8 | + color: #51a351; | |
| 9 | + } | |
| 10 | +} | |
| 11 | +.qr--success { | |
| 12 | + animation-name: qr--success; | |
| 13 | + animation-duration: 3s; | |
| 14 | + animation-timing-function: linear; | |
| 15 | + animation-delay: 0s; | |
| 16 | + animation-iteration-count: 1; | |
| 17 | +} | |
| 1 | 18 | #qr--bar { |
| 2 | 19 | outline: none; |
| 3 | 20 | margin: 0; |
| @@ -41,6 +58,7 @@ | ||
| 41 | 58 | } |
| 42 | 59 | #qr--bar > .qr--buttons, |
| 43 | 60 | #qr--popout > .qr--body > .qr--buttons { |
| 61 | + --qr--color: transparent; | |
| 44 | 62 | margin: 0; |
| 45 | 63 | padding: 0; |
| 46 | 64 | display: flex; |
| @@ -49,10 +67,44 @@ | ||
| 49 | 67 | gap: 5px; |
| 50 | 68 | width: 100%; |
| 51 | 69 | } |
| 70 | +#qr--bar > .qr--buttons.qr--color, | |
| 71 | +#qr--popout > .qr--body > .qr--buttons.qr--color { | |
| 72 | + background-color: var(--qr--color); | |
| 73 | +} | |
| 74 | +#qr--bar > .qr--buttons.qr--borderColor, | |
| 75 | +#qr--popout > .qr--body > .qr--buttons.qr--borderColor { | |
| 76 | + background-color: transparent; | |
| 77 | + border-left: 5px solid var(--qr--color); | |
| 78 | + border-right: 5px solid var(--qr--color); | |
| 79 | +} | |
| 80 | +#qr--bar > .qr--buttons:has(.qr--buttons.qr--color), | |
| 81 | +#qr--popout > .qr--body > .qr--buttons:has(.qr--buttons.qr--color) { | |
| 82 | + margin: 5px; | |
| 83 | +} | |
| 52 | 84 | #qr--bar > .qr--buttons > .qr--buttons, |
| 53 | 85 | #qr--popout > .qr--body > .qr--buttons > .qr--buttons { |
| 54 | 86 | display: contents; |
| 55 | 87 | } |
| 88 | +#qr--bar > .qr--buttons > .qr--buttons.qr--color .qr--button:before, | |
| 89 | +#qr--popout > .qr--body > .qr--buttons > .qr--buttons.qr--color .qr--button:before { | |
| 90 | + content: ''; | |
| 91 | + background-color: var(--qr--color); | |
| 92 | + position: absolute; | |
| 93 | + inset: -5px; | |
| 94 | + z-index: -1; | |
| 95 | +} | |
| 96 | +#qr--bar > .qr--buttons > .qr--buttons.qr--color.qr--borderColor .qr--button:before, | |
| 97 | +#qr--popout > .qr--body > .qr--buttons > .qr--buttons.qr--color.qr--borderColor .qr--button:before { | |
| 98 | + display: none; | |
| 99 | +} | |
| 100 | +#qr--bar > .qr--buttons > .qr--buttons.qr--color.qr--borderColor:before, | |
| 101 | +#qr--popout > .qr--body > .qr--buttons > .qr--buttons.qr--color.qr--borderColor:before, | |
| 102 | +#qr--bar > .qr--buttons > .qr--buttons.qr--color.qr--borderColor:after, | |
| 103 | +#qr--popout > .qr--body > .qr--buttons > .qr--buttons.qr--color.qr--borderColor:after { | |
| 104 | + content: ''; | |
| 105 | + width: 5px; | |
| 106 | + background-color: var(--qr--color); | |
| 107 | +} | |
| 56 | 108 | #qr--bar > .qr--buttons .qr--button, |
| 57 | 109 | #qr--popout > .qr--body > .qr--buttons .qr--button { |
| 58 | 110 | color: var(--SmartThemeBodyColor); |
| @@ -66,11 +118,19 @@ | ||
| 66 | 118 | align-items: center; |
| 67 | 119 | justify-content: center; |
| 68 | 120 | text-align: center; |
| 121 | + position: relative; | |
| 69 | 122 | } |
| 70 | 123 | #qr--bar > .qr--buttons .qr--button:hover, |
| 71 | 124 | #qr--popout > .qr--body > .qr--buttons .qr--button:hover { |
| 72 | 125 | opacitybackground-color: 1#4d4d4d; |
| 73 | - filter: brightness(1.2); | |
| 126 | +} | |
| 127 | +#qr--bar > .qr--buttons .qr--button .qr--hidden, | |
| 128 | +#qr--popout > .qr--body > .qr--buttons .qr--button .qr--hidden { | |
| 129 | + display: none; | |
| 130 | +} | |
| 131 | +#qr--bar > .qr--buttons .qr--button .qr--button-icon, | |
| 132 | +#qr--popout > .qr--body > .qr--buttons .qr--button .qr--button-icon { | |
| 133 | + margin: 0 0.5em; | |
| 74 | 134 | } |
| 75 | 135 | #qr--bar > .qr--buttons .qr--button > .qr--button-expander, |
| 76 | 136 | #qr--popout > .qr--body > .qr--buttons .qr--button > .qr--button-expander { |
| @@ -170,36 +230,80 @@ | ||
| 170 | 230 | #qr--settings #qr--set-qrList .qr--set-qrListContents { |
| 171 | 231 | padding: 0 0.5em; |
| 172 | 232 | } |
| 173 | 233 | #qr--settings #qr--set-qrList .qr--set-qrListContents > .qr--set-item .qr--set-itemAdder { |
| 234 | + display: flex; | |
| 235 | + align-items: center; | |
| 236 | + opacity: 0; | |
| 237 | + transition: 100ms; | |
| 238 | + margin: -2px 0 -11px 0; | |
| 239 | + position: relative; | |
| 240 | +} | |
| 241 | +#qr--settings #qr--set-qrList .qr--set-qrListContents > .qr--set-item .qr--set-itemAdder .qr--actions { | |
| 242 | + display: flex; | |
| 243 | + gap: 0.25em; | |
| 244 | + flex: 0 0 auto; | |
| 245 | +} | |
| 246 | +#qr--settings #qr--set-qrList .qr--set-qrListContents > .qr--set-item .qr--set-itemAdder .qr--actions .qr--action { | |
| 247 | + margin: 0; | |
| 248 | +} | |
| 249 | +#qr--settings #qr--set-qrList .qr--set-qrListContents > .qr--set-item .qr--set-itemAdder:before, | |
| 250 | +#qr--settings #qr--set-qrList .qr--set-qrListContents > .qr--set-item .qr--set-itemAdder:after { | |
| 251 | + content: ""; | |
| 252 | + display: block; | |
| 253 | + flex: 1 1 auto; | |
| 254 | + border: 1px solid; | |
| 255 | + margin: 0 1em; | |
| 256 | + height: 0; | |
| 257 | +} | |
| 258 | +#qr--settings #qr--set-qrList .qr--set-qrListContents > .qr--set-item .qr--set-itemAdder:hover, | |
| 259 | +#qr--settings #qr--set-qrList .qr--set-qrListContents > .qr--set-item .qr--set-itemAdder:focus-within { | |
| 260 | + opacity: 1; | |
| 261 | +} | |
| 262 | +#qr--settings #qr--set-qrList .qr--set-qrListContents > .qr--set-item .qr--content { | |
| 174 | 263 | display: flex; |
| 175 | 264 | flex-direction: row; |
| 176 | 265 | gap: 0.5em; |
| 177 | 266 | align-items: baseline; |
| 178 | 267 | padding: 0.25em 0; |
| 179 | 268 | } |
| 180 | 269 | #qr--settings #qr--set-qrList .qr--set-qrListContents > .qr--set-item .qr--content > :nth-child(12) { |
| 181 | 270 | flex: 0 0 auto; |
| 182 | 271 | } |
| 183 | 272 | #qr--settings #qr--set-qrList .qr--set-qrListContents > .qr--set-item .qr--content > :nth-child(2) { |
| 184 | 273 | flex: 1 1 25%; |
| 185 | 274 | } |
| 186 | 275 | #qr--settings #qr--set-qrList .qr--set-qrListContents > .qr--set-item .qr--content > :nth-child(3) { |
| 187 | 276 | flex: 0 0 auto; |
| 188 | 277 | } |
| 189 | 278 | #qr--settings #qr--set-qrList .qr--set-qrListContents > .qr--set-item .qr--content > :nth-child(4) { |
| 190 | 279 | flex: 1 1 75%; |
| 191 | 280 | } |
| 192 | 281 | #qr--settings #qr--set-qrList .qr--set-qrListContents > .qr--set-item .qr--content > :nth-child(5) { |
| 193 | 282 | flex: 0 01 auto; |
| 283 | + display: flex; | |
| 284 | + gap: 0.25em; | |
| 285 | + justify-content: flex-end; | |
| 286 | + flex-wrap: wrap; | |
| 194 | 287 | } |
| 195 | 288 | #qr--settings #qr--set-qrList .qr--set-qrListContents > .qr--set-item .qr--content > .drag-handle { |
| 196 | 289 | padding: 0.75em; |
| 197 | 290 | } |
| 198 | 291 | #qr--settings #qr--set-qrList .qr--set-qrListContents > .qr--set-item .qr--content .qr--set-itemLabel,itemLabelContainer { |
| 199 | -#qr--settings #qr--set-qrList .qr--set-qrListContents > .qr--set-item .qr--action { | |
| 292 | + display: flex; | |
| 293 | + align-items: center; | |
| 294 | + gap: 0.5em; | |
| 295 | +} | |
| 296 | +#qr--settings #qr--set-qrList .qr--set-qrListContents > .qr--set-item .qr--content .qr--set-itemLabelContainer .qr--set-itemIcon:not(.fa-solid) { | |
| 297 | + display: none; | |
| 298 | +} | |
| 299 | +#qr--settings #qr--set-qrList .qr--set-qrListContents > .qr--set-item .qr--content .qr--set-itemLabelContainer .qr--set-itemLabel { | |
| 300 | + min-width: 24px; | |
| 301 | +} | |
| 302 | +#qr--settings #qr--set-qrList .qr--set-qrListContents > .qr--set-item .qr--content .qr--set-itemLabel, | |
| 303 | +#qr--settings #qr--set-qrList .qr--set-qrListContents > .qr--set-item .qr--content .qr--action { | |
| 200 | 304 | margin: 0; |
| 201 | 305 | } |
| 202 | 306 | #qr--settings #qr--set-qrList .qr--set-qrListContents > .qr--set-item .qr--content .qr--set-itemMessage { |
| 203 | 307 | font-size: smaller; |
| 204 | 308 | } |
| 205 | 309 | #qr--settings .qr--set-qrListActions { |
| @@ -212,6 +316,7 @@ | ||
| 212 | 316 | #qr--qrOptions { |
| 213 | 317 | display: flex; |
| 214 | 318 | flex-direction: column; |
| 319 | + padding-right: 1px; | |
| 215 | 320 | } |
| 216 | 321 | #qr--qrOptions > #qr--ctxEditor .qr--ctxItem { |
| 217 | 322 | display: flex; |
| @@ -219,6 +324,12 @@ | ||
| 219 | 324 | gap: 0.5em; |
| 220 | 325 | align-items: baseline; |
| 221 | 326 | } |
| 327 | +#qr--qrOptions > #qr--autoExec .checkbox_label { | |
| 328 | + text-wrap: nowrap; | |
| 329 | +} | |
| 330 | +#qr--qrOptions > #qr--autoExec .checkbox_label .fa-fw { | |
| 331 | + margin-right: 2px; | |
| 332 | +} | |
| 222 | 333 | @media screen and (max-width: 750px) { |
| 223 | 334 | body .popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor { |
| 224 | 335 | flex-direction: column; |
| @@ -238,6 +349,72 @@ | ||
| 238 | 349 | .popup:has(#qr--modalEditor) { |
| 239 | 350 | aspect-ratio: unset; |
| 240 | 351 | } |
| 352 | +.popup:has(#qr--modalEditor):has(.qr--isExecuting.qr--minimized) { | |
| 353 | + min-width: unset; | |
| 354 | + min-height: unset; | |
| 355 | + height: auto !important; | |
| 356 | + width: min-content !important; | |
| 357 | + position: absolute; | |
| 358 | + right: 1em; | |
| 359 | + top: 1em; | |
| 360 | + left: unset; | |
| 361 | + bottom: unset; | |
| 362 | + margin: unset; | |
| 363 | + padding: 0; | |
| 364 | +} | |
| 365 | +.popup:has(#qr--modalEditor):has(.qr--isExecuting.qr--minimized)::backdrop { | |
| 366 | + backdrop-filter: unset; | |
| 367 | + background-color: transparent; | |
| 368 | +} | |
| 369 | +.popup:has(#qr--modalEditor):has(.qr--isExecuting.qr--minimized) .popup-body { | |
| 370 | + flex: 0 0 auto; | |
| 371 | + height: min-content; | |
| 372 | + width: min-content; | |
| 373 | +} | |
| 374 | +.popup:has(#qr--modalEditor):has(.qr--isExecuting.qr--minimized) .popup-content { | |
| 375 | + flex: 0 0 auto; | |
| 376 | + margin-top: 0; | |
| 377 | +} | |
| 378 | +.popup:has(#qr--modalEditor):has(.qr--isExecuting.qr--minimized) .popup-content > #qr--modalEditor { | |
| 379 | + max-height: 50vh; | |
| 380 | +} | |
| 381 | +.popup:has(#qr--modalEditor):has(.qr--isExecuting.qr--minimized) .popup-content > #qr--modalEditor > #qr--main, | |
| 382 | +.popup:has(#qr--modalEditor):has(.qr--isExecuting.qr--minimized) .popup-content > #qr--modalEditor > #qr--resizeHandle, | |
| 383 | +.popup:has(#qr--modalEditor):has(.qr--isExecuting.qr--minimized) .popup-content > #qr--modalEditor > #qr--qrOptions > h3, | |
| 384 | +.popup:has(#qr--modalEditor):has(.qr--isExecuting.qr--minimized) .popup-content > #qr--modalEditor > #qr--qrOptions > #qr--modal-executeButtons, | |
| 385 | +.popup:has(#qr--modalEditor):has(.qr--isExecuting.qr--minimized) .popup-content > #qr--modalEditor > #qr--qrOptions > #qr--modal-executeProgress { | |
| 386 | + display: none; | |
| 387 | +} | |
| 388 | +.popup:has(#qr--modalEditor):has(.qr--isExecuting.qr--minimized) .popup-content > #qr--modalEditor #qr--qrOptions { | |
| 389 | + width: auto; | |
| 390 | +} | |
| 391 | +.popup:has(#qr--modalEditor):has(.qr--isExecuting.qr--minimized) .popup-content > #qr--modalEditor #qr--modal-debugButtons .qr--modal-debugButton#qr--modal-maximize { | |
| 392 | + display: flex; | |
| 393 | +} | |
| 394 | +.popup:has(#qr--modalEditor):has(.qr--isExecuting.qr--minimized) .popup-content > #qr--modalEditor #qr--modal-debugButtons .qr--modal-debugButton#qr--modal-minimize { | |
| 395 | + display: none; | |
| 396 | +} | |
| 397 | +.popup:has(#qr--modalEditor):has(.qr--isExecuting.qr--minimized) .popup-content > #qr--modalEditor #qr--modal-debugState { | |
| 398 | + padding-top: 0; | |
| 399 | +} | |
| 400 | +.popup:has(#qr--modalEditor):has(.qr--isExecuting) .popup-controls { | |
| 401 | + display: none; | |
| 402 | +} | |
| 403 | +.popup:has(#qr--modalEditor):has(.qr--isExecuting) .qr--highlight { | |
| 404 | + position: absolute; | |
| 405 | + z-index: 50000; | |
| 406 | + pointer-events: none; | |
| 407 | + background-color: rgba(47, 150, 180, 0.5); | |
| 408 | +} | |
| 409 | +.popup:has(#qr--modalEditor):has(.qr--isExecuting) .qr--highlight.qr--unresolved { | |
| 410 | + background-color: rgba(255, 255, 0, 0.5); | |
| 411 | +} | |
| 412 | +.popup:has(#qr--modalEditor):has(.qr--isExecuting) .qr--highlight-secondary { | |
| 413 | + position: absolute; | |
| 414 | + z-index: 50000; | |
| 415 | + pointer-events: none; | |
| 416 | + border: 3px solid red; | |
| 417 | +} | |
| 241 | 418 | .popup:has(#qr--modalEditor) .popup-content { |
| 242 | 419 | display: flex; |
| 243 | 420 | flex-direction: column; |
| @@ -249,6 +426,67 @@ | ||
| 249 | 426 | gap: 1em; |
| 250 | 427 | overflow: hidden; |
| 251 | 428 | } |
| 429 | +.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor.qr--isExecuting #qr--main > h3:first-child, | |
| 430 | +.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor.qr--isExecuting #qr--main > .qr--labels, | |
| 431 | +.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor.qr--isExecuting #qr--main > .qr--modal-messageContainer > .qr--modal-editorSettings, | |
| 432 | +.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor.qr--isExecuting #qr--qrOptions > h3:first-child, | |
| 433 | +.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor.qr--isExecuting #qr--qrOptions > #qr--ctxEditor, | |
| 434 | +.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor.qr--isExecuting #qr--qrOptions > .qr--ctxEditorActions, | |
| 435 | +.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor.qr--isExecuting #qr--qrOptions > .qr--ctxEditorActions + h3, | |
| 436 | +.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor.qr--isExecuting #qr--qrOptions > .qr--ctxEditorActions + h3 + div { | |
| 437 | + display: none; | |
| 438 | +} | |
| 439 | +.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor.qr--isExecuting #qr--main > .qr--modal-messageContainer > #qr--modal-messageHolder > #qr--modal-message { | |
| 440 | + visibility: hidden; | |
| 441 | +} | |
| 442 | +.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor.qr--isExecuting #qr--modal-debugButtons { | |
| 443 | + display: flex; | |
| 444 | +} | |
| 445 | +.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor.qr--isExecuting #qr--modal-debugButtons .menu_button:not(#qr--modal-minimize, #qr--modal-maximize) { | |
| 446 | + cursor: not-allowed; | |
| 447 | + opacity: 0.5; | |
| 448 | + pointer-events: none; | |
| 449 | + transition: 200ms; | |
| 450 | + border-color: transparent; | |
| 451 | +} | |
| 452 | +.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor.qr--isExecuting.qr--isPaused #qr--modal-debugButtons .menu_button:not(#qr--modal-minimize, #qr--modal-maximize) { | |
| 453 | + cursor: pointer; | |
| 454 | + opacity: 1; | |
| 455 | + pointer-events: all; | |
| 456 | +} | |
| 457 | +.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor.qr--isExecuting.qr--isPaused #qr--modal-debugButtons .menu_button:not(#qr--modal-minimize, #qr--modal-maximize)#qr--modal-resume { | |
| 458 | + animation-name: qr--debugPulse; | |
| 459 | + animation-duration: 1500ms; | |
| 460 | + animation-timing-function: ease-in-out; | |
| 461 | + animation-delay: 0s; | |
| 462 | + animation-iteration-count: infinite; | |
| 463 | +} | |
| 464 | +.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor.qr--isExecuting.qr--isPaused #qr--modal-debugButtons .menu_button:not(#qr--modal-minimize, #qr--modal-maximize)#qr--modal-resume { | |
| 465 | + border-color: #51a351; | |
| 466 | +} | |
| 467 | +.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor.qr--isExecuting.qr--isPaused #qr--modal-debugButtons .menu_button:not(#qr--modal-minimize, #qr--modal-maximize)#qr--modal-step { | |
| 468 | + border-color: var(--SmartThemeQuoteColor); | |
| 469 | +} | |
| 470 | +.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor.qr--isExecuting.qr--isPaused #qr--modal-debugButtons .menu_button:not(#qr--modal-minimize, #qr--modal-maximize)#qr--modal-stepInto { | |
| 471 | + border-color: var(--SmartThemeQuoteColor); | |
| 472 | +} | |
| 473 | +.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor.qr--isExecuting.qr--isPaused #qr--modal-debugButtons .menu_button:not(#qr--modal-minimize, #qr--modal-maximize)#qr--modal-stepOut { | |
| 474 | + border-color: var(--SmartThemeQuoteColor); | |
| 475 | +} | |
| 476 | +.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor.qr--isExecuting #qr--resizeHandle { | |
| 477 | + width: 6px; | |
| 478 | + background-color: var(--SmartThemeBorderColor); | |
| 479 | + border: 2px solid var(--SmartThemeBlurTintColor); | |
| 480 | + transition: border-color 200ms, background-color 200ms; | |
| 481 | + cursor: w-resize; | |
| 482 | +} | |
| 483 | +.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor.qr--isExecuting #qr--resizeHandle:hover { | |
| 484 | + background-color: var(--SmartThemeQuoteColor); | |
| 485 | + border-color: var(--SmartThemeQuoteColor); | |
| 486 | +} | |
| 487 | +.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor.qr--isExecuting #qr--qrOptions { | |
| 488 | + width: var(--width, auto); | |
| 489 | +} | |
| 252 | 490 | .popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main { |
| 253 | 491 | flex: 1 1 auto; |
| 254 | 492 | display: flex; |
| @@ -260,21 +498,115 @@ | ||
| 260 | 498 | display: flex; |
| 261 | 499 | flex-direction: row; |
| 262 | 500 | gap: 0.5em; |
| 501 | + padding: 1px; | |
| 263 | 502 | } |
| 264 | 503 | .popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--labels > label {, |
| 504 | +.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--labels > .label { | |
| 265 | 505 | flex: 1 1 1px; |
| 266 | 506 | display: flex; |
| 267 | 507 | flex-direction: column; |
| 508 | + position: relative; | |
| 509 | +} | |
| 510 | +.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--labels > label.qr--fit, | |
| 511 | +.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--labels > .label.qr--fit { | |
| 512 | + flex: 0 0 auto; | |
| 513 | + justify-content: center; | |
| 514 | +} | |
| 515 | +.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--labels > label .qr--inputGroup, | |
| 516 | +.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--labels > .label .qr--inputGroup { | |
| 517 | + display: flex; | |
| 518 | + align-items: baseline; | |
| 519 | + gap: 0.5em; | |
| 520 | +} | |
| 521 | +.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--labels > label .qr--inputGroup input, | |
| 522 | +.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--labels > .label .qr--inputGroup input { | |
| 523 | + flex: 1 1 auto; | |
| 268 | 524 | } |
| 269 | 525 | .popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--labels > label > .qr--labelText {, |
| 526 | +.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--labels > .label .qr--labelText { | |
| 270 | 527 | flex: 1 1 auto; |
| 271 | 528 | } |
| 272 | 529 | .popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--labels > label > .qr--labelHint {, |
| 530 | +.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--labels > .label .qr--labelHint { | |
| 273 | 531 | flex: 1 1 auto; |
| 274 | 532 | } |
| 275 | 533 | .popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--labels > label > input {, |
| 534 | +.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--labels > .label input { | |
| 276 | 535 | flex: 0 0 auto; |
| 277 | 536 | } |
| 537 | +.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--labels > label .qr--modal-switcherList, | |
| 538 | +.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--labels > .label .qr--modal-switcherList { | |
| 539 | + background-color: var(--stcdx--bgColor); | |
| 540 | + border: 1px solid var(--SmartThemeBorderColor); | |
| 541 | + backdrop-filter: blur(var(--SmartThemeBlurStrength)); | |
| 542 | + border-radius: 10px; | |
| 543 | + font-size: smaller; | |
| 544 | + position: absolute; | |
| 545 | + top: 100%; | |
| 546 | + left: 0; | |
| 547 | + right: 0; | |
| 548 | + overflow: auto; | |
| 549 | + margin: 0; | |
| 550 | + padding: 0.5em; | |
| 551 | + max-height: 50vh; | |
| 552 | + list-style: none; | |
| 553 | + z-index: 40000; | |
| 554 | + max-width: 100%; | |
| 555 | +} | |
| 556 | +.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--labels > label .qr--modal-switcherList .qr--modal-switcherItem, | |
| 557 | +.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--labels > .label .qr--modal-switcherList .qr--modal-switcherItem { | |
| 558 | + display: flex; | |
| 559 | + gap: 1em; | |
| 560 | + text-align: left; | |
| 561 | + opacity: 0.75; | |
| 562 | + transition: 200ms; | |
| 563 | + cursor: pointer; | |
| 564 | +} | |
| 565 | +.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--labels > label .qr--modal-switcherList .qr--modal-switcherItem:hover, | |
| 566 | +.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--labels > .label .qr--modal-switcherList .qr--modal-switcherItem:hover { | |
| 567 | + opacity: 1; | |
| 568 | +} | |
| 569 | +.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--labels > label .qr--modal-switcherList .qr--modal-switcherItem.qr--current, | |
| 570 | +.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--labels > .label .qr--modal-switcherList .qr--modal-switcherItem.qr--current { | |
| 571 | + opacity: 1; | |
| 572 | +} | |
| 573 | +.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--labels > label .qr--modal-switcherList .qr--modal-switcherItem.qr--current .qr--label, | |
| 574 | +.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--labels > .label .qr--modal-switcherList .qr--modal-switcherItem.qr--current .qr--label, | |
| 575 | +.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--labels > label .qr--modal-switcherList .qr--modal-switcherItem.qr--current .qr--id, | |
| 576 | +.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--labels > .label .qr--modal-switcherList .qr--modal-switcherItem.qr--current .qr--id { | |
| 577 | + font-weight: bold; | |
| 578 | +} | |
| 579 | +.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--labels > label .qr--modal-switcherList .qr--label, | |
| 580 | +.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--labels > .label .qr--modal-switcherList .qr--label { | |
| 581 | + white-space: nowrap; | |
| 582 | +} | |
| 583 | +.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--labels > label .qr--modal-switcherList .qr--label .menu_button, | |
| 584 | +.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--labels > .label .qr--modal-switcherList .qr--label .menu_button { | |
| 585 | + display: inline-block; | |
| 586 | + height: min-content; | |
| 587 | + width: min-content; | |
| 588 | + margin: 0 0.5em 0 0; | |
| 589 | +} | |
| 590 | +.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--labels > label .qr--modal-switcherList .qr--id, | |
| 591 | +.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--labels > .label .qr--modal-switcherList .qr--id { | |
| 592 | + opacity: 0.5; | |
| 593 | +} | |
| 594 | +.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--labels > label .qr--modal-switcherList .qr--id:before, | |
| 595 | +.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--labels > .label .qr--modal-switcherList .qr--id:before { | |
| 596 | + content: "["; | |
| 597 | +} | |
| 598 | +.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--labels > label .qr--modal-switcherList .qr--id:after, | |
| 599 | +.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--labels > .label .qr--modal-switcherList .qr--id:after { | |
| 600 | + content: "]"; | |
| 601 | +} | |
| 602 | +.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--labels > label .qr--modal-switcherList .qr--message, | |
| 603 | +.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--labels > .label .qr--modal-switcherList .qr--message { | |
| 604 | + height: 1lh; | |
| 605 | + overflow: hidden; | |
| 606 | + text-overflow: ellipsis; | |
| 607 | + white-space: nowrap; | |
| 608 | + opacity: 0.5; | |
| 609 | +} | |
| 278 | 610 | .popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--modal-messageContainer { |
| 279 | 611 | flex: 1 1 auto; |
| 280 | 612 | display: flex; |
| @@ -283,8 +615,9 @@ | ||
| 283 | 615 | } |
| 284 | 616 | .popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--modal-messageContainer > .qr--modal-editorSettings { |
| 285 | 617 | display: flex; |
| 618 | + flex-wrap: wrap; | |
| 286 | 619 | flex-direction: row; |
| 287 | 620 | column-gap: 1em; |
| 288 | 621 | color: var(--grey70); |
| 289 | 622 | font-size: smaller; |
| 290 | 623 | align-items: baseline; |
| @@ -308,6 +641,11 @@ | ||
| 308 | 641 | background-color: var(--ac-style-color-background); |
| 309 | 642 | color: var(--ac-style-color-text); |
| 310 | 643 | } |
| 644 | +.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--modal-messageContainer > #qr--modal-messageHolder.qr--noSyntax > #qr--modal-message::-webkit-scrollbar, | |
| 645 | +.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--modal-messageContainer > #qr--modal-messageHolder.qr--noSyntax > #qr--modal-message::-webkit-scrollbar-thumb { | |
| 646 | + visibility: visible; | |
| 647 | + cursor: unset; | |
| 648 | +} | |
| 311 | 649 | .popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--modal-messageContainer > #qr--modal-messageHolder.qr--noSyntax > #qr--modal-message::selection { |
| 312 | 650 | color: unset; |
| 313 | 651 | background-color: rgba(108 171 251 / 0.25); |
| @@ -357,11 +695,15 @@ | ||
| 357 | 695 | font-family: var(--monoFontFamily); |
| 358 | 696 | padding: 0.75em; |
| 359 | 697 | margin: 0; |
| 360 | - border: none; | |
| 361 | 698 | resize: none; |
| 362 | 699 | line-height: 1.2; |
| 363 | 700 | border: 1px solid var(--SmartThemeBorderColor); |
| 364 | 701 | border-radius: 5px; |
| 702 | + position: relative; | |
| 703 | +} | |
| 704 | +.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-icon { | |
| 705 | + height: 100%; | |
| 706 | + aspect-ratio: 1 / 1; | |
| 365 | 707 | } |
| 366 | 708 | .popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-executeButtons { |
| 367 | 709 | display: flex; |
| @@ -410,6 +752,46 @@ | ||
| 410 | 752 | .popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-executeButtons #qr--modal-stop { |
| 411 | 753 | border-color: #d78872; |
| 412 | 754 | } |
| 755 | +.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugButtons { | |
| 756 | + display: none; | |
| 757 | + gap: 1em; | |
| 758 | +} | |
| 759 | +.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugButtons .qr--modal-debugButton { | |
| 760 | + aspect-ratio: 1.25 / 1; | |
| 761 | + width: 2.25em; | |
| 762 | + position: relative; | |
| 763 | +} | |
| 764 | +.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugButtons .qr--modal-debugButton:not(.fa-solid) { | |
| 765 | + border-width: 1px; | |
| 766 | + border-style: solid; | |
| 767 | +} | |
| 768 | +.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugButtons .qr--modal-debugButton:not(.fa-solid):after { | |
| 769 | + content: ''; | |
| 770 | + position: absolute; | |
| 771 | + inset: 3px; | |
| 772 | + background-color: var(--SmartThemeBodyColor); | |
| 773 | + mask-size: contain; | |
| 774 | + mask-position: center; | |
| 775 | + mask-repeat: no-repeat; | |
| 776 | +} | |
| 777 | +.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugButtons .qr--modal-debugButton#qr--modal-resume:after { | |
| 778 | + mask-image: url('/img/step-resume.svg'); | |
| 779 | +} | |
| 780 | +.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugButtons .qr--modal-debugButton#qr--modal-step:after { | |
| 781 | + mask-image: url('/img/step-over.svg'); | |
| 782 | +} | |
| 783 | +.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugButtons .qr--modal-debugButton#qr--modal-stepInto:after { | |
| 784 | + mask-image: url('/img/step-into.svg'); | |
| 785 | +} | |
| 786 | +.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugButtons .qr--modal-debugButton#qr--modal-stepOut:after { | |
| 787 | + mask-image: url('/img/step-out.svg'); | |
| 788 | +} | |
| 789 | +.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugButtons .qr--modal-debugButton#qr--modal-maximize { | |
| 790 | + display: none; | |
| 791 | +} | |
| 792 | +.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-send_textarea { | |
| 793 | + flex: 0 0 auto; | |
| 794 | +} | |
| 413 | 795 | .popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-executeProgress { |
| 414 | 796 | --prog: 0; |
| 415 | 797 | --progColor: #92befc; |
| @@ -417,6 +799,7 @@ | ||
| 417 | 799 | --progSuccessColor: #51a351; |
| 418 | 800 | --progErrorColor: #bd362f; |
| 419 | 801 | --progAbortedColor: #d78872; |
| 802 | + flex: 0 0 auto; | |
| 420 | 803 | height: 0.5em; |
| 421 | 804 | background-color: var(--black50a); |
| 422 | 805 | position: relative; |
| @@ -469,6 +852,7 @@ | ||
| 469 | 852 | overflow: auto; |
| 470 | 853 | min-width: 100%; |
| 471 | 854 | width: 0; |
| 855 | + white-space: pre-wrap; | |
| 472 | 856 | } |
| 473 | 857 | .popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-executeResult.qr--hasResult { |
| 474 | 858 | display: block; |
| @@ -476,6 +860,150 @@ | ||
| 476 | 860 | .popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-executeResult:before { |
| 477 | 861 | content: 'Result: '; |
| 478 | 862 | } |
| 863 | +.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState { | |
| 864 | + display: none; | |
| 865 | + text-align: left; | |
| 866 | + font-size: smaller; | |
| 867 | + font-family: var(--monoFontFamily); | |
| 868 | + color: white; | |
| 869 | + padding: 0.5em 0; | |
| 870 | + overflow: auto; | |
| 871 | + min-width: 100%; | |
| 872 | + width: 0; | |
| 873 | + white-space: pre-wrap; | |
| 874 | +} | |
| 875 | +.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState.qr--active { | |
| 876 | + display: block; | |
| 877 | +} | |
| 878 | +.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope { | |
| 879 | + display: grid; | |
| 880 | + grid-template-columns: 0fr 1fr 1fr; | |
| 881 | + column-gap: 0em; | |
| 882 | +} | |
| 883 | +.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--title { | |
| 884 | + grid-column: 1 / 4; | |
| 885 | + font-weight: bold; | |
| 886 | + font-family: var(--mainFontFamily); | |
| 887 | + background-color: var(--black50a); | |
| 888 | + padding: 0.25em; | |
| 889 | + margin-top: 0.5em; | |
| 890 | +} | |
| 891 | +.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--var, | |
| 892 | +.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--macro, | |
| 893 | +.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--pipe { | |
| 894 | + display: contents; | |
| 895 | +} | |
| 896 | +.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--var:nth-child(2n + 1) .qr--key, | |
| 897 | +.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--macro:nth-child(2n + 1) .qr--key, | |
| 898 | +.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--pipe:nth-child(2n + 1) .qr--key, | |
| 899 | +.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--var:nth-child(2n + 1) .qr--val, | |
| 900 | +.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--macro:nth-child(2n + 1) .qr--val, | |
| 901 | +.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--pipe:nth-child(2n + 1) .qr--val { | |
| 902 | + background-color: rgb(from var(--SmartThemeEmColor) r g b / 0.25); | |
| 903 | +} | |
| 904 | +.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--var:nth-child(2n + 1) .qr--val:nth-child(2n), | |
| 905 | +.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--macro:nth-child(2n + 1) .qr--val:nth-child(2n), | |
| 906 | +.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--pipe:nth-child(2n + 1) .qr--val:nth-child(2n) { | |
| 907 | + background-color: rgb(from var(--SmartThemeEmColor) r g b / 0.125); | |
| 908 | +} | |
| 909 | +.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--var:nth-child(2n + 1) .qr--val:hover, | |
| 910 | +.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--macro:nth-child(2n + 1) .qr--val:hover, | |
| 911 | +.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--pipe:nth-child(2n + 1) .qr--val:hover { | |
| 912 | + background-color: rgb(from var(--SmartThemeEmColor) r g b / 0.5); | |
| 913 | +} | |
| 914 | +.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--var:nth-child(2n) .qr--val:nth-child(2n), | |
| 915 | +.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--macro:nth-child(2n) .qr--val:nth-child(2n), | |
| 916 | +.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--pipe:nth-child(2n) .qr--val:nth-child(2n) { | |
| 917 | + background-color: rgb(from var(--SmartThemeEmColor) r g b / 0.0625); | |
| 918 | +} | |
| 919 | +.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--var:nth-child(2n) .qr--val:hover, | |
| 920 | +.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--macro:nth-child(2n) .qr--val:hover, | |
| 921 | +.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--pipe:nth-child(2n) .qr--val:hover { | |
| 922 | + background-color: rgb(from var(--SmartThemeEmColor) r g b / 0.5); | |
| 923 | +} | |
| 924 | +.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--var.qr--isHidden .qr--key, | |
| 925 | +.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--macro.qr--isHidden .qr--key, | |
| 926 | +.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--pipe.qr--isHidden .qr--key, | |
| 927 | +.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--var.qr--isHidden .qr--val, | |
| 928 | +.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--macro.qr--isHidden .qr--val, | |
| 929 | +.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--pipe.qr--isHidden .qr--val { | |
| 930 | + opacity: 0.5; | |
| 931 | +} | |
| 932 | +.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--var .qr--val, | |
| 933 | +.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--macro .qr--val, | |
| 934 | +.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--pipe .qr--val { | |
| 935 | + grid-column: 2 / 4; | |
| 936 | +} | |
| 937 | +.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--var .qr--val.qr--singleCol, | |
| 938 | +.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--macro .qr--val.qr--singleCol, | |
| 939 | +.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--pipe .qr--val.qr--singleCol { | |
| 940 | + grid-column: unset; | |
| 941 | +} | |
| 942 | +.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--var .qr--val.qr--simple:before, | |
| 943 | +.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--macro .qr--val.qr--simple:before, | |
| 944 | +.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--pipe .qr--val.qr--simple:before, | |
| 945 | +.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--var .qr--val.qr--simple:after, | |
| 946 | +.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--macro .qr--val.qr--simple:after, | |
| 947 | +.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--pipe .qr--val.qr--simple:after { | |
| 948 | + content: '"'; | |
| 949 | + color: var(--SmartThemeQuoteColor); | |
| 950 | +} | |
| 951 | +.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--var .qr--val.qr--unresolved:after, | |
| 952 | +.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--macro .qr--val.qr--unresolved:after, | |
| 953 | +.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--pipe .qr--val.qr--unresolved:after { | |
| 954 | + content: '-UNRESOLVED-'; | |
| 955 | + font-style: italic; | |
| 956 | + color: var(--SmartThemeQuoteColor); | |
| 957 | +} | |
| 958 | +.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--key { | |
| 959 | + margin-left: 0.5em; | |
| 960 | + padding-right: 1em; | |
| 961 | +} | |
| 962 | +.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--key:after { | |
| 963 | + content: ": "; | |
| 964 | +} | |
| 965 | +.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--pipe > .qr--key:before, | |
| 966 | +.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--macro > .qr--key:before { | |
| 967 | + content: "{{"; | |
| 968 | +} | |
| 969 | +.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--pipe > .qr--key:after, | |
| 970 | +.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--macro > .qr--key:after { | |
| 971 | + content: "}}: "; | |
| 972 | +} | |
| 973 | +.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--scope { | |
| 974 | + display: contents; | |
| 975 | +} | |
| 976 | +.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--scope .qr--pipe .qr--key, | |
| 977 | +.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--scope .qr--pipe .qr--val { | |
| 978 | + opacity: 0.5; | |
| 979 | +} | |
| 980 | +.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--stack { | |
| 981 | + display: grid; | |
| 982 | + grid-template-columns: 1fr 0fr; | |
| 983 | +} | |
| 984 | +.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--stack .qr--title { | |
| 985 | + grid-column: 1 / 3; | |
| 986 | + font-weight: bold; | |
| 987 | + font-family: var(--mainFontFamily); | |
| 988 | + background-color: var(--black50a); | |
| 989 | + padding: 0.25em; | |
| 990 | + margin-top: 1em; | |
| 991 | +} | |
| 992 | +.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--stack .qr--item { | |
| 993 | + display: contents; | |
| 994 | +} | |
| 995 | +.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--stack .qr--item:nth-child(2n + 1) .qr--name, | |
| 996 | +.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--stack .qr--item:nth-child(2n + 1) .qr--source { | |
| 997 | + background-color: rgb(from var(--SmartThemeEmColor) r g b / 0.25); | |
| 998 | +} | |
| 999 | +.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--stack .qr--item .qr--name { | |
| 1000 | + margin-left: 0.5em; | |
| 1001 | +} | |
| 1002 | +.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--stack .qr--item .qr--source { | |
| 1003 | + opacity: 0.5; | |
| 1004 | + text-align: right; | |
| 1005 | + white-space: nowrap; | |
| 1006 | +} | |
| 479 | 1007 | @keyframes qr--progressPulse { |
| 480 | 1008 | 0%, |
| 481 | 1009 | 100% { |
| @@ -485,9 +1013,63 @@ | ||
| 485 | 1013 | background-color: var(--progFlashColor); |
| 486 | 1014 | } |
| 487 | 1015 | } |
| 1016 | +@keyframes qr--debugPulse { | |
| 1017 | + 0%, | |
| 1018 | + 100% { | |
| 1019 | + border-color: #51a351; | |
| 1020 | + } | |
| 1021 | + 50% { | |
| 1022 | + border-color: #92befc; | |
| 1023 | + } | |
| 1024 | +} | |
| 488 | 1025 | .popup.qr--hide { |
| 489 | 1026 | opacity: 0 !important; |
| 490 | 1027 | } |
| 491 | 1028 | .popup.qr--hide::backdrop { |
| 492 | 1029 | opacity: 0 !important; |
| 493 | 1030 | } |
| 1031 | +.popup.qr--hide::backdrop { | |
| 1032 | + opacity: 0 !important; | |
| 1033 | +} | |
| 1034 | +.popup:has(.qr--transferModal) .popup-button-ok { | |
| 1035 | + display: flex; | |
| 1036 | + align-items: center; | |
| 1037 | + flex-direction: column; | |
| 1038 | + white-space: pre; | |
| 1039 | + font-weight: normal; | |
| 1040 | + box-shadow: 0 0 0; | |
| 1041 | + transition: 200ms; | |
| 1042 | +} | |
| 1043 | +.popup:has(.qr--transferModal) .popup-button-ok:after { | |
| 1044 | + content: 'Transfer'; | |
| 1045 | + height: 0; | |
| 1046 | + overflow: hidden; | |
| 1047 | + font-weight: bold; | |
| 1048 | +} | |
| 1049 | +.popup:has(.qr--transferModal) .qr--copy { | |
| 1050 | + display: flex; | |
| 1051 | + align-items: center; | |
| 1052 | + flex-direction: column; | |
| 1053 | + white-space: pre; | |
| 1054 | + font-weight: normal; | |
| 1055 | + box-shadow: 0 0 0; | |
| 1056 | + transition: 200ms; | |
| 1057 | +} | |
| 1058 | +.popup:has(.qr--transferModal) .qr--copy:after { | |
| 1059 | + content: 'Copy'; | |
| 1060 | + height: 0; | |
| 1061 | + overflow: hidden; | |
| 1062 | + font-weight: bold; | |
| 1063 | +} | |
| 1064 | +.popup:has(.qr--transferModal):has(.qr--transferSelect:focus) .popup-button-ok { | |
| 1065 | + font-weight: bold; | |
| 1066 | + box-shadow: 0 0 10px; | |
| 1067 | +} | |
| 1068 | +.popup:has(.qr--transferModal):has(.qr--transferSelect:focus).qr--isCopy .popup-button-ok { | |
| 1069 | + font-weight: normal; | |
| 1070 | + box-shadow: 0 0 0; | |
| 1071 | +} | |
| 1072 | +.popup:has(.qr--transferModal):has(.qr--transferSelect:focus).qr--isCopy .qr--copy { | |
| 1073 | + font-weight: bold; | |
| 1074 | + box-shadow: 0 0 10px; | |
| 1075 | +} | |
| @@ -1,3 +1,18 @@ | ||
| 1 | +@keyframes qr--success { | |
| 2 | + 0%, 100% { | |
| 3 | + color: var(--SmartThemeBodyColor); | |
| 4 | + } | |
| 5 | + 25%, 75% { | |
| 6 | + color: rgb(81, 163, 81); | |
| 7 | + } | |
| 8 | +} | |
| 9 | +&.qr--success { | |
| 10 | + animation-name: qr--success; | |
| 11 | + animation-duration: 3s; | |
| 12 | + animation-timing-function: linear; | |
| 13 | + animation-delay: 0s; | |
| 14 | + animation-iteration-count: 1; | |
| 15 | +} | |
| 1 | 16 | #qr--bar { |
| 2 | 17 | outline: none; |
| 3 | 18 | margin: 0; |
| @@ -50,6 +65,18 @@ | ||
| 50 | 65 | #qr--bar, |
| 51 | 66 | #qr--popout>.qr--body { |
| 52 | 67 | >.qr--buttons { |
| 68 | + --qr--color: transparent; | |
| 69 | + &.qr--color { | |
| 70 | + background-color: var(--qr--color); | |
| 71 | + } | |
| 72 | + &.qr--borderColor { | |
| 73 | + background-color: transparent; | |
| 74 | + border-left: 5px solid var(--qr--color); | |
| 75 | + border-right: 5px solid var(--qr--color); | |
| 76 | + } | |
| 77 | + &:has(.qr--buttons.qr--color) { | |
| 78 | + margin: 5px; | |
| 79 | + } | |
| 53 | 80 | margin: 0; |
| 54 | 81 | padding: 0; |
| 55 | 82 | display: flex; |
| @@ -60,6 +87,25 @@ | ||
| 60 | 87 | |
| 61 | 88 | >.qr--buttons { |
| 62 | 89 | display: contents; |
| 90 | + &.qr--color { | |
| 91 | + .qr--button:before { | |
| 92 | + content: ''; | |
| 93 | + background-color: var(--qr--color); | |
| 94 | + position: absolute; | |
| 95 | + inset: -5px; | |
| 96 | + z-index: -1; | |
| 97 | + } | |
| 98 | + &.qr--borderColor { | |
| 99 | + .qr--button:before { | |
| 100 | + display: none; | |
| 101 | + } | |
| 102 | + &:before, &:after { | |
| 103 | + content: ''; | |
| 104 | + width: 5px; | |
| 105 | + background-color: var(--qr--color); | |
| 106 | + } | |
| 107 | + } | |
| 108 | + } | |
| 63 | 109 | } |
| 64 | 110 | |
| 65 | 111 | .qr--button { |
| @@ -75,10 +121,17 @@ | ||
| 75 | 121 | align-items: center; |
| 76 | 122 | justify-content: center; |
| 77 | 123 | text-align: center; |
| 124 | + position: relative; | |
| 78 | 125 | |
| 79 | 126 | &:hover { |
| 80 | - opacity: 1; | |
| 127 | + background-color: rgb(30% 30% 30%); | |
| 81 | - filter: brightness(1.2); | |
| 128 | + } | |
| 129 | + | |
| 130 | + .qr--hidden { | |
| 131 | + display: none; | |
| 132 | + } | |
| 133 | + .qr--button-icon { | |
| 134 | + margin: 0 0.5em; | |
| 82 | 135 | } |
| 83 | 136 | |
| 84 | 137 | >.qr--button-expander { |
| @@ -211,14 +264,41 @@ | ||
| 211 | 264 | .qr--set-qrListContents> { |
| 212 | 265 | padding: 0 0.5em; |
| 213 | 266 | |
| 214 | 267 | >.qr--set-item .qr--set-itemAdder { |
| 268 | + display: flex; | |
| 269 | + align-items: center; | |
| 270 | + opacity: 0; | |
| 271 | + transition: 100ms; | |
| 272 | + margin: -2px 0 -11px 0; | |
| 273 | + position: relative; | |
| 274 | + .qr--actions { | |
| 275 | + display: flex; | |
| 276 | + gap: 0.25em; | |
| 277 | + flex: 0 0 auto; | |
| 278 | + .qr--action { | |
| 279 | + margin: 0; | |
| 280 | + } | |
| 281 | + } | |
| 282 | + &:before, &:after { | |
| 283 | + content: ""; | |
| 284 | + display: block; | |
| 285 | + flex: 1 1 auto; | |
| 286 | + border: 1px solid; | |
| 287 | + margin: 0 1em; | |
| 288 | + height: 0; | |
| 289 | + } | |
| 290 | + &:hover, &:focus-within { | |
| 291 | + opacity: 1; | |
| 292 | + } | |
| 293 | + } | |
| 294 | + >.qr--set-item .qr--content { | |
| 215 | 295 | display: flex; |
| 216 | 296 | flex-direction: row; |
| 217 | 297 | gap: 0.5em; |
| 218 | 298 | align-items: baseline; |
| 219 | 299 | padding: 0.25em 0; |
| 220 | 300 | |
| 221 | 301 | > :nth-child(12) { |
| 222 | 302 | flex: 0 0 auto; |
| 223 | 303 | } |
| 224 | 304 | |
| @@ -235,13 +315,29 @@ | ||
| 235 | 315 | } |
| 236 | 316 | |
| 237 | 317 | > :nth-child(5) { |
| 238 | 318 | flex: 0 01 auto; |
| 319 | + display: flex; | |
| 320 | + gap: 0.25em; | |
| 321 | + justify-content: flex-end; | |
| 322 | + flex-wrap: wrap; | |
| 239 | 323 | } |
| 240 | 324 | |
| 241 | 325 | >.drag-handle { |
| 242 | 326 | padding: 0.75em; |
| 243 | 327 | } |
| 244 | 328 | |
| 329 | + .qr--set-itemLabelContainer { | |
| 330 | + display: flex; | |
| 331 | + align-items: center; | |
| 332 | + gap: 0.5em; | |
| 333 | + .qr--set-itemIcon:not(.fa-solid) { | |
| 334 | + display: none; | |
| 335 | + } | |
| 336 | + .qr--set-itemLabel { | |
| 337 | + min-width: 24px; | |
| 338 | + } | |
| 339 | + } | |
| 340 | + | |
| 245 | 341 | .qr--set-itemLabel, |
| 246 | 342 | .qr--action { |
| 247 | 343 | margin: 0; |
| @@ -251,6 +347,8 @@ | ||
| 251 | 347 | font-size: smaller; |
| 252 | 348 | } |
| 253 | 349 | } |
| 350 | + | |
| 351 | + | |
| 254 | 352 | } |
| 255 | 353 | } |
| 256 | 354 | |
| @@ -270,6 +368,7 @@ | ||
| 270 | 368 | #qr--qrOptions { |
| 271 | 369 | display: flex; |
| 272 | 370 | flex-direction: column; |
| 371 | + padding-right: 1px; | |
| 273 | 372 | |
| 274 | 373 | >#qr--ctxEditor { |
| 275 | 374 | .qr--ctxItem { |
| @@ -279,6 +378,15 @@ | ||
| 279 | 378 | align-items: baseline; |
| 280 | 379 | } |
| 281 | 380 | } |
| 381 | + >#qr--autoExec { | |
| 382 | + .checkbox_label { | |
| 383 | + text-wrap: nowrap; | |
| 384 | + | |
| 385 | + .fa-fw { | |
| 386 | + margin-right: 2px; | |
| 387 | + } | |
| 388 | + } | |
| 389 | + } | |
| 282 | 390 | } |
| 283 | 391 | |
| 284 | 392 | |
| @@ -306,6 +414,78 @@ | ||
| 306 | 414 | .popup:has(#qr--modalEditor) { |
| 307 | 415 | aspect-ratio: unset; |
| 308 | 416 | |
| 417 | + &:has(.qr--isExecuting.qr--minimized) { | |
| 418 | + min-width: unset; | |
| 419 | + min-height: unset; | |
| 420 | + height: auto !important; | |
| 421 | + width: min-content !important; | |
| 422 | + position: absolute; | |
| 423 | + right: 1em; | |
| 424 | + top: 1em; | |
| 425 | + left: unset; | |
| 426 | + bottom: unset; | |
| 427 | + margin: unset; | |
| 428 | + padding: 0; | |
| 429 | + &::backdrop { | |
| 430 | + backdrop-filter: unset; | |
| 431 | + background-color: transparent; | |
| 432 | + } | |
| 433 | + .popup-body { | |
| 434 | + flex: 0 0 auto; | |
| 435 | + height: min-content; | |
| 436 | + width: min-content; | |
| 437 | + } | |
| 438 | + .popup-content { | |
| 439 | + flex: 0 0 auto; | |
| 440 | + margin-top: 0; | |
| 441 | + | |
| 442 | + > #qr--modalEditor { | |
| 443 | + max-height: 50vh; | |
| 444 | + > #qr--main, | |
| 445 | + > #qr--resizeHandle, | |
| 446 | + > #qr--qrOptions > h3, | |
| 447 | + > #qr--qrOptions > #qr--modal-executeButtons, | |
| 448 | + > #qr--qrOptions > #qr--modal-executeProgress | |
| 449 | + { | |
| 450 | + display: none; | |
| 451 | + } | |
| 452 | + #qr--qrOptions { | |
| 453 | + width: auto; | |
| 454 | + } | |
| 455 | + #qr--modal-debugButtons .qr--modal-debugButton#qr--modal-maximize { | |
| 456 | + display: flex; | |
| 457 | + } | |
| 458 | + #qr--modal-debugButtons .qr--modal-debugButton#qr--modal-minimize { | |
| 459 | + display: none; | |
| 460 | + } | |
| 461 | + #qr--modal-debugState { | |
| 462 | + padding-top: 0; | |
| 463 | + } | |
| 464 | + } | |
| 465 | + } | |
| 466 | + } | |
| 467 | + &:has(.qr--isExecuting) { | |
| 468 | + .popup-controls { | |
| 469 | + display: none; | |
| 470 | + } | |
| 471 | + | |
| 472 | + .qr--highlight { | |
| 473 | + position: absolute; | |
| 474 | + z-index: 50000; | |
| 475 | + pointer-events: none; | |
| 476 | + background-color: rgb(47 150 180 / 0.5); | |
| 477 | + &.qr--unresolved { | |
| 478 | + background-color: rgb(255 255 0 / 0.5); | |
| 479 | + } | |
| 480 | + } | |
| 481 | + .qr--highlight-secondary { | |
| 482 | + position: absolute; | |
| 483 | + z-index: 50000; | |
| 484 | + pointer-events: none; | |
| 485 | + border: 3px solid red; | |
| 486 | + } | |
| 487 | + } | |
| 488 | + | |
| 309 | 489 | .popup-content { |
| 310 | 490 | display: flex; |
| 311 | 491 | flex-direction: column; |
| @@ -317,86 +497,213 @@ | ||
| 317 | 497 | gap: 1em; |
| 318 | 498 | overflow: hidden; |
| 319 | 499 | |
| 500 | + &.qr--isExecuting { | |
| 501 | + #qr--main > h3:first-child, | |
| 502 | + #qr--main > .qr--labels, | |
| 503 | + #qr--main > .qr--modal-messageContainer > .qr--modal-editorSettings, | |
| 504 | + #qr--qrOptions > h3:first-child, | |
| 505 | + #qr--qrOptions > #qr--ctxEditor, | |
| 506 | + #qr--qrOptions > .qr--ctxEditorActions, | |
| 507 | + #qr--qrOptions > .qr--ctxEditorActions + h3, | |
| 508 | + #qr--qrOptions > .qr--ctxEditorActions + h3 + div | |
| 509 | + { | |
| 510 | + display: none; | |
| 511 | + } | |
| 512 | + #qr--main > .qr--modal-messageContainer > #qr--modal-messageHolder > #qr--modal-message { | |
| 513 | + visibility: hidden; | |
| 514 | + } | |
| 515 | + #qr--modal-debugButtons { | |
| 516 | + display: flex; | |
| 517 | + .menu_button:not(#qr--modal-minimize, #qr--modal-maximize) { | |
| 518 | + cursor: not-allowed; | |
| 519 | + opacity: 0.5; | |
| 520 | + pointer-events: none; | |
| 521 | + transition: 200ms; | |
| 522 | + border-color: transparent; | |
| 523 | + } | |
| 524 | + } | |
| 525 | + &.qr--isPaused #qr--modal-debugButtons { | |
| 526 | + .menu_button:not(#qr--modal-minimize, #qr--modal-maximize) { | |
| 527 | + cursor: pointer; | |
| 528 | + opacity: 1; | |
| 529 | + pointer-events: all; | |
| 530 | + &#qr--modal-resume { | |
| 531 | + animation-name: qr--debugPulse; | |
| 532 | + animation-duration: 1500ms; | |
| 533 | + animation-timing-function: ease-in-out; | |
| 534 | + animation-delay: 0s; | |
| 535 | + animation-iteration-count: infinite; | |
| 536 | + } | |
| 537 | + &#qr--modal-resume { | |
| 538 | + border-color: rgb(81, 163, 81); | |
| 539 | + } | |
| 540 | + &#qr--modal-step { | |
| 541 | + border-color: var(--SmartThemeQuoteColor); | |
| 542 | + } | |
| 543 | + &#qr--modal-stepInto { | |
| 544 | + border-color: var(--SmartThemeQuoteColor); | |
| 545 | + } | |
| 546 | + &#qr--modal-stepOut { | |
| 547 | + border-color: var(--SmartThemeQuoteColor); | |
| 548 | + } | |
| 549 | + } | |
| 550 | + } | |
| 551 | + #qr--resizeHandle { | |
| 552 | + width: 6px; | |
| 553 | + background-color: var(--SmartThemeBorderColor); | |
| 554 | + border: 2px solid var(--SmartThemeBlurTintColor); | |
| 555 | + transition: border-color 200ms, background-color 200ms; | |
| 556 | + cursor: w-resize; | |
| 557 | + &:hover { | |
| 558 | + background-color: var(--SmartThemeQuoteColor); | |
| 559 | + border-color: var(--SmartThemeQuoteColor); | |
| 560 | + } | |
| 561 | + } | |
| 562 | + #qr--qrOptions { | |
| 563 | + width: var(--width, auto); | |
| 564 | + } | |
| 565 | + } | |
| 566 | + | |
| 320 | 567 | > #qr--main { |
| 321 | 568 | flex: 1 1 auto; |
| 322 | 569 | display: flex; |
| 323 | 570 | flex-direction: column; |
| 324 | 571 | overflow: hidden; |
| 325 | - | |
| 326 | 572 | > .qr--labels { |
| 327 | 573 | flex: 0 0 auto; |
| 328 | 574 | display: flex; |
| 329 | 575 | flex-direction: row; |
| 330 | 576 | gap: 0.5em; |
| 331 | - | |
| 577 | + padding: 1px; | |
| 332 | - >label { | |
| 578 | + > label, > .label { | |
| 333 | 579 | flex: 1 1 1px; |
| 334 | 580 | display: flex; |
| 335 | 581 | flex-direction: column; |
| 336 | - | |
| 582 | + position: relative; | |
| 337 | 583 | >&.qr--labelTextfit { |
| 584 | + flex: 0 0 auto; | |
| 585 | + justify-content: center; | |
| 586 | + } | |
| 587 | + .qr--inputGroup { | |
| 588 | + display: flex; | |
| 589 | + align-items: baseline; | |
| 590 | + gap: 0.5em; | |
| 591 | + input { | |
| 338 | 592 | flex: 1 1 auto; |
| 339 | 593 | } |
| 340 | - | |
| 594 | + } | |
| 341 | 595 | > .qr--labelHintlabelText { |
| 342 | 596 | flex: 1 1 auto; |
| 343 | 597 | } |
| 344 | - | |
| 598 | + .qr--labelHint { | |
| 345 | - >input { | |
| 599 | + flex: 1 1 auto; | |
| 600 | + } | |
| 601 | + input { | |
| 346 | 602 | flex: 0 0 auto; |
| 347 | 603 | } |
| 604 | + .qr--modal-switcherList { | |
| 605 | + background-color: var(--stcdx--bgColor); | |
| 606 | + border: 1px solid var(--SmartThemeBorderColor); | |
| 607 | + backdrop-filter: blur(var(--SmartThemeBlurStrength)); | |
| 608 | + border-radius: 10px; | |
| 609 | + font-size: smaller; | |
| 610 | + position: absolute; | |
| 611 | + top: 100%; | |
| 612 | + left: 0; | |
| 613 | + right: 0; | |
| 614 | + overflow: auto; | |
| 615 | + margin: 0; | |
| 616 | + padding: 0.5em; | |
| 617 | + max-height: 50vh; | |
| 618 | + list-style: none; | |
| 619 | + z-index: 40000; | |
| 620 | + max-width: 100%; | |
| 621 | + .qr--modal-switcherItem { | |
| 622 | + display: flex; | |
| 623 | + gap: 1em; | |
| 624 | + text-align: left; | |
| 625 | + opacity: 0.75; | |
| 626 | + transition: 200ms; | |
| 627 | + cursor: pointer; | |
| 628 | + &:hover { | |
| 629 | + opacity: 1; | |
| 630 | + } | |
| 631 | + &.qr--current { | |
| 632 | + opacity: 1; | |
| 633 | + .qr--label, .qr--id { | |
| 634 | + font-weight: bold; | |
| 635 | + } | |
| 636 | + } | |
| 637 | + } | |
| 638 | + .qr--label { | |
| 639 | + white-space: nowrap; | |
| 640 | + .menu_button { | |
| 641 | + display: inline-block; | |
| 642 | + height: min-content; | |
| 643 | + width: min-content; | |
| 644 | + margin: 0 0.5em 0 0; | |
| 645 | + } | |
| 646 | + } | |
| 647 | + .qr--id { | |
| 648 | + opacity: 0.5; | |
| 649 | + &:before { content: "["; } | |
| 650 | + &:after { content: "]"; } | |
| 651 | + } | |
| 652 | + .qr--message { | |
| 653 | + height: 1lh; | |
| 654 | + overflow: hidden; | |
| 655 | + text-overflow: ellipsis; | |
| 656 | + white-space: nowrap; | |
| 657 | + opacity: 0.5; | |
| 658 | + } | |
| 659 | + } | |
| 348 | 660 | } |
| 349 | 661 | } |
| 350 | - | |
| 351 | 662 | > .qr--modal-messageContainer { |
| 352 | 663 | flex: 1 1 auto; |
| 353 | 664 | display: flex; |
| 354 | 665 | flex-direction: column; |
| 355 | 666 | overflow: hidden; |
| 356 | - | |
| 357 | 667 | > .qr--modal-editorSettings { |
| 358 | 668 | display: flex; |
| 669 | + flex-wrap: wrap; | |
| 359 | 670 | flex-direction: row; |
| 360 | 671 | column-gap: 1em; |
| 361 | 672 | color: var(--grey70); |
| 362 | 673 | font-size: smaller; |
| 363 | 674 | align-items: baseline; |
| 364 | - | |
| 365 | 675 | > .checkbox_label { |
| 366 | 676 | white-space: nowrap; |
| 367 | - | |
| 368 | 677 | > input { |
| 369 | 678 | font-size: inherit; |
| 370 | 679 | } |
| 371 | 680 | } |
| 372 | 681 | } |
| 373 | - | |
| 374 | 682 | > #qr--modal-messageHolder { |
| 375 | 683 | flex: 1 1 auto; |
| 376 | 684 | display: grid; |
| 377 | 685 | text-align: left; |
| 378 | 686 | overflow: hidden; |
| 379 | - | |
| 380 | 687 | &.qr--noSyntax { |
| 381 | 688 | > #qr--modal-messageSyntax { |
| 382 | 689 | display: none; |
| 383 | 690 | } |
| 384 | - | |
| 385 | 691 | > #qr--modal-message { |
| 386 | 692 | background-color: var(--ac-style-color-background); |
| 387 | 693 | color: var(--ac-style-color-text); |
| 388 | - | |
| 694 | + &::-webkit-scrollbar, &::-webkit-scrollbar-thumb { | |
| 695 | + visibility: visible; | |
| 696 | + cursor: unset; | |
| 697 | + } | |
| 389 | 698 | &::selection { |
| 390 | 699 | color: unset; |
| 391 | 700 | background-color: rgba(108 171 251 / 0.25); |
| 392 | - | |
| 393 | 701 | @supports (color: rgb(from white r g b / 0.25)) { |
| 394 | 702 | background-color: rgb(from var(--ac-style-color-matchedText) r g b / 0.25); |
| 395 | 703 | } |
| 396 | 704 | } |
| 397 | 705 | } |
| 398 | 706 | } |
| 399 | - | |
| 400 | 707 | > #qr--modal-messageSyntax { |
| 401 | 708 | grid-column: 1; |
| 402 | 709 | grid-row: 1; |
| @@ -406,12 +713,10 @@ | ||
| 406 | 713 | overflow: hidden; |
| 407 | 714 | min-width: 100%; |
| 408 | 715 | width: 0; |
| 409 | - | |
| 410 | 716 | > #qr--modal-messageSyntaxInner { |
| 411 | 717 | height: 100%; |
| 412 | 718 | } |
| 413 | 719 | } |
| 414 | - | |
| 415 | 720 | > #qr--modal-message { |
| 416 | 721 | background-color: transparent; |
| 417 | 722 | color: transparent; |
| @@ -419,38 +724,35 @@ | ||
| 419 | 724 | grid-row: 1; |
| 420 | 725 | caret-color: var(--ac-style-color-text); |
| 421 | 726 | overflow: auto; |
| 422 | - | |
| 727 | + &::-webkit-scrollbar, &::-webkit-scrollbar-thumb { | |
| 423 | - &::-webkit-scrollbar, | |
| 424 | - &::-webkit-scrollbar-thumb { | |
| 425 | 728 | visibility: hidden; |
| 426 | 729 | cursor: default; |
| 427 | 730 | } |
| 428 | - | |
| 429 | 731 | &::selection { |
| 430 | 732 | color: transparent; |
| 431 | 733 | background-color: rgba(108 171 251 / 0.25); |
| 432 | - | |
| 433 | 734 | @supports (color: rgb(from white r g b / 0.25)) { |
| 434 | 735 | background-color: rgb(from var(--ac-style-color-matchedText) r g b / 0.25); |
| 435 | 736 | } |
| 436 | 737 | } |
| 437 | 738 | } |
| 438 | - | |
| 739 | + #qr--modal-message, #qr--modal-messageSyntaxInner { | |
| 439 | - #qr--modal-message, | |
| 440 | - #qr--modal-messageSyntaxInner { | |
| 441 | 740 | font-family: var(--monoFontFamily); |
| 442 | 741 | padding: 0.75em; |
| 443 | 742 | margin: 0; |
| 444 | - border: none; | |
| 445 | 743 | resize: none; |
| 446 | 744 | line-height: 1.2; |
| 447 | 745 | border: 1px solid var(--SmartThemeBorderColor); |
| 448 | 746 | border-radius: 5px; |
| 747 | + position: relative; | |
| 449 | 748 | } |
| 450 | 749 | } |
| 451 | 750 | } |
| 452 | 751 | } |
| 453 | - | |
| 752 | + #qr--modal-icon { | |
| 753 | + height: 100%; | |
| 754 | + aspect-ratio: 1 / 1; | |
| 755 | + } | |
| 454 | 756 | #qr--modal-executeButtons { |
| 455 | 757 | display: flex; |
| 456 | 758 | gap: 1em; |
| @@ -510,6 +812,47 @@ | ||
| 510 | 812 | border-color: rgb(215, 136, 114); |
| 511 | 813 | } |
| 512 | 814 | } |
| 815 | + #qr--modal-debugButtons { | |
| 816 | + display: none; | |
| 817 | + gap: 1em; | |
| 818 | + .qr--modal-debugButton { | |
| 819 | + aspect-ratio: 1.25 / 1; | |
| 820 | + width: 2.25em; | |
| 821 | + position: relative; | |
| 822 | + &:not(.fa-solid) { | |
| 823 | + border-width: 1px; | |
| 824 | + border-style: solid; | |
| 825 | + &:after { | |
| 826 | + content: ''; | |
| 827 | + position: absolute; | |
| 828 | + inset: 3px; | |
| 829 | + background-color: var(--SmartThemeBodyColor); | |
| 830 | + mask-size: contain; | |
| 831 | + mask-position: center; | |
| 832 | + mask-repeat: no-repeat; | |
| 833 | + } | |
| 834 | + } | |
| 835 | + &#qr--modal-resume:after { | |
| 836 | + mask-image: url('/img/step-resume.svg'); | |
| 837 | + } | |
| 838 | + &#qr--modal-step:after { | |
| 839 | + mask-image: url('/img/step-over.svg'); | |
| 840 | + } | |
| 841 | + &#qr--modal-stepInto:after { | |
| 842 | + mask-image: url('/img/step-into.svg'); | |
| 843 | + } | |
| 844 | + &#qr--modal-stepOut:after { | |
| 845 | + mask-image: url('/img/step-out.svg'); | |
| 846 | + } | |
| 847 | + &#qr--modal-maximize { | |
| 848 | + display: none; | |
| 849 | + } | |
| 850 | + } | |
| 851 | + } | |
| 852 | + | |
| 853 | + #qr--modal-send_textarea { | |
| 854 | + flex: 0 0 auto; | |
| 855 | + } | |
| 513 | 856 | |
| 514 | 857 | #qr--modal-executeProgress { |
| 515 | 858 | --prog: 0; |
| @@ -518,6 +861,7 @@ | ||
| 518 | 861 | --progSuccessColor: rgb(81, 163, 81); |
| 519 | 862 | --progErrorColor: rgb(189, 54, 47); |
| 520 | 863 | --progAbortedColor: rgb(215, 136, 114); |
| 864 | + flex: 0 0 auto; | |
| 521 | 865 | height: 0.5em; |
| 522 | 866 | background-color: var(--black50a); |
| 523 | 867 | position: relative; |
| @@ -588,6 +932,135 @@ | ||
| 588 | 932 | overflow: auto; |
| 589 | 933 | min-width: 100%; |
| 590 | 934 | width: 0; |
| 935 | + white-space: pre-wrap; | |
| 936 | + } | |
| 937 | + #qr--modal-debugState { | |
| 938 | + display: none; | |
| 939 | + &.qr--active { | |
| 940 | + display: block; | |
| 941 | + } | |
| 942 | + text-align: left; | |
| 943 | + font-size: smaller; | |
| 944 | + font-family: var(--monoFontFamily); | |
| 945 | + // background-color: rgb(146, 190, 252); | |
| 946 | + color: white; | |
| 947 | + padding: 0.5em 0; | |
| 948 | + overflow: auto; | |
| 949 | + min-width: 100%; | |
| 950 | + width: 0; | |
| 951 | + white-space: pre-wrap; | |
| 952 | + | |
| 953 | + .qr--scope { | |
| 954 | + display: grid; | |
| 955 | + grid-template-columns: 0fr 1fr 1fr; | |
| 956 | + column-gap: 0em; | |
| 957 | + .qr--title { | |
| 958 | + grid-column: 1 / 4; | |
| 959 | + font-weight: bold; | |
| 960 | + font-family: var(--mainFontFamily); | |
| 961 | + background-color: var(--black50a); | |
| 962 | + padding: 0.25em; | |
| 963 | + margin-top: 0.5em; | |
| 964 | + } | |
| 965 | + .qr--var, .qr--macro, .qr--pipe { | |
| 966 | + display: contents; | |
| 967 | + &:nth-child(2n + 1) { | |
| 968 | + .qr--key, .qr--val { | |
| 969 | + background-color: rgb(from var(--SmartThemeEmColor) r g b / 0.25); | |
| 970 | + } | |
| 971 | + .qr--val { | |
| 972 | + &:nth-child(2n) { | |
| 973 | + background-color: rgb(from var(--SmartThemeEmColor) r g b / 0.125); | |
| 974 | + } | |
| 975 | + &:hover { | |
| 976 | + background-color: rgb(from var(--SmartThemeEmColor) r g b / 0.5); | |
| 977 | + } | |
| 978 | + } | |
| 979 | + } | |
| 980 | + &:nth-child(2n) { | |
| 981 | + .qr--val { | |
| 982 | + &:nth-child(2n) { | |
| 983 | + background-color: rgb(from var(--SmartThemeEmColor) r g b / 0.0625); | |
| 984 | + } | |
| 985 | + &:hover { | |
| 986 | + background-color: rgb(from var(--SmartThemeEmColor) r g b / 0.5); | |
| 987 | + } | |
| 988 | + } | |
| 989 | + } | |
| 990 | + &.qr--isHidden { | |
| 991 | + .qr--key, .qr--val { | |
| 992 | + opacity: 0.5; | |
| 993 | + } | |
| 994 | + } | |
| 995 | + .qr--val { | |
| 996 | + grid-column: 2 / 4; | |
| 997 | + &.qr--singleCol { | |
| 998 | + grid-column: unset; | |
| 999 | + } | |
| 1000 | + &.qr--simple { | |
| 1001 | + &:before, &:after { | |
| 1002 | + content: '"'; | |
| 1003 | + color: var(--SmartThemeQuoteColor); | |
| 1004 | + } | |
| 1005 | + } | |
| 1006 | + &.qr--unresolved { | |
| 1007 | + &:after { | |
| 1008 | + content: '-UNRESOLVED-'; | |
| 1009 | + font-style: italic; | |
| 1010 | + color: var(--SmartThemeQuoteColor); | |
| 1011 | + } | |
| 1012 | + } | |
| 1013 | + } | |
| 1014 | + } | |
| 1015 | + .qr--key { | |
| 1016 | + margin-left: 0.5em; | |
| 1017 | + padding-right: 1em; | |
| 1018 | + &:after { content: ": "; } | |
| 1019 | + } | |
| 1020 | + .qr--pipe, .qr--macro { | |
| 1021 | + > .qr--key { | |
| 1022 | + &:before { content: "{{"; } | |
| 1023 | + &:after { content: "}}: "; } | |
| 1024 | + } | |
| 1025 | + } | |
| 1026 | + .qr--scope { | |
| 1027 | + display: contents; | |
| 1028 | + .qr--pipe { | |
| 1029 | + .qr--key, .qr--val { | |
| 1030 | + opacity: 0.5; | |
| 1031 | + } | |
| 1032 | + } | |
| 1033 | + } | |
| 1034 | + } | |
| 1035 | + | |
| 1036 | + .qr--stack { | |
| 1037 | + display: grid; | |
| 1038 | + grid-template-columns: 1fr 0fr; | |
| 1039 | + .qr--title { | |
| 1040 | + grid-column: 1 / 3; | |
| 1041 | + font-weight: bold; | |
| 1042 | + font-family: var(--mainFontFamily); | |
| 1043 | + background-color: var(--black50a); | |
| 1044 | + padding: 0.25em; | |
| 1045 | + margin-top: 1em; | |
| 1046 | + } | |
| 1047 | + .qr--item { | |
| 1048 | + display: contents; | |
| 1049 | + &:nth-child(2n + 1) { | |
| 1050 | + .qr--name, .qr--source { | |
| 1051 | + background-color: rgb(from var(--SmartThemeEmColor) r g b / 0.25); | |
| 1052 | + } | |
| 1053 | + } | |
| 1054 | + .qr--name { | |
| 1055 | + margin-left: 0.5em; | |
| 1056 | + } | |
| 1057 | + .qr--source { | |
| 1058 | + opacity: 0.5; | |
| 1059 | + text-align: right; | |
| 1060 | + white-space: nowrap; | |
| 1061 | + } | |
| 1062 | + } | |
| 1063 | + } | |
| 591 | 1064 | } |
| 592 | 1065 | } |
| 593 | 1066 | } |
| @@ -605,10 +1078,75 @@ | ||
| 605 | 1078 | } |
| 606 | 1079 | } |
| 607 | 1080 | |
| 1081 | +@keyframes qr--debugPulse { | |
| 1082 | + 0%, | |
| 1083 | + 100% { | |
| 1084 | + border-color: rgb(81, 163, 81); | |
| 1085 | + } | |
| 1086 | + | |
| 1087 | + 50% { | |
| 1088 | + border-color: rgb(146, 190, 252); | |
| 1089 | + } | |
| 1090 | +} | |
| 1091 | + | |
| 608 | 1092 | .popup.qr--hide { |
| 609 | 1093 | opacity: 0 !important; |
| 1094 | + &::backdrop { | |
| 1095 | + opacity: 0 !important; | |
| 1096 | + } | |
| 610 | 1097 | } |
| 611 | 1098 | |
| 612 | 1099 | .popup.qr--hide::backdrop { |
| 613 | 1100 | opacity: 0 !important; |
| 614 | 1101 | } |
| 1102 | + | |
| 1103 | + | |
| 1104 | + | |
| 1105 | +.popup:has(.qr--transferModal) { | |
| 1106 | + .popup-button-ok { | |
| 1107 | + &:after { | |
| 1108 | + content: 'Transfer'; | |
| 1109 | + height: 0; | |
| 1110 | + overflow: hidden; | |
| 1111 | + font-weight: bold; | |
| 1112 | + } | |
| 1113 | + display: flex; | |
| 1114 | + align-items: center; | |
| 1115 | + flex-direction: column; | |
| 1116 | + white-space: pre; | |
| 1117 | + font-weight: normal; | |
| 1118 | + box-shadow: 0 0 0; | |
| 1119 | + transition: 200ms; | |
| 1120 | + } | |
| 1121 | + .qr--copy { | |
| 1122 | + &:after { | |
| 1123 | + content: 'Copy'; | |
| 1124 | + height: 0; | |
| 1125 | + overflow: hidden; | |
| 1126 | + font-weight: bold; | |
| 1127 | + } | |
| 1128 | + display: flex; | |
| 1129 | + align-items: center; | |
| 1130 | + flex-direction: column; | |
| 1131 | + white-space: pre; | |
| 1132 | + font-weight: normal; | |
| 1133 | + box-shadow: 0 0 0; | |
| 1134 | + transition: 200ms; | |
| 1135 | + } | |
| 1136 | + &:has(.qr--transferSelect:focus) { | |
| 1137 | + .popup-button-ok { | |
| 1138 | + font-weight: bold; | |
| 1139 | + box-shadow: 0 0 10px; | |
| 1140 | + } | |
| 1141 | + &.qr--isCopy { | |
| 1142 | + .popup-button-ok { | |
| 1143 | + font-weight: normal; | |
| 1144 | + box-shadow: 0 0 0; | |
| 1145 | + } | |
| 1146 | + .qr--copy { | |
| 1147 | + font-weight: bold; | |
| 1148 | + box-shadow: 0 0 10px; | |
| 1149 | + } | |
| 1150 | + } | |
| 1151 | + } | |
| 1152 | +} | |
| @@ -526,6 +526,15 @@ export class Popup { | ||
| 526 | 526 | |
| 527 | 527 | return this.#promise; |
| 528 | 528 | } |
| 529 | + async completeAffirmative() { | |
| 530 | + return await this.complete(POPUP_RESULT.AFFIRMATIVE); | |
| 531 | + } | |
| 532 | + async completeNegative() { | |
| 533 | + return await this.complete(POPUP_RESULT.NEGATIVE); | |
| 534 | + } | |
| 535 | + async completeCancelled() { | |
| 536 | + return await this.complete(POPUP_RESULT.CANCELLED); | |
| 537 | + } | |
| 529 | 538 | |
| 530 | 539 | /** |
| 531 | 540 | * Hides the popup, using the internal resolver to return the value to the original show promise |
| @@ -45,7 +45,7 @@ import { FILTER_TYPES } from './filters.js'; | ||
| 45 | 45 | import { PARSER_FLAG, SlashCommandParser } from './slash-commands/SlashCommandParser.js'; |
| 46 | 46 | import { SlashCommand } from './slash-commands/SlashCommand.js'; |
| 47 | 47 | import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from './slash-commands/SlashCommandArgument.js'; |
| 48 | 48 | import { AUTOCOMPLETE_SELECT_KEY, AUTOCOMPLETE_WIDTH } from './autocomplete/AutoComplete.js'; |
| 49 | 49 | import { SlashCommandEnumValue, enumTypes } from './slash-commands/SlashCommandEnumValue.js'; |
| 50 | 50 | import { commonEnumProviders, enumIcons } from './slash-commands/SlashCommandCommonEnumsProvider.js'; |
| 51 | 51 | import { POPUP_TYPE, callGenericPopup } from './popup.js'; |
| @@ -277,6 +277,7 @@ let power_user = { | ||
| 277 | 277 | left: AUTOCOMPLETE_WIDTH.CHAT, |
| 278 | 278 | right: AUTOCOMPLETE_WIDTH.CHAT, |
| 279 | 279 | }, |
| 280 | + select: AUTOCOMPLETE_SELECT_KEY.TAB + AUTOCOMPLETE_SELECT_KEY.ENTER, | |
| 280 | 281 | }, |
| 281 | 282 | parser: { |
| 282 | 283 | /**@type {Object.<PARSER_FLAG,boolean>} */ |
| @@ -1494,6 +1495,9 @@ function loadPowerUserSettings(settings, data) { | ||
| 1494 | 1495 | if (power_user.stscript.autocomplete.style === undefined) { |
| 1495 | 1496 | power_user.stscript.autocomplete.style = power_user.stscript.autocomplete_style || defaultStscript.autocomplete.style; |
| 1496 | 1497 | } |
| 1498 | + if (power_user.stscript.autocomplete.select === undefined) { | |
| 1499 | + power_user.stscript.autocomplete.select = defaultStscript.autocomplete.select; | |
| 1500 | + } | |
| 1497 | 1501 | } |
| 1498 | 1502 | if (power_user.stscript.parser === undefined) { |
| 1499 | 1503 | power_user.stscript.parser = defaultStscript.parser; |
| @@ -1658,6 +1662,7 @@ function loadPowerUserSettings(settings, data) { | ||
| 1658 | 1662 | $('#stscript_matching').val(power_user.stscript.matching ?? 'fuzzy'); |
| 1659 | 1663 | $('#stscript_autocomplete_style').val(power_user.stscript.autocomplete.style ?? 'theme'); |
| 1660 | 1664 | document.body.setAttribute('data-stscript-style', power_user.stscript.autocomplete.style); |
| 1665 | + $('#stscript_autocomplete_select').val(power_user.stscript.autocomplete.select ?? (AUTOCOMPLETE_SELECT_KEY.TAB + AUTOCOMPLETE_SELECT_KEY.ENTER)); | |
| 1661 | 1666 | $('#stscript_parser_flag_strict_escaping').prop('checked', power_user.stscript.parser.flags[PARSER_FLAG.STRICT_ESCAPING] ?? false); |
| 1662 | 1667 | $('#stscript_parser_flag_replace_getvar').prop('checked', power_user.stscript.parser.flags[PARSER_FLAG.REPLACE_GETVAR] ?? false); |
| 1663 | 1668 | $('#stscript_autocomplete_font_scale').val(power_user.stscript.autocomplete.font.scale ?? defaultStscript.autocomplete.font.scale); |
| @@ -3848,6 +3853,12 @@ $(document).ready(() => { | ||
| 3848 | 3853 | saveSettingsDebounced(); |
| 3849 | 3854 | }); |
| 3850 | 3855 | |
| 3856 | + $('#stscript_autocomplete_select').on('change', function () { | |
| 3857 | + const value = $(this).find(':selected').val(); | |
| 3858 | + power_user.stscript.autocomplete.select = parseInt(String(value)); | |
| 3859 | + saveSettingsDebounced(); | |
| 3860 | + }); | |
| 3861 | + | |
| 3851 | 3862 | $('#stscript_autocomplete_font_scale').on('input', function () { |
| 3852 | 3863 | const value = $(this).val(); |
| 3853 | 3864 | $('#stscript_autocomplete_font_scale_counter').val(value); |
| @@ -4016,6 +4027,7 @@ $(document).ready(() => { | ||
| 4016 | 4027 | ), |
| 4017 | 4028 | ], |
| 4018 | 4029 | helpString: 'Enter message deletion mode, and auto-deletes last N messages if numeric argument is provided.', |
| 4030 | + returns: 'The text of the deleted messages.', | |
| 4019 | 4031 | })); |
| 4020 | 4032 | SlashCommandParser.addCommandObject(SlashCommand.fromProps({ |
| 4021 | 4033 | name: 'cut', |
| @@ -51,7 +51,7 @@ import { autoSelectPersona, retriggerFirstMessageOnEmptyChat, setPersonaLockStat | ||
| 51 | 51 | import { addEphemeralStoppingString, chat_styles, flushEphemeralStoppingStrings, power_user } from './power-user.js'; |
| 52 | 52 | import { textgen_types, textgenerationwebui_settings } from './textgen-settings.js'; |
| 53 | 53 | import { decodeTextTokens, getFriendlyTokenizerName, getTextTokens, getTokenCountAsync } from './tokenizers.js'; |
| 54 | 54 | import { debounce, delay, isFalseBoolean, isTrueBoolean, showFontAwesomePicker, stringToRange, trimToEndSentence, trimToStartSentence, waitUntilCondition } from './utils.js'; |
| 55 | 55 | import { registerVariableCommands, resolveVariable } from './variables.js'; |
| 56 | 56 | import { background_settings } from './backgrounds.js'; |
| 57 | 57 | import { SlashCommandClosure } from './slash-commands/SlashCommandClosure.js'; |
| @@ -64,6 +64,9 @@ import { SlashCommandNamedArgumentAssignment } from './slash-commands/SlashComma | ||
| 64 | 64 | import { SlashCommandEnumValue, enumTypes } from './slash-commands/SlashCommandEnumValue.js'; |
| 65 | 65 | import { POPUP_TYPE, Popup, callGenericPopup } from './popup.js'; |
| 66 | 66 | import { commonEnumProviders, enumIcons } from './slash-commands/SlashCommandCommonEnumsProvider.js'; |
| 67 | +import { SlashCommandDebugController } from './slash-commands/SlashCommandDebugController.js'; | |
| 68 | +import { SlashCommandBreakController } from './slash-commands/SlashCommandBreakController.js'; | |
| 69 | +import { SlashCommandExecutionError } from './slash-commands/SlashCommandExecutionError.js'; | |
| 67 | 70 | export { |
| 68 | 71 | executeSlashCommands, executeSlashCommandsWithOptions, getSlashCommandsHelp, registerSlashCommand, |
| 69 | 72 | }; |
| @@ -1120,10 +1123,10 @@ export function initDefaultSlashCommands() { | ||
| 1120 | 1123 | unnamedArgumentList: [ |
| 1121 | 1124 | SlashCommandArgument.fromProps({ |
| 1122 | 1125 | description: 'scoped variable or qr label', |
| 1123 | 1126 | typeList: [ARGUMENT_TYPE.VARIABLE_NAME, ARGUMENT_TYPE.STRING, ARGUMENT_TYPE.CLOSURE], |
| 1124 | 1127 | isRequired: true, |
| 1125 | 1128 | enumProvider: (executor, scope) => [ |
| 1126 | 1129 | ...commonEnumProviders.variables('scope')(executor, scope), |
| 1127 | 1130 | ...(typeof window['qrEnumProviderExecutables'] === 'function') ? window['qrEnumProviderExecutables']() : [], |
| 1128 | 1131 | ], |
| 1129 | 1132 | }), |
| @@ -1477,6 +1480,21 @@ export function initDefaultSlashCommands() { | ||
| 1477 | 1480 | ], |
| 1478 | 1481 | helpString: 'Sets the specified prompt manager entry/entries on or off.', |
| 1479 | 1482 | })); |
| 1483 | + SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'pick-icon', | |
| 1484 | + callback: async()=>((await showFontAwesomePicker()) ?? false).toString(), | |
| 1485 | + returns: 'The chosen icon name or false if cancelled.', | |
| 1486 | + helpString: ` | |
| 1487 | + <div>Opens a popup with all the available Font Awesome icons and returns the selected icon's name.</div> | |
| 1488 | + <div> | |
| 1489 | + <strong>Example:</strong> | |
| 1490 | + <ul> | |
| 1491 | + <li> | |
| 1492 | + <pre><code>/pick-icon |\n/if left={{pipe}} rule=eq right=false\n\telse={: /echo chosen icon: "{{pipe}}" :}\n\t{: /echo cancelled icon selection :}\n|</code></pre> | |
| 1493 | + </li> | |
| 1494 | + </ul> | |
| 1495 | + </div> | |
| 1496 | + `, | |
| 1497 | + })); | |
| 1480 | 1498 | |
| 1481 | 1499 | registerVariableCommands(); |
| 1482 | 1500 | } |
| @@ -1821,6 +1839,11 @@ async function runCallback(args, name) { | ||
| 1821 | 1839 | throw new Error('No name provided for /run command'); |
| 1822 | 1840 | } |
| 1823 | 1841 | |
| 1842 | + if (name instanceof SlashCommandClosure) { | |
| 1843 | + name.breakController = new SlashCommandBreakController(); | |
| 1844 | + return (await name.execute())?.pipe; | |
| 1845 | + } | |
| 1846 | + | |
| 1824 | 1847 | /**@type {SlashCommandScope} */ |
| 1825 | 1848 | const scope = args._scope; |
| 1826 | 1849 | if (scope.existsVariable(name)) { |
| @@ -1829,6 +1852,11 @@ async function runCallback(args, name) { | ||
| 1829 | 1852 | throw new Error(`"${name}" is not callable.`); |
| 1830 | 1853 | } |
| 1831 | 1854 | closure.scope.parent = scope; |
| 1855 | + closure.breakController = new SlashCommandBreakController(); | |
| 1856 | + if (args._debugController && !closure.debugController) { | |
| 1857 | + closure.debugController = args._debugController; | |
| 1858 | + } | |
| 1859 | + while (closure.providedArgumentList.pop()); | |
| 1832 | 1860 | closure.argumentList.forEach(arg => { |
| 1833 | 1861 | if (Object.keys(args).includes(arg.name)) { |
| 1834 | 1862 | const providedArg = new SlashCommandNamedArgumentAssignment(); |
| @@ -1847,9 +1875,14 @@ async function runCallback(args, name) { | ||
| 1847 | 1875 | |
| 1848 | 1876 | try { |
| 1849 | 1877 | name = name.trim(); |
| 1850 | - return await window['executeQuickReplyByName'](name, args); | |
| 1878 | + /**@type {ExecuteSlashCommandsOptions} */ | |
| 1879 | + const options = { | |
| 1880 | + abortController: args._abortController, | |
| 1881 | + debugController: args._debugController, | |
| 1882 | + }; | |
| 1883 | + return await window['executeQuickReplyByName'](name, args, options); | |
| 1851 | 1884 | } catch (error) { |
| 1852 | 1885 | throw new Error(`Error running Quick Reply "${name}": ${error.message}`, 'Error'); |
| 1853 | 1886 | } |
| 1854 | 1887 | } |
| 1855 | 1888 | |
| @@ -3428,7 +3461,9 @@ const clearCommandProgressDebounced = debounce(clearCommandProgress); | ||
| 3428 | 3461 | * @prop {boolean} [handleExecutionErrors] (false) Whether to handle execution errors (show toast on error) or throw |
| 3429 | 3462 | * @prop {{[id:PARSER_FLAG]:boolean}} [parserFlags] (null) Parser flags to apply |
| 3430 | 3463 | * @prop {SlashCommandAbortController} [abortController] (null) Controller used to abort or pause command execution |
| 3464 | + * @prop {SlashCommandDebugController} [debugController] (null) Controller used to control debug execution | |
| 3431 | 3465 | * @prop {(done:number, total:number)=>void} [onProgress] (null) Callback to handle progress events |
| 3466 | + * @prop {string} [source] (null) String indicating where the code come from (e.g., QR name) | |
| 3432 | 3467 | */ |
| 3433 | 3468 | |
| 3434 | 3469 | /** |
| @@ -3436,6 +3471,7 @@ const clearCommandProgressDebounced = debounce(clearCommandProgress); | ||
| 3436 | 3471 | * @prop {SlashCommandScope} [scope] (null) The scope to be used when executing the commands. |
| 3437 | 3472 | * @prop {{[id:PARSER_FLAG]:boolean}} [parserFlags] (null) Parser flags to apply |
| 3438 | 3473 | * @prop {boolean} [clearChatInput] (false) Whether to clear the chat input textarea |
| 3474 | + * @prop {string} [source] (null) String indicating where the code come from (e.g., QR name) | |
| 3439 | 3475 | */ |
| 3440 | 3476 | |
| 3441 | 3477 | /** |
| @@ -3451,6 +3487,7 @@ export async function executeSlashCommandsOnChatInput(text, options = {}) { | ||
| 3451 | 3487 | scope: null, |
| 3452 | 3488 | parserFlags: null, |
| 3453 | 3489 | clearChatInput: false, |
| 3490 | + source: null, | |
| 3454 | 3491 | }, options); |
| 3455 | 3492 | |
| 3456 | 3493 | isExecutingCommandsFromChatInput = true; |
| @@ -3473,13 +3510,21 @@ export async function executeSlashCommandsOnChatInput(text, options = {}) { | ||
| 3473 | 3510 | |
| 3474 | 3511 | /**@type {SlashCommandClosureResult} */ |
| 3475 | 3512 | let result = null; |
| 3513 | + let currentProgress = 0; | |
| 3476 | 3514 | try { |
| 3477 | 3515 | commandsFromChatInputAbortController = new SlashCommandAbortController(); |
| 3478 | 3516 | result = await executeSlashCommandsWithOptions(text, { |
| 3479 | 3517 | abortController: commandsFromChatInputAbortController, |
| 3480 | 3518 | onProgress: (done, total) => ta.style.setProperty('--prog', `${done / total * 100}%`), |
| 3519 | + const newProgress = done / total; | |
| 3520 | + if (newProgress > currentProgress) { | |
| 3521 | + currentProgress = newProgress; | |
| 3522 | + ta.style.setProperty('--prog', `${newProgress * 100}%`); | |
| 3523 | + } | |
| 3524 | + }, | |
| 3481 | 3525 | parserFlags: options.parserFlags, |
| 3482 | 3526 | scope: options.scope, |
| 3527 | + source: options.source, | |
| 3483 | 3528 | }); |
| 3484 | 3529 | if (commandsFromChatInputAbortController.signal.aborted) { |
| 3485 | 3530 | document.querySelector('#form_sheld').classList.add('script_aborted'); |
| @@ -3492,8 +3537,24 @@ export async function executeSlashCommandsOnChatInput(text, options = {}) { | ||
| 3492 | 3537 | result.isError = true; |
| 3493 | 3538 | result.errorMessage = e.message || 'An unknown error occurred'; |
| 3494 | 3539 | if (e.cause !== 'abort') { |
| 3540 | + if (e instanceof SlashCommandExecutionError) { | |
| 3541 | + /**@type {SlashCommandExecutionError}*/ | |
| 3542 | + const ex = e; | |
| 3543 | + const toast = ` | |
| 3544 | + <div>${ex.message}</div> | |
| 3545 | + <div>Line: ${ex.line} Column: ${ex.column}</div> | |
| 3546 | + <pre style="text-align:left;">${ex.hint}</pre> | |
| 3547 | + `; | |
| 3548 | + const clickHint = '<p>Click to see details</p>'; | |
| 3549 | + toastr.error( | |
| 3550 | + `${toast}${clickHint}`, | |
| 3551 | + 'SlashCommandExecutionError', | |
| 3552 | + { escapeHtml: false, timeOut: 10000, onclick: () => callPopup(toast, 'text') }, | |
| 3553 | + ); | |
| 3554 | + } else { | |
| 3495 | 3555 | toastr.error(result.errorMessage); |
| 3496 | 3556 | } |
| 3557 | + } | |
| 3497 | 3558 | } finally { |
| 3498 | 3559 | delay(1000).then(() => clearCommandProgressDebounced()); |
| 3499 | 3560 | |
| @@ -3520,7 +3581,9 @@ async function executeSlashCommandsWithOptions(text, options = {}) { | ||
| 3520 | 3581 | handleExecutionErrors: false, |
| 3521 | 3582 | parserFlags: null, |
| 3522 | 3583 | abortController: null, |
| 3584 | + debugController: null, | |
| 3523 | 3585 | onProgress: null, |
| 3586 | + source: null, | |
| 3524 | 3587 | }, options); |
| 3525 | 3588 | |
| 3526 | 3589 | let closure; |
| @@ -3528,6 +3591,8 @@ async function executeSlashCommandsWithOptions(text, options = {}) { | ||
| 3528 | 3591 | closure = parser.parse(text, true, options.parserFlags, options.abortController ?? new SlashCommandAbortController()); |
| 3529 | 3592 | closure.scope.parent = options.scope; |
| 3530 | 3593 | closure.onProgress = options.onProgress; |
| 3594 | + closure.debugController = options.debugController; | |
| 3595 | + closure.source = options.source; | |
| 3531 | 3596 | } catch (e) { |
| 3532 | 3597 | if (options.handleParserErrors && e instanceof SlashCommandParserError) { |
| 3533 | 3598 | /**@type {SlashCommandParserError}*/ |
| @@ -3559,7 +3624,23 @@ async function executeSlashCommandsWithOptions(text, options = {}) { | ||
| 3559 | 3624 | return result; |
| 3560 | 3625 | } catch (e) { |
| 3561 | 3626 | if (options.handleExecutionErrors) { |
| 3627 | + if (e instanceof SlashCommandExecutionError) { | |
| 3628 | + /**@type {SlashCommandExecutionError}*/ | |
| 3629 | + const ex = e; | |
| 3630 | + const toast = ` | |
| 3631 | + <div>${ex.message}</div> | |
| 3632 | + <div>Line: ${ex.line} Column: ${ex.column}</div> | |
| 3633 | + <pre style="text-align:left;">${ex.hint}</pre> | |
| 3634 | + `; | |
| 3635 | + const clickHint = '<p>Click to see details</p>'; | |
| 3636 | + toastr.error( | |
| 3637 | + `${toast}${clickHint}`, | |
| 3638 | + 'SlashCommandExecutionError', | |
| 3639 | + { escapeHtml: false, timeOut: 10000, onclick: () => callPopup(toast, 'text') }, | |
| 3640 | + ); | |
| 3641 | + } else { | |
| 3562 | 3642 | toastr.error(e.message); |
| 3643 | + } | |
| 3563 | 3644 | const result = new SlashCommandClosureResult(); |
| 3564 | 3645 | result.isError = true; |
| 3565 | 3646 | result.errorMessage = e.message; |
| @@ -3596,6 +3677,7 @@ async function executeSlashCommands(text, handleParserErrors = true, scope = nul | ||
| 3596 | 3677 | * |
| 3597 | 3678 | * @param {HTMLTextAreaElement} textarea The textarea to receive autocomplete |
| 3598 | 3679 | * @param {Boolean} isFloating Whether to show the auto complete as a floating window (e.g., large QR editor) |
| 3680 | + * @returns {Promise<AutoComplete>} | |
| 3599 | 3681 | */ |
| 3600 | 3682 | export async function setSlashCommandAutoComplete(textarea, isFloating = false) { |
| 3601 | 3683 | function canUseNegativeLookbehind() { |
| @@ -3619,6 +3701,7 @@ export async function setSlashCommandAutoComplete(textarea, isFloating = false) | ||
| 3619 | 3701 | async (text, index) => await parser.getNameAt(text, index), |
| 3620 | 3702 | isFloating, |
| 3621 | 3703 | ); |
| 3704 | + return ac; | |
| 3622 | 3705 | } |
| 3623 | 3706 | /**@type {HTMLTextAreaElement} */ |
| 3624 | 3707 | const sendTextarea = document.querySelector('#send_textarea'); |
| @@ -1,6 +1,7 @@ | ||
| 1 | 1 | import { SlashCommandAbortController } from './SlashCommandAbortController.js'; |
| 2 | 2 | import { SlashCommandArgument, SlashCommandNamedArgument } from './SlashCommandArgument.js'; |
| 3 | 3 | import { SlashCommandClosure } from './SlashCommandClosure.js'; |
| 4 | +import { SlashCommandDebugController } from './SlashCommandDebugController.js'; | |
| 4 | 5 | import { PARSER_FLAG } from './SlashCommandParser.js'; |
| 5 | 6 | import { SlashCommandScope } from './SlashCommandScope.js'; |
| 6 | 7 | |
| @@ -12,6 +13,7 @@ import { SlashCommandScope } from './SlashCommandScope.js'; | ||
| 12 | 13 | * _scope:SlashCommandScope, |
| 13 | 14 | * _parserFlags:{[id:PARSER_FLAG]:boolean}, |
| 14 | 15 | * _abortController:SlashCommandAbortController, |
| 16 | + * _debugController:SlashCommandDebugController, | |
| 15 | 17 | * _hasUnnamedArgument:boolean, |
| 16 | 18 | * [id:string]:string|SlashCommandClosure, |
| 17 | 19 | * }} NamedArguments |
| @@ -36,6 +38,7 @@ export class SlashCommand { | ||
| 36 | 38 | * @param {(namedArguments:NamedArguments|NamedArgumentsCapture, unnamedArguments:string|SlashCommandClosure|(string|SlashCommandClosure)[])=>string|SlashCommandClosure|Promise<string|SlashCommandClosure>} [props.callback] |
| 37 | 39 | * @param {string} [props.helpString] |
| 38 | 40 | * @param {boolean} [props.splitUnnamedArgument] |
| 41 | + * @param {Number} [props.splitUnnamedArgumentCount] | |
| 39 | 42 | * @param {string[]} [props.aliases] |
| 40 | 43 | * @param {string} [props.returns] |
| 41 | 44 | * @param {SlashCommandNamedArgument[]} [props.namedArgumentList] |
| @@ -50,9 +53,10 @@ export class SlashCommand { | ||
| 50 | 53 | |
| 51 | 54 | |
| 52 | 55 | /**@type {string}*/ name; |
| 53 | 56 | /**@type {(namedArguments:{_pipe:string|SlashCommandClosure, _scope:SlashCommandScope, _abortController:SlashCommandAbortController, [id:string]:string|SlashCommandClosure}, unnamedArguments:string|SlashCommandClosure|(string|SlashCommandClosure)[])=>string|SlashCommandClosure|Promise<string|SlashCommandClosure>}*/ callback; |
| 54 | 57 | /**@type {string}*/ helpString; |
| 55 | 58 | /**@type {boolean}*/ splitUnnamedArgument = false; |
| 59 | + /**@type {Number}*/ splitUnnamedArgumentCount; | |
| 56 | 60 | /**@type {string[]}*/ aliases = []; |
| 57 | 61 | /**@type {string}*/ returns; |
| 58 | 62 | /**@type {SlashCommandNamedArgument[]}*/ namedArgumentList = []; |
| @@ -61,6 +65,10 @@ export class SlashCommand { | ||
| 61 | 65 | /**@type {Object.<string, HTMLElement>}*/ helpCache = {}; |
| 62 | 66 | /**@type {Object.<string, DocumentFragment>}*/ helpDetailsCache = {}; |
| 63 | 67 | |
| 68 | + /**@type {boolean}*/ isExtension = false; | |
| 69 | + /**@type {boolean}*/ isThirdParty = false; | |
| 70 | + /**@type {string}*/ source; | |
| 71 | + | |
| 64 | 72 | renderHelpItem(key = null) { |
| 65 | 73 | key = key ?? this.name; |
| 66 | 74 | if (!this.helpCache[key]) { |
| @@ -225,12 +233,35 @@ export class SlashCommand { | ||
| 225 | 233 | const aliasList = [cmd.name, ...(cmd.aliases ?? [])].filter(it=>it != key); |
| 226 | 234 | const specs = document.createElement('div'); { |
| 227 | 235 | specs.classList.add('specs'); |
| 236 | + const head = document.createElement('div'); { | |
| 237 | + head.classList.add('head'); | |
| 228 | 238 | const name = document.createElement('div'); { |
| 229 | 239 | name.classList.add('name'); |
| 230 | 240 | name.classList.add('monospace'); |
| 231 | 241 | name.title = 'command name'; |
| 232 | 242 | name.textContent = `/${key}`; |
| 233 | 243 | specs head.append(name); |
| 244 | + } | |
| 245 | + const src = document.createElement('div'); { | |
| 246 | + src.classList.add('source'); | |
| 247 | + src.classList.add('fa-solid'); | |
| 248 | + if (this.isExtension) { | |
| 249 | + src.classList.add('isExtension'); | |
| 250 | + src.classList.add('fa-cubes'); | |
| 251 | + if (this.isThirdParty) src.classList.add('isThirdParty'); | |
| 252 | + else src.classList.add('isCore'); | |
| 253 | + } else { | |
| 254 | + src.classList.add('isCore'); | |
| 255 | + src.classList.add('fa-star-of-life'); | |
| 256 | + } | |
| 257 | + src.title = [ | |
| 258 | + this.isExtension ? 'Extension' : 'Core', | |
| 259 | + this.isThirdParty ? 'Third Party' : (this.isExtension ? 'Core' : null), | |
| 260 | + this.source, | |
| 261 | + ].filter(it=>it).join('\n'); | |
| 262 | + head.append(src); | |
| 263 | + } | |
| 264 | + specs.append(head); | |
| 234 | 265 | } |
| 235 | 266 | const body = document.createElement('div'); { |
| 236 | 267 | body.classList.add('body'); |
| @@ -303,6 +334,8 @@ export class SlashCommand { | ||
| 303 | 334 | for (const arg of unnamedArguments) { |
| 304 | 335 | const listItem = document.createElement('li'); { |
| 305 | 336 | listItem.classList.add('argumentItem'); |
| 337 | + const argSpec = document.createElement('div'); { | |
| 338 | + argSpec.classList.add('argumentSpec'); | |
| 306 | 339 | const argItem = document.createElement('div'); { |
| 307 | 340 | argItem.classList.add('argument'); |
| 308 | 341 | argItem.classList.add('unnamedArgument'); |
| @@ -336,7 +369,17 @@ export class SlashCommand { | ||
| 336 | 369 | argItem.append(types); |
| 337 | 370 | } |
| 338 | 371 | } |
| 339 | 372 | listItem argSpec.append(argItem); |
| 373 | + } | |
| 374 | + if (arg.defaultValue !== null) { | |
| 375 | + const argDefault = document.createElement('div'); { | |
| 376 | + argDefault.classList.add('argument-default'); | |
| 377 | + argDefault.title = 'default value'; | |
| 378 | + argDefault.textContent = arg.defaultValue.toString(); | |
| 379 | + argSpec.append(argDefault); | |
| 380 | + } | |
| 381 | + } | |
| 382 | + listItem.append(argSpec); | |
| 340 | 383 | } |
| 341 | 384 | const desc = document.createElement('div'); { |
| 342 | 385 | desc.classList.add('argument-description'); |
| @@ -2,6 +2,9 @@ import { SlashCommandClosure } from './SlashCommandClosure.js'; | ||
| 2 | 2 | import { commonEnumProviders } from './SlashCommandCommonEnumsProvider.js'; |
| 3 | 3 | import { SlashCommandEnumValue } from './SlashCommandEnumValue.js'; |
| 4 | 4 | import { SlashCommandExecutor } from './SlashCommandExecutor.js'; |
| 5 | +import { SlashCommandScope } from './SlashCommandScope.js'; | |
| 6 | + | |
| 7 | + | |
| 5 | 8 | |
| 6 | 9 | /**@readonly*/ |
| 7 | 10 | /**@enum {string}*/ |
| @@ -27,7 +30,7 @@ export class SlashCommandArgument { | ||
| 27 | 30 | * @param {boolean} [props.acceptsMultiple=false] default: false - whether argument accepts multiple values |
| 28 | 31 | * @param {string|SlashCommandClosure} [props.defaultValue=null] default value if no value is provided |
| 29 | 32 | * @param {string|SlashCommandEnumValue|(string|SlashCommandEnumValue)[]} [props.enumList=[]] list of accepted values |
| 30 | 33 | * @param {(executor:SlashCommandExecutor, scope:SlashCommandScope)=>SlashCommandEnumValue[]} [props.enumProvider=null] function that returns auto complete options |
| 31 | 34 | * @param {boolean} [props.forceEnum=false] default: false - whether the input must match one of the enum values |
| 32 | 35 | */ |
| 33 | 36 | static fromProps(props) { |
| @@ -49,7 +52,7 @@ export class SlashCommandArgument { | ||
| 49 | 52 | /**@type {boolean}*/ acceptsMultiple = false; |
| 50 | 53 | /**@type {string|SlashCommandClosure}*/ defaultValue; |
| 51 | 54 | /**@type {SlashCommandEnumValue[]}*/ enumList = []; |
| 52 | 55 | /**@type {(executor:SlashCommandExecutor, scope:SlashCommandScope)=>SlashCommandEnumValue[]}*/ enumProvider = null; |
| 53 | 56 | /**@type {boolean}*/ forceEnum = false; |
| 54 | 57 | |
| 55 | 58 | /** |
| @@ -57,7 +60,7 @@ export class SlashCommandArgument { | ||
| 57 | 60 | * @param {ARGUMENT_TYPE|ARGUMENT_TYPE[]} types |
| 58 | 61 | * @param {string|SlashCommandClosure} defaultValue |
| 59 | 62 | * @param {string|SlashCommandEnumValue|(string|SlashCommandEnumValue)[]} enums |
| 60 | 63 | * @param {(executor:SlashCommandExecutor, scope:SlashCommandScope)=>SlashCommandEnumValue[]} enumProvider function that returns auto complete options |
| 61 | 64 | */ |
| 62 | 65 | constructor(description, types, isRequired = false, acceptsMultiple = false, defaultValue = null, enums = [], enumProvider = null, forceEnum = false) { |
| 63 | 66 | this.description = description; |
| @@ -89,7 +92,7 @@ export class SlashCommandNamedArgument extends SlashCommandArgument { | ||
| 89 | 92 | * @param {boolean} [props.acceptsMultiple=false] default: false - whether argument accepts multiple values |
| 90 | 93 | * @param {string|SlashCommandClosure} [props.defaultValue=null] default value if no value is provided |
| 91 | 94 | * @param {string|SlashCommandEnumValue|(string|SlashCommandEnumValue)[]} [props.enumList=[]] list of accepted values |
| 92 | 95 | * @param {(executor:SlashCommandExecutor, scope:SlashCommandScope)=>SlashCommandEnumValue[]} [props.enumProvider=null] function that returns auto complete options |
| 93 | 96 | * @param {boolean} [props.forceEnum=false] default: false - whether the input must match one of the enum values |
| 94 | 97 | */ |
| 95 | 98 | static fromProps(props) { |
| @@ -119,7 +122,7 @@ export class SlashCommandNamedArgument extends SlashCommandArgument { | ||
| 119 | 122 | * @param {string|SlashCommandClosure} [defaultValue=null] |
| 120 | 123 | * @param {string|SlashCommandEnumValue|(string|SlashCommandEnumValue)[]} [enums=[]] |
| 121 | 124 | * @param {string[]} [aliases=[]] |
| 122 | 125 | * @param {(executor:SlashCommandExecutor, scope:SlashCommandScope)=>SlashCommandEnumValue[]} [enumProvider=null] function that returns auto complete options |
| 123 | 126 | * @param {boolean} [forceEnum=false] |
| 124 | 127 | */ |
| 125 | 128 | constructor(name, description, types, isRequired = false, acceptsMultiple = false, defaultValue = null, enums = [], aliases = [], enumProvider = null, forceEnum = false) { |
| @@ -8,15 +8,18 @@ import { SlashCommandCommandAutoCompleteOption } from './SlashCommandCommandAuto | ||
| 8 | 8 | import { SlashCommandEnumAutoCompleteOption } from './SlashCommandEnumAutoCompleteOption.js'; |
| 9 | 9 | import { SlashCommandExecutor } from './SlashCommandExecutor.js'; |
| 10 | 10 | import { SlashCommandNamedArgumentAutoCompleteOption } from './SlashCommandNamedArgumentAutoCompleteOption.js'; |
| 11 | +import { SlashCommandScope } from './SlashCommandScope.js'; | |
| 11 | 12 | |
| 12 | 13 | export class SlashCommandAutoCompleteNameResult extends AutoCompleteNameResult { |
| 13 | 14 | /**@type {SlashCommandExecutor}*/ executor; |
| 15 | + /**@type {SlashCommandScope}*/ scope; | |
| 14 | 16 | |
| 15 | 17 | /** |
| 16 | 18 | * @param {SlashCommandExecutor} executor |
| 19 | + * @param {SlashCommandScope} scope | |
| 17 | 20 | * @param {Object.<string,SlashCommand>} commands |
| 18 | 21 | */ |
| 19 | 22 | constructor(executor, scope, commands) { |
| 20 | 23 | super( |
| 21 | 24 | executor.name, |
| 22 | 25 | executor.start, |
| @@ -29,6 +32,7 @@ export class SlashCommandAutoCompleteNameResult extends AutoCompleteNameResult { | ||
| 29 | 32 | ()=>'No slash commands found!', |
| 30 | 33 | ); |
| 31 | 34 | this.executor = executor; |
| 35 | + this.scope = scope; | |
| 32 | 36 | } |
| 33 | 37 | |
| 34 | 38 | getSecondaryNameAt(text, index, isSelect) { |
| @@ -86,8 +90,8 @@ export class SlashCommandAutoCompleteNameResult extends AutoCompleteNameResult { | ||
| 86 | 90 | } |
| 87 | 91 | } else if (unamedArgLength > 0 && index >= this.executor.startUnnamedArgs && index <= this.executor.endUnnamedArgs) { |
| 88 | 92 | // cursor is somewhere within the unnamed arguments |
| 89 | 93 | //TODO if index is in first array item and that is a string, treat it as an unfinished named arg |
| 90 | 94 | if (typeof this.executor.unnamedArgumentList[0]?.value == 'string') { |
| 91 | 95 | if (index <= this.executor.startUnnamedArgs + this.executor.unnamedArgumentList[0].value.length) { |
| 92 | 96 | name = this.executor.unnamedArgumentList[0].value.slice(0, index - this.executor.startUnnamedArgs); |
| 93 | 97 | start = this.executor.startUnnamedArgs; |
| @@ -103,7 +107,7 @@ export class SlashCommandAutoCompleteNameResult extends AutoCompleteNameResult { | ||
| 103 | 107 | |
| 104 | 108 | if (name.includes('=') && cmdArg) { |
| 105 | 109 | // if cursor is already behind "=" check for enums |
| 106 | 110 | const enumList = cmdArg?.enumProvider?.(this.executor, this.scope) ?? cmdArg?.enumList; |
| 107 | 111 | if (cmdArg && enumList?.length) { |
| 108 | 112 | if (isSelect && enumList.find(it=>it.value == value) && argAssign && argAssign.end == index) { |
| 109 | 113 | return null; |
| @@ -111,7 +115,7 @@ export class SlashCommandAutoCompleteNameResult extends AutoCompleteNameResult { | ||
| 111 | 115 | const result = new AutoCompleteSecondaryNameResult( |
| 112 | 116 | value, |
| 113 | 117 | start + name.length, |
| 114 | 118 | enumList.map(it=>new SlashCommandEnumAutoCompleteOption.from(this.executor.command, it)), |
| 115 | 119 | true, |
| 116 | 120 | ); |
| 117 | 121 | result.isRequired = true; |
| @@ -150,7 +154,10 @@ export class SlashCommandAutoCompleteNameResult extends AutoCompleteNameResult { | ||
| 150 | 154 | if (idx > -1) { |
| 151 | 155 | argAssign = this.executor.unnamedArgumentList[idx]; |
| 152 | 156 | cmdArg = this.executor.command.unnamedArgumentList[idx]; |
| 153 | - const enumList = cmdArg?.enumProvider?.(this.executor) ?? cmdArg?.enumList; | |
| 157 | + if (cmdArg === undefined && this.executor.command.unnamedArgumentList.slice(-1)[0]?.acceptsMultiple) { | |
| 158 | + cmdArg = this.executor.command.unnamedArgumentList.slice(-1)[0]; | |
| 159 | + } | |
| 160 | + const enumList = cmdArg?.enumProvider?.(this.executor, this.scope) ?? cmdArg?.enumList; | |
| 154 | 161 | if (cmdArg && enumList.length > 0) { |
| 155 | 162 | value = argAssign.value.toString().slice(0, index - argAssign.start); |
| 156 | 163 | start = argAssign.start; |
| @@ -161,23 +168,26 @@ export class SlashCommandAutoCompleteNameResult extends AutoCompleteNameResult { | ||
| 161 | 168 | value = ''; |
| 162 | 169 | start = index; |
| 163 | 170 | cmdArg = notProvidedArguments[0]; |
| 171 | + if (cmdArg === undefined && this.executor.command.unnamedArgumentList.slice(-1)[0]?.acceptsMultiple) { | |
| 172 | + cmdArg = this.executor.command.unnamedArgumentList.slice(-1)[0]; | |
| 173 | + } | |
| 164 | 174 | } |
| 165 | 175 | } else { |
| 166 | 176 | return null; |
| 167 | 177 | } |
| 168 | 178 | |
| 169 | 179 | const enumList = cmdArg?.enumProvider?.(this.executor, this.scope) ?? cmdArg?.enumList; |
| 170 | 180 | if (cmdArg == null || enumList.length == 0) return null; |
| 171 | 181 | |
| 172 | 182 | const result = new AutoCompleteSecondaryNameResult( |
| 173 | 183 | value, |
| 174 | 184 | start, |
| 175 | 185 | enumList.map(it=>new SlashCommandEnumAutoCompleteOption.from(this.executor.command, it)), |
| 176 | 186 | false, |
| 177 | 187 | ); |
| 178 | 188 | const isCompleteValue = enumList.find(it=>it.value == value); |
| 179 | 189 | const isSelectedValue = isSelect && isCompleteValue; |
| 180 | 190 | result.isRequired = cmdArg.isRequired && !isSelectedValue && !isCompleteValue; |
| 181 | 191 | result.forceMatch = cmdArg.forceEnum; |
| 182 | 192 | return result; |
| 183 | 193 | } |
| @@ -0,0 +1,7 @@ | ||
| 1 | +import { SlashCommandExecutor } from './SlashCommandExecutor.js'; | |
| 2 | + | |
| 3 | +export class SlashCommandBreak extends SlashCommandExecutor { | |
| 4 | + get value() { | |
| 5 | + return this.unnamedArgumentList[0]?.value; | |
| 6 | + } | |
| 7 | +} | |
| @@ -0,0 +1,7 @@ | ||
| 1 | +export class SlashCommandBreakController { | |
| 2 | + /**@type {boolean} */ isBreak = false; | |
| 3 | + | |
| 4 | + break() { | |
| 5 | + this.isBreak = true; | |
| 6 | + } | |
| 7 | +} | |
| @@ -0,0 +1,3 @@ | ||
| 1 | +import { SlashCommandExecutor } from './SlashCommandExecutor.js'; | |
| 2 | + | |
| 3 | +export class SlashCommandBreakPoint extends SlashCommandExecutor {} | |
| @@ -1,9 +1,13 @@ | ||
| 1 | 1 | import { substituteParams } from '../../script.js'; |
| 2 | 2 | import { delay, escapeRegex, uuidv4 } from '../utils.js'; |
| 3 | 3 | import { SlashCommand } from './SlashCommand.js'; |
| 4 | 4 | import { SlashCommandAbortController } from './SlashCommandAbortController.js'; |
| 5 | 5 | import { SlashCommandClosureExecutorSlashCommandBreak } from './SlashCommandClosureExecutorSlashCommandBreak.js'; |
| 6 | +import { SlashCommandBreakController } from './SlashCommandBreakController.js'; | |
| 7 | +import { SlashCommandBreakPoint } from './SlashCommandBreakPoint.js'; | |
| 6 | 8 | import { SlashCommandClosureResult } from './SlashCommandClosureResult.js'; |
| 9 | +import { SlashCommandDebugController } from './SlashCommandDebugController.js'; | |
| 10 | +import { SlashCommandExecutionError } from './SlashCommandExecutionError.js'; | |
| 7 | 11 | import { SlashCommandExecutor } from './SlashCommandExecutor.js'; |
| 8 | 12 | import { SlashCommandNamedArgumentAssignment } from './SlashCommandNamedArgumentAssignment.js'; |
| 9 | 13 | import { SlashCommandScope } from './SlashCommandScope.js'; |
| @@ -17,8 +21,20 @@ export class SlashCommandClosure { | ||
| 17 | 21 | /**@type {SlashCommandNamedArgumentAssignment[]}*/ providedArgumentList = []; |
| 18 | 22 | /**@type {SlashCommandExecutor[]}*/ executorList = []; |
| 19 | 23 | /**@type {SlashCommandAbortController}*/ abortController; |
| 24 | + /**@type {SlashCommandBreakController}*/ breakController; | |
| 25 | + /**@type {SlashCommandDebugController}*/ debugController; | |
| 20 | 26 | /**@type {(done:number, total:number)=>void}*/ onProgress; |
| 21 | 27 | /**@type {string}*/ rawText; |
| 28 | + /**@type {string}*/ fullText; | |
| 29 | + /**@type {string}*/ parserContext; | |
| 30 | + /**@type {string}*/ #source = uuidv4(); | |
| 31 | + get source() { return this.#source; } | |
| 32 | + set source(value) { | |
| 33 | + this.#source = value; | |
| 34 | + for (const executor of this.executorList) { | |
| 35 | + executor.source = value; | |
| 36 | + } | |
| 37 | + } | |
| 22 | 38 | |
| 23 | 39 | /**@type {number}*/ |
| 24 | 40 | get commandCount() { |
| @@ -30,7 +46,7 @@ export class SlashCommandClosure { | ||
| 30 | 46 | } |
| 31 | 47 | |
| 32 | 48 | toString() { |
| 33 | - return '[Closure]'; | |
| 49 | + return `[Closure]${this.executeNow ? '()' : ''}`; | |
| 34 | 50 | } |
| 35 | 51 | |
| 36 | 52 | /** |
| @@ -43,16 +59,37 @@ export class SlashCommandClosure { | ||
| 43 | 59 | let isList = false; |
| 44 | 60 | let listValues = []; |
| 45 | 61 | scope = scope ?? this.scope; |
| 46 | - const macros = scope.macroList.map(it=>escapeRegex(it.key)).join('|'); | |
| 62 | + const escapeMacro = (it, isAnchored = false)=>{ | |
| 47 | - const re = new RegExp(`({{pipe}})|(?:{{var::([^\\s]+?)(?:::((?!}}).+))?}})|(?:{{(${macros})}})`); | |
| 63 | + const regexText = escapeRegex(it.key.replace(/\*/g, '~~~WILDCARD~~~')) | |
| 64 | + .replaceAll('~~~WILDCARD~~~', '(?:(?:(?!(?:::|}})).)*)') | |
| 65 | + ; | |
| 66 | + if (isAnchored) { | |
| 67 | + return `^${regexText}$`; | |
| 68 | + } | |
| 69 | + return regexText; | |
| 70 | + }; | |
| 71 | + const macroList = scope.macroList.toSorted((a,b)=>{ | |
| 72 | + if (a.key.includes('*') && !b.key.includes('*')) return 1; | |
| 73 | + if (!a.key.includes('*') && b.key.includes('*')) return -1; | |
| 74 | + if (a.key.includes('*') && b.key.includes('*')) return b.key.indexOf('*') - a.key.indexOf('*'); | |
| 75 | + return 0; | |
| 76 | + }); | |
| 77 | + const macros = macroList.map(it=>escapeMacro(it)).join('|'); | |
| 78 | + const re = new RegExp(`(?<pipe>{{pipe}})|(?:{{var::(?<var>[^\\s]+?)(?:::(?<varIndex>(?!}}).+))?}})|(?:{{(?<macro>${macros})}})`); | |
| 48 | 79 | let done = ''; |
| 49 | 80 | let remaining = text; |
| 50 | 81 | while (re.test(remaining)) { |
| 51 | 82 | const match = re.exec(remaining); |
| 52 | 83 | const before = substituteParams(remaining.slice(0, match.index)); |
| 53 | 84 | const after = remaining.slice(match.index + match[0].length); |
| 54 | 85 | const replacer = match[1].groups.pipe ? scope.pipe : match[2].groups.var ? scope.getVariable(match[2].groups.var, match[3].groups.index) : scope.macroList.find(it=>it.key == match[4].groups.macro || new RegExp(escapeMacro(it, true)).test(match.groups.macro))?.value; |
| 55 | 86 | if (replacer instanceof SlashCommandClosure) { |
| 87 | + replacer.abortController = this.abortController; | |
| 88 | + replacer.breakController = this.breakController; | |
| 89 | + replacer.scope.parent = this.scope; | |
| 90 | + if (this.debugController && !replacer.debugController) { | |
| 91 | + replacer.debugController = this.debugController; | |
| 92 | + } | |
| 56 | 93 | isList = true; |
| 57 | 94 | if (match.index > 0) { |
| 58 | 95 | listValues.push(before); |
| @@ -87,6 +124,12 @@ export class SlashCommandClosure { | ||
| 87 | 124 | closure.providedArgumentList = this.providedArgumentList; |
| 88 | 125 | closure.executorList = this.executorList; |
| 89 | 126 | closure.abortController = this.abortController; |
| 127 | + closure.breakController = this.breakController; | |
| 128 | + closure.debugController = this.debugController; | |
| 129 | + closure.rawText = this.rawText; | |
| 130 | + closure.fullText = this.fullText; | |
| 131 | + closure.parserContext = this.parserContext; | |
| 132 | + closure.source = this.source; | |
| 90 | 133 | closure.onProgress = this.onProgress; |
| 91 | 134 | return closure; |
| 92 | 135 | } |
| @@ -96,11 +139,22 @@ export class SlashCommandClosure { | ||
| 96 | 139 | * @returns {Promise<SlashCommandClosureResult>} |
| 97 | 140 | */ |
| 98 | 141 | async execute() { |
| 142 | + // execute a copy of the closure to no taint it and its scope with the effects of its execution | |
| 143 | + // as this would affect the closure being called a second time (e.g., loop, multiple /run calls) | |
| 99 | 144 | const closure = this.getCopy(); |
| 100 | 145 | returnconst awaitgen = closure.executeDirect(); |
| 146 | + let step; | |
| 147 | + while (!step?.done) { | |
| 148 | + step = await gen.next(this.debugController?.testStepping(this) ?? false); | |
| 149 | + if (!(step.value instanceof SlashCommandClosureResult) && this.debugController) { | |
| 150 | + this.debugController.isStepping = await this.debugController.awaitBreakPoint(step.value.closure, step.value.executor); | |
| 151 | + } | |
| 152 | + } | |
| 153 | + return step.value; | |
| 101 | 154 | } |
| 102 | 155 | |
| 103 | 156 | async * executeDirect() { |
| 157 | + this.debugController?.down(this); | |
| 104 | 158 | // closure arguments |
| 105 | 159 | for (const arg of this.argumentList) { |
| 106 | 160 | let v = arg.value; |
| @@ -108,6 +162,7 @@ export class SlashCommandClosure { | ||
| 108 | 162 | /**@type {SlashCommandClosure}*/ |
| 109 | 163 | const closure = v; |
| 110 | 164 | closure.scope.parent = this.scope; |
| 165 | + closure.breakController = this.breakController; | |
| 111 | 166 | if (closure.executeNow) { |
| 112 | 167 | v = (await closure.execute())?.pipe; |
| 113 | 168 | } else { |
| @@ -131,6 +186,7 @@ export class SlashCommandClosure { | ||
| 131 | 186 | /**@type {SlashCommandClosure}*/ |
| 132 | 187 | const closure = v; |
| 133 | 188 | closure.scope.parent = this.scope; |
| 189 | + closure.breakController = this.breakController; | |
| 134 | 190 | if (closure.executeNow) { |
| 135 | 191 | v = (await closure.execute())?.pipe; |
| 136 | 192 | } else { |
| @@ -149,34 +205,190 @@ export class SlashCommandClosure { | ||
| 149 | 205 | this.scope.setVariable(arg.name, v); |
| 150 | 206 | } |
| 151 | 207 | |
| 152 | - let done = 0; | |
| 153 | 208 | if (this.executorList.length == 0) { |
| 154 | 209 | this.scope.pipe = ''; |
| 155 | 210 | } |
| 211 | + const stepper = this.executeStep(); | |
| 212 | + let step; | |
| 213 | + while (!step?.done && !this.breakController?.isBreak) { | |
| 214 | + // get executor before execution | |
| 215 | + step = await stepper.next(); | |
| 216 | + if (step.value instanceof SlashCommandBreakPoint) { | |
| 217 | + console.log('encountered SlashCommandBreakPoint'); | |
| 218 | + if (this.debugController) { | |
| 219 | + // resolve args | |
| 220 | + step = await stepper.next(); | |
| 221 | + // "execute" breakpoint | |
| 222 | + step = await stepper.next(); | |
| 223 | + // get next executor | |
| 224 | + step = await stepper.next(); | |
| 225 | + // breakpoint has to yield before arguments are resolved if one of the | |
| 226 | + // arguments is an immediate closure, otherwise you cannot step into the | |
| 227 | + // immediate closure | |
| 228 | + const hasImmediateClosureInNamedArgs = /**@type {SlashCommandExecutor}*/(step.value)?.namedArgumentList?.find(it=>it.value instanceof SlashCommandClosure && it.value.executeNow); | |
| 229 | + const hasImmediateClosureInUnnamedArgs = /**@type {SlashCommandExecutor}*/(step.value)?.unnamedArgumentList?.find(it=>it.value instanceof SlashCommandClosure && it.value.executeNow); | |
| 230 | + if (hasImmediateClosureInNamedArgs || hasImmediateClosureInUnnamedArgs) { | |
| 231 | + this.debugController.isStepping = yield { closure:this, executor:step.value }; | |
| 232 | + } else { | |
| 233 | + this.debugController.isStepping = true; | |
| 234 | + this.debugController.stepStack[this.debugController.stepStack.length - 1] = true; | |
| 235 | + } | |
| 236 | + } | |
| 237 | + } else if (!step.done && this.debugController?.testStepping(this)) { | |
| 238 | + this.debugController.isSteppingInto = false; | |
| 239 | + // if stepping, have to yield before arguments are resolved if one of the arguments | |
| 240 | + // is an immediate closure, otherwise you cannot step into the immediate closure | |
| 241 | + const hasImmediateClosureInNamedArgs = /**@type {SlashCommandExecutor}*/(step.value)?.namedArgumentList?.find(it=>it.value instanceof SlashCommandClosure && it.value.executeNow); | |
| 242 | + const hasImmediateClosureInUnnamedArgs = /**@type {SlashCommandExecutor}*/(step.value)?.unnamedArgumentList?.find(it=>it.value instanceof SlashCommandClosure && it.value.executeNow); | |
| 243 | + if (hasImmediateClosureInNamedArgs || hasImmediateClosureInUnnamedArgs) { | |
| 244 | + this.debugController.isStepping = yield { closure:this, executor:step.value }; | |
| 245 | + } | |
| 246 | + } | |
| 247 | + // resolve args | |
| 248 | + step = await stepper.next(); | |
| 249 | + if (step.value instanceof SlashCommandBreak) { | |
| 250 | + console.log('encountered SlashCommandBreak'); | |
| 251 | + if (this.breakController) { | |
| 252 | + this.breakController?.break(); | |
| 253 | + break; | |
| 254 | + } | |
| 255 | + } else if (!step.done && this.debugController?.testStepping(this)) { | |
| 256 | + this.debugController.isSteppingInto = false; | |
| 257 | + this.debugController.isStepping = yield { closure:this, executor:step.value }; | |
| 258 | + } | |
| 259 | + // execute executor | |
| 260 | + step = await stepper.next(); | |
| 261 | + } | |
| 262 | + | |
| 263 | + // if execution has returned a closure result, return that (should only happen on abort) | |
| 264 | + if (step.value instanceof SlashCommandClosureResult) { | |
| 265 | + this.debugController?.up(); | |
| 266 | + return step.value; | |
| 267 | + } | |
| 268 | + /**@type {SlashCommandClosureResult} */ | |
| 269 | + const result = Object.assign(new SlashCommandClosureResult(), { pipe: this.scope.pipe, isBreak: this.breakController?.isBreak ?? false }); | |
| 270 | + this.debugController?.up(); | |
| 271 | + return result; | |
| 272 | + } | |
| 273 | + /** | |
| 274 | + * Generator that steps through the executor list. | |
| 275 | + * Every executor is split into three steps: | |
| 276 | + * - before arguments are resolved | |
| 277 | + * - after arguments are resolved | |
| 278 | + * - after execution | |
| 279 | + */ | |
| 280 | + async * executeStep() { | |
| 281 | + let done = 0; | |
| 282 | + let isFirst = true; | |
| 156 | 283 | for (const executor of this.executorList) { |
| 157 | 284 | this.onProgress?.(done, this.commandCount); |
| 158 | 285 | if (executor instanceof SlashCommandClosureExecutorthis.debugController) { |
| 159 | 286 | const closure = this.scopedebugController.getVariablesetExecutor(executor.name); |
| 160 | - if (!closure || !(closure instanceof SlashCommandClosure)) throw new Error(`${executor.name} is not a closure.`); | |
| 287 | + this.debugController.namedArguments = undefined; | |
| 161 | 288 | closurethis.scopedebugController.parentunnamedArguments = this.scopeundefined; |
| 162 | - closure.providedArgumentList = executor.providedArgumentList; | |
| 289 | + } | |
| 163 | - const result = await closure.execute(); | |
| 290 | + // yield before doing anything with this executor, the debugger might want to do | |
| 164 | - this.scope.pipe = result.pipe; | |
| 291 | + // something with it (e.g., breakpoint, immediate closures that need resolving | |
| 165 | - } else { | |
| 292 | + // or stepping into) | |
| 293 | + yield executor; | |
| 166 | 294 | /**@type {import('./SlashCommand.js').NamedArguments} */ |
| 295 | + // @ts-ignore | |
| 167 | 296 | let args = { |
| 168 | 297 | _scope: this.scope, |
| 169 | 298 | _parserFlags: executor.parserFlags, |
| 170 | 299 | _abortController: this.abortController, |
| 300 | + _debugController: this.debugController, | |
| 171 | 301 | _hasUnnamedArgument: executor.unnamedArgumentList.length > 0, |
| 172 | 302 | }; |
| 173 | - let value; | |
| 303 | + if (executor instanceof SlashCommandBreakPoint) { | |
| 304 | + // nothing to do for breakpoints, just raise counter and yield for "before exec" | |
| 305 | + done++; | |
| 306 | + yield executor; | |
| 307 | + isFirst = false; | |
| 308 | + } else if (executor instanceof SlashCommandBreak) { | |
| 309 | + // /break need to resolve the unnamed arg and put it into pipe, then yield | |
| 310 | + // for "before exec" | |
| 311 | + const value = await this.substituteUnnamedArgument(executor, isFirst, args); | |
| 312 | + done += this.executorList.length - this.executorList.indexOf(executor); | |
| 313 | + this.scope.pipe = value ?? this.scope.pipe; | |
| 314 | + yield executor; | |
| 315 | + isFirst = false; | |
| 316 | + } else { | |
| 317 | + // regular commands do all the argument resolving logic... | |
| 318 | + await this.substituteNamedArguments(executor, args); | |
| 319 | + let value = await this.substituteUnnamedArgument(executor, isFirst, args); | |
| 320 | + | |
| 321 | + let abortResult = await this.testAbortController(); | |
| 322 | + if (abortResult) { | |
| 323 | + return abortResult; | |
| 324 | + } | |
| 325 | + if (this.debugController) { | |
| 326 | + this.debugController.namedArguments = args; | |
| 327 | + this.debugController.unnamedArguments = value ?? ''; | |
| 328 | + } | |
| 329 | + // then yield for "before exec" | |
| 330 | + yield executor; | |
| 331 | + // followed by command execution | |
| 332 | + executor.onProgress = (subDone, subTotal)=>this.onProgress?.(done + subDone, this.commandCount); | |
| 333 | + const isStepping = this.debugController?.testStepping(this); | |
| 334 | + if (this.debugController) { | |
| 335 | + this.debugController.isStepping = false || this.debugController.isSteppingInto; | |
| 336 | + } | |
| 337 | + try { | |
| 338 | + this.scope.pipe = await executor.command.callback(args, value ?? ''); | |
| 339 | + } catch (ex) { | |
| 340 | + throw new SlashCommandExecutionError(ex, ex.message, executor.name, executor.start, executor.end, this.fullText.slice(executor.start, executor.end), this.fullText); | |
| 341 | + } | |
| 342 | + if (this.debugController) { | |
| 343 | + this.debugController.namedArguments = undefined; | |
| 344 | + this.debugController.unnamedArguments = undefined; | |
| 345 | + this.debugController.isStepping = isStepping; | |
| 346 | + } | |
| 347 | + this.#lintPipe(executor.command); | |
| 348 | + done += executor.commandCount; | |
| 349 | + this.onProgress?.(done, this.commandCount); | |
| 350 | + abortResult = await this.testAbortController(); | |
| 351 | + if (abortResult) { | |
| 352 | + return abortResult; | |
| 353 | + } | |
| 354 | + } | |
| 355 | + // finally, yield for "after exec" | |
| 356 | + yield executor; | |
| 357 | + isFirst = false; | |
| 358 | + } | |
| 359 | + } | |
| 360 | + | |
| 361 | + async testPaused() { | |
| 362 | + while (!this.abortController?.signal?.aborted && this.abortController?.signal?.paused) { | |
| 363 | + await delay(200); | |
| 364 | + } | |
| 365 | + } | |
| 366 | + async testAbortController() { | |
| 367 | + await this.testPaused(); | |
| 368 | + if (this.abortController?.signal?.aborted) { | |
| 369 | + const result = new SlashCommandClosureResult(); | |
| 370 | + result.isAborted = true; | |
| 371 | + result.isQuietlyAborted = this.abortController.signal.isQuiet; | |
| 372 | + result.abortReason = this.abortController.signal.reason.toString(); | |
| 373 | + return result; | |
| 374 | + } | |
| 375 | + } | |
| 376 | + | |
| 377 | + /** | |
| 378 | + * @param {SlashCommandExecutor} executor | |
| 379 | + * @param {import('./SlashCommand.js').NamedArguments} args | |
| 380 | + */ | |
| 381 | + async substituteNamedArguments(executor, args) { | |
| 174 | 382 | // substitute named arguments |
| 175 | 383 | for (const arg of executor.namedArgumentList) { |
| 176 | 384 | if (arg.value instanceof SlashCommandClosure) { |
| 177 | 385 | /**@type {SlashCommandClosure}*/ |
| 178 | 386 | const closure = arg.value; |
| 179 | 387 | closure.scope.parent = this.scope; |
| 388 | + closure.breakController = this.breakController; | |
| 389 | + if (this.debugController && !closure.debugController) { | |
| 390 | + closure.debugController = this.debugController; | |
| 391 | + } | |
| 180 | 392 | if (closure.executeNow) { |
| 181 | 393 | args[arg.name] = (await closure.execute())?.pipe; |
| 182 | 394 | } else { |
| @@ -193,10 +405,19 @@ export class SlashCommandClosure { | ||
| 193 | 405 | ; |
| 194 | 406 | } |
| 195 | 407 | } |
| 408 | + } | |
| 196 | 409 | |
| 410 | + /** | |
| 411 | + * @param {SlashCommandExecutor} executor | |
| 412 | + * @param {boolean} isFirst | |
| 413 | + * @param {import('./SlashCommand.js').NamedArguments} args | |
| 414 | + * @returns {Promise<string|SlashCommandClosure|(string|SlashCommandClosure)[]>} | |
| 415 | + */ | |
| 416 | + async substituteUnnamedArgument(executor, isFirst, args) { | |
| 417 | + let value; | |
| 197 | 418 | // substitute unnamed argument |
| 198 | 419 | if (executor.unnamedArgumentList.length == 0) { |
| 199 | 420 | if (!isFirst && executor.injectPipe) { |
| 200 | 421 | value = this.scope.pipe; |
| 201 | 422 | args._hasUnnamedArgument = this.scope.pipe !== null && this.scope.pipe !== undefined; |
| 202 | 423 | } |
| @@ -208,6 +429,10 @@ export class SlashCommandClosure { | ||
| 208 | 429 | /**@type {SlashCommandClosure}*/ |
| 209 | 430 | const closure = v; |
| 210 | 431 | closure.scope.parent = this.scope; |
| 432 | + closure.breakController = this.breakController; | |
| 433 | + if (this.debugController && !closure.debugController) { | |
| 434 | + closure.debugController = this.debugController; | |
| 435 | + } | |
| 211 | 436 | if (closure.executeNow) { |
| 212 | 437 | v = (await closure.execute())?.pipe; |
| 213 | 438 | } else { |
| @@ -242,41 +467,7 @@ export class SlashCommandClosure { | ||
| 242 | 467 | return v; |
| 243 | 468 | }); |
| 244 | 469 | } |
| 245 | - | |
| 470 | + return value; | |
| 246 | - let abortResult = await this.testAbortController(); | |
| 247 | - if (abortResult) { | |
| 248 | - return abortResult; | |
| 249 | - } | |
| 250 | - executor.onProgress = (subDone, subTotal)=>this.onProgress?.(done + subDone, this.commandCount); | |
| 251 | - this.scope.pipe = await executor.command.callback(args, value ?? ''); | |
| 252 | - this.#lintPipe(executor.command); | |
| 253 | - done += executor.commandCount; | |
| 254 | - this.onProgress?.(done, this.commandCount); | |
| 255 | - abortResult = await this.testAbortController(); | |
| 256 | - if (abortResult) { | |
| 257 | - return abortResult; | |
| 258 | - } | |
| 259 | - } | |
| 260 | - } | |
| 261 | - /**@type {SlashCommandClosureResult} */ | |
| 262 | - const result = Object.assign(new SlashCommandClosureResult(), { pipe: this.scope.pipe }); | |
| 263 | - return result; | |
| 264 | - } | |
| 265 | - | |
| 266 | - async testPaused() { | |
| 267 | - while (!this.abortController?.signal?.aborted && this.abortController?.signal?.paused) { | |
| 268 | - await delay(200); | |
| 269 | - } | |
| 270 | - } | |
| 271 | - async testAbortController() { | |
| 272 | - await this.testPaused(); | |
| 273 | - if (this.abortController?.signal?.aborted) { | |
| 274 | - const result = new SlashCommandClosureResult(); | |
| 275 | - result.isAborted = true; | |
| 276 | - result.isQuietlyAborted = this.abortController.signal.isQuiet; | |
| 277 | - result.abortReason = this.abortController.signal.reason.toString(); | |
| 278 | - return result; | |
| 279 | - } | |
| 280 | 471 | } |
| 281 | 472 | |
| 282 | 473 | /** |
| @@ -285,8 +476,11 @@ export class SlashCommandClosure { | ||
| 285 | 476 | */ |
| 286 | 477 | #lintPipe(command) { |
| 287 | 478 | if (this.scope.pipe === undefined || this.scope.pipe === null) { |
| 288 | 479 | console.warn(`/${command.name} returned undefined or null. Auto-fixing to empty string.`); |
| 289 | 480 | this.scope.pipe = ''; |
| 481 | + } else if (!(typeof this.scope.pipe == 'string' || this.scope.pipe instanceof SlashCommandClosure)) { | |
| 482 | + console.warn(`/${command.name} returned illegal type (${typeof this.scope.pipe} - ${this.scope.pipe.constructor?.name ?? ''}). Auto-fixing to stringified JSON.`); | |
| 483 | + this.scope.pipe = JSON.stringify(this.scope.pipe) ?? ''; | |
| 290 | 484 | } |
| 291 | 485 | } |
| 292 | 486 | } |
| @@ -1,7 +0,0 @@ | ||
| 1 | -import { SlashCommandNamedArgumentAssignment } from './SlashCommandNamedArgumentAssignment.js'; | |
| 2 | - | |
| 3 | -export class SlashCommandClosureExecutor { | |
| 4 | - /**@type {String}*/ name = ''; | |
| 5 | - // @ts-ignore | |
| 6 | - /**@type {SlashCommandNamedArgumentAssignment[]}*/ providedArgumentList = []; | |
| 7 | -} | |
| @@ -1,6 +1,7 @@ | ||
| 1 | 1 | export class SlashCommandClosureResult { |
| 2 | 2 | /**@type {boolean}*/ interrupt = false; |
| 3 | 3 | /**@type {string}*/ pipe; |
| 4 | + /**@type {boolean}*/ isBreak = false; | |
| 4 | 5 | /**@type {boolean}*/ isAborted = false; |
| 5 | 6 | /**@type {boolean}*/ isQuietlyAborted = false; |
| 6 | 7 | /**@type {string}*/ abortReason; |
| @@ -6,6 +6,7 @@ import { searchCharByName, getTagsList, tags } from '../tags.js'; | ||
| 6 | 6 | import { world_names } from '../world-info.js'; |
| 7 | 7 | import { SlashCommandClosure } from './SlashCommandClosure.js'; |
| 8 | 8 | import { SlashCommandEnumValue, enumTypes } from './SlashCommandEnumValue.js'; |
| 9 | +import { SlashCommandScope } from "./SlashCommandScope.js"; | |
| 9 | 10 | |
| 10 | 11 | /** |
| 11 | 12 | * A collection of regularly used enum icons |
| @@ -134,16 +135,16 @@ export const commonEnumProviders = { | ||
| 134 | 135 | * Can be filtered by `type` to only show global or local variables |
| 135 | 136 | * |
| 136 | 137 | * @param {...('global'|'local'|'scope'|'all')} type - The type of variables to include in the array. Can be 'all', 'global', or 'local'. |
| 137 | 138 | * @returns {(executor:SlashCommandExecutor, scope:SlashCommandScope) => SlashCommandEnumValue[]} |
| 138 | 139 | */ |
| 139 | 140 | variables: (...type) => (executor, scope) => { |
| 140 | 141 | const types = type.flat(); |
| 141 | 142 | const isAll = types.includes('all'); |
| 142 | 143 | return [ |
| 143 | 144 | ...isAll || types.includes('globalscope') ? Object.keys(extension_settings.variablesscope.global ?? [])allVariableNames.map(name => new SlashCommandEnumValue(name, null, enumTypes.macrovariable, enumIcons.globalVariablescopeVariable)) : [], |
| 144 | 145 | ...isAll || types.includes('local') ? Object.keys(chat_metadata.variables ?? []).map(name => new SlashCommandEnumValue(name, null, enumTypes.name, enumIcons.localVariable)) : [], |
| 145 | 146 | ...isAll || types.includes('scopeglobal') ? Object.keys(extension_settings.variables.global ?? []).map(name => new SlashCommandEnumValue(name, null, enumTypes.variablemacro, enumIcons.scopeVariableglobalVariable)) : [], // TODO: Add scoped variables here, Lenny |
| 146 | - ]; | |
| 147 | + ].filter((item, idx, list)=>idx == list.findIndex(it=>it.value == item.value)); | |
| 147 | 148 | }, |
| 148 | 149 | |
| 149 | 150 | /** |
| @@ -0,0 +1,83 @@ | ||
| 1 | +import { SlashCommandClosure } from './SlashCommandClosure.js'; | |
| 2 | +import { SlashCommandExecutor } from './SlashCommandExecutor.js'; | |
| 3 | + | |
| 4 | +export class SlashCommandDebugController { | |
| 5 | + /**@type {SlashCommandClosure[]} */ stack = []; | |
| 6 | + /**@type {SlashCommandExecutor[]} */ cmdStack = []; | |
| 7 | + /**@type {boolean[]} */ stepStack = []; | |
| 8 | + /**@type {boolean} */ isStepping = false; | |
| 9 | + /**@type {boolean} */ isSteppingInto = false; | |
| 10 | + /**@type {boolean} */ isSteppingOut = false; | |
| 11 | + | |
| 12 | + /**@type {object} */ namedArguments; | |
| 13 | + /**@type {string|SlashCommandClosure|(string|SlashCommandClosure)[]} */ unnamedArguments; | |
| 14 | + | |
| 15 | + /**@type {Promise<boolean>} */ continuePromise; | |
| 16 | + /**@type {(boolean)=>void} */ continueResolver; | |
| 17 | + | |
| 18 | + /**@type {(closure:SlashCommandClosure, executor:SlashCommandExecutor)=>Promise<boolean>} */ onBreakPoint; | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + testStepping(closure) { | |
| 24 | + return this.stepStack[this.stack.indexOf(closure)]; | |
| 25 | + } | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + down(closure) { | |
| 31 | + this.stack.push(closure); | |
| 32 | + if (this.stepStack.length < this.stack.length) { | |
| 33 | + this.stepStack.push(this.isSteppingInto); | |
| 34 | + } | |
| 35 | + } | |
| 36 | + up() { | |
| 37 | + this.stack.pop(); | |
| 38 | + while (this.cmdStack.length > this.stack.length) this.cmdStack.pop(); | |
| 39 | + this.stepStack.pop(); | |
| 40 | + } | |
| 41 | + | |
| 42 | + setExecutor(executor) { | |
| 43 | + this.cmdStack[this.stack.length - 1] = executor; | |
| 44 | + } | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + resume() { | |
| 49 | + this.continueResolver?.(false); | |
| 50 | + this.continuePromise = null; | |
| 51 | + this.stepStack.forEach((_,idx)=>this.stepStack[idx] = false); | |
| 52 | + } | |
| 53 | + step() { | |
| 54 | + this.stepStack.forEach((_,idx)=>this.stepStack[idx] = true); | |
| 55 | + this.continueResolver?.(true); | |
| 56 | + this.continuePromise = null; | |
| 57 | + } | |
| 58 | + stepInto() { | |
| 59 | + this.isSteppingInto = true; | |
| 60 | + this.stepStack.forEach((_,idx)=>this.stepStack[idx] = true); | |
| 61 | + this.continueResolver?.(true); | |
| 62 | + this.continuePromise = null; | |
| 63 | + } | |
| 64 | + stepOut() { | |
| 65 | + this.isSteppingOut = true; | |
| 66 | + this.stepStack[this.stepStack.length - 1] = false; | |
| 67 | + this.continueResolver?.(false); | |
| 68 | + this.continuePromise = null; | |
| 69 | + } | |
| 70 | + | |
| 71 | + async awaitContinue() { | |
| 72 | + this.continuePromise ??= new Promise(resolve=>{ | |
| 73 | + this.continueResolver = resolve; | |
| 74 | + }); | |
| 75 | + this.isStepping = await this.continuePromise; | |
| 76 | + return this.isStepping; | |
| 77 | + } | |
| 78 | + | |
| 79 | + async awaitBreakPoint(closure, executor) { | |
| 80 | + this.isStepping = await this.onBreakPoint(closure, executor); | |
| 81 | + return this.isStepping; | |
| 82 | + } | |
| 83 | +} | |
| @@ -3,6 +3,17 @@ import { SlashCommand } from './SlashCommand.js'; | ||
| 3 | 3 | import { SlashCommandEnumValue } from './SlashCommandEnumValue.js'; |
| 4 | 4 | |
| 5 | 5 | export class SlashCommandEnumAutoCompleteOption extends AutoCompleteOption { |
| 6 | + /** | |
| 7 | + * @param {SlashCommand} cmd | |
| 8 | + * @param {SlashCommandEnumValue} enumValue | |
| 9 | + * @returns {SlashCommandEnumAutoCompleteOption} | |
| 10 | + */ | |
| 11 | + static from(cmd, enumValue) { | |
| 12 | + const mapped = this.valueToOptionMap.find(it=>enumValue instanceof it.value)?.option ?? this; | |
| 13 | + return new mapped(cmd, enumValue); | |
| 14 | + } | |
| 15 | + /**@type {{value:(typeof SlashCommandEnumValue), option:(typeof SlashCommandEnumAutoCompleteOption)}[]} */ | |
| 16 | + static valueToOptionMap = []; | |
| 6 | 17 | /**@type {SlashCommand}*/ cmd; |
| 7 | 18 | /**@type {SlashCommandEnumValue}*/ enumValue; |
| 8 | 19 | |
| @@ -13,7 +24,7 @@ export class SlashCommandEnumAutoCompleteOption extends AutoCompleteOption { | ||
| 13 | 24 | * @param {SlashCommandEnumValue} enumValue |
| 14 | 25 | */ |
| 15 | 26 | constructor(cmd, enumValue) { |
| 16 | 27 | super(enumValue.value, enumValue.typeIcon, enumValue.type, enumValue.matchProvider, enumValue.valueProvider, enumValue.makeSelectable); |
| 17 | 28 | this.cmd = cmd; |
| 18 | 29 | this.enumValue = enumValue; |
| 19 | 30 | } |
| @@ -1,3 +1,6 @@ | ||
| 1 | +import { SlashCommandExecutor } from './SlashCommandExecutor.js'; | |
| 2 | +import { SlashCommandScope } from './SlashCommandScope.js'; | |
| 3 | + | |
| 1 | 4 | |
| 2 | 5 | /** |
| 3 | 6 | * @typedef {'enum' | 'command' | 'namedArgument' | 'variable' | 'qr' | 'macro' | 'number' | 'name'} EnumType |
| @@ -37,14 +40,17 @@ export const enumTypes = { | ||
| 37 | 40 | getBasedOnIndex(index) { |
| 38 | 41 | const keys = Object.keys(this); |
| 39 | 42 | return this[keys[(index ?? 0) % keys.length]]; |
| 40 | 43 | }, |
| 41 | 44 | }; |
| 42 | 45 | |
| 43 | 46 | export class SlashCommandEnumValue { |
| 44 | 47 | /**@type {string}*/ value; |
| 45 | 48 | /**@type {string}*/ description; |
| 46 | 49 | /**@type {EnumType}*/ type = 'enum'; |
| 47 | 50 | /**@type {string}*/ typeIcon = '◊'; |
| 51 | + /**@type {(input:string)=>boolean}*/ matchProvider; | |
| 52 | + /**@type {(input:string)=>string}*/ valueProvider; | |
| 53 | + /**@type {boolean}*/ makeSelectable = false; | |
| 48 | 54 | |
| 49 | 55 | /** |
| 50 | 56 | * A constructor for creating a SlashCommandEnumValue instance. |
| @@ -52,13 +58,19 @@ export class SlashCommandEnumValue { | ||
| 52 | 58 | * @param {string} value - The value |
| 53 | 59 | * @param {string?} description - Optional description, displayed in a second line |
| 54 | 60 | * @param {EnumType?} type - type of the enum (defining its color) |
| 55 | 61 | * @param {string?} typeIcon - The icon to display (Can be pulled from `enumIcons` for common ones) |
| 62 | + * @param {(input:string)=>boolean?} matchProvider - A custom function to match autocomplete input instead of startsWith/includes/fuzzy. Should only be used for generic options like "any number" or "any string". "input" is the part of the text that is getting auto completed. | |
| 63 | + * @param {(input:string)=>string?} valueProvider - A function returning a value to be used in autocomplete instead of the enum value. "input" is the part of the text that is getting auto completed. By default, values with a valueProvider will not be selectable in the autocomplete (with tab/enter). | |
| 64 | + * @param {boolean?} makeSelectable - Set to true to make the value selectable (through tab/enter) even though a valueProvider exists. | |
| 56 | 65 | */ |
| 57 | 66 | constructor(value, description = null, type = 'enum', typeIcon = '◊', matchProvider = null, valueProvider = null, makeSelectable = false) { |
| 58 | 67 | this.value = value; |
| 59 | 68 | this.description = description; |
| 60 | 69 | this.type = type ?? 'enum'; |
| 61 | 70 | this.typeIcon = typeIcon; |
| 71 | + this.matchProvider = matchProvider; | |
| 72 | + this.valueProvider = valueProvider; | |
| 73 | + this.makeSelectable = makeSelectable; | |
| 62 | 74 | } |
| 63 | 75 | |
| 64 | 76 | toString() { |
| @@ -0,0 +1,60 @@ | ||
| 1 | +export class SlashCommandExecutionError extends Error { | |
| 2 | + /**@type {string} */ commandName; | |
| 3 | + /**@type {number} */ start; | |
| 4 | + /**@type {number} */ end; | |
| 5 | + /**@type {string} */ commandText; | |
| 6 | + | |
| 7 | + /**@type {string} */ text; | |
| 8 | + get index() { return this.start; } | |
| 9 | + | |
| 10 | + get line() { | |
| 11 | + return this.text.slice(0, this.index).replace(/[^\n]/g, '').length; | |
| 12 | + } | |
| 13 | + get column() { | |
| 14 | + return this.text.slice(0, this.index).split('\n').pop().length; | |
| 15 | + } | |
| 16 | + get hint() { | |
| 17 | + let lineOffset = this.line.toString().length; | |
| 18 | + let lineStart = this.index; | |
| 19 | + let start = this.index; | |
| 20 | + let end = this.index; | |
| 21 | + let offset = 0; | |
| 22 | + let lineCount = 0; | |
| 23 | + while (offset < 10000 && lineCount < 3 && start >= 0) { | |
| 24 | + if (this.text[start] == '\n') lineCount++; | |
| 25 | + if (lineCount == 0) lineStart--; | |
| 26 | + offset++; | |
| 27 | + start--; | |
| 28 | + } | |
| 29 | + if (this.text[start + 1] == '\n') start++; | |
| 30 | + offset = 0; | |
| 31 | + while (offset < 10000 && this.text[end] != '\n') { | |
| 32 | + offset++; | |
| 33 | + end++; | |
| 34 | + } | |
| 35 | + let hint = []; | |
| 36 | + let lines = this.text.slice(start + 1, end - 1).split('\n'); | |
| 37 | + let lineNum = this.line - lines.length + 1; | |
| 38 | + let tabOffset = 0; | |
| 39 | + for (const line of lines) { | |
| 40 | + const num = `${' '.repeat(lineOffset - lineNum.toString().length)}${lineNum}`; | |
| 41 | + lineNum++; | |
| 42 | + const untabbedLine = line.replace(/\t/g, ' '.repeat(4)); | |
| 43 | + tabOffset = untabbedLine.length - line.length; | |
| 44 | + hint.push(`${num}: ${untabbedLine}`); | |
| 45 | + } | |
| 46 | + hint.push(`${' '.repeat(this.index - lineStart + lineOffset + 1 + tabOffset)}^^^^^`); | |
| 47 | + return hint.join('\n'); | |
| 48 | + } | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + constructor(cause, message, commandName, start, end, commandText, fullText) { | |
| 53 | + super(message, { cause }); | |
| 54 | + this.commandName = commandName; | |
| 55 | + this.start = start; | |
| 56 | + this.end = end; | |
| 57 | + this.commandText = commandText; | |
| 58 | + this.text = fullText; | |
| 59 | + } | |
| 60 | +} | |
| @@ -1,4 +1,5 @@ | ||
| 1 | 1 | // eslint-disable-next-line no-unused-vars |
| 2 | +import { uuidv4 } from '../utils.js'; | |
| 2 | 3 | import { SlashCommand } from './SlashCommand.js'; |
| 3 | 4 | // eslint-disable-next-line no-unused-vars |
| 4 | 5 | import { SlashCommandClosure } from './SlashCommandClosure.js'; |
| @@ -16,6 +17,17 @@ export class SlashCommandExecutor { | ||
| 16 | 17 | /**@type {Number}*/ startUnnamedArgs; |
| 17 | 18 | /**@type {Number}*/ endUnnamedArgs; |
| 18 | 19 | /**@type {String}*/ name = ''; |
| 20 | + /**@type {String}*/ #source = uuidv4(); | |
| 21 | + get source() { return this.#source; } | |
| 22 | + set source(value) { | |
| 23 | + this.#source = value; | |
| 24 | + for (const arg of this.namedArgumentList.filter(it=>it.value instanceof SlashCommandClosure)) { | |
| 25 | + arg.value.source = value; | |
| 26 | + } | |
| 27 | + for (const arg of this.unnamedArgumentList.filter(it=>it.value instanceof SlashCommandClosure)) { | |
| 28 | + arg.value.source = value; | |
| 29 | + } | |
| 30 | + } | |
| 19 | 31 | /**@type {SlashCommand}*/ command; |
| 20 | 32 | // @ts-ignore |
| 21 | 33 | /**@type {SlashCommandNamedArgumentAssignment[]}*/ namedArgumentList = []; |
| @@ -17,7 +17,10 @@ import { SlashCommandAutoCompleteNameResult } from './SlashCommandAutoCompleteNa | ||
| 17 | 17 | import { SlashCommandUnnamedArgumentAssignment } from './SlashCommandUnnamedArgumentAssignment.js'; |
| 18 | 18 | import { SlashCommandEnumValue } from './SlashCommandEnumValue.js'; |
| 19 | 19 | import { MacroAutoCompleteOption } from '../autocomplete/MacroAutoCompleteOption.js'; |
| 20 | +import { SlashCommandBreakPoint } from './SlashCommandBreakPoint.js'; | |
| 21 | +import { SlashCommandDebugController } from './SlashCommandDebugController.js'; | |
| 20 | 22 | import { commonEnumProviders } from './SlashCommandCommonEnumsProvider.js'; |
| 23 | +import { SlashCommandBreak } from './SlashCommandBreak.js'; | |
| 21 | 24 | |
| 22 | 25 | /** @typedef {import('./SlashCommand.js').NamedArgumentsCapture} NamedArgumentsCapture */ |
| 23 | 26 | /** @typedef {import('./SlashCommand.js').NamedArguments} NamedArguments */ |
| @@ -53,7 +56,7 @@ export class SlashCommandParser { | ||
| 53 | 56 | * @param {SlashCommand} command |
| 54 | 57 | */ |
| 55 | 58 | static addCommandObject(command) { |
| 56 | 59 | const reserved = ['/', '#', ':', 'parser-flag', 'breakpoint']; |
| 57 | 60 | for (const start of reserved) { |
| 58 | 61 | if (command.name.toLowerCase().startsWith(start) || (command.aliases ?? []).find(a=>a.toLowerCase().startsWith(start))) { |
| 59 | 62 | throw new Error(`Illegal Name. Slash command name cannot begin with "${start}".`); |
| @@ -70,6 +73,18 @@ export class SlashCommandParser { | ||
| 70 | 73 | console.trace('WARN: Duplicate slash command registered!', [command.name, ...command.aliases]); |
| 71 | 74 | } |
| 72 | 75 | |
| 76 | + const stack = new Error().stack.split('\n').map(it=>it.trim()); | |
| 77 | + command.isExtension = stack.find(it=>it.includes('/scripts/extensions/')) != null; | |
| 78 | + command.isThirdParty = stack.find(it=>it.includes('/scripts/extensions/third-party/')) != null; | |
| 79 | + if (command.isThirdParty) { | |
| 80 | + command.source = stack.find(it=>it.includes('/scripts/extensions/third-party/')).replace(/^.*?\/scripts\/extensions\/third-party\/([^/]+)\/.*$/, '$1'); | |
| 81 | + } else if (command.isExtension) { | |
| 82 | + command.source = stack.find(it=>it.includes('/scripts/extensions/')).replace(/^.*?\/scripts\/extensions\/([^/]+)\/.*$/, '$1'); | |
| 83 | + } else { | |
| 84 | + const idx = stack.findLastIndex(it=>it.includes('at SlashCommandParser.')) + 1; | |
| 85 | + command.source = stack[idx].replace(/^.*?\/((?:scripts\/)?(?:[^/]+)\.js).*$/, '$1'); | |
| 86 | + } | |
| 87 | + | |
| 73 | 88 | this.commands[command.name] = command; |
| 74 | 89 | |
| 75 | 90 | if (Array.isArray(command.aliases)) { |
| @@ -89,6 +104,7 @@ export class SlashCommandParser { | ||
| 89 | 104 | /**@type {string}*/ text; |
| 90 | 105 | /**@type {number}*/ index; |
| 91 | 106 | /**@type {SlashCommandAbortController}*/ abortController; |
| 107 | + /**@type {SlashCommandDebugController}*/ debugController; | |
| 92 | 108 | /**@type {SlashCommandScope}*/ scope; |
| 93 | 109 | /**@type {SlashCommandClosure}*/ closure; |
| 94 | 110 | |
| @@ -101,6 +117,8 @@ export class SlashCommandParser { | ||
| 101 | 117 | /**@type {SlashCommandExecutor[]}*/ commandIndex; |
| 102 | 118 | /**@type {SlashCommandScope[]}*/ scopeIndex; |
| 103 | 119 | |
| 120 | + /**@type {string}*/ parserContext; | |
| 121 | + | |
| 104 | 122 | get userIndex() { return this.index; } |
| 105 | 123 | |
| 106 | 124 | get ahead() { |
| @@ -154,6 +172,21 @@ export class SlashCommandParser { | ||
| 154 | 172 | helpString: 'Write a comment.', |
| 155 | 173 | })); |
| 156 | 174 | } |
| 175 | + if (!Object.keys(this.commands).includes('breakpoint')) { | |
| 176 | + SlashCommandParser.addCommandObjectUnsafe(SlashCommand.fromProps({ name: 'breakpoint', | |
| 177 | + helpString: 'Set a breakpoint for debugging in the QR Editor.', | |
| 178 | + })); | |
| 179 | + } | |
| 180 | + if (!Object.keys(this.commands).includes('break')) { | |
| 181 | + SlashCommandParser.addCommandObjectUnsafe(SlashCommand.fromProps({ name: 'break', | |
| 182 | + helpString: 'Break out of a loop or closure executed through /run or /:', | |
| 183 | + unnamedArgumentList: [ | |
| 184 | + SlashCommandArgument.fromProps({ description: 'value to pass down the pipe instead of the current pipe value', | |
| 185 | + typeList: Object.values(ARGUMENT_TYPE), | |
| 186 | + }), | |
| 187 | + ], | |
| 188 | + })); | |
| 189 | + } | |
| 157 | 190 | |
| 158 | 191 | //TODO should not be re-registered from every instance |
| 159 | 192 | this.registerLanguage(); |
| @@ -191,13 +224,19 @@ export class SlashCommandParser { | ||
| 191 | 224 | |
| 192 | 225 | function getQuotedRunRegex() { |
| 193 | 226 | try { |
| 194 | 227 | return new RegExp('(".+?(?<!\\\\)")|(\\S+?)(\\||$|\\s)'); |
| 195 | 228 | } catch { |
| 196 | 229 | // fallback for browsers that don't support lookbehind |
| 197 | 230 | return /(".+?")|(\S+?)(\||$|\s)/; |
| 198 | 231 | } |
| 199 | 232 | } |
| 200 | 233 | |
| 234 | + const BLOCK_COMMENT = { | |
| 235 | + scope: 'comment', | |
| 236 | + begin: /\/\*/, | |
| 237 | + end: /\*\|/, | |
| 238 | + contains: [], | |
| 239 | + }; | |
| 201 | 240 | const COMMENT = { |
| 202 | 241 | scope: 'comment', |
| 203 | 242 | begin: /\/[/#]/, |
| @@ -205,9 +244,29 @@ export class SlashCommandParser { | ||
| 205 | 244 | contains: [], |
| 206 | 245 | }; |
| 207 | 246 | const ABORT = { |
| 208 | - scope: 'abort', | |
| 247 | + begin: /\/(abort|breakpoint)/, | |
| 209 | 248 | beginbeginScope: /\/'abort/', |
| 210 | 249 | end: /\||$|(?=:})/, |
| 250 | + excludeEnd: false, | |
| 251 | + returnEnd: true, | |
| 252 | + contains: [], | |
| 253 | + }; | |
| 254 | + const IMPORT = { | |
| 255 | + scope: 'command', | |
| 256 | + begin: /\/(import)/, | |
| 257 | + beginScope: 'keyword', | |
| 258 | + end: /\||$|(?=:})/, | |
| 259 | + excludeEnd: false, | |
| 260 | + returnEnd: true, | |
| 261 | + contains: [], | |
| 262 | + }; | |
| 263 | + const BREAK = { | |
| 264 | + scope: 'command', | |
| 265 | + begin: /\/(break)/, | |
| 266 | + beginScope: 'keyword', | |
| 267 | + end: /\||$|(?=:})/, | |
| 268 | + excludeEnd: false, | |
| 269 | + returnEnd: true, | |
| 211 | 270 | contains: [], |
| 212 | 271 | }; |
| 213 | 272 | const LET = { |
| @@ -218,26 +277,31 @@ export class SlashCommandParser { | ||
| 218 | 277 | 1: 'variable', |
| 219 | 278 | }, |
| 220 | 279 | end: /\||$|:}/, |
| 280 | + excludeEnd: false, | |
| 281 | + returnEnd: true, | |
| 221 | 282 | contains: [], |
| 222 | 283 | }; |
| 223 | 284 | const SETVAR = { |
| 224 | 285 | begin: /\/(setvar|setglobalvar)\s+/, |
| 225 | 286 | beginScope: 'variable', |
| 226 | 287 | end: /\||$|:}/, |
| 227 | 288 | excludeEnd: truefalse, |
| 289 | + returnEnd: true, | |
| 228 | 290 | contains: [], |
| 229 | 291 | }; |
| 230 | 292 | const GETVAR = { |
| 231 | 293 | begin: /\/(getvar|getglobalvar)\s+/, |
| 232 | 294 | beginScope: 'variable', |
| 233 | 295 | end: /\||$|:}/, |
| 234 | 296 | excludeEnd: truefalse, |
| 297 | + returnEnd: true, | |
| 235 | 298 | contains: [], |
| 236 | 299 | }; |
| 237 | 300 | const RUN = { |
| 238 | 301 | match: [ |
| 239 | 302 | /\/:/, |
| 240 | 303 | getQuotedRunRegex(), |
| 304 | + /\||$|(?=:})/, | |
| 241 | 305 | ], |
| 242 | 306 | className: { |
| 243 | 307 | 1: 'variable.language', |
| @@ -250,7 +314,8 @@ export class SlashCommandParser { | ||
| 250 | 314 | begin: /\/\S+/, |
| 251 | 315 | beginScope: 'title.function', |
| 252 | 316 | end: /\||$|(?=:})/, |
| 253 | 317 | excludeEnd: truefalse, |
| 318 | + returnEnd: true, | |
| 254 | 319 | contains: [], // defined later |
| 255 | 320 | }; |
| 256 | 321 | const CLOSURE = { |
| @@ -271,6 +336,19 @@ export class SlashCommandParser { | ||
| 271 | 336 | begin: /{{/, |
| 272 | 337 | end: /}}/, |
| 273 | 338 | }; |
| 339 | + const PIPEBREAK = { | |
| 340 | + beginScope: 'pipebreak', | |
| 341 | + begin: /\|\|/, | |
| 342 | + end: '', | |
| 343 | + }; | |
| 344 | + const PIPE = { | |
| 345 | + beginScope: 'pipe', | |
| 346 | + begin: /\|/, | |
| 347 | + end: '', | |
| 348 | + }; | |
| 349 | + BLOCK_COMMENT.contains.push( | |
| 350 | + BLOCK_COMMENT, | |
| 351 | + ); | |
| 274 | 352 | RUN.contains.push( |
| 275 | 353 | hljs.BACKSLASH_ESCAPE, |
| 276 | 354 | NAMED_ARG, |
| @@ -279,6 +357,22 @@ export class SlashCommandParser { | ||
| 279 | 357 | MACRO, |
| 280 | 358 | CLOSURE, |
| 281 | 359 | ); |
| 360 | + IMPORT.contains.push( | |
| 361 | + hljs.BACKSLASH_ESCAPE, | |
| 362 | + NAMED_ARG, | |
| 363 | + NUMBER, | |
| 364 | + MACRO, | |
| 365 | + CLOSURE, | |
| 366 | + hljs.QUOTE_STRING_MODE, | |
| 367 | + ); | |
| 368 | + BREAK.contains.push( | |
| 369 | + hljs.BACKSLASH_ESCAPE, | |
| 370 | + NAMED_ARG, | |
| 371 | + NUMBER, | |
| 372 | + MACRO, | |
| 373 | + CLOSURE, | |
| 374 | + hljs.QUOTE_STRING_MODE, | |
| 375 | + ); | |
| 282 | 376 | LET.contains.push( |
| 283 | 377 | hljs.BACKSLASH_ESCAPE, |
| 284 | 378 | NAMED_ARG, |
| @@ -303,6 +397,14 @@ export class SlashCommandParser { | ||
| 303 | 397 | MACRO, |
| 304 | 398 | CLOSURE, |
| 305 | 399 | ); |
| 400 | + ABORT.contains.push( | |
| 401 | + hljs.BACKSLASH_ESCAPE, | |
| 402 | + NAMED_ARG, | |
| 403 | + NUMBER, | |
| 404 | + MACRO, | |
| 405 | + CLOSURE, | |
| 406 | + hljs.QUOTE_STRING_MODE, | |
| 407 | + ); | |
| 306 | 408 | COMMAND.contains.push( |
| 307 | 409 | hljs.BACKSLASH_ESCAPE, |
| 308 | 410 | NAMED_ARG, |
| @@ -313,8 +415,11 @@ export class SlashCommandParser { | ||
| 313 | 415 | ); |
| 314 | 416 | CLOSURE.contains.push( |
| 315 | 417 | hljs.BACKSLASH_ESCAPE, |
| 418 | + BLOCK_COMMENT, | |
| 316 | 419 | COMMENT, |
| 317 | 420 | ABORT, |
| 421 | + IMPORT, | |
| 422 | + BREAK, | |
| 318 | 423 | NAMED_ARG, |
| 319 | 424 | NUMBER, |
| 320 | 425 | MACRO, |
| @@ -325,20 +430,27 @@ export class SlashCommandParser { | ||
| 325 | 430 | COMMAND, |
| 326 | 431 | 'self', |
| 327 | 432 | hljs.QUOTE_STRING_MODE, |
| 433 | + PIPEBREAK, | |
| 434 | + PIPE, | |
| 328 | 435 | ); |
| 329 | 436 | hljs.registerLanguage('stscript', ()=>({ |
| 330 | 437 | case_insensitive: false, |
| 331 | 438 | keywords: ['|'], |
| 332 | 439 | contains: [ |
| 333 | 440 | hljs.BACKSLASH_ESCAPE, |
| 441 | + BLOCK_COMMENT, | |
| 334 | 442 | COMMENT, |
| 335 | 443 | ABORT, |
| 444 | + IMPORT, | |
| 445 | + BREAK, | |
| 336 | 446 | RUN, |
| 337 | 447 | LET, |
| 338 | 448 | GETVAR, |
| 339 | 449 | SETVAR, |
| 340 | 450 | COMMAND, |
| 341 | 451 | CLOSURE, |
| 452 | + PIPEBREAK, | |
| 453 | + PIPE, | |
| 342 | 454 | ], |
| 343 | 455 | })); |
| 344 | 456 | } |
| @@ -415,7 +527,7 @@ export class SlashCommandParser { | ||
| 415 | 527 | ); |
| 416 | 528 | return result; |
| 417 | 529 | } |
| 418 | 530 | const result = new SlashCommandAutoCompleteNameResult(executor, this.scopeIndex[this.commandIndex.indexOf(executor)], this.commands); |
| 419 | 531 | return result; |
| 420 | 532 | } |
| 421 | 533 | return null; |
| @@ -515,11 +627,14 @@ export class SlashCommandParser { | ||
| 515 | 627 | } |
| 516 | 628 | |
| 517 | 629 | replaceGetvar(value) { |
| 518 | 630 | return value.replace(/{{(get(?:global)?var)::([^}]+)}}/gi, (_match, cmd, name, idx) => { |
| 519 | 631 | name = name.trim(); |
| 632 | + const startIdx = this.index - value.length + idx; | |
| 633 | + const endIdx = this.index - value.length + idx + match.length; | |
| 520 | 634 | // store pipe |
| 521 | 635 | const pipeName = `_PARSER__PARSER_PIPE_${uuidv4()}`; |
| 522 | 636 | const storePipe = new SlashCommandExecutor(nullstartIdx); { |
| 637 | + storePipe.end = endIdx; | |
| 523 | 638 | storePipe.command = this.commands['let']; |
| 524 | 639 | storePipe.name = 'let'; |
| 525 | 640 | const nameAss = new SlashCommandUnnamedArgumentAssignment(); |
| @@ -530,17 +645,19 @@ export class SlashCommandParser { | ||
| 530 | 645 | this.closure.executorList.push(storePipe); |
| 531 | 646 | } |
| 532 | 647 | // getvar / getglobalvar |
| 533 | 648 | const getvar = new SlashCommandExecutor(nullstartIdx); { |
| 649 | + getvar.end = endIdx; | |
| 534 | 650 | getvar.command = this.commands[cmd]; |
| 535 | 651 | getvar.name = 'cmd'; |
| 536 | 652 | const nameAss = new SlashCommandUnnamedArgumentAssignment(); |
| 537 | 653 | nameAss.value = name; |
| 538 | 654 | getvar.unnamedArgumentList = [nameAss]; |
| 539 | 655 | this.closure.executorList.push(getvar); |
| 540 | 656 | } |
| 541 | 657 | // set to temp scoped var |
| 542 | 658 | const varName = `_PARSER__PARSER_VAR_${uuidv4()}`; |
| 543 | 659 | const setvar = new SlashCommandExecutor(nullstartIdx); { |
| 660 | + setvar.end = endIdx; | |
| 544 | 661 | setvar.command = this.commands['let']; |
| 545 | 662 | setvar.name = 'let'; |
| 546 | 663 | const nameAss = new SlashCommandUnnamedArgumentAssignment(); |
| @@ -551,7 +668,8 @@ export class SlashCommandParser { | ||
| 551 | 668 | this.closure.executorList.push(setvar); |
| 552 | 669 | } |
| 553 | 670 | // return pipe |
| 554 | 671 | const returnPipe = new SlashCommandExecutor(nullstartIdx); { |
| 672 | + returnPipe.end = endIdx; | |
| 555 | 673 | returnPipe.command = this.commands['return']; |
| 556 | 674 | returnPipe.name = 'return'; |
| 557 | 675 | const varAss = new SlashCommandUnnamedArgumentAssignment(); |
| @@ -564,12 +682,13 @@ export class SlashCommandParser { | ||
| 564 | 682 | } |
| 565 | 683 | |
| 566 | 684 | |
| 567 | 685 | parse(text, verifyCommandNames = true, flags = null, abortController = null, debugController = null) { |
| 568 | 686 | this.verifyCommandNames = verifyCommandNames; |
| 569 | 687 | for (const key of Object.keys(PARSER_FLAG)) { |
| 570 | 688 | this.flags[PARSER_FLAG[key]] = flags?.[PARSER_FLAG[key]] ?? power_user.stscript.parser.flags[PARSER_FLAG[key]] ?? false; |
| 571 | 689 | } |
| 572 | 690 | this.abortController = abortController; |
| 691 | + this.debugController = debugController; | |
| 573 | 692 | this.text = text; |
| 574 | 693 | this.index = 0; |
| 575 | 694 | this.scope = null; |
| @@ -577,6 +696,7 @@ export class SlashCommandParser { | ||
| 577 | 696 | this.commandIndex = []; |
| 578 | 697 | this.scopeIndex = []; |
| 579 | 698 | this.macroIndex = []; |
| 699 | + this.parserContext = uuidv4(); | |
| 580 | 700 | const closure = this.parseClosure(true); |
| 581 | 701 | return closure; |
| 582 | 702 | } |
| @@ -604,8 +724,12 @@ export class SlashCommandParser { | ||
| 604 | 724 | if (!isRoot) this.take(2); // discard opening {: |
| 605 | 725 | const textStart = this.index; |
| 606 | 726 | let closure = new SlashCommandClosure(this.scope); |
| 727 | + closure.parserContext = this.parserContext; | |
| 728 | + closure.fullText = this.text; | |
| 607 | 729 | closure.abortController = this.abortController; |
| 730 | + closure.debugController = this.debugController; | |
| 608 | 731 | this.scope = closure.scope; |
| 732 | + const oldClosure = this.closure; | |
| 609 | 733 | this.closure = closure; |
| 610 | 734 | this.discardWhitespace(); |
| 611 | 735 | while (this.testNamedArgument()) { |
| @@ -615,7 +739,9 @@ export class SlashCommandParser { | ||
| 615 | 739 | this.discardWhitespace(); |
| 616 | 740 | } |
| 617 | 741 | while (!this.testClosureEnd()) { |
| 618 | 742 | if (this.testCommenttestBlockComment()) { |
| 743 | + this.parseBlockComment(); | |
| 744 | + } else if (this.testComment()) { | |
| 619 | 745 | this.parseComment(); |
| 620 | 746 | } else if (this.testParserFlag()) { |
| 621 | 747 | this.parseParserFlag(); |
| @@ -623,6 +749,14 @@ export class SlashCommandParser { | ||
| 623 | 749 | const cmd = this.parseRunShorthand(); |
| 624 | 750 | closure.executorList.push(cmd); |
| 625 | 751 | injectPipe = true; |
| 752 | + } else if (this.testBreakPoint()) { | |
| 753 | + const bp = this.parseBreakPoint(); | |
| 754 | + if (this.debugController) { | |
| 755 | + closure.executorList.push(bp); | |
| 756 | + } | |
| 757 | + } else if (this.testBreak()) { | |
| 758 | + const b = this.parseBreak(); | |
| 759 | + closure.executorList.push(b); | |
| 626 | 760 | } else if (this.testCommand()) { |
| 627 | 761 | const cmd = this.parseCommand(); |
| 628 | 762 | cmd.injectPipe = injectPipe; |
| @@ -651,14 +785,83 @@ export class SlashCommandParser { | ||
| 651 | 785 | } |
| 652 | 786 | closureIndexEntry.end = this.index - 1; |
| 653 | 787 | this.scope = closure.scope.parent; |
| 788 | + this.closure = oldClosure ?? closure; | |
| 654 | 789 | return closure; |
| 655 | 790 | } |
| 656 | 791 | |
| 792 | + testBreakPoint() { | |
| 793 | + return this.testSymbol(/\/breakpoint\s*\|/); | |
| 794 | + } | |
| 795 | + parseBreakPoint() { | |
| 796 | + const cmd = new SlashCommandBreakPoint(); | |
| 797 | + cmd.name = 'breakpoint'; | |
| 798 | + cmd.command = this.commands['breakpoint']; | |
| 799 | + cmd.start = this.index + 1; | |
| 800 | + this.take('/breakpoint'.length); | |
| 801 | + cmd.end = this.index; | |
| 802 | + this.commandIndex.push(cmd); | |
| 803 | + this.scopeIndex.push(this.scope.getCopy()); | |
| 804 | + return cmd; | |
| 805 | + } | |
| 806 | + | |
| 807 | + testBreak() { | |
| 808 | + return this.testSymbol(/\/break(\s|\||$)/); | |
| 809 | + } | |
| 810 | + parseBreak() { | |
| 811 | + const cmd = new SlashCommandBreak(); | |
| 812 | + cmd.name = 'break'; | |
| 813 | + cmd.command = this.commands['break']; | |
| 814 | + cmd.start = this.index + 1; | |
| 815 | + this.take('/break'.length); | |
| 816 | + this.discardWhitespace(); | |
| 817 | + if (this.testUnnamedArgument()) { | |
| 818 | + cmd.unnamedArgumentList.push(...this.parseUnnamedArgument()); | |
| 819 | + } | |
| 820 | + cmd.end = this.index; | |
| 821 | + this.commandIndex.push(cmd); | |
| 822 | + this.scopeIndex.push(this.scope.getCopy()); | |
| 823 | + return cmd; | |
| 824 | + } | |
| 825 | + | |
| 826 | + testBlockComment() { | |
| 827 | + return this.testSymbol('/*'); | |
| 828 | + } | |
| 829 | + testBlockCommentEnd() { | |
| 830 | + if (!this.verifyCommandNames) { | |
| 831 | + if (this.index >= this.text.length) return true; | |
| 832 | + } else { | |
| 833 | + if (this.ahead.length < 1) throw new SlashCommandParserError(`Unclosed block comment at position ${this.userIndex}`, this.text, this.index); | |
| 834 | + } | |
| 835 | + return this.testSymbol('*|'); | |
| 836 | + } | |
| 837 | + parseBlockComment() { | |
| 838 | + const start = this.index + 1; | |
| 839 | + const cmd = new SlashCommandExecutor(start); | |
| 840 | + cmd.command = this.commands['*']; | |
| 841 | + this.commandIndex.push(cmd); | |
| 842 | + this.scopeIndex.push(this.scope.getCopy()); | |
| 843 | + this.take(); // discard "/" | |
| 844 | + cmd.name = this.take(); //set "*" as name | |
| 845 | + while (!this.testBlockCommentEnd()) { | |
| 846 | + if (this.testBlockComment()) { | |
| 847 | + this.parseBlockComment(); | |
| 848 | + } | |
| 849 | + this.take(); | |
| 850 | + } | |
| 851 | + this.take(2); // take closing "*|" | |
| 852 | + cmd.end = this.index - 1; | |
| 853 | + } | |
| 854 | + | |
| 657 | 855 | testComment() { |
| 658 | 856 | return this.testSymbol(/\/[/#]/); |
| 659 | 857 | } |
| 660 | 858 | testCommentEnd() { |
| 661 | 859 | returnif (!this.testCommandEnd(verifyCommandNames); { |
| 860 | + if (this.index >= this.text.length) return true; | |
| 861 | + } else { | |
| 862 | + if (this.endOfText) throw new SlashCommandParserError(`Unclosed comment at position ${this.userIndex}`, this.text, this.index); | |
| 863 | + } | |
| 864 | + return this.testSymbol('|'); | |
| 662 | 865 | } |
| 663 | 866 | parseComment() { |
| 664 | 867 | const start = this.index + 1; |
| @@ -719,11 +922,13 @@ export class SlashCommandParser { | ||
| 719 | 922 | else assignment.value = this.parseValue(); |
| 720 | 923 | cmd.unnamedArgumentList = [assignment]; |
| 721 | 924 | this.discardWhitespace(); |
| 925 | + cmd.startNamedArgs = this.index; | |
| 722 | 926 | while (this.testNamedArgument()) { |
| 723 | 927 | const arg = this.parseNamedArgument(); |
| 724 | 928 | cmd.namedArgumentList.push(arg); |
| 725 | 929 | this.discardWhitespace(); |
| 726 | 930 | } |
| 931 | + cmd.endNamedArgs = this.index; | |
| 727 | 932 | this.discardWhitespace(); |
| 728 | 933 | // /run shorthand does not take unnamed arguments (the command name practically *is* the unnamed argument) |
| 729 | 934 | if (this.testRunShorthandEnd()) { |
| @@ -761,10 +966,10 @@ export class SlashCommandParser { | ||
| 761 | 966 | this.discardWhitespace(); |
| 762 | 967 | } |
| 763 | 968 | this.discardWhitespace(); |
| 764 | - cmd.startUnnamedArgs = this.index; | |
| 969 | + cmd.startUnnamedArgs = this.index - (/\s(\s*)$/s.exec(this.behind)?.[1]?.length ?? 0); | |
| 765 | 970 | cmd.endUnnamedArgs = this.index; |
| 766 | 971 | if (this.testUnnamedArgument()) { |
| 767 | 972 | cmd.unnamedArgumentList = this.parseUnnamedArgument(cmd.command?.unnamedArgumentList?.length && cmd?.command?.splitUnnamedArgument, cmd?.command?.splitUnnamedArgumentCount); |
| 768 | 973 | cmd.endUnnamedArgs = this.index; |
| 769 | 974 | if (cmd.name == 'let') { |
| 770 | 975 | const keyArg = cmd.namedArgumentList.find(it=>it.name == 'key'); |
| @@ -773,6 +978,17 @@ export class SlashCommandParser { | ||
| 773 | 978 | } else if (typeof cmd.unnamedArgumentList[0]?.value == 'string') { |
| 774 | 979 | this.scope.variableNames.push(cmd.unnamedArgumentList[0].value); |
| 775 | 980 | } |
| 981 | + } else if (cmd.name == 'import') { | |
| 982 | + const value = /**@type {string[]}*/(cmd.unnamedArgumentList.map(it=>it.value)); | |
| 983 | + for (let i = 0; i < value.length; i++) { | |
| 984 | + const srcName = value[i]; | |
| 985 | + let dstName = srcName; | |
| 986 | + if (i + 2 < value.length && value[i + 1] == 'as') { | |
| 987 | + dstName = value[i + 2]; | |
| 988 | + i += 2; | |
| 989 | + } | |
| 990 | + this.scope.variableNames.push(dstName); | |
| 991 | + } | |
| 776 | 992 | } |
| 777 | 993 | } |
| 778 | 994 | if (this.testCommandEnd()) { |
| @@ -813,25 +1029,61 @@ export class SlashCommandParser { | ||
| 813 | 1029 | testUnnamedArgumentEnd() { |
| 814 | 1030 | return this.testCommandEnd(); |
| 815 | 1031 | } |
| 816 | 1032 | parseUnnamedArgument(split, splitCount = null) { |
| 1033 | + const wasSplit = split; | |
| 817 | 1034 | /**@type {SlashCommandClosure|String}*/ |
| 818 | 1035 | let value = this.jumpedEscapeSequence ? this.take() : ''; // take the first, already tested, char if it is an escaped one |
| 819 | 1036 | let isList = split; |
| 820 | 1037 | let listValues = []; |
| 1038 | + let listQuoted = []; // keep track of which listValues were quoted | |
| 821 | 1039 | /**@type {SlashCommandUnnamedArgumentAssignment}*/ |
| 822 | 1040 | let assignment = new SlashCommandUnnamedArgumentAssignment(); |
| 823 | 1041 | assignment.start = this.index; |
| 1042 | + if (!split && this.testQuotedValue()) { | |
| 1043 | + // if the next bit is a quoted value, take the whole value and gather contents as a list | |
| 1044 | + assignment.value = this.parseQuotedValue(); | |
| 1045 | + assignment.end = this.index; | |
| 1046 | + isList = true; | |
| 1047 | + listValues.push(assignment); | |
| 1048 | + listQuoted.push(true); | |
| 1049 | + assignment = new SlashCommandUnnamedArgumentAssignment(); | |
| 1050 | + assignment.start = this.index; | |
| 1051 | + } | |
| 824 | 1052 | while (!this.testUnnamedArgumentEnd()) { |
| 1053 | + if (split && splitCount && listValues.length >= splitCount) { | |
| 1054 | + // the split count has just been reached: stop splitting, the rest is one singular value | |
| 1055 | + split = false; | |
| 1056 | + if (this.testQuotedValue()) { | |
| 1057 | + // if the next bit is a quoted value, take the whole value | |
| 1058 | + assignment.value = this.parseQuotedValue(); | |
| 1059 | + assignment.end = this.index; | |
| 1060 | + listValues.push(assignment); | |
| 1061 | + listQuoted.push(true); | |
| 1062 | + assignment = new SlashCommandUnnamedArgumentAssignment(); | |
| 1063 | + assignment.start = this.index; | |
| 1064 | + } | |
| 1065 | + } | |
| 825 | 1066 | if (this.testClosure()) { |
| 826 | 1067 | isList = true; |
| 827 | 1068 | if (value.length > 0) { |
| 828 | 1069 | this.indexMacros(this.index - value.length, value); |
| 829 | 1070 | assignment.value = value; |
| 830 | 1071 | listValues.push(assignment); |
| 1072 | + listQuoted.push(false); | |
| 1073 | + assignment = new SlashCommandUnnamedArgumentAssignment(); | |
| 1074 | + assignment.start = this.index; | |
| 1075 | + if (!split && this.testQuotedValue()) { | |
| 1076 | + // if where currently not splitting and the next bit is a quoted value, take the whole value | |
| 1077 | + assignment.value = this.parseQuotedValue(); | |
| 1078 | + assignment.end = this.index; | |
| 1079 | + listValues.push(assignment); | |
| 1080 | + listQuoted.push(true); | |
| 831 | 1081 | assignment = new SlashCommandUnnamedArgumentAssignment(); |
| 832 | 1082 | assignment.start = this.index; |
| 1083 | + } else { | |
| 833 | 1084 | value = ''; |
| 834 | 1085 | } |
| 1086 | + } | |
| 835 | 1087 | assignment.start = this.index; |
| 836 | 1088 | assignment.value = this.parseClosure(); |
| 837 | 1089 | assignment.end = this.index; |
| @@ -845,18 +1097,21 @@ export class SlashCommandParser { | ||
| 845 | 1097 | assignment.value = this.parseQuotedValue(); |
| 846 | 1098 | assignment.end = this.index; |
| 847 | 1099 | listValues.push(assignment); |
| 1100 | + listQuoted.push(true); | |
| 848 | 1101 | assignment = new SlashCommandUnnamedArgumentAssignment(); |
| 849 | 1102 | } else if (this.testListValue()) { |
| 850 | 1103 | assignment.start = this.index; |
| 851 | 1104 | assignment.value = this.parseListValue(); |
| 852 | 1105 | assignment.end = this.index; |
| 853 | 1106 | listValues.push(assignment); |
| 1107 | + listQuoted.push(false); | |
| 854 | 1108 | assignment = new SlashCommandUnnamedArgumentAssignment(); |
| 855 | 1109 | } else if (this.testValue()) { |
| 856 | 1110 | assignment.start = this.index; |
| 857 | 1111 | assignment.value = this.parseValue(); |
| 858 | 1112 | assignment.end = this.index; |
| 859 | 1113 | listValues.push(assignment); |
| 1114 | + listQuoted.push(false); | |
| 860 | 1115 | assignment = new SlashCommandUnnamedArgumentAssignment(); |
| 861 | 1116 | } else { |
| 862 | 1117 | throw new SlashCommandParserError(`Unexpected end of unnamed argument at index ${this.userIndex}.`); |
| @@ -870,8 +1125,48 @@ export class SlashCommandParser { | ||
| 870 | 1125 | if (isList && value.length > 0) { |
| 871 | 1126 | assignment.value = value; |
| 872 | 1127 | listValues.push(assignment); |
| 1128 | + listQuoted.push(false); | |
| 873 | 1129 | } |
| 874 | 1130 | if (isList) { |
| 1131 | + const firstVal = listValues[0]; | |
| 1132 | + if (typeof firstVal?.value == 'string') { | |
| 1133 | + if (!listQuoted[0]) { | |
| 1134 | + // only trim the first part if it wasn't quoted | |
| 1135 | + firstVal.value = firstVal.value.trimStart(); | |
| 1136 | + } | |
| 1137 | + if (firstVal.value.length == 0) { | |
| 1138 | + listValues.shift(); | |
| 1139 | + listQuoted.shift(); | |
| 1140 | + } | |
| 1141 | + } | |
| 1142 | + const lastVal = listValues.slice(-1)[0]; | |
| 1143 | + if (typeof lastVal?.value == 'string') { | |
| 1144 | + if (!listQuoted.slice(-1)[0]) { | |
| 1145 | + // only trim the last part if it wasn't quoted | |
| 1146 | + lastVal.value = lastVal.value.trimEnd(); | |
| 1147 | + } | |
| 1148 | + if (lastVal.value.length == 0) { | |
| 1149 | + listValues.pop(); | |
| 1150 | + listQuoted.pop(); | |
| 1151 | + } | |
| 1152 | + } | |
| 1153 | + if (wasSplit && splitCount && splitCount + 1 < listValues.length) { | |
| 1154 | + // if split with a split count and there are more values than expected | |
| 1155 | + // -> should be result of quoting + additional (non-whitespace) text | |
| 1156 | + // -> join the parts into one and restore quotes | |
| 1157 | + const joined = new SlashCommandUnnamedArgumentAssignment(); | |
| 1158 | + joined.start = listValues[splitCount].start; | |
| 1159 | + joined.end = listValues.slice(-1)[0].end; | |
| 1160 | + joined.value = ''; | |
| 1161 | + for (let i = splitCount; i < listValues.length; i++) { | |
| 1162 | + if (listQuoted[i]) joined.value += `"${listValues[i].value}"`; | |
| 1163 | + else joined.value += listValues[i].value; | |
| 1164 | + } | |
| 1165 | + listValues = [ | |
| 1166 | + ...listValues.slice(0, splitCount), | |
| 1167 | + joined, | |
| 1168 | + ]; | |
| 1169 | + } | |
| 875 | 1170 | return listValues; |
| 876 | 1171 | } |
| 877 | 1172 | this.indexMacros(this.index - value.length, value); |
| @@ -38,9 +38,11 @@ export class SlashCommandScope { | ||
| 38 | 38 | } |
| 39 | 39 | |
| 40 | 40 | |
| 41 | 41 | setMacro(key, value, overwrite = true) { |
| 42 | + if (overwrite || !this.macroList.find(it=>it.key == key)) { | |
| 42 | 43 | this.macros[key] = value; |
| 43 | 44 | } |
| 45 | + } | |
| 44 | 46 | |
| 45 | 47 | |
| 46 | 48 | existsVariableInScope(key) { |
| @@ -95,7 +97,7 @@ export class SlashCommandScope { | ||
| 95 | 97 | return v ?? ''; |
| 96 | 98 | } else { |
| 97 | 99 | const value = this.variables[key]; |
| 98 | 100 | return (value?.trim?.() === '' || isNaN(Number(value))) ? (value || '') : Number(value); |
| 99 | 101 | } |
| 100 | 102 | } |
| 101 | 103 | if (this.parent) { |
| @@ -3,7 +3,7 @@ import { getRequestHeaders } from '../script.js'; | ||
| 3 | 3 | import { isMobile } from './RossAscends-mods.js'; |
| 4 | 4 | import { collapseNewlines } from './power-user.js'; |
| 5 | 5 | import { debounce_timeout } from './constants.js'; |
| 6 | 6 | import { Popup, POPUP_RESULT, POPUP_TYPE } from './popup.js'; |
| 7 | 7 | |
| 8 | 8 | /** |
| 9 | 9 | * Pagination status string template. |
| @@ -1929,3 +1929,75 @@ export function getFreeName(name, list, numberFormatter = (n) => ` #${n}`) { | ||
| 1929 | 1929 | } |
| 1930 | 1930 | return `${name}${numberFormatter(counter)}`; |
| 1931 | 1931 | } |
| 1932 | + | |
| 1933 | +export async function fetchFaFile(name) { | |
| 1934 | + const style = document.createElement('style'); | |
| 1935 | + style.innerHTML = await (await fetch(`/css/${name}`)).text(); | |
| 1936 | + document.head.append(style); | |
| 1937 | + const sheet = style.sheet; | |
| 1938 | + style.remove(); | |
| 1939 | + return [...sheet.cssRules].filter(it=>it.style?.content).map(it=>it.selectorText.split('::').shift().slice(1)); | |
| 1940 | +} | |
| 1941 | +export async function fetchFa() { | |
| 1942 | + return [...new Set((await Promise.all([ | |
| 1943 | + fetchFaFile('fontawesome.min.css'), | |
| 1944 | + ])).flat())]; | |
| 1945 | +} | |
| 1946 | +/** | |
| 1947 | + * Opens a popup with all the available Font Awesome icons and returns the selected icon's name. | |
| 1948 | + * @prop {string[]} customList A custom list of Font Awesome icons to use instead of all available icons. | |
| 1949 | + * @returns {Promise<string>} The icon name (fa-pencil) or null if cancelled. | |
| 1950 | + */ | |
| 1951 | +export async function showFontAwesomePicker(customList = null) { | |
| 1952 | + const faList = customList ?? await fetchFa(); | |
| 1953 | + const fas = {}; | |
| 1954 | + const dom = document.createElement('div'); { | |
| 1955 | + dom.classList.add('faPicker-container'); | |
| 1956 | + const search = document.createElement('div'); { | |
| 1957 | + search.classList.add('faQuery-container'); | |
| 1958 | + const qry = document.createElement('input'); { | |
| 1959 | + qry.classList.add('text_pole'); | |
| 1960 | + qry.classList.add('faQuery'); | |
| 1961 | + qry.type = 'search'; | |
| 1962 | + qry.placeholder = 'Filter icons'; | |
| 1963 | + qry.autofocus = true; | |
| 1964 | + const qryDebounced = debounce(()=>{ | |
| 1965 | + const result = faList.filter(it=>it.includes(qry.value)); | |
| 1966 | + for (const fa of faList) { | |
| 1967 | + if (!result.includes(fa)) { | |
| 1968 | + fas[fa].classList.add('hidden'); | |
| 1969 | + } else { | |
| 1970 | + fas[fa].classList.remove('hidden'); | |
| 1971 | + } | |
| 1972 | + } | |
| 1973 | + }); | |
| 1974 | + qry.addEventListener('input', () => qryDebounced()); | |
| 1975 | + search.append(qry); | |
| 1976 | + } | |
| 1977 | + dom.append(search); | |
| 1978 | + } | |
| 1979 | + const grid = document.createElement('div'); { | |
| 1980 | + grid.classList.add('faPicker'); | |
| 1981 | + for (const fa of faList) { | |
| 1982 | + const opt = document.createElement('div'); { | |
| 1983 | + fas[fa] = opt; | |
| 1984 | + opt.classList.add('menu_button'); | |
| 1985 | + opt.classList.add('fa-solid'); | |
| 1986 | + opt.classList.add(fa); | |
| 1987 | + opt.title = fa.slice(3); | |
| 1988 | + opt.dataset.result = POPUP_RESULT.AFFIRMATIVE.toString(); | |
| 1989 | + opt.addEventListener('click', () => value = fa); | |
| 1990 | + grid.append(opt); | |
| 1991 | + } | |
| 1992 | + } | |
| 1993 | + dom.append(grid); | |
| 1994 | + } | |
| 1995 | + } | |
| 1996 | + let value = ''; | |
| 1997 | + const picker = new Popup(dom, POPUP_TYPE.TEXT, null, { allowVerticalScrolling: true, okButton: 'No Icon', cancelButton: 'Cancel' }); | |
| 1998 | + await picker.show(); | |
| 1999 | + if (picker.result == POPUP_RESULT.AFFIRMATIVE) { | |
| 2000 | + return value; | |
| 2001 | + } | |
| 2002 | + return null; | |
| 2003 | +} | |
| @@ -4,10 +4,11 @@ import { executeSlashCommandsWithOptions } from './slash-commands.js'; | ||
| 4 | 4 | import { SlashCommand } from './slash-commands/SlashCommand.js'; |
| 5 | 5 | import { SlashCommandAbortController } from './slash-commands/SlashCommandAbortController.js'; |
| 6 | 6 | import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from './slash-commands/SlashCommandArgument.js'; |
| 7 | +import { SlashCommandBreakController } from './slash-commands/SlashCommandBreakController.js'; | |
| 7 | 8 | import { SlashCommandClosure } from './slash-commands/SlashCommandClosure.js'; |
| 8 | 9 | import { SlashCommandClosureResult } from './slash-commands/SlashCommandClosureResult.js'; |
| 9 | 10 | import { commonEnumProviders, enumIcons } from './slash-commands/SlashCommandCommonEnumsProvider.js'; |
| 10 | 11 | import { SlashCommandEnumValue, enumTypes } from './slash-commands/SlashCommandEnumValue.js'; |
| 11 | 12 | import { PARSER_FLAG, SlashCommandParser } from './slash-commands/SlashCommandParser.js'; |
| 12 | 13 | import { SlashCommandScope } from './slash-commands/SlashCommandScope.js'; |
| 13 | 14 | import { isFalseBoolean } from './utils.js'; |
| @@ -40,7 +41,7 @@ function getLocalVariable(name, args = {}) { | ||
| 40 | 41 | } |
| 41 | 42 | } |
| 42 | 43 | |
| 43 | 44 | return (localVariable?.trim?.() === '' || isNaN(Number(localVariable))) ? (localVariable || '') : Number(localVariable); |
| 44 | 45 | } |
| 45 | 46 | |
| 46 | 47 | function setLocalVariable(name, value, args = {}) { |
| @@ -93,7 +94,7 @@ function getGlobalVariable(name, args = {}) { | ||
| 93 | 94 | } |
| 94 | 95 | } |
| 95 | 96 | |
| 96 | 97 | return (globalVariable?.trim?.() === '' || isNaN(Number(globalVariable))) ? (globalVariable || '') : Number(globalVariable); |
| 97 | 98 | } |
| 98 | 99 | |
| 99 | 100 | function setGlobalVariable(name, value, args = {}) { |
| @@ -348,11 +349,13 @@ async function whileCallback(args, value) { | ||
| 348 | 349 | |
| 349 | 350 | if (result && command) { |
| 350 | 351 | if (command instanceof SlashCommandClosure) { |
| 352 | + command.breakController = new SlashCommandBreakController(); | |
| 351 | 353 | commandResult = await command.execute(); |
| 352 | 354 | } else { |
| 353 | 355 | commandResult = await executeSubCommands(command, args._scope, args._parserFlags, args._abortController); |
| 354 | 356 | } |
| 355 | 357 | if (commandResult.isAborted) break; |
| 358 | + if (commandResult.isBreak) break; | |
| 356 | 359 | } else { |
| 357 | 360 | break; |
| 358 | 361 | } |
| @@ -390,8 +393,8 @@ async function timesCallback(args, value) { | ||
| 390 | 393 | const iterations = Math.min(Number(repeats), isGuardOff ? Number.MAX_SAFE_INTEGER : MAX_LOOPS); |
| 391 | 394 | let result; |
| 392 | 395 | for (let i = 0; i < iterations; i++) { |
| 393 | - /**@type {SlashCommandClosureResult}*/ | |
| 394 | 396 | if (command instanceof SlashCommandClosure) { |
| 397 | + command.breakController = new SlashCommandBreakController(); | |
| 395 | 398 | command.scope.setMacro('timesIndex', i); |
| 396 | 399 | result = await command.execute(); |
| 397 | 400 | } |
| @@ -399,6 +402,7 @@ async function timesCallback(args, value) { | ||
| 399 | 402 | result = await executeSubCommands(command.replace(/\{\{timesIndex\}\}/g, i.toString()), args._scope, args._parserFlags, args._abortController); |
| 400 | 403 | } |
| 401 | 404 | if (result.isAborted) break; |
| 405 | + if (result.isBreak) break; | |
| 402 | 406 | } |
| 403 | 407 | |
| 404 | 408 | return result?.pipe ?? ''; |
| @@ -461,7 +465,7 @@ function existsGlobalVariable(name) { | ||
| 461 | 465 | * @param {object} args Command arguments |
| 462 | 466 | * @returns {{a: string | number, b: string | number, rule: string}} Boolean operands |
| 463 | 467 | */ |
| 464 | 468 | export function parseBooleanOperands(args) { |
| 465 | 469 | // Resolution order: numeric literal, local variable, global variable, string literal |
| 466 | 470 | /** |
| 467 | 471 | * @param {string} operand Boolean operand candidate |
| @@ -510,36 +514,15 @@ function parseBooleanOperands(args) { | ||
| 510 | 514 | * @param {string|number} b The right operand |
| 511 | 515 | * @returns {boolean} True if the rule yields true, false otherwise |
| 512 | 516 | */ |
| 513 | 517 | export function evalBoolean(rule, a, b) { |
| 514 | 518 | if (!rule) { |
| 515 | 519 | toastr.warning('The rule must be specified for the boolean comparison.', 'Invalid command'); |
| 516 | 520 | throw new Error('Invalid command.'); |
| 517 | 521 | } |
| 518 | 522 | |
| 519 | 523 | let result = false; |
| 520 | - | |
| 524 | + if (typeof a === 'number' && typeof b === 'number') { | |
| 521 | - if (typeof a === 'string' && typeof b !== 'number') { | |
| 525 | + // only do numeric comparison if both operands are numbers | |
| 522 | - const aString = String(a).toLowerCase(); | |
| 523 | - const bString = String(b).toLowerCase(); | |
| 524 | - | |
| 525 | - switch (rule) { | |
| 526 | - case 'in': | |
| 527 | - result = aString.includes(bString); | |
| 528 | - break; | |
| 529 | - case 'nin': | |
| 530 | - result = !aString.includes(bString); | |
| 531 | - break; | |
| 532 | - case 'eq': | |
| 533 | - result = aString === bString; | |
| 534 | - break; | |
| 535 | - case 'neq': | |
| 536 | - result = aString !== bString; | |
| 537 | - break; | |
| 538 | - default: | |
| 539 | - toastr.error('Unknown boolean comparison rule for type string.', 'Invalid /if command'); | |
| 540 | - throw new Error('Invalid command.'); | |
| 541 | - } | |
| 542 | - } else if (typeof a === 'number') { | |
| 543 | 526 | const aNumber = Number(a); |
| 544 | 527 | const bNumber = Number(b); |
| 545 | 528 | |
| @@ -569,6 +552,38 @@ function evalBoolean(rule, a, b) { | ||
| 569 | 552 | toastr.error('Unknown boolean comparison rule for type number.', 'Invalid command'); |
| 570 | 553 | throw new Error('Invalid command.'); |
| 571 | 554 | } |
| 555 | + } else { | |
| 556 | + // otherwise do case-insensitive string comparsion, stringify non-strings | |
| 557 | + let aString; | |
| 558 | + let bString; | |
| 559 | + if (typeof a == 'string') { | |
| 560 | + aString = a.toLowerCase(); | |
| 561 | + } else { | |
| 562 | + aString = JSON.stringify(a).toLowerCase(); | |
| 563 | + } | |
| 564 | + if (typeof b == 'string') { | |
| 565 | + bString = b.toLowerCase(); | |
| 566 | + } else { | |
| 567 | + bString = JSON.stringify(b).toLowerCase(); | |
| 568 | + } | |
| 569 | + | |
| 570 | + switch (rule) { | |
| 571 | + case 'in': | |
| 572 | + result = aString.includes(bString); | |
| 573 | + break; | |
| 574 | + case 'nin': | |
| 575 | + result = !aString.includes(bString); | |
| 576 | + break; | |
| 577 | + case 'eq': | |
| 578 | + result = aString === bString; | |
| 579 | + break; | |
| 580 | + case 'neq': | |
| 581 | + result = aString !== bString; | |
| 582 | + break; | |
| 583 | + default: | |
| 584 | + toastr.error('Unknown boolean comparison rule for type string.', 'Invalid /if command'); | |
| 585 | + throw new Error('Invalid command.'); | |
| 586 | + } | |
| 572 | 587 | } |
| 573 | 588 | |
| 574 | 589 | return result; |
| @@ -783,24 +798,29 @@ function randValuesCallback(from, to, args) { | ||
| 783 | 798 | * @returns The variable's value |
| 784 | 799 | */ |
| 785 | 800 | function letCallback(args, value) { |
| 786 | 801 | if (!Array.isArray(value)) {value = [value]; |
| 787 | - args._scope.letVariable(value[0], typeof value[1] == 'string' ? value.slice(1).join(' ') : value[1]); | |
| 788 | - return value[1]; | |
| 789 | - } | |
| 790 | 802 | if (args.key !== undefined) { |
| 791 | 803 | const key = args.key; |
| 792 | - const val = value; | |
| 804 | + if (typeof key != 'string') throw new Error('Key must be a string'); | |
| 805 | + if (args._hasUnnamedArgument) { | |
| 806 | + const val = typeof value[0] == 'string' ? value.join(' ') : value[0]; | |
| 793 | 807 | args._scope.letVariable(key, val); |
| 794 | 808 | return val; |
| 809 | + } else { | |
| 810 | + args._scope.letVariable(key); | |
| 811 | + return ''; | |
| 812 | + } | |
| 795 | 813 | } |
| 796 | - if (value instanceof SlashCommandClosure) throw new Error('/let unnamed argument does not support closures if no key is provided'); | |
| 814 | + const key = value.shift(); | |
| 797 | - if (value.includes(' ')) { | |
| 815 | + if (typeof key != 'string') throw new Error('Key must be a string'); | |
| 798 | - const key = value.split(' ')[0]; | |
| 816 | + if (value.length > 0) { | |
| 799 | 817 | const val = typeof value.split('[0] == ').slice(1)string' ? value.join(' ') : value[0]; |
| 800 | 818 | args._scope.letVariable(key, val); |
| 801 | 819 | return val; |
| 820 | + } else { | |
| 821 | + args._scope.letVariable(key); | |
| 822 | + return ''; | |
| 802 | 823 | } |
| 803 | - args._scope.letVariable(value); | |
| 804 | 824 | } |
| 805 | 825 | |
| 806 | 826 | /** |
| @@ -813,8 +833,9 @@ function varCallback(args, value) { | ||
| 813 | 833 | if (!Array.isArray(value)) value = [value]; |
| 814 | 834 | if (args.key !== undefined) { |
| 815 | 835 | const key = args.key; |
| 836 | + if (typeof key != 'string') throw new Error('Key must be a string'); | |
| 816 | 837 | if (args._hasUnnamedArgument) { |
| 817 | 838 | const val = typeof value[0] == 'string' ? value.join(' ') : value[0]; |
| 818 | 839 | args._scope.setVariable(key, val, args.index); |
| 819 | 840 | return val; |
| 820 | 841 | } else { |
| @@ -822,8 +843,9 @@ function varCallback(args, value) { | ||
| 822 | 843 | } |
| 823 | 844 | } |
| 824 | 845 | const key = value.shift(); |
| 846 | + if (typeof key != 'string') throw new Error('Key must be a string'); | |
| 825 | 847 | if (value.length > 0) { |
| 826 | 848 | const val = typeof value[0] == 'string' ? value.join(' ') : value[0]; |
| 827 | 849 | args._scope.setVariable(key, val, args.index); |
| 828 | 850 | return val; |
| 829 | 851 | } else { |
| @@ -1382,6 +1404,7 @@ export function registerVariableCommands() { | ||
| 1382 | 1404 | ), |
| 1383 | 1405 | ], |
| 1384 | 1406 | splitUnnamedArgument: true, |
| 1407 | + splitUnnamedArgumentCount: 1, | |
| 1385 | 1408 | helpString: ` |
| 1386 | 1409 | <div> |
| 1387 | 1410 | Execute any valid slash command enclosed in quotes <code>repeats</code> number of times. |
| @@ -1459,7 +1482,7 @@ export function registerVariableCommands() { | ||
| 1459 | 1482 | })); |
| 1460 | 1483 | SlashCommandParser.addCommandObject(SlashCommand.fromProps({ |
| 1461 | 1484 | name: 'add', |
| 1462 | - callback: addValuesCallback, | |
| 1485 | + callback: (args, /**@type {string[]}*/value) => addValuesCallback(args, value.join(' ')), | |
| 1463 | 1486 | returns: 'sum of the provided values', |
| 1464 | 1487 | unnamedArgumentList: [ |
| 1465 | 1488 | SlashCommandArgument.fromProps({ |
| @@ -1467,10 +1490,32 @@ export function registerVariableCommands() { | ||
| 1467 | 1490 | typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.VARIABLE_NAME], |
| 1468 | 1491 | isRequired: true, |
| 1469 | 1492 | acceptsMultiple: true, |
| 1470 | - enumProvider: commonEnumProviders.variables('all'), | |
| 1493 | + enumProvider: (executor, scope)=>{ | |
| 1494 | + const vars = commonEnumProviders.variables('all')(executor, scope); | |
| 1495 | + vars.push( | |
| 1496 | + new SlashCommandEnumValue( | |
| 1497 | + 'any variable name', | |
| 1498 | + null, | |
| 1499 | + enumTypes.variable, | |
| 1500 | + enumIcons.variable, | |
| 1501 | + (input)=>/^\w*$/.test(input), | |
| 1502 | + (input)=>input, | |
| 1503 | + ), | |
| 1504 | + new SlashCommandEnumValue( | |
| 1505 | + 'any number', | |
| 1506 | + null, | |
| 1507 | + enumTypes.number, | |
| 1508 | + enumIcons.number, | |
| 1509 | + (input)=>input == '' || !Number.isNaN(Number(input)), | |
| 1510 | + (input)=>input, | |
| 1511 | + ), | |
| 1512 | + ); | |
| 1513 | + return vars; | |
| 1514 | + }, | |
| 1471 | 1515 | forceEnum: false, |
| 1472 | 1516 | }), |
| 1473 | 1517 | ], |
| 1518 | + splitUnnamedArgument: true, | |
| 1474 | 1519 | helpString: ` |
| 1475 | 1520 | <div> |
| 1476 | 1521 | Performs an addition of the set of values and passes the result down the pipe. |
| @@ -2001,6 +2046,7 @@ export function registerVariableCommands() { | ||
| 2001 | 2046 | ), |
| 2002 | 2047 | ], |
| 2003 | 2048 | splitUnnamedArgument: true, |
| 2049 | + splitUnnamedArgumentCount: 1, | |
| 2004 | 2050 | helpString: ` |
| 2005 | 2051 | <div> |
| 2006 | 2052 | Get or set a variable. |
| @@ -2043,6 +2089,7 @@ export function registerVariableCommands() { | ||
| 2043 | 2089 | ), |
| 2044 | 2090 | ], |
| 2045 | 2091 | splitUnnamedArgument: true, |
| 2092 | + splitUnnamedArgumentCount: 1, | |
| 2046 | 2093 | helpString: ` |
| 2047 | 2094 | <div> |
| 2048 | 2095 | Declares a new variable in the current scope. |
| @@ -1383,6 +1383,8 @@ body[data-stscript-style="dark"] { | ||
| 1383 | 1383 | --ac-style-color-matchedText: rgba(108 171 251 / 1); |
| 1384 | 1384 | --ac-style-color-selectedBackground: rgba(32 57 92 / 1); |
| 1385 | 1385 | --ac-style-color-selectedText: rgba(255 255 255 / 1); |
| 1386 | + --ac-style-color-notSelectableBackground: rgb(65, 78, 95); | |
| 1387 | + --ac-style-color-notSelectableText: rgba(255 255 255 / 1); | |
| 1386 | 1388 | --ac-style-color-hoveredBackground: rgba(43 45 46 / 1); |
| 1387 | 1389 | --ac-style-color-hoveredText: rgba(204 204 204 / 1); |
| 1388 | 1390 | --ac-style-color-argName: rgba(171 209 239 / 1); |
| @@ -1398,6 +1400,7 @@ body[data-stscript-style="dark"] { | ||
| 1398 | 1400 | --ac-style-color-punctuationL2: rgba(98 160 251 / 1); |
| 1399 | 1401 | --ac-style-color-currentParenthesis: rgba(195 118 210 / 1); |
| 1400 | 1402 | --ac-style-color-comment: rgba(122 151 90 / 1); |
| 1403 | + --ac-style-color-keyword: rgba(182 137 190 / 1); | |
| 1401 | 1404 | } |
| 1402 | 1405 | |
| 1403 | 1406 | body[data-stscript-style="light"] { |
| @@ -1408,6 +1411,8 @@ body[data-stscript-style="light"] { | ||
| 1408 | 1411 | --ac-style-color-matchedText: rgba(61 104 188 / 1); |
| 1409 | 1412 | --ac-style-color-selectedBackground: rgba(232 232 232 / 1); |
| 1410 | 1413 | --ac-style-color-selectedText: rgba(0 0 0 / 1); |
| 1414 | + --ac-style-color-notSelectableBackground: rgba(232 232 232 / 1); | |
| 1415 | + --ac-style-color-notSelectableText: rgb(83, 83, 83); | |
| 1411 | 1416 | --ac-style-color-hoveredBackground: rgba(242 242 242 / 1); |
| 1412 | 1417 | --ac-style-color-hoveredText: rgba(59 59 59 / 1); |
| 1413 | 1418 | --ac-style-color-argName: rgba(16 24 125 / 1); |
| @@ -1419,6 +1424,7 @@ body[data-stscript-style="light"] { | ||
| 1419 | 1424 | --ac-style-color-variable: rgba(16 24 125 / 1); |
| 1420 | 1425 | --ac-style-color-currentParenthesis: rgba(195 118 210 / 1); |
| 1421 | 1426 | --ac-style-color-comment: rgba(70 126 26 / 1); |
| 1427 | + --ac-style-color-keyword: rgba(182 137 190 / 1); | |
| 1422 | 1428 | } |
| 1423 | 1429 | |
| 1424 | 1430 | body[data-stscript-style="theme"] { |
| @@ -1429,6 +1435,8 @@ body[data-stscript-style="theme"] { | ||
| 1429 | 1435 | --ac-style-color-matchedText: var(--SmartThemeQuoteColor); |
| 1430 | 1436 | --ac-style-color-selectedBackground: color-mix(in srgb, rgb(128 128 128) 75%, var(--SmartThemeChatTintColor)); |
| 1431 | 1437 | --ac-style-color-selectedText: var(--SmartThemeBodyColor); |
| 1438 | + --ac-style-color-notSelectableBackground: color-mix(in srgb, rgb(128 128 128) 50%, var(--SmartThemeChatTintColor)); | |
| 1439 | + --ac-style-color-notSelectableText: var(--SmartThemeBodyColor); | |
| 1432 | 1440 | --ac-style-color-hoveredBackground: color-mix(in srgb, rgb(128 128 128) 30%, var(--SmartThemeChatTintColor)); |
| 1433 | 1441 | --ac-style-color-hoveredText: var(--SmartThemeEmColor); |
| 1434 | 1442 | --ac-style-color-argName: rgba(171 209 239 / 1); |
| @@ -1439,6 +1447,7 @@ body[data-stscript-style="theme"] { | ||
| 1439 | 1447 | --ac-style-color-variable: rgba(131 193 252 / 1); |
| 1440 | 1448 | --ac-style-color-currentParenthesis: rgba(195 118 210 / 1); |
| 1441 | 1449 | --ac-style-color-comment: rgba(122 151 90 / 1); |
| 1450 | + --ac-style-color-keyword: rgba(182 137 190 / 1); | |
| 1442 | 1451 | } |
| 1443 | 1452 | |
| 1444 | 1453 | body { |
| @@ -1454,6 +1463,8 @@ body { | ||
| 1454 | 1463 | --ac-color-matchedText: var(--ac-style-color-matchedText, rgba(108 171 251 / 1)); |
| 1455 | 1464 | --ac-color-selectedBackground: var(--ac-style-color-selectedBackground, rgba(32 57 92 / 1)); |
| 1456 | 1465 | --ac-color-selectedText: var(--ac-style-color-selectedText, rgba(255 255 255 / 1)); |
| 1466 | + --ac-color-notSelectableBackground: var(--ac-style-color-notSelectableBackground, rgb(60, 73, 92)); | |
| 1467 | + --ac-color-notSelectableText: var(--ac-style-color-notSelectableText, rgba(255 255 255 / 1)); | |
| 1457 | 1468 | --ac-color-hoveredBackground: var(--ac-style-color-hoveredBackground, rgba(43 45 46 / 1)); |
| 1458 | 1469 | --ac-color-hoveredText: var(--ac-style-color-hoveredText, rgba(204 204 204 / 1)); |
| 1459 | 1470 | --ac-color-argName: var(--ac-style-color-argName, rgba(171 209 239 / 1)); |
| @@ -1469,6 +1480,7 @@ body { | ||
| 1469 | 1480 | --ac-color-punctuationL2: var(--ac-style-color-punctuationL2, rgba(98 160 251 / 1)); |
| 1470 | 1481 | --ac-color-currentParenthesis: var(--ac-style-color-currentParenthesis, rgba(195 118 210 / 1)); |
| 1471 | 1482 | --ac-color-comment: var(--ac-style-color-comment, rgba(122 151 90 / 1)); |
| 1483 | + --ac-color-keyword: var(--ac-style-color-keyword, rgba(182 137 190 / 1)); | |
| 1472 | 1484 | |
| 1473 | 1485 | font-size: calc(var(--ac-font-scale) * 1em); |
| 1474 | 1486 | |
| @@ -1569,16 +1581,25 @@ body[data-stscript-style] .hljs.language-stscript { | ||
| 1569 | 1581 | color: var(--ac-style-color-punctuation); |
| 1570 | 1582 | } |
| 1571 | 1583 | |
| 1572 | - .hljs-keyword { | |
| 1573 | - color: var(--ac-style-color-variableLanguage); | |
| 1574 | - } | |
| 1575 | - | |
| 1576 | 1584 | .hljs-comment { |
| 1577 | 1585 | color: var(--ac-style-color-comment); |
| 1578 | 1586 | } |
| 1579 | 1587 | |
| 1580 | 1588 | .hljs-abort { |
| 1581 | 1589 | color: var(--ac-style-color-abort, #e38e23); |
| 1590 | + font-weight: bold; | |
| 1591 | + } | |
| 1592 | + | |
| 1593 | + .hljs-keyword { | |
| 1594 | + color: var(--ac-style-color-keyword); | |
| 1595 | + font-weight: bold; | |
| 1596 | + } | |
| 1597 | + | |
| 1598 | + .hljs-pipe { | |
| 1599 | + color: var(--ac-style-color-punctuation); | |
| 1600 | + } | |
| 1601 | + .hljs-pipebreak { | |
| 1602 | + color: var(--ac-style-color-type); | |
| 1582 | 1603 | } |
| 1583 | 1604 | |
| 1584 | 1605 | .hljs-closure { |
| @@ -1670,6 +1691,10 @@ body[data-stscript-style] .hljs.language-stscript { | ||
| 1670 | 1691 | background-color: var(--ac-color-selectedBackground); |
| 1671 | 1692 | color: var(--ac-color-selectedText); |
| 1672 | 1693 | } |
| 1694 | + &.selected.not-selectable>* { | |
| 1695 | + background-color: var(--ac-color-notSelectableBackground); | |
| 1696 | + color: var(--ac-color-notSelectableText); | |
| 1697 | + } | |
| 1673 | 1698 | |
| 1674 | 1699 | >* { |
| 1675 | 1700 | height: 100%; |
| @@ -1747,7 +1772,12 @@ body[data-stscript-style] .hljs.language-stscript { | ||
| 1747 | 1772 | padding: 0.25em 0.25em 0.5em 0.25em; |
| 1748 | 1773 | border-bottom: 1px solid var(--ac-color-border); |
| 1749 | 1774 | |
| 1750 | 1775 | > .namehead { |
| 1776 | + display: flex; | |
| 1777 | + gap: 0.5em; | |
| 1778 | + } | |
| 1779 | + > .head > .name, > .name { | |
| 1780 | + flex: 1 1 auto; | |
| 1751 | 1781 | font-weight: bold; |
| 1752 | 1782 | color: var(--ac-color-text); |
| 1753 | 1783 | cursor: help; |
| @@ -1756,6 +1786,35 @@ body[data-stscript-style] .hljs.language-stscript { | ||
| 1756 | 1786 | text-decoration: 1px dotted underline; |
| 1757 | 1787 | } |
| 1758 | 1788 | } |
| 1789 | + > .head > .source { | |
| 1790 | + padding: 0 0.5em; | |
| 1791 | + cursor: help; | |
| 1792 | + display: flex; | |
| 1793 | + align-items: center; | |
| 1794 | + gap: 0.5em; | |
| 1795 | + &.isThirdParty.isExtension { | |
| 1796 | + color: #F89406; | |
| 1797 | + } | |
| 1798 | + &.isCore { | |
| 1799 | + color: transparent; | |
| 1800 | + &.isExtension { | |
| 1801 | + color: #51A351; | |
| 1802 | + } | |
| 1803 | + &:after { | |
| 1804 | + content: ''; | |
| 1805 | + order: -1; | |
| 1806 | + height: 14px; | |
| 1807 | + aspect-ratio: 1 / 1; | |
| 1808 | + background-image: url('/favicon.ico'); | |
| 1809 | + background-size: contain; | |
| 1810 | + background-repeat: no-repeat; | |
| 1811 | + } | |
| 1812 | + } | |
| 1813 | + | |
| 1814 | + &:hover { | |
| 1815 | + text-decoration: 1px dotted underline; | |
| 1816 | + } | |
| 1817 | + } | |
| 1759 | 1818 | |
| 1760 | 1819 | >.body { |
| 1761 | 1820 | flex-direction: column; |
| @@ -1843,7 +1902,8 @@ body[data-stscript-style] .hljs.language-stscript { | ||
| 1843 | 1902 | |
| 1844 | 1903 | >code { |
| 1845 | 1904 | display: block; |
| 1846 | 1905 | padding: 01px; |
| 1906 | + tab-size: 4; | |
| 1847 | 1907 | } |
| 1848 | 1908 | } |
| 1849 | 1909 | } |
| @@ -3291,6 +3351,11 @@ grammarly-extension { | ||
| 3291 | 3351 | text-align: center; |
| 3292 | 3352 | } |
| 3293 | 3353 | |
| 3354 | +.menu_button.fa-fw { | |
| 3355 | + /* Font Awesome icons that are a menu button and should be fixed width need a slight fix. 1.25em is their default, but we need to account for button spacing. */ | |
| 3356 | + min-width: calc(1.25em + 12px); | |
| 3357 | +} | |
| 3358 | + | |
| 3294 | 3359 | .avatar_div .menu_button, |
| 3295 | 3360 | .form_create_bottom_buttons_block .menu_button { |
| 3296 | 3361 | padding: 5px; |
| @@ -5300,3 +5365,49 @@ body:not(.movingUI) .drawer-content.maximized { | ||
| 5300 | 5365 | .regex-highlight { |
| 5301 | 5366 | color: #FAF8F6; |
| 5302 | 5367 | } |
| 5368 | + | |
| 5369 | +.popup:has(.faPicker) { | |
| 5370 | + /* Fix height for fa picker popup, otherwise search is making it resize weirdly */ | |
| 5371 | + height: 70%; | |
| 5372 | + .popup-content { | |
| 5373 | + display: flex; | |
| 5374 | + flex-direction: column; | |
| 5375 | + } | |
| 5376 | +} | |
| 5377 | + | |
| 5378 | +.faPicker-container { | |
| 5379 | + display: flex; | |
| 5380 | + flex-direction: column; | |
| 5381 | + overflow: hidden;; | |
| 5382 | +} | |
| 5383 | + | |
| 5384 | +.faQuery-container { | |
| 5385 | + flex: 0 0 auto; | |
| 5386 | +} | |
| 5387 | + | |
| 5388 | +.faPicker { | |
| 5389 | + flex: 1 1 auto; | |
| 5390 | + overflow: auto; | |
| 5391 | + gap: 1em; | |
| 5392 | + display: grid; | |
| 5393 | + grid-template-columns: repeat(auto-fill, minmax(3.5em, 1fr)); | |
| 5394 | + | |
| 5395 | + .menu_button { | |
| 5396 | + aspect-ratio: 1 / 1; | |
| 5397 | + font-size: 2em; | |
| 5398 | + height: 1lh; | |
| 5399 | + line-height: 1.2; | |
| 5400 | + padding: 0.25em; | |
| 5401 | + width: unset; | |
| 5402 | + box-sizing: content-box; | |
| 5403 | + &.hidden { | |
| 5404 | + display: none; | |
| 5405 | + } | |
| 5406 | + } | |
| 5407 | +} | |
| 5408 | + | |
| 5409 | +.faPicker:not(:has(:not(.hidden)))::after { | |
| 5410 | + content: 'No icons found'; | |
| 5411 | + color: var(--SmartThemeBodyColor); | |
| 5412 | + opacity: 0.7; | |
| 5413 | +} | |