diff --git a/images/icons/search-gray.svg b/images/icons/search-gray.svg
new file mode 100644
index 000000000..748e42a1c
--- /dev/null
+++ b/images/icons/search-gray.svg
@@ -0,0 +1,46 @@
+
+
diff --git a/images/icons/search-white.svg b/images/icons/search-white.svg
new file mode 100644
index 000000000..6af8c4670
--- /dev/null
+++ b/images/icons/search-white.svg
@@ -0,0 +1,50 @@
+
+
diff --git a/index.html b/index.html
index d7681ca2d..4be81ee47 100644
--- a/index.html
+++ b/index.html
@@ -318,6 +318,10 @@
+
+
+
+
diff --git a/js/configurator_main.js b/js/configurator_main.js
index fa25c9a72..468f074cf 100644
--- a/js/configurator_main.js
+++ b/js/configurator_main.js
@@ -281,6 +281,10 @@ $(function() {
require('./../tabs/ez_tune');
TABS.ez_tune.initialize(content_ready);
break;
+ case 'search':
+ require('./../tabs/search');
+ TABS.search.initialize(content_ready);
+ break;
default:
console.log('Tab not found:' + tab);
}
diff --git a/js/gui.js b/js/gui.js
index 18ad01517..994bf8ad6 100644
--- a/js/gui.js
+++ b/js/gui.js
@@ -50,7 +50,8 @@ var GUI_control = function () {
'mission_control',
'mixer',
'programming',
- 'ez_tune'
+ 'ez_tune',
+ 'search'
];
this.allowedTabs = this.defaultAllowedTabsWhenDisconnected;
diff --git a/locale/en/messages.json b/locale/en/messages.json
index b1b33399c..82a234d7a 100644
--- a/locale/en/messages.json
+++ b/locale/en/messages.json
@@ -6178,5 +6178,8 @@
},
"currentLanguage": {
"message": "en"
+ },
+ "search": {
+ "message": "Search"
}
}
diff --git a/src/css/main.css b/src/css/main.css
index 9dec4129e..0c4740c5a 100644
--- a/src/css/main.css
+++ b/src/css/main.css
@@ -1009,6 +1009,16 @@ li.active .ic_mag {
background-image: url("./../../images/icons/cf_icon_mag_white.svg");
}
+.ic_search {
+ background-image: url("./../../images/icons/search-gray.svg");
+}
+
+.ic_search:hover {
+ background-image: url("./../../images/icons/search-white.svg");
+}
+
+
+
#content {
margin-top: 0;
padding: 0;
diff --git a/tabs/search.html b/tabs/search.html
new file mode 100644
index 000000000..eb061037d
--- /dev/null
+++ b/tabs/search.html
@@ -0,0 +1,19 @@
+
\ No newline at end of file
diff --git a/tabs/search.js b/tabs/search.js
new file mode 100644
index 000000000..cdafb8d45
--- /dev/null
+++ b/tabs/search.js
@@ -0,0 +1,204 @@
+const { GUI, TABS } = require('./../js/gui');
+const path = require('path');
+const i18n = require('./../js/localization');
+
+
+
+TABS.search = { };
+
+
+tabNames = [
+ "adjustments",
+ "advanced_tuning",
+ "auxiliary",
+ "calibration",
+ "cli",
+ "configuration",
+ "debug_trace",
+ "failsafe",
+ "firmware_flasher",
+ "gps",
+ "landing",
+ "led_strip",
+ "logging",
+ "magnetometer",
+ "mission_control",
+ "mixer",
+ "onboard_logging",
+ "options",
+ "osd",
+ "outputs",
+ "pid_tuning",
+ "ports",
+ "programming",
+ "receiver",
+ "receiver_msp",
+ "sensors",
+ "setup",
+ "sitl"
+];
+
+
+ TABS.search.searchMessages = function (keyword) {
+ var resultsDiv = document.getElementById('search-results');
+ keyword = keyword.toLowerCase();
+ resultsDiv.innerHTML = '';
+
+ simClick = function (evt) {
+ tabName = evt.currentTarget.getAttribute("tabName");
+ tabLink = document.getElementsByClassName("tab_".concat(tabName))[0].getElementsByTagName("a")[0];
+ tabLink.click();
+ };
+
+
+ for (const [key, value] of Object.entries(this.messages)) {
+ // Get plain text of message (with tags)
+ var message = value.message.toLowerCase().replace(new RegExp('<[^>]*>'), '');
+
+ if ( message.includes(keyword) ) {
+ if (this.key2page.get(key) ) {
+ var pages = this.key2page.get(key);
+ var kwEscaped = keyword.replace(/[\-\[\]{}()*+?.,\\\^$|#\s]/g, "\\$&");
+ var context = message.match( new RegExp( `([a-z]{0,10}.{0,14}${kwEscaped}.{0,14}[a-z]{0,10})`, 'i') )[1];
+ for (const page of pages) {
+ var rHTML = ": {2}".format(page, page, context);
+ resultsDiv.innerHTML= resultsDiv.innerHTML.concat(rHTML);
+ }
+ }
+ }
+ }
+
+ for (const [key, value] of this.setting2page) {
+ // for (const [key, value] of Object.entries(this.setting2page)) {
+ if ( key.toLowerCase().includes(keyword) ) {
+ for (const page of value) {
+ var rHTML = ": {2}".format(page, page, key);
+ resultsDiv.innerHTML= resultsDiv.innerHTML.concat(rHTML);
+ }
+ }
+ }
+
+ for ( result of document.getElementsByClassName("searchResult") ) {
+ result.addEventListener('click', simClick, false);
+ }
+ }
+
+
+ TABS.search.getMessages = function () {
+ const res_messages = fetch('locale/en/messages.json');
+ res_messages
+ .then (data => data.json())
+ .then (data => {
+ this.messages = data;
+ })
+ .catch((error) => {
+ console.error(error)
+ });
+ }
+
+ TABS.search.geti18nHTML = function (filename, filecontents) {
+
+ const parser = new DOMParser();
+ const htmlDoc = parser.parseFromString(filecontents, 'text/html');
+ var hasDataI18n = htmlDoc.querySelectorAll('[data-i18n]:not([data-i18n=""])');
+ for (const element of hasDataI18n) {
+ key = element.getAttribute('data-i18n');
+ if (! this.key2page.has(key) ) {
+ this.key2page.set( key, new Set() );
+ }
+ this.key2page.get(key).add(filename);
+ }
+ hasDataI18n = htmlDoc.querySelectorAll('[i18n]:not([i18n=""])');
+ for (const element of hasDataI18n) {
+ key = element.getAttribute('i18n');
+ if (! this.key2page.has(key) ) {
+ this.key2page.set( key, new Set() );
+ }
+ this.key2page.get(key).add(filename);
+ }
+
+ settings = htmlDoc.querySelectorAll('[data-setting]:not([data-setting=""])');
+ for (const element of settings) {
+ key = element.getAttribute('data-setting');
+ if (! this.setting2page.has(key) ) {
+ this.setting2page.set( key, new Set() );
+ }
+ this.setting2page.get(key).add(filename);
+ }
+
+ }
+
+
+ TABS.search.geti18nJs = function (filename, filecontents) {
+ var re = /(?:data-i18n=|i18n.getMessage\()["']([^"']*)['"]/g
+
+ while (match = re.exec(filecontents)) {
+ key = match[1];
+ if (! this.key2page.has(key) ) {
+ this.key2page.set( key, new Set() );
+ }
+ this.key2page.get(key).add(filename);
+ }
+ }
+
+
+ TABS.search.indexTab = async function indexTab(tabName) {
+ var response = fetch(`tabs/${tabName}.js`);
+ response
+ .then (data => data.text())
+ .then (data => {
+ this.geti18nJs(tabName, data);
+ })
+ .catch((error) => {
+ console.error(error)
+ });
+
+
+ response = fetch(`tabs/${tabName}.html`);
+ response
+ .then (data => data.text())
+ .then (data => {
+ this.geti18nHTML(tabName, data);
+ })
+ .catch((error) => {
+ console.error(error)
+ });
+ };
+
+
+TABS.search.initialize = function (callback) {
+ var self = this;
+ this.key2page = new Map();
+ this.setting2page = new Map();
+ this.messages;
+
+ if (GUI.active_tab != 'search') {
+ GUI.active_tab = 'search';
+ }
+
+ function searchKeyword() {
+ TABS.search.searchMessages(document.getElementById('search-keyword').value);
+ }
+
+ function searchKeywordTyping() {
+ if (document.getElementById('search-keyword').value.length > 2) {
+ TABS.search.searchMessages(document.getElementById('search-keyword').value);
+ }
+ }
+ GUI.load(path.join(__dirname, "search.html"), function () {
+ i18n.localize();
+ document.getElementById('search-label').addEventListener('click', searchKeyword, false);
+ document.getElementById('search-keyword').addEventListener('keyup', searchKeywordTyping, false);
+ GUI.content_ready(callback);
+ } );
+ self.getMessages();
+ for (let tab of tabNames) {
+ self.indexTab(tab);
+ }
+}
+
+
+TABS.search.cleanup = function (callback) {
+ if (callback) callback();
+};
+