├── .babelrc ├── .gitignore ├── LICENSE ├── README.md ├── Sketch Search Everywhere.sketchplugin └── Contents │ ├── Resources │ ├── index_bundle.js │ └── window.html │ └── Sketch │ ├── MochaJSDelegate.js │ ├── app.js │ ├── main.js │ ├── manifest.json │ ├── webView.js │ └── window.js ├── package.json ├── src ├── components │ ├── App.jsx │ ├── App.less │ ├── Filter.jsx │ └── Filter.less └── index.html └── webpack.config.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "es2015", 4 | "react" 5 | ], 6 | "plugins": [ 7 | [ 8 | "import", 9 | { 10 | "libraryName": "antd", 11 | "style": true // or 'css' 12 | } 13 | ] 14 | ] 15 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | npm-debug.log.* 3 | .DS_Store -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 捻捻转儿 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 | ## Sketch Search Everywhere 2 | 3 | > Sketch App plugin 4 | 5 | Search layer and select it, by matching `textValue`, `name` or `ObjectID`. 6 | 7 | ## Installation 8 | 9 | [Download](https://github.com/MrPeak/sketch-search-everywhere/archive/master.zip) and extract the contents of this repository. Then double-click the `Sketch Search Everywhere.sketchplugin` bundle to install the plugin. 10 | 11 | ## Usage 12 | 13 | Just type Alt+Command+F everywhere. 14 | 15 | And enter the textContent/Name/ObjectId that the layers cotain 16 | 17 | Then select the layer you find! 18 | 19 | ![show](https://user-images.githubusercontent.com/2953176/27261189-6d36c61a-5470-11e7-8230-89fbe7c5a2a4.gif) 20 | 21 | 22 | ## Change Log 23 | 24 | #### v1.0.3: June 25, 2017 25 | 26 | - [x] Add 'All' Class filter 27 | 28 | #### v1.0.2: June 23, 2017 29 | 30 | - [x] Disable ClassType filter search 31 | 32 | #### v1.0.1: June 19, 2017 33 | 34 | - [x] Fix filter delay issue 35 | - [x] Limit search results' length to 15 36 | 37 | #### v1.0.0: June 18, 2017 38 | 39 | - Initial release 40 | -------------------------------------------------------------------------------- /Sketch Search Everywhere.sketchplugin/Contents/Resources/window.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Search Everywhere 6 | 7 | 8 |
9 |
10 | 11 |
12 |
13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /Sketch Search Everywhere.sketchplugin/Contents/Sketch/MochaJSDelegate.js: -------------------------------------------------------------------------------- 1 | // @see https://github.com/matt-curtis/MochaJSDelegate/blob/master/MochaJSDelegate.js 2 | 3 | // 4 | // MochaJSDelegate.js 5 | // MochaJSDelegate 6 | // 7 | // Created by Matt Curtis 8 | // Copyright (c) 2015. All rights reserved. 9 | // 10 | 11 | var MochaJSDelegate = function(selectorHandlerDict) { 12 | var uniqueClassName = 13 | "MochaJSDelegate_DynamicClass_" + NSUUID.UUID().UUIDString(); 14 | 15 | var delegateClassDesc = MOClassDescription.allocateDescriptionForClassWithName_superclass_( 16 | uniqueClassName, 17 | NSObject 18 | ); 19 | 20 | delegateClassDesc.registerClass(); 21 | 22 | // Handler storage 23 | 24 | var handlers = {}; 25 | 26 | // Define interface 27 | 28 | this.setHandlerForSelector = function(selectorString, func) { 29 | var handlerHasBeenSet = selectorString in handlers; 30 | var selector = NSSelectorFromString(selectorString); 31 | 32 | handlers[selectorString] = func; 33 | 34 | if (!handlerHasBeenSet) { 35 | /* 36 | For some reason, Mocha acts weird about arguments: 37 | https://github.com/logancollins/Mocha/issues/28 38 | 39 | We have to basically create a dynamic handler with a likewise dynamic number of predefined arguments. 40 | */ 41 | 42 | var dynamicHandler = function() { 43 | var functionToCall = handlers[selectorString]; 44 | 45 | if (!functionToCall) return; 46 | 47 | return functionToCall.apply(delegateClassDesc, arguments); 48 | }; 49 | 50 | var args = [], regex = /:/g; 51 | while ((match = regex.exec(selectorString))) 52 | args.push("arg" + args.length); 53 | 54 | dynamicFunction = eval( 55 | "(function(" + 56 | args.join(",") + 57 | "){ return dynamicHandler.apply(this, arguments); })" 58 | ); 59 | 60 | delegateClassDesc.addInstanceMethodWithSelector_function_( 61 | selector, 62 | dynamicFunction 63 | ); 64 | } 65 | }; 66 | 67 | this.removeHandlerForSelector = function(selectorString) { 68 | delete handlers[selectorString]; 69 | }; 70 | 71 | this.getHandlerForSelector = function(selectorString) { 72 | return handlers[selectorString]; 73 | }; 74 | 75 | this.getAllHandlers = function() { 76 | return handlers; 77 | }; 78 | 79 | this.getClass = function() { 80 | return NSClassFromString(uniqueClassName); 81 | }; 82 | 83 | this.getClassInstance = function() { 84 | return NSClassFromString(uniqueClassName).new(); 85 | }; 86 | 87 | // Conveience 88 | 89 | if (typeof selectorHandlerDict == "object") { 90 | for (var selectorString in selectorHandlerDict) { 91 | this.setHandlerForSelector( 92 | selectorString, 93 | selectorHandlerDict[selectorString] 94 | ); 95 | } 96 | } 97 | }; 98 | -------------------------------------------------------------------------------- /Sketch Search Everywhere.sketchplugin/Contents/Sketch/app.js: -------------------------------------------------------------------------------- 1 | var App = { 2 | ADDITIONS_HEIGHT: 200, 3 | IS_EXPAND: false, 4 | filters: { 5 | type: "stringValue", 6 | layerClass: "TextLayer" 7 | }, 8 | init: function(ctx) { 9 | this.ctx = ctx; 10 | this.doc = ctx.document; 11 | this.sketch = ctx.api(); 12 | }, 13 | closeWindow: function() { 14 | log("Window closed!"); 15 | App.COScript.setShouldKeepAround(false); 16 | App.threadDictionary.removeObjectForKey(App.identifier); 17 | App.SearchEverywhere.close(); 18 | }, 19 | setFrameExpand: function() { 20 | if (this.IS_EXPAND) return false; 21 | 22 | var _frame = App.SearchEverywhere.frame(); 23 | _frame.size.height += this.ADDITIONS_HEIGHT; 24 | _frame.origin.y -= this.ADDITIONS_HEIGHT; 25 | App.SearchEverywhere.setFrame_display(_frame, true); 26 | 27 | var _frame_ = App.webView.frame(); 28 | _frame_.size.height += this.ADDITIONS_HEIGHT; 29 | App.webView.setFrame(_frame_); 30 | 31 | log("expanded"); 32 | this.IS_EXPAND = true; 33 | }, 34 | setFrameContract: function(_coscript) { 35 | if (!App.IS_EXPAND) return false; 36 | 37 | var _frame = App.SearchEverywhere.frame(); 38 | _frame.size.height -= App.ADDITIONS_HEIGHT; 39 | _frame.origin.y += App.ADDITIONS_HEIGHT; 40 | App.SearchEverywhere.setFrame_display(_frame, true); 41 | 42 | var _frame_ = App.webView.frame(); 43 | _frame_.size.height -= App.ADDITIONS_HEIGHT; 44 | App.webView.setFrame(_frame_); 45 | 46 | App.IS_EXPAND = false; 47 | }, 48 | findLayersMatchingPredicate_inContainer_filterByType: function( 49 | predicate, 50 | container, 51 | layerType 52 | ) { 53 | var scope; 54 | switch (layerType) { 55 | case MSPage: 56 | scope = this.doc.pages(); 57 | return scope.filteredArrayUsingPredicate(predicate); 58 | 59 | case MSArtboardGroup: 60 | if (typeof container !== "undefined" && container != nil) { 61 | if (container.className == "MSPage") { 62 | scope = container.artboards(); 63 | return scope.filteredArrayUsingPredicate(predicate); 64 | } 65 | } else { 66 | // search all pages 67 | var filteredArray = NSArray.array(); 68 | var loopPages = this.doc.pages().objectEnumerator(), page; 69 | while ((page = loopPages.nextObject())) { 70 | scope = page.artboards(); 71 | filteredArray = filteredArray.arrayByAddingObjectsFromArray( 72 | scope.filteredArrayUsingPredicate(predicate) 73 | ); 74 | } 75 | return filteredArray; 76 | } 77 | break; 78 | 79 | default: 80 | if (typeof container !== "undefined" && container != nil) { 81 | scope = container.children(); 82 | return scope.filteredArrayUsingPredicate(predicate); 83 | } else { 84 | // search all pages 85 | var filteredArray = NSArray.array(); 86 | var loopPages = this.doc.pages().objectEnumerator(), page; 87 | var pages = this.doc.pages(); 88 | 89 | while ((page = loopPages.nextObject())) { 90 | scope = page.children(); 91 | filteredArray = filteredArray.arrayByAddingObjectsFromArray( 92 | scope.filteredArrayUsingPredicate(predicate) 93 | ); 94 | } 95 | return filteredArray; 96 | } 97 | } 98 | return NSArray.array(); // Return an empty array if no matches were found 99 | }, 100 | findLayers_inContainer_filterByType: function( 101 | textContent, 102 | container, 103 | layerType, 104 | predicateString 105 | ) { 106 | var predicate = typeof layerType == nil || !layerType 107 | ? NSPredicate.predicateWithFormat(predicateString, textContent) 108 | : NSPredicate.predicateWithFormat( 109 | predicateString + " && className == %@", 110 | textContent, 111 | "MS" + layerType 112 | ); 113 | 114 | log(predicate); 115 | return this.findLayersMatchingPredicate_inContainer_filterByType( 116 | predicate, 117 | container, 118 | undefined 119 | ); 120 | }, 121 | getLayersAttrs: function(layers) { 122 | var arr = []; 123 | layers.forEach(function(layer) { 124 | var str = 125 | "name: " + 126 | encodeURIComponent(layer.name()) + 127 | ";" + 128 | "id: " + 129 | layer.objectID() + 130 | ";" + 131 | "class: " + 132 | layer.class() + 133 | ";" + 134 | // "isFlippedVertical: " + 135 | // layer.isFlippedVertical() + 136 | // ";" + 137 | // "isVisible: " + 138 | // layer.isVisible() + 139 | // ";" + 140 | // "nameIsFixed: " + 141 | // layer.nameIsFixed() + 142 | // ";" + 143 | // "heightIsClipped: " + 144 | // layer.heightIsClipped() + 145 | // ";" + 146 | layer.CSSAttributes().join(""); 147 | 148 | if (layer.class() == "MSTextLayer") { 149 | str += "value: " + encodeURIComponent(layer.stringValue()) + ";"; 150 | } 151 | 152 | if (layer.parentGroup()) { 153 | str += 154 | "parentClass: " + 155 | layer.parentGroup().class() + 156 | ";" + 157 | "parentName: " + 158 | encodeURIComponent(layer.parentGroup().name()) + 159 | ";"; 160 | } 161 | 162 | arr.push(str); 163 | }); 164 | 165 | return arr; 166 | }, 167 | // debounce: function(fn, delay) { 168 | // var timer = null; 169 | // return function() { 170 | // var context = this, args = arguments; 171 | // clearTimeout(timer); 172 | 173 | // coscript.setShouldKeepAround_(true); 174 | // coscript.scheduleWithInterval_jsFunction(delay / 1000, function() { 175 | // fn.apply(context, args); 176 | // }); 177 | // }; 178 | // }, 179 | selectLayer: function(objectID) { 180 | var selectedLayer = App.findLayers_inContainer_filterByType( 181 | objectID, 182 | nil, 183 | App.filters.layerClass, 184 | "objectID == %@" 185 | ); 186 | 187 | selectedLayer = selectedLayer.firstObject(); 188 | 189 | var pageOfLayer = this.findPageOfLayer(selectedLayer); 190 | 191 | // log(pageOfLayer); 192 | 193 | // Set current page 194 | this.doc.setCurrentPage(pageOfLayer); 195 | 196 | // Clear other selections, and select this layer 197 | selectedLayer.select_byExpandingSelection(true, false); 198 | 199 | // Center the selection 200 | this.doc.currentView().centerRect_(selectedLayer.absoluteRect().rect()); 201 | 202 | // Tell user that choose layer sucess! 203 | this.doc.showMessage("Select success!!!"); 204 | 205 | this.IS_EXPAND = false; 206 | App.closeWindow(); 207 | }, 208 | findPageOfLayer(layer) { 209 | var _layer = layer; 210 | // log(_layer); 211 | while (_layer.class().toString().toLowerCase() != "mspage") { 212 | // log(_layer); 213 | _layer = _layer.parentGroup(); 214 | } 215 | 216 | return _layer; 217 | } 218 | }; 219 | -------------------------------------------------------------------------------- /Sketch Search Everywhere.sketchplugin/Contents/Sketch/main.js: -------------------------------------------------------------------------------- 1 | @import 'MochaJSDelegate.js'; 2 | @import 'webView.js'; 3 | @import 'window.js'; 4 | @import 'app.js'; 5 | 6 | function onRun(context) { 7 | // initializing... 8 | App.init(context); 9 | 10 | openWindow( 11 | getWebView( 12 | context.plugin.urlForResourceNamed("window.html").path() 13 | ) 14 | ); 15 | } 16 | -------------------------------------------------------------------------------- /Sketch Search Everywhere.sketchplugin/Contents/Sketch/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "author" : "MrPeak", 3 | "commands" : [ 4 | { 5 | "script" : "main.js", 6 | "handler" : "onRun", 7 | "shortcut" : "command alt f", 8 | "name" : "Search Everywhere", 9 | "identifier" : "run" 10 | } 11 | ], 12 | "menu": { 13 | "items": [ 14 | "run" 15 | ] 16 | }, 17 | "identifier" : "com.sketchapp.examples.search-text", 18 | "version" : "1.0", 19 | "description" : "Search layer and select it, by matching `textValue`, `name` or `ObjectID`.", 20 | "authorEmail" : "gfeng.peak@gmail.com", 21 | "name" : "Search Everywhere" 22 | } 23 | -------------------------------------------------------------------------------- /Sketch Search Everywhere.sketchplugin/Contents/Sketch/webView.js: -------------------------------------------------------------------------------- 1 | // Add Web View to window 2 | 3 | @import 'app.js'; 4 | 5 | function getWebView(urlPath) { 6 | var webView = WebView.alloc().initWithFrame(NSMakeRect(0, 0, 480, 120)); 7 | var windowObject = webView.windowScriptObject(); 8 | 9 | // Awesome library 10 | var delegate = new MochaJSDelegate({ 11 | "webView:didFinishLoadForFrame:": function(webView, webFrame) { 12 | // Something to be done after loading the webview 13 | // 14 | // 15 | }, 16 | 17 | "webView:didChangeLocationWithinPageForFrame:": function( 18 | webView, 19 | webFrame 20 | ) { 21 | var locationHash = windowObject.evaluateWebScript("window.location.hash"); 22 | // log(locationHash); 23 | if (locationHash.indexOf("@selectedLayerID=") > -1) { 24 | // Recieve "selete" signal 25 | var objectID = locationHash.slice("#@selectedLayerID=".length).trim(); 26 | App.selectLayer(objectID); 27 | } else if (locationHash.indexOf("@query=") > -1) { 28 | // Reciew "query" signal 29 | var queryData = JSON.parse( 30 | locationHash.slice("#@query=".length).trim() 31 | ); 32 | var arr = []; 33 | 34 | if (queryData.value.trim()) { 35 | var filters = queryData.filters; 36 | // log(queryData); 37 | 38 | App.filters = filters; 39 | 40 | // Find qualified layers 41 | var matchedLayers = App.findLayers_inContainer_filterByType( 42 | queryData.value, 43 | nil, 44 | App.filters.layerClass || nil, 45 | App.filters.type + " CONTAINS [c] %@" 46 | ); 47 | 48 | // Get layers' info 49 | arr = App.getLayersAttrs(matchedLayers); 50 | } 51 | 52 | // Limit results' length to 15. 53 | arr = arr.slice(0, 15); 54 | 55 | // Execute webview's gloabl function, and pass data to it 56 | windowObject.evaluateWebScript( 57 | 'window.App.renderList("' + arr.join("|||") + '")' 58 | ); 59 | 60 | if (arr.length) { 61 | // Expand box 62 | App.setFrameExpand(); 63 | } else { 64 | // Contract box 65 | App.setFrameContract(); 66 | } 67 | } 68 | } 69 | }); 70 | 71 | webView.setFrameLoadDelegate_(delegate.getClassInstance()); 72 | webView.setMainFrameURL_(urlPath); 73 | 74 | App.webView = webView; 75 | return webView; 76 | } 77 | -------------------------------------------------------------------------------- /Sketch Search Everywhere.sketchplugin/Contents/Sketch/window.js: -------------------------------------------------------------------------------- 1 | @import 'webView.js'; 2 | @import 'app.js'; 3 | 4 | function openWindow(webView) { 5 | // SearchEverywhere main window 6 | var threadDictionary = NSThread.mainThread().threadDictionary(); 7 | var identifier = "com.github.mrpeak.search_text"; 8 | 9 | // Set to global 10 | App.threadDictionary = threadDictionary; 11 | App.identifier = identifier; 12 | 13 | if (threadDictionary[identifier]) { 14 | return; 15 | } 16 | 17 | var windowWidth = 480, windowHeight = 120; 18 | var SearchEverywhere = NSPanel.alloc().init(); 19 | SearchEverywhere.setFrame_display( 20 | NSMakeRect(0, 0, windowWidth, windowHeight), 21 | true 22 | ); 23 | 24 | SearchEverywhere.setStyleMask( 25 | NSTexturedBackgroundWindowMask | NSTitledWindowMask | NSClosableWindowMask 26 | ); 27 | 28 | SearchEverywhere.setBackgroundColor(NSColor.colorWithRed_green_blue_alpha(255 / 255, 250 / 255, 240 / 255, 1)); 29 | 30 | // Titlebar 31 | SearchEverywhere.setTitle("Search Everywhere"); 32 | SearchEverywhere.setTitlebarAppearsTransparent(true); 33 | SearchEverywhere.becomeKeyWindow(); 34 | SearchEverywhere.setLevel(NSFloatingWindowLevel); 35 | 36 | threadDictionary[identifier] = SearchEverywhere; 37 | 38 | // Long-running script 39 | COScript.currentCOScript().setShouldKeepAround_(true); 40 | 41 | App.COScript = COScript.currentCOScript(); 42 | 43 | SearchEverywhere.contentView().addSubview(webView); 44 | SearchEverywhere.center(); 45 | SearchEverywhere.makeKeyAndOrderFront(nil); 46 | 47 | SearchEverywhere.standardWindowButton(NSWindowZoomButton).setHidden(true); 48 | SearchEverywhere.standardWindowButton(NSWindowMiniaturizeButton).setHidden(true); 49 | // Close Window 50 | var closeButton = SearchEverywhere.standardWindowButton(NSWindowCloseButton); 51 | 52 | App.SearchEverywhere = SearchEverywhere; // set to global 53 | 54 | closeButton.setCOSJSTargetFunction(function() { 55 | App.closeWindow(); 56 | }); 57 | 58 | closeButton.setAction("callAction:"); 59 | 60 | return SearchEverywhere; 61 | } 62 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sketch-search-everywhere", 3 | "version": "1.0.3", 4 | "description": "Search layer and select it, by matching `textValue`, `name` or `ObjectID`.", 5 | "main": "index.js", 6 | "scripts": { 7 | "build": "webpack", 8 | "start": "webpack-dev-server", 9 | "watch": "webpack --progress --colors --watch", 10 | "test": "echo \"Error: no test specified\" && exit 1" 11 | }, 12 | "author": "MrPeak", 13 | "license": "MIT", 14 | "dependencies": { 15 | "antd": "^2.11.0", 16 | "react": "^15.6.0", 17 | "react-dom": "^15.6.0" 18 | }, 19 | "devDependencies": { 20 | "babel-plugin-import": "^1.2.1", 21 | "css-loader": "^0.28.4", 22 | "html-webpack-plugin": "^2.28.0", 23 | "less": "^2.7.2", 24 | "less-loader": "^4.0.4", 25 | "style-loader": "^0.18.2", 26 | "babel-core": "^6.25.0", 27 | "babel-loader": "^7.0.0", 28 | "babel-preset-es2015": "^6.24.1", 29 | "babel-preset-react": "^6.24.1", 30 | "webpack": "^2.6.1", 31 | "webpack-dev-server": "^2.4.5" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/components/App.jsx: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | import React from "react"; 4 | import ReactDOM, { findDOMNode } from "react-dom"; 5 | import { LocaleProvider, Select, Row, Col } from "antd"; 6 | import enUS from "antd/lib/locale-provider/en_US"; 7 | import Filter from "./Filter.jsx"; 8 | const Option = Select.Option; 9 | 10 | import "./App.less"; 11 | 12 | class App extends React.Component { 13 | constructor(props) { 14 | super(props); 15 | this.isSelecting = false; 16 | this.query = ""; 17 | this.state = { 18 | selectID: null, 19 | filters: { 20 | type: "stringValue", 21 | layerClass: "TextLayer" 22 | } 23 | }; 24 | } 25 | 26 | componentDidMount() { 27 | this.el = findDOMNode(this.refs.searcher); 28 | this.el.querySelector(".ant-select-search__field").focus(); 29 | } 30 | 31 | onChangeSearchType(ev) { 32 | const value = ev.target.value; 33 | console.log("onChangeSearchType", value); 34 | const filters = { 35 | type: value, 36 | layerClass: this.state.filters.layerClass 37 | }; 38 | 39 | this._query(filters); 40 | this.setState({ filters }); 41 | } 42 | 43 | onFilterClassType(value) { 44 | console.log("onFilterClassType", value); 45 | 46 | const filters = { 47 | type: this.state.filters.type, 48 | layerClass: value || "" 49 | }; 50 | this._query(filters); 51 | this.setState({ filters }); 52 | } 53 | 54 | renderList(value) { 55 | console.log('responseData:', value.length); 56 | let list = value.split("|||"); 57 | 58 | list = list.map(el => { 59 | const obj = {}; 60 | el.split(";").forEach(_el => { 61 | const item = _el.split(":"); 62 | obj[item[0]] = item[1]; 63 | }); 64 | return obj; 65 | }); 66 | 67 | this.isSelecting = false; 68 | const options = []; 69 | list.forEach((el, i) => { 70 | if (Object.keys(el).length && el.id) { 71 | const isTextLayer = el.class.toLowerCase().trim() == "mstextlayer"; 72 | options.push( 73 |