├── .eslintignore ├── .eslintrc.js ├── .gitignore ├── .npmignore ├── LICENSE ├── README.md ├── SECURITY.md ├── assets └── icon.png ├── capabilities.json ├── package-lock.json ├── package.json ├── pbiviz.json ├── src ├── settings.ts └── visual.ts ├── stringResources └── en-US │ └── resources.resjson ├── style └── visual.less └── tsconfig.json /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | coverage 4 | test 5 | .eslintrc.js 6 | karma.conf.ts 7 | test.webpack.config.js -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | "browser": true, 4 | "es6": true, 5 | "es2017": true 6 | }, 7 | root: true, 8 | parser: "@typescript-eslint/parser", 9 | parserOptions: { 10 | project: "tsconfig.json", 11 | tsconfigRootDir: ".", 12 | }, 13 | plugins: [ 14 | "powerbi-visuals" 15 | ], 16 | extends: [ 17 | "plugin:powerbi-visuals/recommended" 18 | ], 19 | rules: {} 20 | }; -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Temp folders 9 | .tmp 10 | 11 | # Output 12 | dist 13 | 14 | # Runtime data 15 | pids 16 | *.pid 17 | *.seed 18 | *.pid.lock 19 | 20 | # Directory for instrumented libs generated by jscoverage/JSCover 21 | lib-cov 22 | 23 | # Coverage directory used by tools like istanbul 24 | coverage 25 | 26 | # nyc test coverage 27 | .nyc_output 28 | 29 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 30 | .grunt 31 | 32 | # Bower dependency directory (https://bower.io/) 33 | bower_components 34 | 35 | # node-waf configuration 36 | .lock-wscript 37 | 38 | # Compiled binary addons (http://nodejs.org/api/addons.html) 39 | build/Release 40 | 41 | # Dependency directories 42 | node_modules/ 43 | jspm_packages/ 44 | 45 | # Typescript v1 declaration files 46 | typings/ 47 | 48 | # Optional npm cache directory 49 | .npm 50 | 51 | # Optional eslint cache 52 | .eslintcache 53 | 54 | # Optional REPL history 55 | .node_repl_history 56 | 57 | # Output of 'npm pack' 58 | *.tgz 59 | 60 | # Yarn Integrity file 61 | .yarn-integrity 62 | 63 | # dotenv environment variables file 64 | .env 65 | 66 | # webpack files 67 | webpack.statistics*.html 68 | 69 | # VS code local config 70 | .vscode -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | typings 2 | node_modules 3 | .DS_Store 4 | .tmp 5 | dist -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Microsoft Corporation. All rights reserved. 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 all 13 | 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 THE 21 | SOFTWARE 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Text Filter PowerBI Visual 2 | 3 | # Contributing 4 | 5 | This project welcomes contributions and suggestions. Most contributions require you to agree to a 6 | Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us 7 | the rights to use your contribution. For details, visit https://cla.microsoft.com. 8 | 9 | When you submit a pull request, a CLA-bot will automatically determine whether you need to provide 10 | a CLA and decorate the PR appropriately (e.g., label, comment). Simply follow the instructions 11 | provided by the bot. You will only need to do this once across all repos using our CLA. 12 | 13 | This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). 14 | For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or 15 | contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. 16 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## Security 4 | 5 | Microsoft takes the security of our software products and services seriously, which includes all source code repositories managed through our GitHub organizations, which include [Microsoft](https://github.com/Microsoft), [Azure](https://github.com/Azure), [DotNet](https://github.com/dotnet), [AspNet](https://github.com/aspnet), [Xamarin](https://github.com/xamarin), and [our GitHub organizations](https://opensource.microsoft.com/). 6 | 7 | If you believe you have found a security vulnerability in any Microsoft-owned repository that meets [Microsoft's definition of a security vulnerability](https://aka.ms/opensource/security/definition), please report it to us as described below. 8 | 9 | ## Reporting Security Issues 10 | 11 | **Please do not report security vulnerabilities through public GitHub issues.** 12 | 13 | Instead, please report them to the Microsoft Security Response Center (MSRC) at [https://msrc.microsoft.com/create-report](https://aka.ms/opensource/security/create-report). 14 | 15 | If you prefer to submit without logging in, send email to [secure@microsoft.com](mailto:secure@microsoft.com). If possible, encrypt your message with our PGP key; please download it from the [Microsoft Security Response Center PGP Key page](https://aka.ms/opensource/security/pgpkey). 16 | 17 | You should receive a response within 24 hours. If for some reason you do not, please follow up via email to ensure we received your original message. Additional information can be found at [microsoft.com/msrc](https://aka.ms/opensource/security/msrc). 18 | 19 | Please include the requested information listed below (as much as you can provide) to help us better understand the nature and scope of the possible issue: 20 | 21 | * Type of issue (e.g. buffer overflow, SQL injection, cross-site scripting, etc.) 22 | * Full paths of source file(s) related to the manifestation of the issue 23 | * The location of the affected source code (tag/branch/commit or direct URL) 24 | * Any special configuration required to reproduce the issue 25 | * Step-by-step instructions to reproduce the issue 26 | * Proof-of-concept or exploit code (if possible) 27 | * Impact of the issue, including how an attacker might exploit the issue 28 | 29 | This information will help us triage your report more quickly. 30 | 31 | If you are reporting for a bug bounty, more complete reports can contribute to a higher bounty award. Please visit our [Microsoft Bug Bounty Program](https://aka.ms/opensource/security/bounty) page for more details about our active programs. 32 | 33 | ## Preferred Languages 34 | 35 | We prefer all communications to be in English. 36 | 37 | ## Policy 38 | 39 | Microsoft follows the principle of [Coordinated Vulnerability Disclosure](https://aka.ms/opensource/security/cvd). 40 | 41 | 42 | -------------------------------------------------------------------------------- /assets/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/PowerBI-visuals-TextFilter/823d920a70c668a2f339bccf1a1a4feff6f1b1e8/assets/icon.png -------------------------------------------------------------------------------- /capabilities.json: -------------------------------------------------------------------------------- 1 | { 2 | "dataRoles": [ 3 | { 4 | "displayName": "Field", 5 | "name": "field", 6 | "kind": "Grouping" 7 | } 8 | ], 9 | "objects": { 10 | "general": { 11 | "displayName": "General", 12 | "properties": { 13 | "filter": { 14 | "type": { 15 | "filter": true 16 | } 17 | } 18 | } 19 | }, 20 | "textBox": { 21 | "properties": { 22 | "fontFamily": { 23 | "type": { 24 | "formatting": { 25 | "fontFamily": true 26 | } 27 | } 28 | }, 29 | "fontSize": { 30 | "type": { 31 | "formatting": { 32 | "fontSize": true 33 | } 34 | } 35 | }, 36 | "border": { 37 | "type": { 38 | "bool": true 39 | } 40 | }, 41 | "borderColor": { 42 | "type": { 43 | "fill": { 44 | "solid": { 45 | "color": true 46 | } 47 | } 48 | } 49 | } 50 | } 51 | } 52 | }, 53 | "dataViewMappings": [ 54 | { 55 | "conditions": [ 56 | { 57 | "field": { 58 | "max": 1 59 | } 60 | } 61 | ], 62 | "categorical": { 63 | "categories": { 64 | "for": { 65 | "in": "field" 66 | }, 67 | "dataReductionAlgorithm": { 68 | "top": { 69 | "count": 30000 70 | } 71 | } 72 | } 73 | } 74 | } 75 | ], 76 | "privileges": [], 77 | "supportsSynchronizingFilterState": true, 78 | "supportsKeyboardFocus": true 79 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "text-filter", 3 | "version": "2.2.9.0", 4 | "scripts": { 5 | "pbiviz": "pbiviz", 6 | "start": "pbiviz start", 7 | "package": "pbiviz package", 8 | "lint": "npm run eslint", 9 | "eslint": "npx eslint . --ext .js,.jsx,.ts,.tsx" 10 | }, 11 | "devDependencies": { 12 | "@types/d3-selection": "^3.0.5", 13 | "@typescript-eslint/eslint-plugin": "^5.59.1", 14 | "@typescript-eslint/parser": "^5.59.1", 15 | "eslint": "^8.39.0", 16 | "eslint-plugin-powerbi-visuals": "^0.8.1", 17 | "powerbi-visuals-tools": "^4.3.2", 18 | "ts-loader": "6.1.0", 19 | "typescript": "^5.0.4" 20 | }, 21 | "dependencies": { 22 | "d3-selection": "^3.0.0", 23 | "powerbi-models": "^1.7.0", 24 | "powerbi-visuals-api": "~5.3.0", 25 | "powerbi-visuals-utils-formattingmodel": "^5.0.1" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /pbiviz.json: -------------------------------------------------------------------------------- 1 | { 2 | "visual": { 3 | "name": "textFilter", 4 | "displayName": "Text Filter", 5 | "guid": "textFilter25A4896A83E0487089E2B90C9AE57C8A", 6 | "visualClassName": "Visual", 7 | "version": "2.2.9.0", 8 | "description": "Provides a search box that can be placed anywhere in your dashboard. This adds a text filtering capability for quick searching across your data.", 9 | "supportUrl": "https://community.powerbi.com", 10 | "gitHubUrl": "https://github.com/microsoft/PowerBI-visuals-TextFilter" 11 | }, 12 | "output": "dist/textFilter.pbiviz", 13 | "apiVersion": "5.3.0", 14 | "author": { 15 | "name": "Microsoft", 16 | "email": "pbicvsupport@microsoft.com" 17 | }, 18 | "assets": { 19 | "icon": "assets/icon.png" 20 | }, 21 | "externalJS": null, 22 | "style": "style/visual.less", 23 | "capabilities": "capabilities.json", 24 | "dependencies": null, 25 | "stringResources": [] 26 | } 27 | -------------------------------------------------------------------------------- /src/settings.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Power BI Visual CLI 3 | * 4 | * Copyright (c) Microsoft Corporation 5 | * All rights reserved. 6 | * MIT License 7 | * 8 | * Permission is hereby granted, free of charge, to any person obtaining a copy 9 | * of this software and associated documentation files (the ""Software""), to deal 10 | * in the Software without restriction, including without limitation the rights 11 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | * copies of the Software, and to permit persons to whom the Software is 13 | * furnished to do so, subject to the following conditions: 14 | * 15 | * The above copyright notice and this permission notice shall be included in 16 | * all copies or substantial portions of the Software. 17 | * 18 | * THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 24 | * THE SOFTWARE. 25 | */ 26 | 27 | 28 | import powerbi from "powerbi-visuals-api"; 29 | 30 | import { Card, ColorPicker, FontControl, FontPicker, Model, NumUpDown, Slice, ToggleSwitch } from "powerbi-visuals-utils-formattingmodel/lib/FormattingSettingsComponents"; 31 | 32 | export class TextFilterSettingsModel extends Model { 33 | textBox = new TextBoxSettingsCard(); 34 | cards: Card[] = [this.textBox]; 35 | 36 | // we don't need color picker for border color if the border is disabled 37 | public removeBorderColor() { 38 | this.textBox.slices = [this.textBox.font, this.textBox.enableBorder] 39 | } 40 | } 41 | 42 | 43 | 44 | class TextBoxSettingsCard extends Card { 45 | 46 | name: string = "textBox"; 47 | displayNameKey?: string = "Visual_Textbox_Settings"; 48 | placeholderTextKey: string = "Visual_Input_Placeholder" 49 | 50 | 51 | private minFontSize: number = 8; 52 | private defaultFontSize: number = 11; 53 | 54 | enableBorder = new ToggleSwitch({ 55 | name: "border", 56 | displayNameKey: "Visual_Enable_Border", 57 | value: true 58 | }); 59 | 60 | borderColor = new ColorPicker({ 61 | name: "borderColor", 62 | displayNameKey: "Visual_Border_color", 63 | value: { value: "#000000" } 64 | }); 65 | 66 | font = new FontControl({ 67 | name: "font", 68 | displayNameKey: "Visual_Font", 69 | fontFamily: new FontPicker({ 70 | name: "fontFamily", 71 | displayNameKey: "Visual_Font_Family", 72 | value: "Segoe UI, wf_segoe-ui_normal, helvetica, arial, sans-serif" 73 | }), 74 | fontSize: new NumUpDown({ 75 | name: "fontSize", 76 | displayNameKey: "Visual_Font_Size", 77 | value: this.defaultFontSize, 78 | options: { 79 | minValue: { 80 | type: powerbi.visuals.ValidatorType.Min, 81 | value: this.minFontSize, 82 | } 83 | } 84 | }) 85 | }); 86 | 87 | slices: Slice[] = [this.font, this.enableBorder, this.borderColor]; 88 | } 89 | 90 | -------------------------------------------------------------------------------- /src/visual.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Power BI Visual CLI 3 | * 4 | * Copyright (c) Microsoft Corporation 5 | * All rights reserved. 6 | * MIT License 7 | * 8 | * Permission is hereby granted, free of charge, to any person obtaining a copy 9 | * of this software and associated documentation files (the ""Software""), to deal 10 | * in the Software without restriction, including without limitation the rights 11 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | * copies of the Software, and to permit persons to whom the Software is 13 | * furnished to do so, subject to the following conditions: 14 | * 15 | * The above copyright notice and this permission notice shall be included in 16 | * all copies or substantial portions of the Software. 17 | * 18 | * THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 24 | * THE SOFTWARE. 25 | */ 26 | "use strict"; 27 | 28 | import "./../style/visual.less"; 29 | import powerbi from "powerbi-visuals-api"; 30 | import VisualConstructorOptions = powerbi.extensibility.visual.VisualConstructorOptions; 31 | import VisualUpdateOptions = powerbi.extensibility.visual.VisualUpdateOptions; 32 | import IVisual = powerbi.extensibility.visual.IVisual; 33 | import IVisualEventService = powerbi.extensibility.IVisualEventService; 34 | import ILocalizationManager = powerbi.extensibility.ILocalizationManager; 35 | import FilterAction = powerbi.FilterAction; 36 | import { IAdvancedFilter, AdvancedFilter } from "powerbi-models"; 37 | 38 | import { Selection as d3Selection, select as d3Select } from "d3-selection"; 39 | 40 | import { TextFilterSettingsModel } from "./settings"; 41 | 42 | import { FormattingSettingsService } from "powerbi-visuals-utils-formattingmodel"; 43 | 44 | const pxToPt = 0.75, 45 | fontPxAdjSml = 20, 46 | fontPxAdjStd = 24, 47 | fontPxAdjLrg = 26; 48 | 49 | 50 | export class Visual implements IVisual { 51 | 52 | private target: HTMLElement; 53 | private searchUi: d3Selection; 54 | private searchBox: d3Selection; 55 | private searchButton: d3Selection; 56 | private clearButton: d3Selection; 57 | private column: powerbi.DataViewMetadataColumn; 58 | private host: powerbi.extensibility.visual.IVisualHost; 59 | private events: IVisualEventService; 60 | private formattingSettingsService: FormattingSettingsService; 61 | private formattingSettings: TextFilterSettingsModel; 62 | private localizationManager: ILocalizationManager; 63 | 64 | constructor(options: VisualConstructorOptions) { 65 | this.events = options.host.eventService; 66 | this.target = options.element; 67 | this.searchUi = d3Select(this.target) 68 | .append("div") 69 | .classed("text-filter-search", true); 70 | this.searchBox = this.searchUi 71 | .append("input") 72 | .attr("aria-label", "Enter your search") 73 | .attr("type", "text") 74 | .attr("name", "search-field") 75 | .attr("autofocus", true) 76 | .attr("tabindex", 0) 77 | .classed("accessibility-compliant", true) 78 | .classed("border-on-focus", true); 79 | this.searchButton = this.searchUi 80 | .append("button") 81 | .classed("c-glyph search-button", true) 82 | .attr("name", "search-button") 83 | .classed("border-on-focus", true); 84 | this.searchButton 85 | .append("span") 86 | .classed("x-screen-reader", true) 87 | .text("Search"); 88 | this.clearButton = this.searchUi 89 | .append("button") 90 | .classed("c-glyph clear-button", true) 91 | .attr("name", "clear-button") 92 | .classed("border-on-focus", true); 93 | this.clearButton 94 | .append("span") 95 | .classed("x-screen-reader", true) 96 | .text("Clear"); 97 | 98 | // custom visuals require 2 tabs to get to the first focusable element (by design) 99 | // event listener below is designed to overcome this limitation 100 | window.addEventListener('focus', (event) => { 101 | // focus entered from the parent window 102 | if (event.target === window){ 103 | this.searchBox.node().focus(); 104 | } 105 | }) 106 | 107 | // focus input after clear button 108 | this.clearButton.on("keydown", event => { 109 | if (event.key === "Tab") { 110 | event.preventDefault(); 111 | this.searchBox.node()?.focus(); 112 | event.stopPropagation(); 113 | } 114 | }) 115 | 116 | this.searchBox.on("keydown", (event) => { 117 | if (event.key === "Enter") { 118 | this.performSearch(this.searchBox.property("value")); 119 | } 120 | }); 121 | 122 | // these click handlers also handle "Enter" key press with keyboard navigation 123 | this.searchButton 124 | .on("click", () => this.performSearch(this.searchBox.property("value"))); 125 | this.clearButton 126 | .on("click", () => this.performSearch("")); 127 | 128 | d3Select(this.target) 129 | .on("contextmenu", (event) => { 130 | const 131 | mouseEvent: MouseEvent = event, 132 | selectionManager = options.host.createSelectionManager(); 133 | selectionManager.showContextMenu({}, { 134 | x: mouseEvent.clientX, 135 | y: mouseEvent.clientY 136 | }); 137 | mouseEvent.preventDefault(); 138 | }); 139 | 140 | this.localizationManager = options.host.createLocalizationManager() 141 | this.formattingSettingsService = new FormattingSettingsService(this.localizationManager); 142 | 143 | this.host = options.host; 144 | } 145 | 146 | public getFormattingModel(): powerbi.visuals.FormattingModel { 147 | // removes border color 148 | if (this.formattingSettings?.textBox.enableBorder.value === false) { 149 | this.formattingSettings.removeBorderColor(); 150 | } 151 | const model = this.formattingSettingsService.buildFormattingModel(this.formattingSettings); 152 | 153 | return model; 154 | } 155 | 156 | public update(options: VisualUpdateOptions) { 157 | this.events.renderingStarted(options); 158 | this.formattingSettings = this.formattingSettingsService.populateFormattingSettingsModel(TextFilterSettingsModel, options.dataViews); 159 | const metadata = options.dataViews && options.dataViews[0] && options.dataViews[0].metadata; 160 | const newColumn = metadata && metadata.columns && metadata.columns[0]; 161 | let searchText = ""; 162 | this.updateUiSizing(); 163 | 164 | // We had a column, but now it is empty, or it has changed. 165 | if (options.dataViews && options.dataViews.length > 0 && this.column && (!newColumn || this.column.queryName !== newColumn.queryName)) { 166 | this.performSearch(""); 167 | 168 | // Well, it hasn't changed, then lets try to load the existing search text. 169 | } else if (options?.jsonFilters?.length > 0) { 170 | searchText = `${(options.jsonFilters).map((f) => f.conditions.map((c) => c.value)).join(" ")}`; 171 | } 172 | 173 | this.searchBox.property("value", searchText); 174 | this.column = newColumn; 175 | 176 | this.events.renderingFinished(options); 177 | 178 | } 179 | 180 | /** 181 | * Ensures that the UI is sized according to the specified properties (or defaults, if not overridden). 182 | */ 183 | private updateUiSizing() { 184 | const 185 | textBox = this.formattingSettings?.textBox, 186 | fontSize = textBox.font.fontSize.value, 187 | fontScaleSml = Math.floor((fontSize / pxToPt) + fontPxAdjSml), 188 | fontScaleStd = Math.floor((fontSize / pxToPt) + fontPxAdjStd), 189 | fontScaleLrg = Math.floor((fontSize / pxToPt) + fontPxAdjLrg); 190 | this.searchUi 191 | .style('height', `${fontScaleStd}px`) 192 | .style('font-size', `${fontSize}pt`) 193 | .style('font-family', textBox.font.fontFamily.value); 194 | this.searchBox 195 | .attr('placeholder', this.localizationManager.getDisplayName(textBox.placeholderTextKey)) 196 | .style('width', `calc(100% - ${fontScaleStd}px)`) 197 | .style('padding-right', `${fontScaleStd}px`) 198 | .style('border-style', textBox.enableBorder.value && 'solid' || 'none') 199 | .style('border-color', textBox.borderColor.value.value); 200 | this.searchButton 201 | .style('right', `${fontScaleLrg}px`) 202 | .style('width', `${fontScaleSml}px`) 203 | .style('height', `${fontScaleSml}px`) 204 | .style('font-size', `${fontSize}pt`); 205 | this.clearButton 206 | .style('width', `${fontScaleStd}px`) 207 | .style('height', `${fontScaleStd}px`); 208 | } 209 | 210 | /** 211 | * Perfom search/filtering in a column 212 | * @param {string} text - text to filter on 213 | */ 214 | public performSearch(text: string) { 215 | if (this.column) { 216 | const isBlank = ((text || "") + "").match(/^\s*$/); 217 | const target = { 218 | table: this.column.queryName.substr(0, this.column.queryName.indexOf(".")), 219 | column: this.column.queryName.substr(this.column.queryName.indexOf(".") + 1) 220 | }; 221 | 222 | let filter: any = null; 223 | let action = FilterAction.remove; 224 | if (!isBlank) { 225 | filter = new AdvancedFilter( 226 | target, 227 | "And", 228 | { 229 | operator: "Contains", 230 | value: text 231 | } 232 | ); 233 | action = FilterAction.merge; 234 | } 235 | this.host.applyJsonFilter(filter, "general", "filter", action); 236 | } 237 | this.searchBox.property("value", text); 238 | } 239 | } -------------------------------------------------------------------------------- /stringResources/en-US/resources.resjson: -------------------------------------------------------------------------------- 1 | { 2 | "Visual_Textbox_Settings": "Text box", 3 | "Visual_Input_Placeholder": "Search", 4 | "Visual_Enable_Border": "Show border", 5 | "Visual_Border_color": "Border color", 6 | "Visual_Font_Size": "Text Size", 7 | "Visual_Font_Family": "Font family", 8 | "Visual_Font": "Font" 9 | } -------------------------------------------------------------------------------- /style/visual.less: -------------------------------------------------------------------------------- 1 | div { 2 | font-weight: normal; 3 | 4 | em { 5 | background: yellow; 6 | padding: 5px; 7 | 8 | } 9 | } 10 | 11 | .text-filter-search { 12 | position: relative; 13 | min-width: 92px; 14 | width: 100%; 15 | display: inline-block; 16 | } 17 | 18 | .text-filter-search input { 19 | line-height: normal; 20 | } 21 | 22 | .text-filter-search button, 23 | .text-filter-search input { 24 | color: inherit; 25 | font: inherit; 26 | margin: 0; 27 | } 28 | 29 | .text-filter-search input[type="text"] { 30 | border-radius: 0; 31 | -webkit-appearance: none; 32 | -moz-appearance: none; 33 | } 34 | 35 | .text-filter-search input[type="text"], 36 | .text-filter-search button.search-button { 37 | float: left; 38 | height: 100%; 39 | outline: 0; 40 | background-color: #fff; 41 | } 42 | 43 | .text-filter-search input[type="text"] { 44 | box-sizing: border-box; 45 | height: 100%; 46 | padding: 7px 10px; 47 | border: 1px solid rgba(0, 0, 0, .6); 48 | } 49 | 50 | .text-filter-search button { 51 | text-transform: none; 52 | overflow: visible; 53 | } 54 | 55 | .text-filter-search .x-screen-reader { 56 | position: absolute !important; 57 | overflow: hidden !important; 58 | clip: rect(1px, 1px, 1px, 1px) !important; 59 | width: 1px !important; 60 | height: 1px !important; 61 | border: 0 !important; 62 | padding: 0 !important; 63 | margin: 0 !important; 64 | } 65 | 66 | .text-filter-search button { 67 | -webkit-appearance: button; 68 | cursor: pointer; 69 | } 70 | 71 | .text-filter-search button.search-button { 72 | position: absolute; 73 | top: 0; 74 | margin: 2px 1px 1px; 75 | padding: 9px; 76 | transition: color .1s, background-color .1s; 77 | border: 0; 78 | } 79 | 80 | .text-filter-search .clear-button { 81 | border: 0; 82 | background-color: white; 83 | display: inline-block; 84 | transition: color .1s, background-color .1s; 85 | outline: none; 86 | } 87 | 88 | .text-filter-search .c-glyph:before, 89 | .c-glyph:after { 90 | display: inline-block; 91 | text-decoration: underline; 92 | } 93 | 94 | .text-filter-search .c-glyph:before, 95 | .c-glyph:after, 96 | .c-glyph:hover:before, 97 | .c-glyph:hover:after { 98 | text-decoration: none; 99 | } 100 | 101 | .text-filter-search button:hover { 102 | background-color: lightblue; 103 | } 104 | 105 | .text-filter-search button.search-button:before { 106 | content: ""; 107 | width: 1em; 108 | height: 1em; 109 | background-image: url('data:image/svg+xml;base64,PHN2ZyBpZD0iTGF5ZXJfMSIgc3R5bGU9ImVuYWJsZS1iYWNrZ3JvdW5kOm5ldyAwIDAgNTEyIDUxMjsiIHZlcnNpb249IjEuMSIgdmlld0JveD0iMCAwIDUxMiA1MTIiIHhtbDpzcGFjZT0icHJlc2VydmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiPjxwYXRoIGZpbGw9IiM3NTc1NzUiIGQ9Ik0zNDQuNSwyOThjMTUtMjMuNiwyMy44LTUxLjYsMjMuOC04MS43YzAtODQuMS02OC4xLTE1Mi4zLTE1Mi4xLTE1Mi4zQzEzMi4xLDY0LDY0LDEzMi4yLDY0LDIxNi4zICBjMCw4NC4xLDY4LjEsMTUyLjMsMTUyLjEsMTUyLjNjMzAuNSwwLDU4LjktOSw4Mi43LTI0LjRsNi45LTQuOEw0MTQuMyw0NDhsMzMuNy0zNC4zTDMzOS41LDMwNS4xTDM0NC41LDI5OHogTTMwMS40LDEzMS4yICBjMjIuNywyMi43LDM1LjIsNTIuOSwzNS4yLDg1YzAsMzIuMS0xMi41LDYyLjMtMzUuMiw4NWMtMjIuNywyMi43LTUyLjksMzUuMi04NSwzNS4yYy0zMi4xLDAtNjIuMy0xMi41LTg1LTM1LjIgIGMtMjIuNy0yMi43LTM1LjItNTIuOS0zNS4yLTg1YzAtMzIuMSwxMi41LTYyLjMsMzUuMi04NWMyMi43LTIyLjcsNTIuOS0zNS4yLDg1LTM1LjJDMjQ4LjUsOTYsMjc4LjcsMTA4LjUsMzAxLjQsMTMxLjJ6Ii8+PC9zdmc+'); 110 | text-indent: 0; 111 | } 112 | 113 | .text-filter-search .clear-button:before { 114 | content: " "; 115 | width: 1em; 116 | height: 1em; 117 | background-image: url('data:image/svg+xml;base64,PHN2ZyBmaWxsPSIjNzU3NTc1IiB2aWV3Qm94PSIwIDAgMTc5MiAxNzkyIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPjxwYXRoIGQ9Ik04MzIgMTQwOGwzMzYtMzg0aC03NjhsLTMzNiAzODRoNzY4em0xMDEzLTEwNzdxMTUgMzQgOS41IDcxLjV0LTMwLjUgNjUuNWwtODk2IDEwMjRxLTM4IDQ0LTk2IDQ0aC03NjhxLTM4IDAtNjkuNS0yMC41dC00Ny41LTU0LjVxLTE1LTM0LTkuNS03MS41dDMwLjUtNjUuNWw4OTYtMTAyNHEzOC00NCA5Ni00NGg3NjhxMzggMCA2OS41IDIwLjV0NDcuNSA1NC41eiIvPjwvc3ZnPg==') 118 | } 119 | 120 | 121 | // for contrast ratio 122 | .accessibility-compliant::placeholder { 123 | color: #757575; 124 | } 125 | 126 | .accessibility-compliant::-moz-placeholder { 127 | /* Mozilla Firefox 19+ */ 128 | color: #757575; 129 | opacity: 1; 130 | } 131 | 132 | .border-on-focus:focus { 133 | border: solid 2px black !important; 134 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowJs": false, 4 | "emitDecoratorMetadata": true, 5 | "experimentalDecorators": true, 6 | "target": "es6", 7 | "sourceMap": true, 8 | "outDir": "./.tmp/build/", 9 | "moduleResolution": "node", 10 | "declaration": true, 11 | "lib": [ 12 | "es2015", 13 | "dom" 14 | ] 15 | }, 16 | "files": [ 17 | "./src/visual.ts" 18 | ] 19 | } --------------------------------------------------------------------------------