/*! * SAPUI5 * Copyright (c) 2025 SAP SE or an SAP affiliate company. All rights reserved. */ import SearchCompositeControl from "./SearchCompositeControl"; import SearchModel from "sap/esh/search/ui/SearchModel"; import MessageBox from "sap/m/MessageBox"; import FileSizeFormat from "sap/ui/core/format/FileSizeFormat"; import NumberFormat from "sap/ui/core/format/NumberFormat"; import i18n from "./i18n"; import { UrlParameters } from "./SearchModelTypes"; import SearchResultListItem from "./controls/resultview/SearchResultListItem"; import Locale from "sap/ui/core/Locale"; import Localization from "sap/base/i18n/Localization"; import EventBus from "sap/ui/core/EventBus"; import UI5Element from "sap/ui/core/Element"; // import Rendering from "sap/ui/core/Rendering"; // no yet available in UI5 2.0 import Control from "sap/ui/core/Control"; import CustomData from "sap/ui/core/CustomData"; import List from "sap/m/List"; import Table from "sap/m/Table"; import GridContainer from "sap/f/GridContainer"; import jQuery from "sap/ui/thirdparty/jquery"; export type AbortableFunction = T & { abort: () => void }; // ======================================================================= // Regex Tester // ======================================================================= export class Tester { readonly onlyAsteriskRegExp = new RegExp("^[\\s*]*$"); readonly alphaNumeric = "[a-zA-Z0-9]"; readonly nonAlphaNumeric = "[^a-zA-Z0-9]"; readonly PL = "(?:[\0-@[-`{-\xA9\xAB-\xB4\xB6-\xB9\xBB-\xBF\xD7\xF7\u02C2-\u02C5\u02D2-\u02DF\u02E5-\u02EB\u02ED\u02EF-\u036F\u0375\u0378\u0379\u037E\u0380-\u0385\u0387\u038B\u038D\u03A2\u03F6\u0482-\u0489\u0530\u0557\u0558\u055A-\u0560\u0588-\u05CF\u05EB-\u05EF\u05F3-\u061F\u064B-\u066D\u0670\u06D4\u06D6-\u06E4\u06E7-\u06ED\u06F0-\u06F9\u06FD\u06FE\u0700-\u070F\u0711\u0730-\u074C\u07A6-\u07B0\u07B2-\u07C9\u07EB-\u07F3\u07F6-\u07F9\u07FB-\u07FF\u0816-\u0819\u081B-\u0823\u0825-\u0827\u0829-\u083F\u0859-\u089F\u08B5\u08BE-\u0903\u093A-\u093C\u093E-\u094F\u0951-\u0957\u0962-\u0970\u0981-\u0984\u098D\u098E\u0991\u0992\u09A9\u09B1\u09B3-\u09B5\u09BA-\u09BC\u09BE-\u09CD\u09CF-\u09DB\u09DE\u09E2-\u09EF\u09F2-\u0A04\u0A0B-\u0A0E\u0A11\u0A12\u0A29\u0A31\u0A34\u0A37\u0A3A-\u0A58\u0A5D\u0A5F-\u0A71\u0A75-\u0A84\u0A8E\u0A92\u0AA9\u0AB1\u0AB4\u0ABA-\u0ABC\u0ABE-\u0ACF\u0AD1-\u0ADF\u0AE2-\u0AF8\u0AFA-\u0B04\u0B0D\u0B0E\u0B11\u0B12\u0B29\u0B31\u0B34\u0B3A-\u0B3C\u0B3E-\u0B5B\u0B5E\u0B62-\u0B70\u0B72-\u0B82\u0B84\u0B8B-\u0B8D\u0B91\u0B96-\u0B98\u0B9B\u0B9D\u0BA0-\u0BA2\u0BA5-\u0BA7\u0BAB-\u0BAD\u0BBA-\u0BCF\u0BD1-\u0C04\u0C0D\u0C11\u0C29\u0C3A-\u0C3C\u0C3E-\u0C57\u0C5B-\u0C5F\u0C62-\u0C7F\u0C81-\u0C84\u0C8D\u0C91\u0CA9\u0CB4\u0CBA-\u0CBC\u0CBE-\u0CDD\u0CDF\u0CE2-\u0CF0\u0CF3-\u0D04\u0D0D\u0D11\u0D3B\u0D3C\u0D3E-\u0D4D\u0D4F-\u0D53\u0D57-\u0D5E\u0D62-\u0D79\u0D80-\u0D84\u0D97-\u0D99\u0DB2\u0DBC\u0DBE\u0DBF\u0DC7-\u0E00\u0E31\u0E34-\u0E3F\u0E47-\u0E80\u0E83\u0E85\u0E86\u0E89\u0E8B\u0E8C\u0E8E-\u0E93\u0E98\u0EA0\u0EA4\u0EA6\u0EA8\u0EA9\u0EAC\u0EB1\u0EB4-\u0EBC\u0EBE\u0EBF\u0EC5\u0EC7-\u0EDB\u0EE0-\u0EFF\u0F01-\u0F3F\u0F48\u0F6D-\u0F87\u0F8D-\u0FFF\u102B-\u103E\u1040-\u104F\u1056-\u1059\u105E-\u1060\u1062-\u1064\u1067-\u106D\u1071-\u1074\u1082-\u108D\u108F-\u109F\u10C6\u10C8-\u10CC\u10CE\u10CF\u10FB\u1249\u124E\u124F\u1257\u1259\u125E\u125F\u1289\u128E\u128F\u12B1\u12B6\u12B7\u12BF\u12C1\u12C6\u12C7\u12D7\u1311\u1316\u1317\u135B-\u137F\u1390-\u139F\u13F6\u13F7\u13FE-\u1400\u166D\u166E\u1680\u169B-\u169F\u16EB-\u16F0\u16F9-\u16FF\u170D\u1712-\u171F\u1732-\u173F\u1752-\u175F\u176D\u1771-\u177F\u17B4-\u17D6\u17D8-\u17DB\u17DD-\u181F\u1878-\u187F\u1885\u1886\u18A9\u18AB-\u18AF\u18F6-\u18FF\u191F-\u194F\u196E\u196F\u1975-\u197F\u19AC-\u19AF\u19CA-\u19FF\u1A17-\u1A1F\u1A55-\u1AA6\u1AA8-\u1B04\u1B34-\u1B44\u1B4C-\u1B82\u1BA1-\u1BAD\u1BB0-\u1BB9\u1BE6-\u1BFF\u1C24-\u1C4C\u1C50-\u1C59\u1C7E\u1C7F\u1C89-\u1CE8\u1CED\u1CF2-\u1CF4\u1CF7-\u1CFF\u1DC0-\u1DFF\u1F16\u1F17\u1F1E\u1F1F\u1F46\u1F47\u1F4E\u1F4F\u1F58\u1F5A\u1F5C\u1F5E\u1F7E\u1F7F\u1FB5\u1FBD\u1FBF-\u1FC1\u1FC5\u1FCD-\u1FCF\u1FD4\u1FD5\u1FDC-\u1FDF\u1FED-\u1FF1\u1FF5\u1FFD-\u2070\u2072-\u207E\u2080-\u208F\u209D-\u2101\u2103-\u2106\u2108\u2109\u2114\u2116-\u2118\u211E-\u2123\u2125\u2127\u2129\u212E\u213A\u213B\u2140-\u2144\u214A-\u214D\u214F-\u2182\u2185-\u2BFF\u2C2F\u2C5F\u2CE5-\u2CEA\u2CEF-\u2CF1\u2CF4-\u2CFF\u2D26\u2D28-\u2D2C\u2D2E\u2D2F\u2D68-\u2D6E\u2D70-\u2D7F\u2D97-\u2D9F\u2DA7\u2DAF\u2DB7\u2DBF\u2DC7\u2DCF\u2DD7\u2DDF-\u2E2E\u2E30-\u3004\u3007-\u3030\u3036-\u303A\u303D-\u3040\u3097-\u309C\u30A0\u30FB\u3100-\u3104\u312E-\u3130\u318F-\u319F\u31BB-\u31EF\u3200-\u33FF\u4DB6-\u4DFF\u9FD6-\u9FFF\uA48D-\uA4CF\uA4FE\uA4FF\uA60D-\uA60F\uA620-\uA629\uA62C-\uA63F\uA66F-\uA67E\uA69E\uA69F\uA6E6-\uA716\uA720\uA721\uA789\uA78A\uA7AF\uA7B8-\uA7F6\uA802\uA806\uA80B\uA823-\uA83F\uA874-\uA881\uA8B4-\uA8F1\uA8F8-\uA8FA\uA8FC\uA8FE-\uA909\uA926-\uA92F\uA947-\uA95F\uA97D-\uA983\uA9B3-\uA9CE\uA9D0-\uA9DF\uA9E5\uA9F0-\uA9F9\uA9FF\uAA29-\uAA3F\uAA43\uAA4C-\uAA5F\uAA77-\uAA79\uAA7B-\uAA7D\uAAB0\uAAB2-\uAAB4\uAAB7\uAAB8\uAABE\uAABF\uAAC1\uAAC3-\uAADA\uAADE\uAADF\uAAEB-\uAAF1\uAAF5-\uAB00\uAB07\uAB08\uAB0F\uAB10\uAB17-\uAB1F\uAB27\uAB2F\uAB5B\uAB66-\uAB6F\uABE3-\uABFF\uD7A4-\uD7AF\uD7C7-\uD7CA\uD7FC-\uD7FF\uE000-\uF8FF\uFA6E\uFA6F\uFADA-\uFAFF\uFB07-\uFB12\uFB18-\uFB1C\uFB1E\uFB29\uFB37\uFB3D\uFB3F\uFB42\uFB45\uFBB2-\uFBD2\uFD3E-\uFD4F\uFD90\uFD91\uFDC8-\uFDEF\uFDFC-\uFE6F\uFE75\uFEFD-\uFF20\uFF3B-\uFF40\uFF5B-\uFF65\uFFBF-\uFFC1\uFFC8\uFFC9\uFFD0\uFFD1\uFFD8\uFFD9\uFFDD-\uFFFF]|\uD800[\uDC0C\uDC27\uDC3B\uDC3E\uDC4E\uDC4F\uDC5E-\uDC7F\uDCFB-\uDE7F\uDE9D-\uDE9F\uDED1-\uDEFF\uDF20-\uDF2F\uDF41\uDF4A-\uDF4F\uDF76-\uDF7F\uDF9E\uDF9F\uDFC4-\uDFC7\uDFD0-\uDFFF]|\uD801[\uDC9E-\uDCAF\uDCD4-\uDCD7\uDCFC-\uDCFF\uDD28-\uDD2F\uDD64-\uDDFF\uDF37-\uDF3F\uDF56-\uDF5F\uDF68-\uDFFF]|\uD802[\uDC06\uDC07\uDC09\uDC36\uDC39-\uDC3B\uDC3D\uDC3E\uDC56-\uDC5F\uDC77-\uDC7F\uDC9F-\uDCDF\uDCF3\uDCF6-\uDCFF\uDD16-\uDD1F\uDD3A-\uDD7F\uDDB8-\uDDBD\uDDC0-\uDDFF\uDE01-\uDE0F\uDE14\uDE18\uDE34-\uDE5F\uDE7D-\uDE7F\uDE9D-\uDEBF\uDEC8\uDEE5-\uDEFF\uDF36-\uDF3F\uDF56-\uDF5F\uDF73-\uDF7F\uDF92-\uDFFF]|\uD803[\uDC49-\uDC7F\uDCB3-\uDCBF\uDCF3-\uDFFF]|\uD804[\uDC00-\uDC02\uDC38-\uDC82\uDCB0-\uDCCF\uDCE9-\uDD02\uDD27-\uDD4F\uDD73-\uDD75\uDD77-\uDD82\uDDB3-\uDDC0\uDDC5-\uDDD9\uDDDB\uDDDD-\uDDFF\uDE12\uDE2C-\uDE7F\uDE87\uDE89\uDE8E\uDE9E\uDEA9-\uDEAF\uDEDF-\uDF04\uDF0D\uDF0E\uDF11\uDF12\uDF29\uDF31\uDF34\uDF3A-\uDF3C\uDF3E-\uDF4F\uDF51-\uDF5C\uDF62-\uDFFF]|\uD805[\uDC35-\uDC46\uDC4B-\uDC7F\uDCB0-\uDCC3\uDCC6\uDCC8-\uDD7F\uDDAF-\uDDD7\uDDDC-\uDDFF\uDE30-\uDE43\uDE45-\uDE7F\uDEAB-\uDEFF\uDF1A-\uDFFF]|\uD806[\uDC00-\uDC9F\uDCE0-\uDCFE\uDD00-\uDEBF\uDEF9-\uDFFF]|\uD807[\uDC09\uDC2F-\uDC3F\uDC41-\uDC71\uDC90-\uDFFF]|\uD808[\uDF9A-\uDFFF]|\uD809[\uDC00-\uDC7F\uDD44-\uDFFF]|[\uD80A\uD80B\uD80E-\uD810\uD812-\uD819\uD823-\uD82B\uD82D\uD82E\uD830-\uD834\uD836-\uD839\uD83C-\uD83F\uD874-\uD87D\uD87F-\uDBFF][\uDC00-\uDFFF]|\uD80D[\uDC2F-\uDFFF]|\uD811[\uDE47-\uDFFF]|\uD81A[\uDE39-\uDE3F\uDE5F-\uDECF\uDEEE-\uDEFF\uDF30-\uDF3F\uDF44-\uDF62\uDF78-\uDF7C\uDF90-\uDFFF]|\uD81B[\uDC00-\uDEFF\uDF45-\uDF4F\uDF51-\uDF92\uDFA0-\uDFDF\uDFE1-\uDFFF]|\uD821[\uDFED-\uDFFF]|\uD822[\uDEF3-\uDFFF]|\uD82C[\uDC02-\uDFFF]|\uD82F[\uDC6B-\uDC6F\uDC7D-\uDC7F\uDC89-\uDC8F\uDC9A-\uDFFF]|\uD835[\uDC55\uDC9D\uDCA0\uDCA1\uDCA3\uDCA4\uDCA7\uDCA8\uDCAD\uDCBA\uDCBC\uDCC4\uDD06\uDD0B\uDD0C\uDD15\uDD1D\uDD3A\uDD3F\uDD45\uDD47-\uDD49\uDD51\uDEA6\uDEA7\uDEC1\uDEDB\uDEFB\uDF15\uDF35\uDF4F\uDF6F\uDF89\uDFA9\uDFC3\uDFCC-\uDFFF]|\uD83A[\uDCC5-\uDCFF\uDD44-\uDFFF]|\uD83B[\uDC00-\uDDFF\uDE04\uDE20\uDE23\uDE25\uDE26\uDE28\uDE33\uDE38\uDE3A\uDE3C-\uDE41\uDE43-\uDE46\uDE48\uDE4A\uDE4C\uDE50\uDE53\uDE55\uDE56\uDE58\uDE5A\uDE5C\uDE5E\uDE60\uDE63\uDE65\uDE66\uDE6B\uDE73\uDE78\uDE7D\uDE7F\uDE8A\uDE9C-\uDEA0\uDEA4\uDEAA\uDEBC-\uDFFF]|\uD869[\uDED7-\uDEFF]|\uD86D[\uDF35-\uDF3F]|\uD86E[\uDC1E\uDC1F]|\uD873[\uDEA2-\uDFFF]|\uD87E[\uDE1E-\uDFFF]|[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?:[^\uD800-\uDBFF]|^)[\uDC00-\uDFFF])"; readonly nonepL = "(?:[^A-Za-z\xAA\xB5\xBA\xC0-\xD6\xD8-\xF6\xF8-\u02C1\u02C6-\u02D1\u02E0-\u02E4\u02EC\u02EE\u0370-\u0374\u0376\u0377\u037A-\u037D\u037F\u0386\u0388-\u038A\u038C\u038E-\u03A1\u03A3-\u03F5\u03F7-\u0481\u048A-\u052F\u0531-\u0556\u0559\u0561-\u0587\u05D0-\u05EA\u05F0-\u05F2\u0620-\u064A\u066E\u066F\u0671-\u06D3\u06D5\u06E5\u06E6\u06EE\u06EF\u06FA-\u06FC\u06FF\u0710\u0712-\u072F\u074D-\u07A5\u07B1\u07CA-\u07EA\u07F4\u07F5\u07FA\u0800-\u0815\u081A\u0824\u0828\u0840-\u0858\u08A0-\u08B4\u08B6-\u08BD\u0904-\u0939\u093D\u0950\u0958-\u0961\u0971-\u0980\u0985-\u098C\u098F\u0990\u0993-\u09A8\u09AA-\u09B0\u09B2\u09B6-\u09B9\u09BD\u09CE\u09DC\u09DD\u09DF-\u09E1\u09F0\u09F1\u0A05-\u0A0A\u0A0F\u0A10\u0A13-\u0A28\u0A2A-\u0A30\u0A32\u0A33\u0A35\u0A36\u0A38\u0A39\u0A59-\u0A5C\u0A5E\u0A72-\u0A74\u0A85-\u0A8D\u0A8F-\u0A91\u0A93-\u0AA8\u0AAA-\u0AB0\u0AB2\u0AB3\u0AB5-\u0AB9\u0ABD\u0AD0\u0AE0\u0AE1\u0AF9\u0B05-\u0B0C\u0B0F\u0B10\u0B13-\u0B28\u0B2A-\u0B30\u0B32\u0B33\u0B35-\u0B39\u0B3D\u0B5C\u0B5D\u0B5F-\u0B61\u0B71\u0B83\u0B85-\u0B8A\u0B8E-\u0B90\u0B92-\u0B95\u0B99\u0B9A\u0B9C\u0B9E\u0B9F\u0BA3\u0BA4\u0BA8-\u0BAA\u0BAE-\u0BB9\u0BD0\u0C05-\u0C0C\u0C0E-\u0C10\u0C12-\u0C28\u0C2A-\u0C39\u0C3D\u0C58-\u0C5A\u0C60\u0C61\u0C80\u0C85-\u0C8C\u0C8E-\u0C90\u0C92-\u0CA8\u0CAA-\u0CB3\u0CB5-\u0CB9\u0CBD\u0CDE\u0CE0\u0CE1\u0CF1\u0CF2\u0D05-\u0D0C\u0D0E-\u0D10\u0D12-\u0D3A\u0D3D\u0D4E\u0D54-\u0D56\u0D5F-\u0D61\u0D7A-\u0D7F\u0D85-\u0D96\u0D9A-\u0DB1\u0DB3-\u0DBB\u0DBD\u0DC0-\u0DC6\u0E01-\u0E30\u0E32\u0E33\u0E40-\u0E46\u0E81\u0E82\u0E84\u0E87\u0E88\u0E8A\u0E8D\u0E94-\u0E97\u0E99-\u0E9F\u0EA1-\u0EA3\u0EA5\u0EA7\u0EAA\u0EAB\u0EAD-\u0EB0\u0EB2\u0EB3\u0EBD\u0EC0-\u0EC4\u0EC6\u0EDC-\u0EDF\u0F00\u0F40-\u0F47\u0F49-\u0F6C\u0F88-\u0F8C\u1000-\u102A\u103F\u1050-\u1055\u105A-\u105D\u1061\u1065\u1066\u106E-\u1070\u1075-\u1081\u108E\u10A0-\u10C5\u10C7\u10CD\u10D0-\u10FA\u10FC-\u1248\u124A-\u124D\u1250-\u1256\u1258\u125A-\u125D\u1260-\u1288\u128A-\u128D\u1290-\u12B0\u12B2-\u12B5\u12B8-\u12BE\u12C0\u12C2-\u12C5\u12C8-\u12D6\u12D8-\u1310\u1312-\u1315\u1318-\u135A\u1380-\u138F\u13A0-\u13F5\u13F8-\u13FD\u1401-\u166C\u166F-\u167F\u1681-\u169A\u16A0-\u16EA\u16F1-\u16F8\u1700-\u170C\u170E-\u1711\u1720-\u1731\u1740-\u1751\u1760-\u176C\u176E-\u1770\u1780-\u17B3\u17D7\u17DC\u1820-\u1877\u1880-\u1884\u1887-\u18A8\u18AA\u18B0-\u18F5\u1900-\u191E\u1950-\u196D\u1970-\u1974\u1980-\u19AB\u19B0-\u19C9\u1A00-\u1A16\u1A20-\u1A54\u1AA7\u1B05-\u1B33\u1B45-\u1B4B\u1B83-\u1BA0\u1BAE\u1BAF\u1BBA-\u1BE5\u1C00-\u1C23\u1C4D-\u1C4F\u1C5A-\u1C7D\u1C80-\u1C88\u1CE9-\u1CEC\u1CEE-\u1CF1\u1CF5\u1CF6\u1D00-\u1DBF\u1E00-\u1F15\u1F18-\u1F1D\u1F20-\u1F45\u1F48-\u1F4D\u1F50-\u1F57\u1F59\u1F5B\u1F5D\u1F5F-\u1F7D\u1F80-\u1FB4\u1FB6-\u1FBC\u1FBE\u1FC2-\u1FC4\u1FC6-\u1FCC\u1FD0-\u1FD3\u1FD6-\u1FDB\u1FE0-\u1FEC\u1FF2-\u1FF4\u1FF6-\u1FFC\u2071\u207F\u2090-\u209C\u2102\u2107\u210A-\u2113\u2115\u2119-\u211D\u2124\u2126\u2128\u212A-\u212D\u212F-\u2139\u213C-\u213F\u2145-\u2149\u214E\u2183\u2184\u2C00-\u2C2E\u2C30-\u2C5E\u2C60-\u2CE4\u2CEB-\u2CEE\u2CF2\u2CF3\u2D00-\u2D25\u2D27\u2D2D\u2D30-\u2D67\u2D6F\u2D80-\u2D96\u2DA0-\u2DA6\u2DA8-\u2DAE\u2DB0-\u2DB6\u2DB8-\u2DBE\u2DC0-\u2DC6\u2DC8-\u2DCE\u2DD0-\u2DD6\u2DD8-\u2DDE\u2E2F\u3005\u3006\u3031-\u3035\u303B\u303C\u3041-\u3096\u309D-\u309F\u30A1-\u30FA\u30FC-\u30FF\u3105-\u312D\u3131-\u318E\u31A0-\u31BA\u31F0-\u31FF\u3400-\u4DB5\u4E00-\u9FD5\uA000-\uA48C\uA4D0-\uA4FD\uA500-\uA60C\uA610-\uA61F\uA62A\uA62B\uA640-\uA66E\uA67F-\uA69D\uA6A0-\uA6E5\uA717-\uA71F\uA722-\uA788\uA78B-\uA7AE\uA7B0-\uA7B7\uA7F7-\uA801\uA803-\uA805\uA807-\uA80A\uA80C-\uA822\uA840-\uA873\uA882-\uA8B3\uA8F2-\uA8F7\uA8FB\uA8FD\uA90A-\uA925\uA930-\uA946\uA960-\uA97C\uA984-\uA9B2\uA9CF\uA9E0-\uA9E4\uA9E6-\uA9EF\uA9FA-\uA9FE\uAA00-\uAA28\uAA40-\uAA42\uAA44-\uAA4B\uAA60-\uAA76\uAA7A\uAA7E-\uAAAF\uAAB1\uAAB5\uAAB6\uAAB9-\uAABD\uAAC0\uAAC2\uAADB-\uAADD\uAAE0-\uAAEA\uAAF2-\uAAF4\uAB01-\uAB06\uAB09-\uAB0E\uAB11-\uAB16\uAB20-\uAB26\uAB28-\uAB2E\uAB30-\uAB5A\uAB5C-\uAB65\uAB70-\uABE2\uAC00-\uD7A3\uD7B0-\uD7C6\uD7CB-\uD7FB\uF900-\uFA6D\uFA70-\uFAD9\uFB00-\uFB06\uFB13-\uFB17\uFB1D\uFB1F-\uFB28\uFB2A-\uFB36\uFB38-\uFB3C\uFB3E\uFB40\uFB41\uFB43\uFB44\uFB46-\uFBB1\uFBD3-\uFD3D\uFD50-\uFD8F\uFD92-\uFDC7\uFDF0-\uFDFB\uFE70-\uFE74\uFE76-\uFEFC\uFF21-\uFF3A\uFF41-\uFF5A\uFF66-\uFFBE\uFFC2-\uFFC7\uFFCA-\uFFCF\uFFD2-\uFFD7\uFFDA-\uFFDC]|\uD800[\uDC00-\uDC0B\uDC0D-\uDC26\uDC28-\uDC3A\uDC3C\uDC3D\uDC3F-\uDC4D\uDC50-\uDC5D\uDC80-\uDCFA\uDE80-\uDE9C\uDEA0-\uDED0\uDF00-\uDF1F\uDF30-\uDF40\uDF42-\uDF49\uDF50-\uDF75\uDF80-\uDF9D\uDFA0-\uDFC3\uDFC8-\uDFCF]|\uD801[\uDC00-\uDC9D\uDCB0-\uDCD3\uDCD8-\uDCFB\uDD00-\uDD27\uDD30-\uDD63\uDE00-\uDF36\uDF40-\uDF55\uDF60-\uDF67]|\uD802[\uDC00-\uDC05\uDC08\uDC0A-\uDC35\uDC37\uDC38\uDC3C\uDC3F-\uDC55\uDC60-\uDC76\uDC80-\uDC9E\uDCE0-\uDCF2\uDCF4\uDCF5\uDD00-\uDD15\uDD20-\uDD39\uDD80-\uDDB7\uDDBE\uDDBF\uDE00\uDE10-\uDE13\uDE15-\uDE17\uDE19-\uDE33\uDE60-\uDE7C\uDE80-\uDE9C\uDEC0-\uDEC7\uDEC9-\uDEE4\uDF00-\uDF35\uDF40-\uDF55\uDF60-\uDF72\uDF80-\uDF91]|\uD803[\uDC00-\uDC48\uDC80-\uDCB2\uDCC0-\uDCF2]|\uD804[\uDC03-\uDC37\uDC83-\uDCAF\uDCD0-\uDCE8\uDD03-\uDD26\uDD50-\uDD72\uDD76\uDD83-\uDDB2\uDDC1-\uDDC4\uDDDA\uDDDC\uDE00-\uDE11\uDE13-\uDE2B\uDE80-\uDE86\uDE88\uDE8A-\uDE8D\uDE8F-\uDE9D\uDE9F-\uDEA8\uDEB0-\uDEDE\uDF05-\uDF0C\uDF0F\uDF10\uDF13-\uDF28\uDF2A-\uDF30\uDF32\uDF33\uDF35-\uDF39\uDF3D\uDF50\uDF5D-\uDF61]|\uD805[\uDC00-\uDC34\uDC47-\uDC4A\uDC80-\uDCAF\uDCC4\uDCC5\uDCC7\uDD80-\uDDAE\uDDD8-\uDDDB\uDE00-\uDE2F\uDE44\uDE80-\uDEAA\uDF00-\uDF19]|\uD806[\uDCA0-\uDCDF\uDCFF\uDEC0-\uDEF8]|\uD807[\uDC00-\uDC08\uDC0A-\uDC2E\uDC40\uDC72-\uDC8F]|\uD808[\uDC00-\uDF99]|\uD809[\uDC80-\uDD43]|[\uD80C\uD81C-\uD820\uD840-\uD868\uD86A-\uD86C\uD86F-\uD872][\uDC00-\uDFFF]|\uD80D[\uDC00-\uDC2E]|\uD811[\uDC00-\uDE46]|\uD81A[\uDC00-\uDE38\uDE40-\uDE5E\uDED0-\uDEED\uDF00-\uDF2F\uDF40-\uDF43\uDF63-\uDF77\uDF7D-\uDF8F]|\uD81B[\uDF00-\uDF44\uDF50\uDF93-\uDF9F\uDFE0]|\uD821[\uDC00-\uDFEC]|\uD822[\uDC00-\uDEF2]|\uD82C[\uDC00\uDC01]|\uD82F[\uDC00-\uDC6A\uDC70-\uDC7C\uDC80-\uDC88\uDC90-\uDC99]|\uD835[\uDC00-\uDC54\uDC56-\uDC9C\uDC9E\uDC9F\uDCA2\uDCA5\uDCA6\uDCA9-\uDCAC\uDCAE-\uDCB9\uDCBB\uDCBD-\uDCC3\uDCC5-\uDD05\uDD07-\uDD0A\uDD0D-\uDD14\uDD16-\uDD1C\uDD1E-\uDD39\uDD3B-\uDD3E\uDD40-\uDD44\uDD46\uDD4A-\uDD50\uDD52-\uDEA5\uDEA8-\uDEC0\uDEC2-\uDEDA\uDEDC-\uDEFA\uDEFC-\uDF14\uDF16-\uDF34\uDF36-\uDF4E\uDF50-\uDF6E\uDF70-\uDF88\uDF8A-\uDFA8\uDFAA-\uDFC2\uDFC4-\uDFCB]|\uD83A[\uDC00-\uDCC4\uDD00-\uDD43]|\uD83B[\uDE00-\uDE03\uDE05-\uDE1F\uDE21\uDE22\uDE24\uDE27\uDE29-\uDE32\uDE34-\uDE37\uDE39\uDE3B\uDE42\uDE47\uDE49\uDE4B\uDE4D-\uDE4F\uDE51\uDE52\uDE54\uDE57\uDE59\uDE5B\uDE5D\uDE5F\uDE61\uDE62\uDE64\uDE67-\uDE6A\uDE6C-\uDE72\uDE74-\uDE77\uDE79-\uDE7C\uDE7E\uDE80-\uDE89\uDE8B-\uDE9B\uDEA1-\uDEA3\uDEA5-\uDEA9\uDEAB-\uDEBB]|\uD869[\uDC00-\uDED6\uDF00-\uDFFF]|\uD86D[\uDC00-\uDF34\uDF40-\uDFFF]|\uD86E[\uDC00-\uDC1D\uDC20-\uDFFF]|\uD873[\uDC00-\uDEA1]|\uD87E[\uDC00-\uDE1D])"; readonly pL = "(?:[A-Za-z\xAA\xB5\xBA\xC0-\xD6\xD8-\xF6\xF8-\u02C1\u02C6-\u02D1\u02E0-\u02E4\u02EC\u02EE\u0370-\u0374\u0376\u0377\u037A-\u037D\u037F\u0386\u0388-\u038A\u038C\u038E-\u03A1\u03A3-\u03F5\u03F7-\u0481\u048A-\u052F\u0531-\u0556\u0559\u0561-\u0587\u05D0-\u05EA\u05F0-\u05F2\u0620-\u064A\u066E\u066F\u0671-\u06D3\u06D5\u06E5\u06E6\u06EE\u06EF\u06FA-\u06FC\u06FF\u0710\u0712-\u072F\u074D-\u07A5\u07B1\u07CA-\u07EA\u07F4\u07F5\u07FA\u0800-\u0815\u081A\u0824\u0828\u0840-\u0858\u08A0-\u08B4\u08B6-\u08BD\u0904-\u0939\u093D\u0950\u0958-\u0961\u0971-\u0980\u0985-\u098C\u098F\u0990\u0993-\u09A8\u09AA-\u09B0\u09B2\u09B6-\u09B9\u09BD\u09CE\u09DC\u09DD\u09DF-\u09E1\u09F0\u09F1\u0A05-\u0A0A\u0A0F\u0A10\u0A13-\u0A28\u0A2A-\u0A30\u0A32\u0A33\u0A35\u0A36\u0A38\u0A39\u0A59-\u0A5C\u0A5E\u0A72-\u0A74\u0A85-\u0A8D\u0A8F-\u0A91\u0A93-\u0AA8\u0AAA-\u0AB0\u0AB2\u0AB3\u0AB5-\u0AB9\u0ABD\u0AD0\u0AE0\u0AE1\u0AF9\u0B05-\u0B0C\u0B0F\u0B10\u0B13-\u0B28\u0B2A-\u0B30\u0B32\u0B33\u0B35-\u0B39\u0B3D\u0B5C\u0B5D\u0B5F-\u0B61\u0B71\u0B83\u0B85-\u0B8A\u0B8E-\u0B90\u0B92-\u0B95\u0B99\u0B9A\u0B9C\u0B9E\u0B9F\u0BA3\u0BA4\u0BA8-\u0BAA\u0BAE-\u0BB9\u0BD0\u0C05-\u0C0C\u0C0E-\u0C10\u0C12-\u0C28\u0C2A-\u0C39\u0C3D\u0C58-\u0C5A\u0C60\u0C61\u0C80\u0C85-\u0C8C\u0C8E-\u0C90\u0C92-\u0CA8\u0CAA-\u0CB3\u0CB5-\u0CB9\u0CBD\u0CDE\u0CE0\u0CE1\u0CF1\u0CF2\u0D05-\u0D0C\u0D0E-\u0D10\u0D12-\u0D3A\u0D3D\u0D4E\u0D54-\u0D56\u0D5F-\u0D61\u0D7A-\u0D7F\u0D85-\u0D96\u0D9A-\u0DB1\u0DB3-\u0DBB\u0DBD\u0DC0-\u0DC6\u0E01-\u0E30\u0E32\u0E33\u0E40-\u0E46\u0E81\u0E82\u0E84\u0E87\u0E88\u0E8A\u0E8D\u0E94-\u0E97\u0E99-\u0E9F\u0EA1-\u0EA3\u0EA5\u0EA7\u0EAA\u0EAB\u0EAD-\u0EB0\u0EB2\u0EB3\u0EBD\u0EC0-\u0EC4\u0EC6\u0EDC-\u0EDF\u0F00\u0F40-\u0F47\u0F49-\u0F6C\u0F88-\u0F8C\u1000-\u102A\u103F\u1050-\u1055\u105A-\u105D\u1061\u1065\u1066\u106E-\u1070\u1075-\u1081\u108E\u10A0-\u10C5\u10C7\u10CD\u10D0-\u10FA\u10FC-\u1248\u124A-\u124D\u1250-\u1256\u1258\u125A-\u125D\u1260-\u1288\u128A-\u128D\u1290-\u12B0\u12B2-\u12B5\u12B8-\u12BE\u12C0\u12C2-\u12C5\u12C8-\u12D6\u12D8-\u1310\u1312-\u1315\u1318-\u135A\u1380-\u138F\u13A0-\u13F5\u13F8-\u13FD\u1401-\u166C\u166F-\u167F\u1681-\u169A\u16A0-\u16EA\u16F1-\u16F8\u1700-\u170C\u170E-\u1711\u1720-\u1731\u1740-\u1751\u1760-\u176C\u176E-\u1770\u1780-\u17B3\u17D7\u17DC\u1820-\u1877\u1880-\u1884\u1887-\u18A8\u18AA\u18B0-\u18F5\u1900-\u191E\u1950-\u196D\u1970-\u1974\u1980-\u19AB\u19B0-\u19C9\u1A00-\u1A16\u1A20-\u1A54\u1AA7\u1B05-\u1B33\u1B45-\u1B4B\u1B83-\u1BA0\u1BAE\u1BAF\u1BBA-\u1BE5\u1C00-\u1C23\u1C4D-\u1C4F\u1C5A-\u1C7D\u1C80-\u1C88\u1CE9-\u1CEC\u1CEE-\u1CF1\u1CF5\u1CF6\u1D00-\u1DBF\u1E00-\u1F15\u1F18-\u1F1D\u1F20-\u1F45\u1F48-\u1F4D\u1F50-\u1F57\u1F59\u1F5B\u1F5D\u1F5F-\u1F7D\u1F80-\u1FB4\u1FB6-\u1FBC\u1FBE\u1FC2-\u1FC4\u1FC6-\u1FCC\u1FD0-\u1FD3\u1FD6-\u1FDB\u1FE0-\u1FEC\u1FF2-\u1FF4\u1FF6-\u1FFC\u2071\u207F\u2090-\u209C\u2102\u2107\u210A-\u2113\u2115\u2119-\u211D\u2124\u2126\u2128\u212A-\u212D\u212F-\u2139\u213C-\u213F\u2145-\u2149\u214E\u2183\u2184\u2C00-\u2C2E\u2C30-\u2C5E\u2C60-\u2CE4\u2CEB-\u2CEE\u2CF2\u2CF3\u2D00-\u2D25\u2D27\u2D2D\u2D30-\u2D67\u2D6F\u2D80-\u2D96\u2DA0-\u2DA6\u2DA8-\u2DAE\u2DB0-\u2DB6\u2DB8-\u2DBE\u2DC0-\u2DC6\u2DC8-\u2DCE\u2DD0-\u2DD6\u2DD8-\u2DDE\u2E2F\u3005\u3006\u3031-\u3035\u303B\u303C\u3041-\u3096\u309D-\u309F\u30A1-\u30FA\u30FC-\u30FF\u3105-\u312D\u3131-\u318E\u31A0-\u31BA\u31F0-\u31FF\u3400-\u4DB5\u4E00-\u9FD5\uA000-\uA48C\uA4D0-\uA4FD\uA500-\uA60C\uA610-\uA61F\uA62A\uA62B\uA640-\uA66E\uA67F-\uA69D\uA6A0-\uA6E5\uA717-\uA71F\uA722-\uA788\uA78B-\uA7AE\uA7B0-\uA7B7\uA7F7-\uA801\uA803-\uA805\uA807-\uA80A\uA80C-\uA822\uA840-\uA873\uA882-\uA8B3\uA8F2-\uA8F7\uA8FB\uA8FD\uA90A-\uA925\uA930-\uA946\uA960-\uA97C\uA984-\uA9B2\uA9CF\uA9E0-\uA9E4\uA9E6-\uA9EF\uA9FA-\uA9FE\uAA00-\uAA28\uAA40-\uAA42\uAA44-\uAA4B\uAA60-\uAA76\uAA7A\uAA7E-\uAAAF\uAAB1\uAAB5\uAAB6\uAAB9-\uAABD\uAAC0\uAAC2\uAADB-\uAADD\uAAE0-\uAAEA\uAAF2-\uAAF4\uAB01-\uAB06\uAB09-\uAB0E\uAB11-\uAB16\uAB20-\uAB26\uAB28-\uAB2E\uAB30-\uAB5A\uAB5C-\uAB65\uAB70-\uABE2\uAC00-\uD7A3\uD7B0-\uD7C6\uD7CB-\uD7FB\uF900-\uFA6D\uFA70-\uFAD9\uFB00-\uFB06\uFB13-\uFB17\uFB1D\uFB1F-\uFB28\uFB2A-\uFB36\uFB38-\uFB3C\uFB3E\uFB40\uFB41\uFB43\uFB44\uFB46-\uFBB1\uFBD3-\uFD3D\uFD50-\uFD8F\uFD92-\uFDC7\uFDF0-\uFDFB\uFE70-\uFE74\uFE76-\uFEFC\uFF21-\uFF3A\uFF41-\uFF5A\uFF66-\uFFBE\uFFC2-\uFFC7\uFFCA-\uFFCF\uFFD2-\uFFD7\uFFDA-\uFFDC]|\uD800[\uDC00-\uDC0B\uDC0D-\uDC26\uDC28-\uDC3A\uDC3C\uDC3D\uDC3F-\uDC4D\uDC50-\uDC5D\uDC80-\uDCFA\uDE80-\uDE9C\uDEA0-\uDED0\uDF00-\uDF1F\uDF30-\uDF40\uDF42-\uDF49\uDF50-\uDF75\uDF80-\uDF9D\uDFA0-\uDFC3\uDFC8-\uDFCF]|\uD801[\uDC00-\uDC9D\uDCB0-\uDCD3\uDCD8-\uDCFB\uDD00-\uDD27\uDD30-\uDD63\uDE00-\uDF36\uDF40-\uDF55\uDF60-\uDF67]|\uD802[\uDC00-\uDC05\uDC08\uDC0A-\uDC35\uDC37\uDC38\uDC3C\uDC3F-\uDC55\uDC60-\uDC76\uDC80-\uDC9E\uDCE0-\uDCF2\uDCF4\uDCF5\uDD00-\uDD15\uDD20-\uDD39\uDD80-\uDDB7\uDDBE\uDDBF\uDE00\uDE10-\uDE13\uDE15-\uDE17\uDE19-\uDE33\uDE60-\uDE7C\uDE80-\uDE9C\uDEC0-\uDEC7\uDEC9-\uDEE4\uDF00-\uDF35\uDF40-\uDF55\uDF60-\uDF72\uDF80-\uDF91]|\uD803[\uDC00-\uDC48\uDC80-\uDCB2\uDCC0-\uDCF2]|\uD804[\uDC03-\uDC37\uDC83-\uDCAF\uDCD0-\uDCE8\uDD03-\uDD26\uDD50-\uDD72\uDD76\uDD83-\uDDB2\uDDC1-\uDDC4\uDDDA\uDDDC\uDE00-\uDE11\uDE13-\uDE2B\uDE80-\uDE86\uDE88\uDE8A-\uDE8D\uDE8F-\uDE9D\uDE9F-\uDEA8\uDEB0-\uDEDE\uDF05-\uDF0C\uDF0F\uDF10\uDF13-\uDF28\uDF2A-\uDF30\uDF32\uDF33\uDF35-\uDF39\uDF3D\uDF50\uDF5D-\uDF61]|\uD805[\uDC00-\uDC34\uDC47-\uDC4A\uDC80-\uDCAF\uDCC4\uDCC5\uDCC7\uDD80-\uDDAE\uDDD8-\uDDDB\uDE00-\uDE2F\uDE44\uDE80-\uDEAA\uDF00-\uDF19]|\uD806[\uDCA0-\uDCDF\uDCFF\uDEC0-\uDEF8]|\uD807[\uDC00-\uDC08\uDC0A-\uDC2E\uDC40\uDC72-\uDC8F]|\uD808[\uDC00-\uDF99]|\uD809[\uDC80-\uDD43]|[\uD80C\uD81C-\uD820\uD840-\uD868\uD86A-\uD86C\uD86F-\uD872][\uDC00-\uDFFF]|\uD80D[\uDC00-\uDC2E]|\uD811[\uDC00-\uDE46]|\uD81A[\uDC00-\uDE38\uDE40-\uDE5E\uDED0-\uDEED\uDF00-\uDF2F\uDF40-\uDF43\uDF63-\uDF77\uDF7D-\uDF8F]|\uD81B[\uDF00-\uDF44\uDF50\uDF93-\uDF9F\uDFE0]|\uD821[\uDC00-\uDFEC]|\uD822[\uDC00-\uDEF2]|\uD82C[\uDC00\uDC01]|\uD82F[\uDC00-\uDC6A\uDC70-\uDC7C\uDC80-\uDC88\uDC90-\uDC99]|\uD835[\uDC00-\uDC54\uDC56-\uDC9C\uDC9E\uDC9F\uDCA2\uDCA5\uDCA6\uDCA9-\uDCAC\uDCAE-\uDCB9\uDCBB\uDCBD-\uDCC3\uDCC5-\uDD05\uDD07-\uDD0A\uDD0D-\uDD14\uDD16-\uDD1C\uDD1E-\uDD39\uDD3B-\uDD3E\uDD40-\uDD44\uDD46\uDD4A-\uDD50\uDD52-\uDEA5\uDEA8-\uDEC0\uDEC2-\uDEDA\uDEDC-\uDEFA\uDEFC-\uDF14\uDF16-\uDF34\uDF36-\uDF4E\uDF50-\uDF6E\uDF70-\uDF88\uDF8A-\uDFA8\uDFAA-\uDFC2\uDFC4-\uDFCB]|\uD83A[\uDC00-\uDCC4\uDD00-\uDD43]|\uD83B[\uDE00-\uDE03\uDE05-\uDE1F\uDE21\uDE22\uDE24\uDE27\uDE29-\uDE32\uDE34-\uDE37\uDE39\uDE3B\uDE42\uDE47\uDE49\uDE4B\uDE4D-\uDE4F\uDE51\uDE52\uDE54\uDE57\uDE59\uDE5B\uDE5D\uDE5F\uDE61\uDE62\uDE64\uDE67-\uDE6A\uDE6C-\uDE72\uDE74-\uDE77\uDE79-\uDE7C\uDE7E\uDE80-\uDE89\uDE8B-\uDE9B\uDEA1-\uDEA3\uDEA5-\uDEA9\uDEAB-\uDEBB]|\uD869[\uDC00-\uDED6\uDF00-\uDFFF]|\uD86D[\uDC00-\uDF34\uDF40-\uDFFF]|\uD86E[\uDC00-\uDC1D\uDC20-\uDFFF]|\uD873[\uDC00-\uDEA1]|\uD87E[\uDC00-\uDE1D])"; onlyAsterisk?: boolean; aSearchTerms: Array; bDisjunctionType: "and" | "or"; globalBold?: boolean; bold?: Array; highlightStyle?: string; // CSS class constructor( sSearchTerms: string, highlightStyle?: string, // CSS class matchContains?: boolean, bDisjunctionType?: "and" | "or" ) { // normalize searchterms in string format sSearchTerms = sSearchTerms || "*"; // check for pure asterisk search terms if (this.onlyAsteriskRegExp.test(sSearchTerms)) { this.onlyAsterisk = true; } // escape special chars // eslint-disable-next-line no-useless-escape sSearchTerms = sSearchTerms.replace(/([.^=!:${}()|\+\-\[\]\/\\])/g, "\\$1"); // store tokenized search terms array this.aSearchTerms = this.tokenizeSearchTerms(sSearchTerms); for (let j = 0; j < this.aSearchTerms.length; ++j) { let searchTerm: string | RegExp = this.aSearchTerms[j]; searchTerm = (searchTerm as string).replace(/\?/g, "."); searchTerm = (searchTerm as string).replace(/\*/g, ".*?"); // check if search term is Chinese (in unicode Chinese characters interval). const bIsChinese = (searchTerm as string).match(/[\u3400-\u9faf]/) !== null; if (bIsChinese || matchContains) { // match any place of the word, case insensitive // \b \w are evil regarding unicode searchTerm = new RegExp(searchTerm, "gi"); } else { // suggestion scenario // last term append * (for filter) if ((searchTerm as string).substr(-3) === ".*?") { // if * at end anyway, change matching to get different highlighting searchTerm = (searchTerm as string).substr(0, (searchTerm as string).length - 3) + this.pL + "*"; } searchTerm = new RegExp("(" + this.nonepL + "|^|\\s)(" + searchTerm + ")()", "gi"); } this.aSearchTerms[j] = searchTerm; } this.highlightStyle = highlightStyle; if (!bDisjunctionType) { bDisjunctionType = "and"; } this.bDisjunctionType = bDisjunctionType; // ("and", "or") } // if the text to be searched contains all of search terms, // return object with match flag and highlighted text or space in case of not match test(sText2BeSearched: string): { bMatch: boolean; sHighlightedText: string; } { const oReturn = { bMatch: false, sHighlightedText: "", }; if (!sText2BeSearched) { return oReturn; } this.initializeBoldArray(sText2BeSearched); // global flag is there is any bold char this.globalBold = false; let oRegSearchTerm; let aMatchResult; for (let j = 0; j < this.aSearchTerms.length; ++j) { // only match beginnings of the word, case insensitive oRegSearchTerm = this.aSearchTerms[j]; // match? let lastIndex = -1; let bLocalMatch = false; while ((aMatchResult = oRegSearchTerm.exec(sText2BeSearched)) !== null) { bLocalMatch = true; // prevent endless loop, should not happen but who knows... if (oRegSearchTerm.lastIndex === lastIndex) { break; } lastIndex = oRegSearchTerm.lastIndex; // aMatchResult.index: start position of matching term // oRegSearchTerm.lastIndex: start position of next search const startIndex = this.calStartIndex(sText2BeSearched, aMatchResult.index); if (startIndex < 0) { continue; } lastIndex = this.calLastIndex(sText2BeSearched, startIndex, oRegSearchTerm.lastIndex); this.markBoldArray(startIndex, lastIndex); } switch (this.bDisjunctionType) { case "or": if (bLocalMatch) { oReturn.bMatch = true; } break; case "and": default: if (!bLocalMatch) { return oReturn; } break; } } // search terms have logical "and" relation, all of them must be available in text oReturn.bMatch = true; // generate highligted text if (this.onlyAsterisk) { oReturn.sHighlightedText = sText2BeSearched; } else { oReturn.sHighlightedText = this.render(sText2BeSearched); } return oReturn; } private calStartIndex(text: string, startIndex: number): number { text = text.substring(startIndex); const reg = new RegExp(this.pL); const match = reg.exec(text); if (!match) { return startIndex; } return match.index + startIndex; } private calLastIndex(text: string, startIndex: number, lastIndex: number): number { text = text.substring(lastIndex, lastIndex); const reg = new RegExp(this.nonepL); const match = reg.exec(text); if (!match) { return lastIndex; } return lastIndex - 1; } // tokenize search terms splitted by spaces private tokenizeSearchTerms(terms: string): Array { const termsSeparatedBySpace = terms.split(" "); const newTerms = []; // split search terms with space and wildcard into array for (let i = 0; i < termsSeparatedBySpace.length; i++) { const termSpace = termsSeparatedBySpace[i].trim(); if (termSpace.length > 0 && termSpace !== ".*") { newTerms.push(termSpace); } } return newTerms; } // initialize the bold array private initializeBoldArray(sText: string): void { // create array which stores flag whether character is bold or not this.bold = new Array(sText.length); for (let i = 0; i < this.bold.length; ++i) { this.bold[i] = false; } } // mark bold array private markBoldArray(nStartIndex: number, nEndIndexPlus1: number): void { // mark bold characters in global array for (let i = nStartIndex; i < nEndIndexPlus1; i++) { this.bold[i] = true; this.globalBold = true; } } // render original text with tag private render(sOriginalText: string): string { // short cut if there is nothing to do if (!this.globalBold) { return sOriginalText; } // highlight sOriginalText according to information in this.bold let bold = false; const result = []; let start = 0; let i; for (i = 0; i < sOriginalText.length; ++i) { if ( (!bold && this.bold[i]) || // check for begin of bold sequence (bold && !this.bold[i]) ) { // check for end of bold sequence result.push(sOriginalText.substring(start, i)); if (bold) { // bold section ends result.push(""); } else { // bold section starts if (this.highlightStyle) { result.push(''); } else { result.push(""); } } bold = !bold; start = i; } } // add last part result.push(sOriginalText.substring(start, i)); if (bold) { result.push(""); } return result.join(""); } } // ======================================================================= // decorator for delayed execution // ======================================================================= // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type export function delayedExecution( originalFunction: T, delay: number ): T & { abort: () => void } { let timerId = null; const decorator = function (...args: Array) { // eslint-disable-next-line @typescript-eslint/no-this-alias const that = this; if (timerId) { window.clearTimeout(timerId); } timerId = window.setTimeout(function () { timerId = null; originalFunction.apply(that, args); }, delay); }; decorator.abort = function () { if (timerId) { window.clearTimeout(timerId); } }; return decorator as unknown as AbortableFunction; } // ======================================================================= // decorator for refusing outdated requests // ======================================================================= // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type export function refuseOutdatedRequests( originalFunction: T, requestGroupId?: string ): T & { abort: () => void } { /* eslint new-cap:0 */ let lastRequestId = 0; const decorator = (...args: Array) => { const requestId = ++lastRequestId; // console.log(requestGroupId + ' start ', requestId); const thisPromise = new Promise((resolve, reject) => { originalFunction .apply(this, args) .then((result) => { if (requestId !== lastRequestId) { // console.log(requestGroupId + ' throw ', requestId, ' because max', maxRequestId); return; // throw away outdated requests } // console.log(requestGroupId + ' accept ', requestId); resolve(result); }) .catch((error) => { if (requestId !== lastRequestId) { return; } // throw away outdated requests reject(error); }); }); return thisPromise; }; decorator.abort = () => { ++lastRequestId; // console.log(id + ' abort', maxRequestId); }; if (requestGroupId) { outdatedRequestAdministration.registerDecorator(requestGroupId, decorator); } return decorator as unknown as AbortableFunction; } // ======================================================================= // abort all requests for a given requestGroupId // ======================================================================= export function abortRequests(requestGroupId: number): void { const decorators = outdatedRequestAdministration.getDecorators(requestGroupId); for (const decorator of decorators) { decorator.abort(); } } // ======================================================================= // administration of outdated request decorators // ======================================================================= const outdatedRequestAdministration = { decoratorMap: {}, registerDecorator: function (requestGroupId, decorator) { let decorators = this.decoratorMap[requestGroupId]; if (!decorators) { decorators = []; this.decoratorMap[requestGroupId] = decorators; } decorators.push(decorator); }, getDecorators: function (requestGroupId) { let decorators = this.decoratorMap[requestGroupId]; if (!decorators) { decorators = []; } return decorators; }, }; /** * Applies custom style class to result items of UI5 table/list/grid control based on item property "customItemStyleClass". * Add/remove custom style class for result item * @param control Control to apply custom item style class * @param customItemStyleClass Apply or remove default style class of result item */ export function resultItemCustomStyleClassSetter(control: Control): void { if (!control) { return; } let items; // table/list if (typeof control["getItems"] === "function") { items = control["getItems"](); } // grid else if (typeof control["getContent"] === "function") { items = control["getContent"](); } if (items) { for (let i = 0; i < items.length; i++) { const item = items[i]; let itemObject; if (item.getBindingContext()) { itemObject = item.getBindingContext().getObject(); } else if (item.getBindingContext("publicSearchModel")) { itemObject = item.getBindingContext("publicSearchModel").getObject(); } if (itemObject?.customItemStyleClass) { item.addStyleClass(itemObject.customItemStyleClass); } else { // item.removeStyleClass(...); // ToDo (priority low) -> do we need to remove previously set style class? } } } } export function updateResultViewSelectionEnablement( resultViewItemsControl: List | Table | GridContainer, singleSelectionMode = false ): void { if (!resultViewItemsControl) { return; } const dataKey = singleSelectionMode ? "selection-enabled-singleSelection" : "selection-enabled"; let items; // table/list if (typeof resultViewItemsControl["getItems"] === "function") { items = resultViewItemsControl["getItems"](); } // grid else if (typeof resultViewItemsControl["getContent"] === "function") { items = resultViewItemsControl["getContent"](); } items.forEach((item) => { let itemObject; if (item.getBindingContext()) { itemObject = item.getBindingContext().getObject(); } else if (item.getBindingContext("publicSearchModel")) { itemObject = item.getBindingContext("publicSearchModel").getObject(); } // header row has no checkbox if (itemObject.selectionEnabled) { // selection enabled for this item item.addCustomData( new CustomData({ key: dataKey, // checkboxes enabled value: "true", writeToDom: true, }) ); } else { // selection disabled for this item item.addCustomData( new CustomData({ key: dataKey, // checkboxes disabled value: "false", writeToDom: true, }) ); } }); } export function updateResultViewSelected(resultViewItemsControl: List | Table | GridContainer): void { // console.log("SELECTION: updateResultViewSelected called,\nview type: " + resultviewClassType); if (!resultViewItemsControl) { return; } let resultviewClassType = "Custom Grid"; if (resultViewItemsControl.getMetadata()["_sClassName"].endsWith("SearchResultList")) { resultviewClassType = "List"; } else if (resultViewItemsControl.getMetadata()["_sClassName"].endsWith("SearchResultTable")) { resultviewClassType = "Table"; } else if (resultViewItemsControl.getMetadata()["_sClassName"].endsWith("SearchResultGrid")) { resultviewClassType = "Grid"; } let items; // table/list if (typeof resultViewItemsControl["getItems"] === "function") { items = resultViewItemsControl["getItems"](); } // grid else if (typeof resultViewItemsControl["getContent"] === "function") { items = resultViewItemsControl["getContent"](); } items.forEach((item) => { let itemObject; if (item.getBindingContext()) { itemObject = item.getBindingContext().getObject(); } else if (item.getBindingContext("publicSearchModel")) { itemObject = item.getBindingContext("publicSearchModel").getObject(); } // header row has no checkbox if (itemObject.selected) { // item selected for this item item.addStyleClass("sapUshellSearchResultListItem-Selected"); /* console.log( " SELECTION: add 'sapUshellSearchResultListItem-Selected', id:\n - " + item.getId() ); */ } else { // item not selected for this item item.removeStyleClass("sapUshellSearchResultListItem-Selected"); /* console.log( " SELECTION: remove 'sapUshellSearchResultListItem-Selected', id:\n - " + item.getId() ); */ if (resultviewClassType === "List") { item.setProperty("selected", false); // workaround for single select master -> after selecting item in table, item background of list item still in place '' } } }); } /** * Unescapes previously escaped html so that it can be rendered as html. * It only will unescape to "\", "\"" and "\"" elements. * @param domref html element to be unescaped * @param highlightStyle css class of the to be bold text */ export function boldTagUnescaper(domref: HTMLElement, highlightStyle?: string): void { if (!domref) { return; } let innerhtml = domref.innerHTML; innerhtml = boldTagUnescaperForStrings(innerhtml, highlightStyle); if (domref.innerHTML !== innerhtml) { domref.innerHTML = innerhtml; } } export function boldTagUnescaperForStrings(innerhtml: string, highlightStyle?: string): string { let boldStartES = ""; let boldStart = ""; if (highlightStyle) { boldStartES = '<b class="' + highlightStyle + '">'; boldStart = ''; } else { boldStartES = "<b>"; boldStart = ""; } const boldEndES = "</b>"; const boldEnd = ""; while (innerhtml.indexOf(boldStartES) + innerhtml.indexOf(boldEndES) >= -1) { // while these tags are found innerhtml = innerhtml.replace(boldStartES, boldStart); innerhtml = innerhtml.replace(boldEndES, boldEnd); } while (innerhtml.indexOf("<i>") + innerhtml.indexOf("</i>") >= -1) { // while these tags are found innerhtml = innerhtml.replace("<i>", ""); innerhtml = innerhtml.replace("</i>", ""); } while (innerhtml.indexOf("<span>") + innerhtml.indexOf("</span>") >= -1) { // while these tags are found innerhtml = innerhtml.replace("<span>", ""); innerhtml = innerhtml.replace("</span>", ""); } return innerhtml; } // ======================================================================= // get url hash // http://stackoverflow.com/questions/1703552/encoding-of-window-location-hash // ======================================================================= export function getHashFromUrl(): string { return window.location.href.substring(window.location.href.indexOf("#")) || "#"; } export function getUrlParameters(): UrlParameters { const urlParameters = parseUrlParameters(getHashFromUrl()); return urlParameters; } export function parseUrlParameters(unescapedSearchUrl: string): UrlParameters { // workaround for url ushells broken url escaping if special chars // like [] are used in urls (like in app tiles with search filters). // restriction of this workaround: // - tiles with searchterms like the names of the parameters dont // work (for example a tile with searchterm "top=") const oParametersLowerCased: UrlParameters = {}; const knownSearchUrlParameters = [ { name: "filter", pos: -1, value: "", }, { name: "top", pos: -1, value: "", }, { name: "datasource", pos: -1, value: "", }, { name: "searchterm", pos: -1, value: "", }, { name: "orderby", pos: -1, value: "", }, { name: "sortorder", pos: -1, value: "", }, ]; // find the parameters: for (let i = 0; i < knownSearchUrlParameters.length; i++) { knownSearchUrlParameters[i].pos = unescapedSearchUrl .toLowerCase() .indexOf(knownSearchUrlParameters[i].name + "="); } knownSearchUrlParameters.sort(function (a, b) { return a.pos - b.pos; }); // find the parameter boundaries: for (let j = 0; j < knownSearchUrlParameters.length; j++) { if (knownSearchUrlParameters[j].pos !== -1) { if (knownSearchUrlParameters[j + 1] && knownSearchUrlParameters[j + 1].pos !== -1) { knownSearchUrlParameters[j].value = unescapedSearchUrl.substring( knownSearchUrlParameters[j].pos, knownSearchUrlParameters[j + 1].pos ); } else { knownSearchUrlParameters[j].value = unescapedSearchUrl.substring( knownSearchUrlParameters[j].pos ); } // remove the parameter name and "=": knownSearchUrlParameters[j].value = knownSearchUrlParameters[j].value.substring( knownSearchUrlParameters[j].name.length + 1 ); if ( knownSearchUrlParameters[j].value.charAt(knownSearchUrlParameters[j].value.length - 1) === "&" ) { knownSearchUrlParameters[j].value = knownSearchUrlParameters[j].value.substring( 0, knownSearchUrlParameters[j].value.length - 1 ); } knownSearchUrlParameters[j].value = window.decodeURIComponent(knownSearchUrlParameters[j].value); oParametersLowerCased[knownSearchUrlParameters[j].name] = knownSearchUrlParameters[j].value; } } return oParametersLowerCased; } // ======================================================================= // check if search app is running // ======================================================================= export function isSearchAppActive(): boolean { if (getHashFromUrl().substr(1, 13) === "Action-search") { return true; } return false; } // ======================================================================= // Hasher // using window.hasher does not work because // hasher always use encodeURL for the whole hash but for example we need // - to encode '=' in a value (of name value pair) // but not the '=' separating name and value // ======================================================================= export const hasher = { hash: null, setHash: function (hash: string): void { // compare using decodeURIComponent because encoding may slightly differ // (Saved tiles scramble the URL. The URL of a saved tile is different // to the URL serialized by search app) if (window.decodeURIComponent(getHashFromUrl()) !== window.decodeURIComponent(hash)) { try { window.location.hash = hash; this.hash = hash; // save hash for later comparison in hasher.hashChanged. // This call of hasher.setHash triggers via the hash changed handler a call of hasher.hasChanged. // hasher.hasChanged needs to return false in order to ignore hash changes caused by // the search application itself. Therefore we save the hash for comparison in hasher.hasChanged. } catch (error) { // in IE url cannot be update if longer than 2083 chars -> show error message to the user this.showUrlUpdateError(error); } } else { // in case url does not change the current setHash call does not trigger the hash changed event. // Therefore hashChanged (called from hash changed handler) is not called. // Any subsequent call to hasher.hasChanged (triggered by URL changes) // shall return true therefore we set hash to null this.hash = null; } }, init: function (): void { this.hash = getHashFromUrl(); }, reset: function (): void { this.hash = null; }, hasChanged: function (): boolean { try { return window.decodeURIComponent(this.hash) !== window.decodeURIComponent(getHashFromUrl()); } finally { this.hash = null; // always set hash to null: hasChanged returns only one times false. // this way we can ignore hash changed events triggered by the search application itself. } }, showUrlUpdateError: function (): void { // display error only one times if (this.urlError) { return; } this.urlError = true; // show message box const message = i18n.getText("searchUrlErrorMessage"); MessageBox.warning(message, { title: i18n.getText("searchUrlErrorTitle"), styleClass: "sapUshellSearchMessageBox", // selector for closePopovers }); }, }; // ======================================================================= // Subscribe the given event only once // ======================================================================= const idMap = {}; export function subscribeOnlyOnce( id: string, eventName: string, // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type callBack: Function, selfControl: object ): void { if (idMap[id]) { idMap[id].unsubscribe(); } const wrapper = function () { callBack.apply(selfControl); EventBus.getInstance().unsubscribe(eventName, wrapper, selfControl); }; EventBus.getInstance().subscribe(eventName, wrapper, selfControl); idMap[id] = { unsubscribe: function () { EventBus.getInstance().unsubscribe(eventName, wrapper, selfControl); }, }; } // ======================================================================= // check if element is displayed // ======================================================================= function _isDisplayed(element) { // element is DOM object if (element && element.nodeType) { // check if element is visible (not display: none and in DOM) if ( element instanceof HTMLElement && element.offsetParent !== null && !!(element.offsetWidth || element.offsetHeight || element.getClientRects().length) ) { return true; } } // element if SAPUI5 object if (element?.getMetadata()?.getName()) { if (element.getDomRef && element.getDomRef()) { return true; } } return false; } // ======================================================================= // Focus Handler // ======================================================================= export class SearchFocusHandler { oSearchComposite: SearchCompositeControl; oModel: SearchModel; focusSetter?: number | string | NodeJS.Timeout; constructor(oSearchComposite: SearchCompositeControl) { this.oSearchComposite = oSearchComposite; } // get the controlDomRef to be focused get2BeFocusedControlDomRef(): HTMLElement { if (!this.oModel) { this.oModel = this.oSearchComposite.getModelInternal() as SearchModel; // ToDo } let index = 0; let control = null; let controlDomRef = null; let isListDisplayed = false; let isTableDisplayed = false; let isGridDisplayed = false; if (this.oModel.getDataSource() !== this.oModel.appDataSource) { // 1. mixed result list if (this.oModel.getProperty("/boCount") > 0 && this.oModel.getProperty("/appCount") > 0) { // 1.1 bos + apps index = this.oModel.getProperty("/focusIndex"); index = index > 0 ? index + 1 : index; control = this.oSearchComposite.searchResultList.getItems()[index]; if (control && control.getDomRef) { controlDomRef = control.getDomRef(); } } else if ( this.oModel.getProperty("/boCount") > 0 && this.oModel.getProperty("/appCount") === 0 ) { // 1.2 only bos index = this.oModel.getProperty("/focusIndex"); isListDisplayed = this.oModel.getResultViewType() === "searchResultList"; isTableDisplayed = this.oModel.getResultViewType() === "searchResultTable"; isGridDisplayed = this.oModel.getResultViewType() === "searchResultGrid"; if (isListDisplayed) { // 1.2.1 list view control = this.oSearchComposite["searchResultList"].getItems()[index]; } else if (isTableDisplayed) { // 1.2.2 table view control = this.oSearchComposite["searchResultTable"].getItems()[index]; } else if (isGridDisplayed) { // 1.2.3 grid view if (this.oSearchComposite.searchResultGrid.getAggregation("items")) { control = this.oSearchComposite.searchResultGrid.getAggregation("items")[index]; } else if ( this.oSearchComposite.searchResultGrid["getContent"] && this.oSearchComposite.searchResultGrid["getContent"]() ) { control = this.oSearchComposite.searchResultGrid["getContent"]()[index]; } } if (control && control.getDomRef) { controlDomRef = control.getDomRef(); } } else if ( this.oModel.getProperty("/boCount") === 0 && this.oModel.getProperty("/appCount") > 0 ) { // 1.3 only apps control = this.oSearchComposite["searchResultList"].getItems()[0]; if (_isDisplayed(control)) { // get the DOM ref and find the first element with tabindex or a button const domRef = control.getDomRef(); if (domRef) { // looks into a tile and return the first focusable child element controlDomRef = domRef.querySelector("[tabindex], button"); } } } } else { // 2. pure apps result list (tile matrix) const oTilesContainer = this.oSearchComposite["appSearchResult"].getContent()[0]; if (oTilesContainer.getDomRef() === null) { return null; } index = this.oModel.getProperty("/focusIndex"); oTilesContainer["_oItemNavigation"].setFocusedIndex(index); controlDomRef = oTilesContainer.getDomRef(); // with new app search in grid container, just need set focus in tiles container } return controlDomRef; } // set focus // =================================================================== setFocus(): void { // this method is called // 1) after event allSearchFinished (see registration in Search.controller) // 2) after event afterNavigate (see registration in searchshellhelper) // 3) after event appComponentLoaded (see registration in searchshellhelper) let retries = 20; // method for setting the focus with periodic retry const doSetFocus = function () { this.focusSetter = null; let controlDomRef = this.get2BeFocusedControlDomRef(); // this is a workaround to prevent first app tile and input share the same sapenter event // need to fix in better way if (retries === 20) { controlDomRef = null; } if ( !controlDomRef || // condition 1 document.querySelectorAll(".sapUshellSearchTileContainerDirty").length > 0 || // condition 3 document.querySelectorAll(".sapMBusyDialog").length > 0 ) { // condition 4 if (--retries) { this.focusSetter = setTimeout(doSetFocus.bind(this), 100); } return; } // condition 1: // control and its domref do need to exist // condition 2: // no rendering process is running // focus can only be set after ui5 rendering is finished because ui5 preserves the focus // condition 3: // loading dialog (app loading) is closed // loading dialog restores old focus (using timeout 300ms) so we need to wait until loading dialog has finished // condition 4: // wait that app tile container has finished rendering // app tile container has two rendering steps. First step is just for calculating number of tiles. // condition 5: // wait that buys indicators are finished // set focus controlDomRef.focus(); // automatic expand only the first result líst item if (this.oModel.getProperty("/focusIndex") === 0) { const control = UI5Element.getElementById(controlDomRef.getAttribute("id")); if (control && control.getAggregation("content") && control.getAggregation("content")[0]) { const resultListItem = control.getAggregation("content")[0] as SearchResultListItem; if (typeof resultListItem?.showDetails === "function") { resultListItem?.showDetails(); } } } }; // cancel any scheduled focusSetter if (this.focusSetter) { clearTimeout(this.focusSetter); this.focusSetter = null; } // set focus doSetFocus.bind(this)(); } } // ======================================================================= // format integer // ======================================================================= let _integerShortFormatter: NumberFormat; let _integerStandardFormatter: NumberFormat; const oLocale = new Locale(Localization.getLanguageTag().toString()); export function formatInteger(value: number): string { // lazy create integerShortFormatter if (!_integerShortFormatter) { _integerShortFormatter = NumberFormat.getIntegerInstance( { style: "short", precision: 3, groupingEnabled: true, }, oLocale ); } // lazy create integerStandardFormatter if (!_integerStandardFormatter) { _integerStandardFormatter = NumberFormat.getIntegerInstance( { style: "standard", precision: 3, groupingEnabled: true, }, oLocale ); } // 99950 is the first number (with precision 3 rounding) that will map to 100000; same as "parseFloat((Math.abs(number)).toPrecision(3)) >= 100000" if (Math.abs(value) >= 99950) { return _integerShortFormatter.format(value); } return _integerStandardFormatter.format(value); } // ======================================================================= // format file size humanfriendly // ======================================================================= let _fileSizeFormatter: FileSizeFormat; export function formatFileSize(value: number): string { // lazy create _fileSizeFormatter if (!_fileSizeFormatter) { _fileSizeFormatter = FileSizeFormat.getInstance( { binaryFilesize: true, }, oLocale ); } return _fileSizeFormatter.format(value); } /** * Adds a tooltip if the content is overflowing. * If the first element is not visible, * it applies forward ellipsis truncation to the text before the first element to ensure it is visible. * It also sets the title attribute of the element to the full text content. * If the content is not overflowing or truncated, no tooltip is set. * @param elementToBeChecked The DOM element to check for truncation or overflowing and set tooltip. */ export function calculateTooltipForwordEllipsis(elementToBeChecked: HTMLElement): void { const text = elementToBeChecked.textContent || ""; // long text attributes have their own tooltip set from the InvisibleText element — skip entirely if (elementToBeChecked.closest("[data-islongtext]")) { return; } // there could be nested layers of elements between elements and elementToBeChecked. // Find the target measure element, which is the direct parent of elements const targetMeasureElement = findChildWithClass( elementToBeChecked, ["sapUshellSearchLink", "sapUshellSearchTextText"], ["sapMLnkText", "sapMTextLineClamp"] //sapMTextLineClamp span only occurs in multiline SearchText ); // check whether content is overflowing if ( !( targetMeasureElement.offsetWidth < targetMeasureElement.scrollWidth || targetMeasureElement.offsetHeight < targetMeasureElement.scrollHeight ) ) { // Everything is fine, no tooltip needed return; } else { // Content is overflowing, tooltip needed anyway if (!elementToBeChecked.hasAttribute("title") && !targetMeasureElement.hasAttribute("title")) { elementToBeChecked.setAttribute("title", text); } // check if first highlighted text is available const firstBElement = elementToBeChecked?.querySelector("b"); // No highlighted text available if (!firstBElement) { return; } // Highlighted text is available const isFirstBElementVisible = isElementVisible(firstBElement, targetMeasureElement); // highlighted text is visible, no forward ellipsis needed if (isFirstBElementVisible) { return; } else { // highlighted text is not visible, forward ellipsis needed applyForwardEllipsisTruncation(targetMeasureElement, firstBElement); } } } // apply forward ellipsis truncation // This function truncates the text before the first element in the targetMeasureElement // until the first element is visible, ensuring that the first element // is always visible in the targetMeasureElement. // For performance optimization purposes, it uses a simplified binary search approach to find // the maximum text length that can be displayed before the first element. // Sometimes minor inaccuracies can occur due to rounding issues, but in acceptable ranges. function applyForwardEllipsisTruncation(targetMeasureElement: HTMLElement, firstBElement: HTMLElement): void { const iconSpan = targetMeasureElement.querySelector(".sapUshellSearchTextIcon") as HTMLElement; const range = document.createRange(); if (iconSpan) { range.setStartAfter(iconSpan); } else { range.setStartBefore(targetMeasureElement.firstChild); } range.setEndBefore(firstBElement); const textBeforeFirstB = range.toString(); if (textBeforeFirstB.trim().length > 0) { // preserve the original text for restoration and tooltip // preserveElement(domref, classNameForwardEllipsis); let truncatedText = textBeforeFirstB; let isBElementVisible = false; let end = textBeforeFirstB.length; // binary search truncation while (1 < end) { const mid = Math.floor(end / 2); // cut in the mid from beginning to the end truncatedText = textBeforeFirstB.slice(-mid); // temporarily add with ellipsis const tempTextNode = document.createTextNode("... " + truncatedText); range.deleteContents(); range.insertNode(tempTextNode); // check if the first element is now visible isBElementVisible = isElementVisible(firstBElement, targetMeasureElement); if (isBElementVisible) { return; } else { end = mid; } } // fallback: cut off all the text before the first element if (!isBElementVisible) { const newTextNode = document.createTextNode("... "); range.deleteContents(); range.insertNode(newTextNode); } } } function isElementVisible(elementToBeMeasured: HTMLElement, parentElement: HTMLElement): boolean { if (!elementToBeMeasured) { return false; } // Get the bounding rectangle of the element const elToBeMeasuredRect: DOMRect = elementToBeMeasured.getBoundingClientRect(); // Get the bounding rectangle of the parent container const parentElRect: DOMRect = parentElement.getBoundingClientRect(); // Check if the element's rectangle is within the parent's rectangle const isVisible = elToBeMeasuredRect.top >= parentElRect.top && elToBeMeasuredRect.left >= parentElRect.left && elToBeMeasuredRect.bottom <= parentElRect.bottom && elToBeMeasuredRect.right <= parentElRect.right; return isVisible; } /** * Utility function to find a child element with specific classes. * @param currentElement The parent element. * @param parentClasses Array of class names for the parent element as extra filter to avoid searching of irrelevant elements. * @param childClasses Array of class names for the child element to find. * @returns The child element if found, otherwise the parent element. */ function findChildWithClass( currentElement: HTMLElement, parentClasses: string[], childClasses: string[] ): HTMLElement { // check if currentElement has at least one of parentClasses const hasAnyParentClass = parentClasses.some((cls) => currentElement.classList.contains(cls)); if (hasAnyParentClass) { // find the first child with any of the childClasses for (const childClass of childClasses) { const found = currentElement.querySelector(`.${childClass}`) as HTMLElement; if (found) { return found; } } return currentElement; } return currentElement; } // ======================================================================= // convert promise to jQuery deferred // ======================================================================= export function convertPromiseTojQueryDeferred(promise: Promise): JQueryDeferred { if (promise.finally) { // promise has property "finally" // is promise, need to convert const deferred = jQuery.Deferred(); promise.then( function () { // eslint-disable-next-line deferred.resolve.apply(deferred, arguments); }, function () { // eslint-disable-next-line deferred.reject.apply(deferred, arguments); } ); return deferred; } else { // is deferred, don't convert return promise as unknown as JQueryDeferred; } } // ======================================================================= // convert jQuery to promise // ======================================================================= export function convertJQueryDeferredToPromise(deferred: JQueryDeferred): Promise { if (deferred.always) { // deferred has property "always" // is deferred, need to convert return new Promise(function (resolve, reject) { deferred.then(resolve, reject); }); } else { // is promise, don't convert return deferred as unknown as Promise; } } // ======================================================================= // Async Module Require // return Promise // ======================================================================= export function requireUI5Modules(modules: Array): Promise> { return new Promise(function (resolve) { sap.ui.require(modules, function (...args) { resolve(Array.from(args)); }); }); } // ======================================================================= // check one array is subset of another // ======================================================================= export function isSubsetOf(subset: Array, superset: Array): boolean { if (!Array.isArray(subset) || subset.length === 0) { return false; } if (!Array.isArray(superset) || superset.length === 0) { return false; } let valid = true; for (let i = 0; i < subset.length; i++) { if (!superset.includes(subset[i])) { valid = false; break; } } return valid; }