commit 6b4ec166526e68d0d639a5c07c35cfff50f76808 Author: zhongjin Date: Thu Dec 20 22:02:07 2018 +0800 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d30d2fb --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +node_modules +.idea +tmp +admin/i18n/flat.txt +admin/i18n/*/flat.txt +iob_npm.done +package-lock.json +/userdata diff --git a/.npmignore b/.npmignore new file mode 100644 index 0000000..10a99f6 --- /dev/null +++ b/.npmignore @@ -0,0 +1,9 @@ +gulpfile.js +tasks +tmp +test +.travis.yml +appveyor.yml +admin/i18n +iob_npm.done +package-lock.json \ No newline at end of file diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..2456688 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,23 @@ +os: + - linux + - osx +language: node_js +node_js: + - '4' + - '6' + - '8' + - '10' +before_script: + - export NPMVERSION=$(echo "$($(which npm) -v)"|cut -c1) + - 'if [[ $NPMVERSION == 5 ]]; then npm install -g npm@5; fi' + - npm -v + - npm install winston@2.3.1 + - 'npm install https://github.com/ioBroker/ioBroker.js-controller/tarball/master --production' +env: + - CXX=g++-4.8 +addons: + apt: + sources: + - ubuntu-toolchain-r-test + packages: + - g++-4.8 diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..f433b1a --- /dev/null +++ b/LICENSE @@ -0,0 +1,177 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS diff --git a/README.md b/README.md new file mode 100644 index 0000000..53420bd --- /dev/null +++ b/README.md @@ -0,0 +1,191 @@ +![Logo](admin/node-red.png) +# ioBroker node-red Adapter +============== +[![NPM version](http://img.shields.io/npm/v/iobroker.node-red.svg)](https://www.npmjs.com/package/iobroker.node-red) +[![Downloads](https://img.shields.io/npm/dm/iobroker.node-red.svg)](https://www.npmjs.com/package/iobroker.node-red) +[![Tests](https://travis-ci.org/ioBroker/ioBroker.node-red.svg?branch=master)](https://travis-ci.org/ioBroker/ioBroker.node-red) + +[![NPM](https://nodei.co/npm/iobroker.node-red.png?downloads=true)](https://nodei.co/npm/iobroker.node-red/) + +# Starts node-red instance and communicates with it. + +***This adapter needs at least nodejs 4.x to work*** + +This adapter uses the node-red server from https://github.com/node-red/node-red + +**Note:** If in select ID dialog of the ioBroker node you cannot find some variable, restart node-red instance. By restarting the new list of objects will be created. + +## Changelog +### 1.7.1 (2017-09-24) +* (bluefox) use newer version of node-red 0.19.4 +* (bluefox) Basic authentication was added + +### 1.7.0 (2017-08-23) +* (bluefox) use newer version of node-red 0.19.1 + +### 1.6.0 (2017-08-06) +* (bluefox) use newer version of node-red 0.18.7 +* (bluefox) Admin3 dialog implemented +* (bluefox) RAM settings were added +* (bluefox) add credentialSecret option + +### 1.5.1 (2017-02-16) +* (Apollon77) queue set state requests till ioBroker connection has been initialized + +### 1.5.0 (2018-02-14) +* (Apollon77) use newer version of node-red 0.18.2 + +### 1.4.1 (2017-10-03) +* (twonky4) fix blank topic support + +### 1.4.0 (2017-08-06) +* (bluefox) use newer version of node-red 0.17.5 + +### 1.3.0 (2017-04-13) +* (bluefox) Update the select ID dialog +* (bluefox) Add node-red-contrib-polymer + +### 1.2.0 (2017-02-14) +* (bluefox) use newer version of node-red 0.16.2 + +### 1.1.6 (2017-01-24) +* (bluefox) use newer version of node-red 0.16.2 + +### 1.1.5 (2017-01-03) +* (Erhard Weinell) support concurrent access to GetNode + +### 1.1.4 (2016-11-04) +* (bluefox) use newer version of node-red 0.15.2 + +### 1.1.2 (2016-07-23) +* (nobodyMO) use newer version of node-red 0.14.6 +* (nobodyMO) change topic name processing + +### 1.1.1 (2016-07-08) +* (nobodyMO) use newer version of node-red 0.14.4 + +### 1.1.0 (2016-05-22) +* (ploebb) configurable: convert values to string +* (nobodyMO) use newer version of node-red 0.14.3 + +### 1.0.1 (2016-05-22) +* (bluefox) on some systems node-red was available under wrong URL http://ip:1881/undefined. Fixed + +### 1.0.0 (2016-04-29) +* (bluefox) support of npm 2/3 + +### 0.4.4 (2016-04-29) +* (bluefox) install with flag unsafePerm + +### 0.4.3 (2016-04-23) +* (bluefox) use node-red 0.13.4 + +### 0.4.2 (2016-01-21) +* (nobodyMO) Add httpRoot setting +* (nobodyMO) add filter settings to nodes + +### 0.4.1 (2016-01-14) +* (nobodyMO) Add --max-old-space-size=128 to support systems with low memory. +* (nobodyMO) Add version 0.12.5 for node-red because it works. +* (nobodyMO) Add ioBroker get node. +* (nobodyMO) Set _maxListeners = 100 to suppress warnings in the log. + +### 0.3.5 (2015-08-23) +* (bluefox) fix error if many additional npm packets + +### 0.3.4 (2015-08-10) +* (bluefox) do not include node-red packages into global context + +### 0.3.3 (2015-07-24) +* (bluefox) enable node-red 0.11.x + +### 0.3.2 (2015-06-29) +* (bluefox) fix error with ioBroker nodes + +### 0.3.1 (2015-06-28) +* (bluefox) change link in admin to node-red web server + +### 0.3.0 (2015-05-18) +* (bluefox) add flag "stopBeforeUpdate" +* (bluefox) store data in iobroker-data directory + +### 0.2.2 (2015-05-17) +* (bluefox) fix error with invalid additional npm package + +### 0.2.1 (2015-05-17) +* (bluefox) fix readme link + +### 0.2.0 (2015-05-16) +* (bluefox) allow install of additional npm and node-red packets + +### 0.1.9 (2015-03-26) +* (bluefox) fix first start + +### 0.1.7 (2015-03-25) +* (bluefox) remove warnings + +### 0.1.6 (2015-03-18) +* (bluefox) make node-red compatible with ioBroker again + +### 0.1.5 (2015-02-12) +* (bluefox) update node-red to 0.10.1 +* (bluefox) update select ID dialog + +### 0.1.4 (2015-01-07) +* (bluefox) create variables without need to be extra called with "__create__" + +### 0.1.3 (2015-01-06) +* (bluefox) make possible creation of variables + +### 0.1.2 (2015-01-04) +* (bluefox) print debug message by saving + +### 0.1.1 (2015-01-03) +* (bluefox) fix errors with utils.js + +### 0.1.0 (2015-01-02) +* (bluefox) enable npm install + +### 0.0.8 (2014-12-20) +* (bluefox) support signal stopInstance + +### 0.0.7 (2014-12-14) +* (bluefox) support of select ID dialogs + +### 0.0.6 (2014-11-26) +* (bluefox) use names like in mqtt: "adapter/instance/device/channel/state" +* (bluefox) suport of "value" or "object" for input node + +### 0.0.5 (2014-11-22) +* (bluefox) support of new naming concept + +### 0.0.4 (2014-11-05) +* (bluefox) fix some errors + +### 0.0.2 (2014-11-04) +* (bluefox) use adapter.js to communicate with ioBroker + +### 0.0.1 (2014-11-03) +* (bluefox) initial commit + +## Install + +```node iobroker.js add node-red``` + +## Configuration + +## License + +Copyright 2014-2018 bluefox . + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/admin/index.html b/admin/index.html new file mode 100644 index 0000000..b95ba4b --- /dev/null +++ b/admin/index.html @@ -0,0 +1,132 @@ + + + + + + + + + + + + + + + + + + + +
+ + + +

node-red adapter settings

+ +

node-red settings

+ + + + + + + + + + + + + + +
Divided by comma
+

node-red update select dialog

+ +
+ diff --git a/admin/index_m.html b/admin/index_m.html new file mode 100644 index 0000000..9ab645c --- /dev/null +++ b/admin/index_m.html @@ -0,0 +1,271 @@ + + + + + + + + + + + + + + + + + + + + + +
+
+
+
+
+ +
+
+
+
+ +
+
+
+
+
+ + +
+
+ + +
+
+
+
+ + + Leave blank to disable basic authentication +
+
+ + +
+
+
+
+ + +
+
+
+
+ + +
+
+ + +
+
+ +
+
+
+ + diff --git a/admin/node-red.png b/admin/node-red.png new file mode 100644 index 0000000..1171fd9 Binary files /dev/null and b/admin/node-red.png differ diff --git a/admin/words.js b/admin/words.js new file mode 100644 index 0000000..3c75aee --- /dev/null +++ b/admin/words.js @@ -0,0 +1,20 @@ +// DO NOT EDIT THIS FILE!!! IT WILL BE AUTOMATICALLY GENERATED FROM src/i18n +/*global systemDictionary:true */ +'use strict'; + +systemDictionary = { + "node-red settings": { "en": "node-red settings", "de": "node-red Einstellungen", "ru": "node-red Настройки", "pt": "configurações de nó vermelho", "nl": "node-red instellingen", "fr": "paramètres de node-red", "it": "impostazioni del node-red", "es": "configuración de node-red", "pl": "ustawienia node-red"}, + "Web server port:": { "en": "Web server port", "de": "Web-Server-Port", "ru": "Порт веб сервера", "pt": "Porta do servidor da Web", "nl": "Webserverpoort", "fr": "Port du serveur Web", "it": "Porta del server Web", "es": "Puerto del servidor web", "pl": "Port serwera internetowego"}, + "node-red update select dialog": { "en": "node-red update select dialog", "de": "node-red SelectID Dialog aktualisieren", "ru": "Обновить переменные в диалоге node-red", "pt": "caixa de diálogo de seleção de atualização do nó-vermelho", "nl": "update select dialoogvenster", "fr": "boîte de dialogue de sélection de mise à jour de noeud", "it": "finestra di dialogo di selezione aggiornamento node-red", "es": "diálogo de selección de actualización node-red", "pl": "okno dialogowe aktualizacji uaktualnienia węzła"}, + "Update select dialog": { "en": "Update select dialog", "de": "Update Dialog um ID zu selektieren", "ru": "Обновить переменные в диалоге выбора объектов", "pt": "Atualizar caixa de diálogo de seleção", "nl": "Update select dialoogvenster", "fr": "Mettre à jour le dialogue", "it": "Aggiorna la finestra di selezione", "es": "Actualizar diálogo de selección", "pl": "Zaktualizuj wybrane okno dialogowe"}, + "Divided by comma": { "en": "Divided by comma", "de": "Getrennt mit Komma", "ru": "Через запятую", "pt": "Dividido por vírgula", "nl": "Verdeeld door een komma", "fr": "Divisé par une virgule", "it": "Diviso in virgola", "es": "Dividido por coma", "pl": "Podzielone przecinkiem"}, + "Additional npm modules:": { "en": "Additional npm modules", "de": "Zusätzliche NPM-Module", "ru": "Дополнительные NPM Модули", "pt": "Módulos npm adicionais", "nl": "Extra npm-modules", "fr": "Modules NPM supplémentaires", "it": "Ulteriori moduli npm", "es": "Módulos npm adicionales", "pl": "Dodatkowe moduły npm"}, + "http root directory:": { "en": "http root directory", "de": "http Stammpfad", "ru": "http root directory", "pt": "diretório raiz http", "nl": "http root directory", "fr": "répertoire racine http", "it": "directory root http", "es": "directorio raíz http", "pl": "główny katalog http"}, + "Convert values to string:": { "en": "Convert ioBroker values to string", "de": "ioBroker-Werte in String konvertieren:", "ru": "Конвертировать значения из ioBroker в строки", "pt": "Converter valores de ioBroker em string", "nl": "IoBroker-waarden converteren naar tekenreeks", "fr": "Convertir les valeurs de ioBroker en chaîne", "it": "Converti i valori di ioBroker in stringa", "es": "Convierta los valores de ioBroker en una cadena", "pl": "Konwertuj wartości ioBroker na ciąg"}, + "Add module": { "en": "Add module", "de": "Modul hinzufügen", "ru": "Добавить модуль", "pt": "Adicionar módulo", "nl": "Module toevoegen", "fr": "Ajouter un module", "it": "Aggiungi modulo", "es": "Agregar módulo", "pl": "Dodaj moduł"}, + "Max allocated RAM:": { "en": "Max allocated RAM", "de": "Max zugewiesener RAM", "ru": "Выделено RAM", "pt": "RAM alocada máxima", "nl": "Max toegewezen RAM", "fr": "Max allouée RAM", "it": "RAM allocata massima", "es": "RAM máxima asignada", "pl": "Maksymalna przydzielona pamięć RAM"}, + "Module names": { "en": "Module names", "de": "Modulnamen", "ru": "Имена модулей", "pt": "Nomes de módulos", "nl": "Module namen", "fr": "Noms de modules", "it": "Nomi dei moduli", "es": "Nombres de módulos", "pl": "Nazwy modułów"}, + "User": { "en": "User", "de": "Benutzer", "ru": "Имя пользователя", "pt": "Do utilizador", "nl": "Gebruiker", "fr": "Utilisateur", "it": "Utente", "es": "Usuario", "pl": "Użytkownik"}, + "Leave blank to disable basic authentication": { "en": "Leave blank to disable basic authentication", "de": "Leer lassen, um die Basic-Authentication zu deaktivieren", "ru": "Оставить пустым, чтобы отключить basic authentication", "pt": "Deixe em branco para desativar a autenticação básica", "nl": "Laat dit leeg om basisverificatie uit te schakelen", "fr": "Laissez vide pour désactiver l'authentification de base", "it": "Lascia vuoto per disabilitare l'autenticazione di base", "es": "Dejar en blanco para deshabilitar la autenticación básica", "pl": "Pozostaw puste, aby wyłączyć podstawowe uwierzytelnianie"}, + "Password": { "en": "Password", "de": "Passwort", "ru": "Пароль", "pt": "Senha", "nl": "Wachtwoord", "fr": "Mot de passe", "it": "Parola d'ordine", "es": "Contraseña", "pl": "Hasło"}, +}; \ No newline at end of file diff --git a/appveyor.yml b/appveyor.yml new file mode 100644 index 0000000..84ca0a9 --- /dev/null +++ b/appveyor.yml @@ -0,0 +1,25 @@ +version: 'test-{build}' +environment: + matrix: + - nodejs_version: '4' + - nodejs_version: '6' + - nodejs_version: '8' + - nodejs_version: '10' +platform: + - x86 + - x64 +clone_folder: 'c:\projects\%APPVEYOR_PROJECT_NAME%' +install: + - ps: 'Install-Product node $env:nodejs_version $env:platform' + - ps: '$NpmVersion = (npm -v).Substring(0,1)' + - ps: 'if($NpmVersion -eq 5) { npm install -g npm@5 }' + - ps: npm --version + - npm install + - npm install winston@2.3.1 + - 'npm install https://github.com/ioBroker/ioBroker.js-controller/tarball/master --production' +test_script: + - echo %cd% + - node --version + - npm --version + - npm test +build: 'off' diff --git a/gulpfile.js b/gulpfile.js new file mode 100644 index 0000000..8d40257 --- /dev/null +++ b/gulpfile.js @@ -0,0 +1,400 @@ +'use strict'; + +const gulp = require('gulp'); +const fs = require('fs'); +const pkg = require('./package.json'); +const iopackage = require('./io-package.json'); +const version = (pkg && pkg.version) ? pkg.version : iopackage.common.version; +/*const appName = getAppName(); + +function getAppName() { + const parts = __dirname.replace(/\\/g, '/').split('/'); + return parts[parts.length - 1].split('.')[0].toLowerCase(); +} +*/ +const fileName = 'words.js'; +const languages = { + en: {}, + de: {}, + ru: {}, + pt: {}, + nl: {}, + fr: {}, + it: {}, + es: {}, + pl: {} +}; + +function lang2data(lang, isFlat) { + let str = isFlat ? '' : '{\n'; + let count = 0; + for (const w in lang) { + if (lang.hasOwnProperty(w)) { + count++; + if (isFlat) { + str += (lang[w] === '' ? (isFlat[w] || w) : lang[w]) + '\n'; + } else { + const key = ' "' + w.replace(/"/g, '\\"') + '": '; + str += key + '"' + lang[w].replace(/"/g, '\\"') + '",\n'; + } + } + } + if (!count) return isFlat ? '' : '{\n}'; + if (isFlat) { + return str; + } else { + return str.substring(0, str.length - 2) + '\n}'; + } +} + +function readWordJs(src) { + try { + let words; + if (fs.existsSync(src + 'js/' + fileName)) { + words = fs.readFileSync(src + 'js/' + fileName).toString(); + } else { + words = fs.readFileSync(src + fileName).toString(); + } + + const lines = words.split(/\r\n|\r|\n/g); + let i = 0; + while (!lines[i].match(/^systemDictionary = {/)) { + i++; + } + lines.splice(0, i); + + // remove last empty lines + i = lines.length - 1; + while (!lines[i]) { + i--; + } + if (i < lines.length - 1) { + lines.splice(i + 1); + } + + lines[0] = lines[0].replace('systemDictionary = ', ''); + lines[lines.length - 1] = lines[lines.length - 1].trim().replace(/};$/, '}'); + words = lines.join('\n'); + const resultFunc = new Function('return ' + words + ';'); + + return resultFunc(); + } catch (e) { + return null; + } +} +function padRight(text, totalLength) { + return text + (text.length < totalLength ? new Array(totalLength - text.length).join(' ') : ''); +} +function writeWordJs(data, src) { + let text = '// DO NOT EDIT THIS FILE!!! IT WILL BE AUTOMATICALLY GENERATED FROM src/i18n\n'; + text += '/*global systemDictionary:true */\n'; + text += '\'use strict\';\n\n'; + + text += 'systemDictionary = {\n'; + for (const word in data) { + if (data.hasOwnProperty(word)) { + text += ' ' + padRight('"' + word.replace(/"/g, '\\"') + '": {', 50); + let line = ''; + for (const lang in data[word]) { + if (data[word].hasOwnProperty(lang)) { + line += '"' + lang + '": "' + padRight(data[word][lang].replace(/"/g, '\\"') + '",', 50) + ' '; + } + } + if (line) { + line = line.trim(); + line = line.substring(0, line.length - 1); + } + text += line + '},\n'; + } + } + text += '};'; + + if (fs.existsSync(src + 'js/' + fileName)) { + fs.writeFileSync(src + 'js/' + fileName, text); + } else { + fs.writeFileSync(src + '' + fileName, text); + } +} + +const EMPTY = ''; + +function words2languages(src) { + const langs = Object.assign({}, languages); + const data = readWordJs(src); + if (data) { + for (const word in data) { + if (data.hasOwnProperty(word)) { + for (const lang in data[word]) { + if (data[word].hasOwnProperty(lang)) { + langs[lang][word] = data[word][lang]; + // pre-fill all other languages + for (const j in langs) { + if (langs.hasOwnProperty(j)) { + langs[j][word] = langs[j][word] || EMPTY; + } + } + } + } + } + } + if (!fs.existsSync(src + 'i18n/')) { + fs.mkdirSync(src + 'i18n/'); + } + for (const l in langs) { + if (!langs.hasOwnProperty(l)) continue; + const keys = Object.keys(langs[l]); + //keys.sort(); + const obj = {}; + for (let k = 0; k < keys.length; k++) { + obj[keys[k]] = langs[l][keys[k]]; + } + if (!fs.existsSync(src + 'i18n/' + l)) { + fs.mkdirSync(src + 'i18n/' + l); + } + + fs.writeFileSync(src + 'i18n/' + l + '/translations.json', lang2data(obj)); + } + } else { + console.error('Cannot read or parse ' + fileName); + } +} +function words2languagesFlat(src) { + const langs = Object.assign({}, languages); + const data = readWordJs(src); + if (data) { + for (const word in data) { + if (data.hasOwnProperty(word)) { + for (const lang in data[word]) { + if (data[word].hasOwnProperty(lang)) { + langs[lang][word] = data[word][lang]; + // pre-fill all other languages + for (const j in langs) { + if (langs.hasOwnProperty(j)) { + langs[j][word] = langs[j][word] || EMPTY; + } + } + } + } + } + } + const keys = Object.keys(langs.en); + //keys.sort(); + for (const l in langs) { + if (!langs.hasOwnProperty(l)) continue; + const obj = {}; + for (let k = 0; k < keys.length; k++) { + obj[keys[k]] = langs[l][keys[k]]; + } + langs[l] = obj; + } + if (!fs.existsSync(src + 'i18n/')) { + fs.mkdirSync(src + 'i18n/'); + } + for (const ll in langs) { + if (!langs.hasOwnProperty(ll)) continue; + if (!fs.existsSync(src + 'i18n/' + ll)) { + fs.mkdirSync(src + 'i18n/' + ll); + } + + fs.writeFileSync(src + 'i18n/' + ll + '/flat.txt', lang2data(langs[ll], langs.en)); + } + fs.writeFileSync(src + 'i18n/flat.txt', keys.join('\n')); + } else { + console.error('Cannot read or parse ' + fileName); + } +} +function languagesFlat2words(src) { + const dirs = fs.readdirSync(src + 'i18n/'); + const langs = {}; + const bigOne = {}; + const order = Object.keys(languages); + dirs.sort(function (a, b) { + const posA = order.indexOf(a); + const posB = order.indexOf(b); + if (posA === -1 && posB === -1) { + if (a > b) return 1; + if (a < b) return -1; + return 0; + } else if (posA === -1) { + return -1; + } else if (posB === -1) { + return 1; + } else { + if (posA > posB) return 1; + if (posA < posB) return -1; + return 0; + } + }); + const keys = fs.readFileSync(src + 'i18n/flat.txt').toString().split('\n'); + + for (let l = 0; l < dirs.length; l++) { + if (dirs[l] === 'flat.txt') continue; + const lang = dirs[l]; + const values = fs.readFileSync(src + 'i18n/' + lang + '/flat.txt').toString().split('\n'); + langs[lang] = {}; + keys.forEach(function (word, i) { + langs[lang][word] = values[i].replace(/<\/ i>/g, '').replace(/<\/ b>/g, '').replace(/<\/ span>/g, '').replace(/% s/g, ' %s'); + }); + + const words = langs[lang]; + for (const word in words) { + if (words.hasOwnProperty(word)) { + bigOne[word] = bigOne[word] || {}; + if (words[word] !== EMPTY) { + bigOne[word][lang] = words[word]; + } + } + } + } + // read actual words.js + const aWords = readWordJs(); + + const temporaryIgnore = ['pt', 'fr', 'nl', 'flat.txt']; + if (aWords) { + // Merge words together + for (const w in aWords) { + if (aWords.hasOwnProperty(w)) { + if (!bigOne[w]) { + console.warn('Take from actual words.js: ' + w); + bigOne[w] = aWords[w] + } + dirs.forEach(function (lang) { + if (temporaryIgnore.indexOf(lang) !== -1) return; + if (!bigOne[w][lang]) { + console.warn('Missing "' + lang + '": ' + w); + } + }); + } + } + + } + + writeWordJs(bigOne, src); +} +function languages2words(src) { + const dirs = fs.readdirSync(src + 'i18n/'); + const langs = {}; + const bigOne = {}; + const order = Object.keys(languages); + dirs.sort(function (a, b) { + const posA = order.indexOf(a); + const posB = order.indexOf(b); + if (posA === -1 && posB === -1) { + if (a > b) return 1; + if (a < b) return -1; + return 0; + } else if (posA === -1) { + return -1; + } else if (posB === -1) { + return 1; + } else { + if (posA > posB) return 1; + if (posA < posB) return -1; + return 0; + } + }); + for (let l = 0; l < dirs.length; l++) { + if (dirs[l] === 'flat.txt') continue; + const lang = dirs[l]; + langs[lang] = fs.readFileSync(src + 'i18n/' + lang + '/translations.json').toString(); + langs[lang] = JSON.parse(langs[lang]); + const words = langs[lang]; + for (const word in words) { + if (words.hasOwnProperty(word)) { + bigOne[word] = bigOne[word] || {}; + if (words[word] !== EMPTY) { + bigOne[word][lang] = words[word]; + } + } + } + } + // read actual words.js + const aWords = readWordJs(); + + const temporaryIgnore = ['pt', 'fr', 'nl', 'it']; + if (aWords) { + // Merge words together + for (const w in aWords) { + if (aWords.hasOwnProperty(w)) { + if (!bigOne[w]) { + console.warn('Take from actual words.js: ' + w); + bigOne[w] = aWords[w] + } + dirs.forEach(function (lang) { + if (temporaryIgnore.indexOf(lang) !== -1) return; + if (!bigOne[w][lang]) { + console.warn('Missing "' + lang + '": ' + w); + } + }); + } + } + + } + + writeWordJs(bigOne, src); +} + +gulp.task('adminWords2languages', function (done) { + words2languages('./admin/'); + done(); +}); + +gulp.task('adminWords2languagesFlat', function (done) { + words2languagesFlat('./admin/'); + done(); +}); + +gulp.task('adminLanguagesFlat2words', function (done) { + languagesFlat2words('./admin/'); + done(); +}); + +gulp.task('adminLanguages2words', function (done) { + languages2words('./admin/'); + done(); +}); + + +gulp.task('updatePackages', function (done) { + iopackage.common.version = pkg.version; + iopackage.common.news = iopackage.common.news || {}; + if (!iopackage.common.news[pkg.version]) { + const news = iopackage.common.news; + const newNews = {}; + + newNews[pkg.version] = { + en: 'news', + de: 'neues', + ru: 'новое' + }; + iopackage.common.news = Object.assign(newNews, news); + } + fs.writeFileSync('io-package.json', JSON.stringify(iopackage, null, 4)); + done(); +}); + +gulp.task('updateReadme', function (done) { + const readme = fs.readFileSync('README.md').toString(); + const pos = readme.indexOf('## Changelog\n'); + if (pos !== -1) { + const readmeStart = readme.substring(0, pos + '## Changelog\n'.length); + const readmeEnd = readme.substring(pos + '## Changelog\n'.length); + + if (readme.indexOf(version) === -1) { + const timestamp = new Date(); + const date = timestamp.getFullYear() + '-' + + ('0' + (timestamp.getMonth() + 1).toString(10)).slice(-2) + '-' + + ('0' + (timestamp.getDate()).toString(10)).slice(-2); + + let news = ''; + if (iopackage.common.news && iopackage.common.news[pkg.version]) { + news += '* ' + iopackage.common.news[pkg.version].en; + } + + fs.writeFileSync('README.md', readmeStart + '### ' + version + ' (' + date + ')\n' + (news ? news + '\n\n' : '\n') + readmeEnd); + } + } + done(); +}); + +gulp.task('default', ['updatePackages', 'updateReadme']); \ No newline at end of file diff --git a/io-package.json b/io-package.json new file mode 100644 index 0000000..749cf09 --- /dev/null +++ b/io-package.json @@ -0,0 +1,138 @@ +{ + "common": { + "name": "node-red", + "version": "1.7.1", + "title": "node-red", + "news": { + "1.7.1": { + "en": "Used newer version of node-red 0.19.4\nBasic authentication was added", + "de": "Verwendete neuere Version von node-red 0.19.4\nStandardauthentifizierung wurde hinzugefügt", + "ru": "Используется новая версия node-red 0.19.4\nДобавлена ​​базовая аутентификация", + "pt": "Usado versão mais recente do nó vermelho 0.19.4\nAutenticação básica foi adicionada", + "nl": "Gebruikte nieuwere versie van knoop-rood 0.19.4\nBasisverificatie is toegevoegd", + "fr": "Version plus récente de node-red 0.19.4\nL'authentification de base a été ajoutée", + "it": "Utilizzata la versione più recente di node-red 0.19.4\nÈ stata aggiunta l'autenticazione di base", + "es": "Versión más nueva usada de node-red 0.19.4\nAutenticación básica fue agregada", + "pl": "Używana nowsza wersja węzła-czerwonego 0.19.4\nDodano uwierzytelnianie podstawowe" + }, + "1.7.0": { + "en": "Used newer version of node-red 0.19.1", + "de": "Benutzte neuere Version von node-red 0.19.1", + "ru": "Используется новая версия node-red 0.19.1", + "pt": "Usado versão mais recente do nó vermelho 0.19.1", + "nl": "Gebruikte nieuwere versie van knoop-rood 0.19.1", + "fr": "Version plus récente de node-red 0.19.1", + "it": "Utilizzata la versione più recente di node-red 0.19.1", + "es": "Versión más nueva usada de node-red 0.19.1", + "pl": "Używana nowsza wersja węzła-czerwonego 0.19.1" + }, + "1.6.0": { + "en": "newer version of node-red 0.18.7 used\nAdmin3 dialog implemented\nRAM settings were added", + "de": "neuere Version von node-red 0.18.7 verwendet\nAdmin3-Dialog implementiert\nRAM-Einstellungen wurden hinzugefügt", + "ru": "более новая версия node-red 0.18.7 используется\nДиалог Admin3 реализован\nДобавлены настройки RAM", + "pt": "versão mais recente do node-red 0.18.7 usado\nDiálogo Admin3 implementado\nConfigurações de RAM foram adicionadas", + "nl": "nieuwere versie van knooppunt-rood 0.18.7 gebruikt\nAdmin3-dialoogvenster geïmplementeerd\nRAM-instellingen zijn toegevoegd", + "fr": "version plus récente de node-red 0.18.7 d'occasion\nBoîte de dialogue Admin3 implémentée\nParamètres RAM ont été ajoutés", + "it": "versione più recente di node-red 0.18.7 usata\nFinestra di dialogo Admin3 implementata\nSono state aggiunte le impostazioni della RAM", + "es": "nueva versión de node-red 0.18.7 usada\nDiálogo Admin3 implementado\nSe agregaron configuraciones de RAM", + "pl": "zastosowano nowszą wersję węzła-czerwonego 0.18.7\nZaimplementowano okno Admin3\nDodano ustawienia pamięci RAM" + }, + "1.5.1": { + "en": "queue set state requests till ioBroker connection has been initialized", + "de": "Verzögern von ioBroker-Schreibaktionen bis Verbindung zu ioBroker initialisiert wurde", + "ru": "queue set state requests till ioBroker connection has been initialized" + }, + "1.5.0": { + "en": "use newer version of node-red 0.18.2", + "de": "Neue Version von node-red 0.18.2", + "ru": "Новая версия node-red 0.18.2" + }, + "1.4.1": { + "en": "fix blank topic support", + "de": "Korrigiere lehre Topics", + "ru": "Поправлены пустые топики" + }, + "1.4.0": { + "en": "use newer version of node-red 0.17.5", + "de": "Neue Version von node-red 0.17.5", + "ru": "Новая версия node-red 0.17.5" + }, + "1.3.0": { + "en": "Update the select ID dialog", + "de": "Update ID Auswahl", + "ru": "Обновлён диалог выбора объектов" + }, + "1.2.0": { + "en": "add node-red-contrib-os, node-red-dashboard, node-red-contrib-aggregator by default", + "de": "Hinzugefügt: node-red-contrib-os, node-red-dashboard, node-red-contrib-aggregator", + "ru": "Добавлены: node-red-contrib-os, node-red-dashboard, node-red-contrib-aggregator" + }, + "1.1.6": { + "en": "use newer version of node-red 0.16.2", + "de": "Neue Version von node-red 0.16.2", + "ru": "Новая версия node-red 0.16.2" + } + }, + "desc": { + "en": "This adapter uses node-red as a service. No additional node-red instance required.", + "de": "Adapter benutzt node-red als Servcie. Kein zusätzliches node-red Programm nötig.", + "ru": "Драйвер создает node-red сервер и позволяет общаться с ним.", + "pt": "Este adaptador usa node-red como um serviço. Nenhuma instância node-red adicional é necessária.", + "nl": "Deze adapter gebruikt node-red als een service. Geen extra node-red exemplaar vereist.", + "fr": "Cet adaptateur utilise node-red en tant que service. Aucune instance node-red supplémentaire requise.", + "it": "Questo adattatore utilizza node-red come servizio. Nessuna istanza aggiuntiva node-red richiesta.", + "es": "Este adaptador usa node-red como un servicio. No se requiere ninguna instancia adicional de node-red.", + "pl": "Ten adapter używa node-red jako usługi. Żadna dodatkowa instancja node-red nie jest wymagana." + }, + "authors": [ + "bluefox " + ], + "license": "Apache-2.0", + "platform": "Javascript/Node.js", + "mode": "daemon", + "messagebox": true, + "loglevel": "info", + "icon": "node-red.png", + "keywords": [ + "node-red", + "logic", + "script" + ], + "extIcon": "https://raw.githubusercontent.com/ioBroker/ioBroker.node-red/master/admin/node-red.png", + "localLink": "http://%ip%:%port%%httpRoot%", + "enabled": true, + "singletonHost": true, + "supportStopInstance": true, + "unsafePerm": true, + "materialize": true, + "type": "logic", + "readme": "https://github.com/ioBroker/ioBroker.node-red/blob/master/README.md", + "stopBeforeUpdate": true, + "adminTab": { + "link": "http://%ip%:%port%%httpRoot%", + "fa-icon": "settings_input_composite" + } + }, + "native": { + "bind": "0.0.0.0", + "port": 1880, + "httpRoot": "/", + "npmLibs": "", + "valueConvert": true, + "maxMemory": 128, + "user": "", + "pass": "" + }, + "objects": [], + "instanceObjects": [ + { + "_id": "", + "type": "channel", + "common": { + "name": "States created by node-red.%INSTANCE%", + "role": "info" + }, + "native": {} + } + ] +} diff --git a/lib/utils.js b/lib/utils.js new file mode 100644 index 0000000..c8a0eb7 --- /dev/null +++ b/lib/utils.js @@ -0,0 +1,83 @@ +'use strict'; + +const fs = require('fs'); +const path = require('path'); + +let controllerDir; +let appName; + +/** + * returns application name + * + * The name of the application can be different and this function finds it out. + * + * @returns {string} + */ + function getAppName() { + const parts = __dirname.replace(/\\/g, '/').split('/'); + return parts[parts.length - 2].split('.')[0]; +} + +/** + * looks for js-controller home folder + * + * @param {boolean} isInstall + * @returns {string} + */ +function getControllerDir(isInstall) { + // Find the js-controller location + const possibilities = [ + 'iobroker.js-controller', + 'ioBroker.js-controller', + ]; + /** @type {string} */ + let controllerPath; + for (const pkg of possibilities) { + try { + const possiblePath = require.resolve(pkg); + if (fs.existsSync(possiblePath)) { + controllerPath = possiblePath; + break; + } + } catch (e) { /* not found */ } + } + if (controllerPath == null) { + if (!isInstall) { + console.log('Cannot find js-controller'); + process.exit(10); + } else { + process.exit(); + } + } + // we found the controller + return path.dirname(controllerPath); +} + +/** + * reads controller base settings + * + * @alias getConfig + * @returns {object} + */ + function getConfig() { + let configPath; + if (fs.existsSync( + configPath = path.join(controllerDir, 'conf', appName + '.json') + )) { + return JSON.parse(fs.readFileSync(configPath, 'utf8')); + } else if (fs.existsSync( + configPath = path.join(controllerDir, 'conf', + appName.toLowerCase() + '.json') + )) { + return JSON.parse(fs.readFileSync(configPath, 'utf8')); + } else { + throw new Error('Cannot find ' + controllerDir + '/conf/' + appName + '.json'); + } +} +appName = getAppName(); +controllerDir = getControllerDir(typeof process !== 'undefined' && process.argv && process.argv.indexOf('--install') !== -1); +const adapter = require(path.join(controllerDir, 'lib/adapter.js')); + +exports.controllerDir = controllerDir; +exports.getConfig = getConfig; +exports.Adapter = adapter; +exports.appName = appName; diff --git a/main.js b/main.js new file mode 100644 index 0000000..cb621c6 --- /dev/null +++ b/main.js @@ -0,0 +1,443 @@ +/** + * + * ioBroker node-red Adapter + * + * (c) 2014 bluefox + * + * Apache 2.0 License + * + */ +/* jshint -W097 */// jshint strict:false +/*jslint node: true */ +'use strict'; + +const utils = require(__dirname + '/lib/utils'); // Get common adapter utils +const adapter = utils.Adapter({ + name: 'node-red', + systemConfig: true, // get the system configuration as systemConfig parameter of adapter + unload: unloadRed +}); + +const fs = require('fs'); +const path = require('path'); +const spawn = require('child_process').spawn; +const Notify = require('fs.notify'); +const attempts = {}; +const additional = []; +let secret; + +let userdataDir = __dirname + '/userdata/'; + +adapter.on('message', function (obj) { + if (obj) processMessage(obj); + processMessages(); +}); + +adapter.on('ready', function () { + installLibraries(main); +}); + +function installNpm(npmLib, callback) { + const path = __dirname; + if (typeof npmLib === 'function') { + callback = npmLib; + npmLib = undefined; + } + + const cmd = 'npm install ' + npmLib + ' --production --prefix "' + path + '" --save'; + adapter.log.info(cmd + ' (System call)'); + // Install node modules as system call + + // System call used for update of js-controller itself, + // because during installation npm packet will be deleted too, but some files must be loaded even during the install process. + const exec = require('child_process').exec; + const child = exec(cmd); + child.stdout.on('data', function(buf) { + adapter.log.info(buf.toString('utf8')); + }); + child.stderr.on('data', function(buf) { + adapter.log.error(buf.toString('utf8')); + }); + + child.on('exit', function (code, signal) { + if (code) { + adapter.log.error('Cannot install ' + npmLib + ': ' + code); + } + // command succeeded + if (callback) callback(npmLib); + }); +} + +function installLibraries(callback) { + let allInstalled = true; + if (typeof adapter.common.npmLibs === 'string') { + adapter.common.npmLibs = adapter.common.npmLibs.split(/[,;\s]+/); + } + + if (adapter.common && adapter.common.npmLibs) { + for (let lib = 0; lib < adapter.common.npmLibs.length; lib++) { + if (adapter.common.npmLibs[lib] && adapter.common.npmLibs[lib].trim()) { + adapter.common.npmLibs[lib] = adapter.common.npmLibs[lib].trim(); + if (!fs.existsSync(__dirname + '/node_modules/' + adapter.common.npmLibs[lib] + '/package.json')) { + + if (!attempts[adapter.common.npmLibs[lib]]) { + attempts[adapter.common.npmLibs[lib]] = 1; + } else { + attempts[adapter.common.npmLibs[lib]]++; + } + if (attempts[adapter.common.npmLibs[lib]] > 3) { + adapter.log.error('Cannot install npm packet: ' + adapter.common.npmLibs[lib]); + continue; + } + + installNpm(adapter.common.npmLibs[lib], function () { + installLibraries(callback); + }); + allInstalled = false; + break; + } else { + if (additional.indexOf(adapter.common.npmLibs[lib]) === -1) additional.push(adapter.common.npmLibs[lib]); + } + } + } + } + if (allInstalled) callback(); +} + +// is called if a subscribed state changes +//adapter.on('stateChange', function (id, state) { +//}); +function unloadRed (callback) { + // Stop node-red + stopping = true; + if (redProcess) { + adapter.log.info("kill node-red task"); + redProcess.kill(); + redProcess = null; + } + if (notificationsCreds) notificationsCreds.close(); + if (notificationsFlows) notificationsFlows.close(); + + if (callback) callback(); +} + +function processMessage(obj) { + if (!obj || !obj.command) return; + switch (obj.command) { + case 'update': + writeStateList(error => { + if (typeof obj.callback === 'function') adapter.sendTo(obj.from, obj.command, error, obj.callback); + }); + break; + + case 'stopInstance': + unloadRed(); + break; + + } +} + +function processMessages() { + adapter.getMessage((err, obj) => { + if (obj) { + processMessage(obj.command, obj.message); + processMessages(); + } + }); +} + +function getNodeRedPath() { + let nodeRed = __dirname + '/node_modules/node-red'; + if (!fs.existsSync(nodeRed)) { + nodeRed = path.normalize(__dirname + '/../node-red'); + if (!fs.existsSync(nodeRed)) { + nodeRed = path.normalize(__dirname + '/../node_modules/node-red'); + if (!fs.existsSync(nodeRed)) { + adapter && adapter.log && adapter.log.error('Cannot find node-red packet!'); + throw new Error('Cannot find node-red packet!'); + } + } + } + + return nodeRed; +} + +let redProcess; +let stopping; +let notificationsFlows; +let notificationsCreds; +let saveTimer; +const nodePath = getNodeRedPath(); + +function startNodeRed() { + adapter.config.maxMemory = parseInt(adapter.config.maxMemory, 10) || 128; + const args = ['--max-old-space-size=' + adapter.config.maxMemory, nodePath + '/red.js', '-v', '--settings', userdataDir + 'settings.js']; + adapter.log.info('Starting node-red: ' + args.join(' ')); + + redProcess = spawn('node', args); + + redProcess.on('error', function (err) { + adapter.log.error('catched exception from node-red:' + JSON.stringify(err)); + }); + + redProcess.stdout.on('data', function (data) { + if (!data) return; + data = data.toString(); + if (data[data.length - 2] === '\r' && data[data.length - 1] === '\n') data = data.substring(0, data.length - 2); + if (data[data.length - 2] === '\n' && data[data.length - 1] === '\r') data = data.substring(0, data.length - 2); + if (data[data.length - 1] === '\r') data = data.substring(0, data.length - 1); + + if (data.indexOf('[err') !== -1) { + adapter.log.error(data); + } else if (data.indexOf('[warn]') !== -1) { + adapter.log.warn(data); + } else { + adapter.log.debug(data); + } + }); + redProcess.stderr.on('data', function (data) { + if (!data) return; + if (data[0]) { + let text = ''; + for (let i = 0; i < data.length; i++) { + text += String.fromCharCode(data[i]); + } + data = text; + } + if (data.indexOf && data.indexOf('[warn]') === -1) { + adapter.log.warn(data); + } else { + adapter.log.error(JSON.stringify(data)); + } + }); + + redProcess.on('exit', function (exitCode) { + adapter.log.info('node-red exited with ' + exitCode); + redProcess = null; + if (!stopping) { + setTimeout(startNodeRed, 5000); + } + }); +} + +function setOption(line, option, value) { + const toFind = "'%%" + option + "%%'"; + const pos = line.indexOf(toFind); + if (pos !== -1) { + return line.substring(0, pos) + ((value !== undefined) ? value : (adapter.config[option] === null || adapter.config[option] === undefined) ? '' : adapter.config[option]) + line.substring(pos + toFind.length); + } + return line; +} + +function writeSettings() { + const config = JSON.stringify(adapter.systemConfig); + const text = fs.readFileSync(__dirname + '/settings.js').toString(); + const lines = text.split('\n'); + let npms = '\r\n'; + const dir = __dirname.replace(/\\/g, '/') + '/node_modules/'; + const nodesDir = '"' + __dirname.replace(/\\/g, '/') + '/nodes/"'; + + const bind = '"' + (adapter.config.bind || '0.0.0.0') + '"'; + const auth = adapter.config.user && adapter.config.pass ? JSON.stringify({user: adapter.config.user, pass: adapter.config.pass}) : '""'; + const pass = '"' + adapter.config.pass + '"'; + + for (let a = 0; a < additional.length; a++) { + if (additional[a].match(/^node-red-/)) continue; + npms += ' "' + additional[a] + '": require("' + dir + additional[a] + '")'; + if (a !== additional.length - 1) { + npms += ', \r\n'; + } + } + + // update from 1.0.1 (new convert-option) + if (adapter.config.valueConvert === null || + adapter.config.valueConvert === undefined || + adapter.config.valueConvert === '' || + adapter.config.valueConvert === 'true' || + adapter.config.valueConvert === '1' || + adapter.config.valueConvert === 1) { + adapter.config.valueConvert = true; + } + if (adapter.config.valueConvert === 0 || + adapter.config.valueConvert === '0' || + adapter.config.valueConvert === 'false') { + adapter.config.valueConvert = false; + } + for (let i = 0; i < lines.length; i++) { + lines[i] = setOption(lines[i], 'port'); + lines[i] = setOption(lines[i], 'auth', auth); + lines[i] = setOption(lines[i], 'pass', pass); + lines[i] = setOption(lines[i], 'bind', bind); + lines[i] = setOption(lines[i], 'port'); + lines[i] = setOption(lines[i], 'instance', adapter.instance); + lines[i] = setOption(lines[i], 'config', config); + lines[i] = setOption(lines[i], 'functionGlobalContext', npms); + lines[i] = setOption(lines[i], 'nodesdir', nodesDir); + lines[i] = setOption(lines[i], 'httpRoot'); + lines[i] = setOption(lines[i], 'credentialSecret', secret); + lines[i] = setOption(lines[i], 'valueConvert'); + } + fs.writeFileSync(userdataDir + 'settings.js', lines.join('\n')); +} + +function writeStateList(callback) { + adapter.getForeignObjects('*', 'state', ['rooms', 'functions'], function (err, obj) { + // remove native information + for (const i in obj) { + if (obj.hasOwnProperty(i) && obj[i].native) { + delete obj[i].native; + } + } + + fs.writeFileSync(nodePath + '/public/iobroker.json', JSON.stringify(obj)); + if (callback) callback(err); + }); +} + +function saveObjects() { + if (saveTimer) { + clearTimeout(saveTimer); + saveTimer = null; + } + let cred = undefined; + let flows = undefined; + + try { + if (fs.existsSync(userdataDir + 'flows_cred.json')) { + cred = JSON.parse(fs.readFileSync(userdataDir + 'flows_cred.json')); + } + } catch(e) { + adapter.log.error('Cannot save ' + userdataDir + 'flows_cred.json'); + } + try { + if (fs.existsSync(userdataDir + 'flows.json')) { + flows = JSON.parse(fs.readFileSync(userdataDir + 'flows.json')); + } + } catch(e) { + adapter.log.error('Cannot save ' + userdataDir + 'flows.json'); + } + //upload it to config + adapter.setObject('flows', { + common: { + name: 'Flows for node-red' + }, + native: { + cred: cred, + flows: flows + }, + type: 'config' + }, function () { + adapter.log.info('Save ' + userdataDir + 'flows.json'); + }); +} + +function syncPublic(path) { + if (!path) path = '/public'; + + const dir = fs.readdirSync(__dirname + path); + + if (!fs.existsSync(nodePath + path)) { + fs.mkdirSync(nodePath + path); + } + + for (let i = 0; i < dir.length; i++) { + const stat = fs.statSync(__dirname + path + '/' + dir[i]); + if (stat.isDirectory()) { + syncPublic(path + '/' + dir[i]); + } else { + if (!fs.existsSync(nodePath + path + '/' + dir[i])) { + fs.createReadStream(__dirname + path + '/' + dir[i]).pipe(fs.createWriteStream(nodePath + path + '/' + dir[i])); + } + } + } +} + +function installNotifierFlows(isFirst) { + if (!notificationsFlows) { + if (fs.existsSync(userdataDir + 'flows.json')) { + if (!isFirst) saveObjects(); + // monitor project file + notificationsFlows = new Notify([userdataDir + 'flows.json']); + notificationsFlows.on('change', function () { + if (saveTimer) clearTimeout(saveTimer); + saveTimer = setTimeout(saveObjects, 500); + }); + } else { + // Try to install notifier every 10 seconds till the file will be created + setTimeout(function () { + installNotifierFlows(); + }, 10000); + } + } +} + +function installNotifierCreds(isFirst) { + if (!notificationsCreds) { + if (fs.existsSync(userdataDir + 'flows_cred.json')) { + if (!isFirst) saveObjects(); + // monitor project file + notificationsCreds = new Notify([userdataDir + 'flows_cred.json']); + notificationsCreds.on('change', function () { + if (saveTimer) clearTimeout(saveTimer); + saveTimer = setTimeout(saveObjects, 500); + }); + } else { + // Try to install notifier every 10 seconds till the file will be created + setTimeout(function () { + installNotifierCreds(); + }, 10000); + } + } +} + +function main() { + // Find userdata directory + + // normally /opt/iobroker/node_modules/iobroker.js-controller + // but can be /example/ioBroker.js-controller + const controllerDir = utils.controllerDir; + const parts = controllerDir.split('/'); + if (parts.length > 1 && parts[parts.length - 2] === 'node_modules') { + parts.splice(parts.length - 2, 2); + userdataDir = parts.join('/'); + userdataDir += '/iobroker-data/node-red/'; + } + + // create userdata directory + if (!fs.existsSync(userdataDir)) { + fs.mkdirSync(userdataDir); + } + + syncPublic(); + + // Read configuration + adapter.getObject('flows', function (err, obj) { + if (obj && obj.native && obj.native.cred) { + const c = JSON.stringify(obj.native.cred); + // If really not empty + if (c !== '{}' && c !== '[]') { + fs.writeFileSync(userdataDir + 'flows_cred.json', JSON.stringify(obj.native.cred)); + } + } + if (obj && obj.native && obj.native.flows) { + const f = JSON.stringify(obj.native.flows); + // If really not empty + if (f !== '{}' && f !== '[]') { + fs.writeFileSync(userdataDir + 'flows.json', JSON.stringify(obj.native.flows)); + } + } + + installNotifierFlows(true); + installNotifierCreds(true); + + adapter.getForeignObject('system.config', (err, obj) => { + if (obj && obj.native && obj.native.secret) { + //noinspection JSUnresolvedVariable + secret = obj.native.secret; + } + // Create settings for node-red + writeSettings(); + writeStateList(() => startNodeRed()); + }); + }); +} diff --git a/nodes/icons/iobroker.png b/nodes/icons/iobroker.png new file mode 100644 index 0000000..5d7f62e Binary files /dev/null and b/nodes/icons/iobroker.png differ diff --git a/nodes/ioBroker.html b/nodes/ioBroker.html new file mode 100644 index 0000000..28abd18 --- /dev/null +++ b/nodes/ioBroker.html @@ -0,0 +1,330 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/nodes/ioBroker.js b/nodes/ioBroker.js new file mode 100644 index 0000000..cf22b92 --- /dev/null +++ b/nodes/ioBroker.js @@ -0,0 +1,389 @@ +/** + * Copyright 2013,2014 IBM Corp. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + **/ + +module.exports = function(RED) { + 'use strict'; + require('events').EventEmitter.prototype._maxListeners = 100; + var util = require('util'); + var utils = require(__dirname + '/../lib/utils'); + //var redis = require("redis"); + //var hashFieldRE = /^([^=]+)=(.*)$/; + // Get the redis address + + var settings = require(process.env.NODE_RED_HOME + '/red/red').settings; + var instance = settings.get('iobrokerInstance') || 0; + var config = settings.get('iobrokerConfig'); + var valueConvert = settings.get('valueConvert'); + if (typeof config == 'string') { + config = JSON.parse(config); + } + var adapter; + + try { + adapter = utils.Adapter({name: 'node-red', instance: instance, config: config}); + } catch(e) { + console.log(e); + } + var nodes = []; + var nodeSets = []; + var ready = false; + var log = adapter && adapter.log && adapter.log.warn ? adapter.log.warn : console.log; + + adapter.on('ready', function () { + ready = true; + adapter.subscribeForeignStates('*'); + while (nodes.length) { + var node = nodes.pop(); + if (node instanceof IOBrokerInNode) { + adapter.on('stateChange', node.stateChange); + } + node.status({fill: 'green', shape: 'dot', text: 'connected'}); + } + var count = 0; + while (nodeSets.length) { + var nodeSetData = nodeSets.pop(); + nodeSetData.node.emit('input', nodeSetData.msg); + count++; + } + if (count > 0) log(count + ' queued state values set in ioBroker'); + + }); + + // name is like system.state, pattern is like "*.state" or "*" or "*system*" + function getRegex(pattern) { + if (!pattern || pattern === '*') return null; + if (pattern.indexOf('*') === -1) return null; + if (pattern[pattern.length - 1] !== '*') pattern = pattern + '$'; + if (pattern[0] !== '*') pattern = '^' + pattern; + pattern = pattern.replace(/\*/g, '[a-zA-Z0-9.\s]'); + pattern = pattern.replace(/\./g, '\\.'); + return new RegExp(pattern); + } + + function checkState(node, id, val, callback) { + if (node.idChecked) { + return callback && callback(); + } + if (node.topic) { + node.idChecked = true; + } + + adapter.getObject(id, function (err, obj) { + if (!obj) { + adapter.getForeignObject(id, function (err, obj) { + if (!obj) { + log('State "' + id + '" was created in the ioBroker as ' + adapter._fixId(id)); + // Create object + adapter.setObject(id, { + common: { + name: id, + role: 'info', + type: 'state', + desc: 'Created by Node-Red' + }, + native: {}, + type: 'state' + }, function (err) { + if (val !== undefined && val !== null && val !== '__create__') { + adapter.setState(id, val, function () { + callback && callback(); + }); + } else { + adapter.setState(id, undefined, function () { + callback && callback(); + }); + } + }); + } else { + node._id = obj._id; + if (val !== undefined && val !== null && val !== '__create__') { + adapter.setForeignState(obj._id, val, function () { + callback && callback(); + }); + } else { + callback && callback(); + } + } + }); + } else { + if (val !== undefined && val !== null && val !== '__create__') { + adapter.setForeignState(obj._id, val, function () { + callback && callback(); + }); + } else { + callback && callback(); + } + } + }); + } + + function IOBrokerInNode(n) { + var node = this; + RED.nodes.createNode(node,n); + node.topic = (n.topic || '*').replace(/\//g, '.'); + node.regex = new RegExp('^node-red\\.' + instance + '\\.'); + + // If no adapter prefix, add own adapter prefix + if (node.topic && node.topic.indexOf('.') === -1) { + node.topic = adapter.namespace + '.' + node.topic; + } + + node.regexTopic = getRegex(this.topic); + node.payloadType = n.payloadType; + node.onlyack = (n.onlyack == true || false); + node.func = n.func || 'all'; + node.gap = n.gap || '0'; + node.pc = false; + + if (node.gap.substr(-1) === '%') { + node.pc = true; + node.gap = parseFloat(node.gap); + } + node.g = node.gap; + + node.previous = {}; + + if (node.topic) { + var id = node.topic; + // If no wildchars and belongs to this adapter + if (id.indexOf('*') === -1 && (node.regex.test(id) || id.indexOf('.') !== -1)) { + checkState(node, id); + } + } + + if (ready) { + node.status({fill: 'green', shape: 'dot', text: 'connected'}); + } else { + node.status({fill: 'red', shape: 'ring', text: 'disconnected'}, true); + } + + node.stateChange = function(topic, obj) { + if (node.regexTopic) { + if (!node.regexTopic.test(topic)) return; + } else if (node.topic !== '*' && node.topic !== topic) { + return; + } + + if (node.onlyack && obj.ack != true) return; + + var t = topic.replace(/\./g, '/') || '_no_topic'; + //node.log ("Function: " + node.func); + + if (node.func === 'rbe') { + if (obj.val === node.previous[t]) { + return; + } + } else if (node.func === 'deadband') { + var n = parseFloat(obj.val.toString()); + if (!isNaN(n)) { + //node.log('Old Value: ' + node.previous[t] + ' New Value: ' + n); + if (node.pc) { node.gap = (node.previous[t] * node.g / 100) || 0; } + if (!node.previous.hasOwnProperty(t)) { + node.previous[t] = n - node.gap; + } + if (!Math.abs(n - node.previous[t]) >= node.gap) { + return; + } + } else { + node.warn('no number found in value'); + return; + } + } + node.previous[t] = obj.val; + + node.send({ + topic: t, + payload: (node.payloadType === 'object') ? obj : ((obj.val === null || obj.val === undefined) ? '' : (valueConvert ? obj.val.toString() : obj.val)), + acknowledged:obj.ack, + timestamp: obj.ts, + lastchange: obj.lc, + from: obj.from + }); + + node.status({fill: 'green', shape: 'dot', text: (node.payloadType === 'object') ? JSON.stringify(obj) : ((obj.val === null || obj.val === undefined) ? '' : obj.val.toString() ) }); + }; + + node.on('close', function() { + adapter.removeListener('stateChange', node.stateChange); + }); + + if (ready) { + adapter.on('stateChange', node.stateChange); + } else { + nodes.push(node); + } + } + RED.nodes.registerType('ioBroker in', IOBrokerInNode); + + function IOBrokerOutNode(n) { + var node = this; + RED.nodes.createNode(node,n); + node.topic = n.topic; + + node.ack = (n.ack === 'true' || n.ack === true); + node.autoCreate = (n.autoCreate === 'true' || n.autoCreate === true); + node.regex = new RegExp('^node-red\\.' + instance + '\\.'); + + if (ready) { + node.status({fill: 'green', shape: 'dot', text: 'connected'}); + } else { + node.status({fill: 'red', shape: 'ring', text: 'disconnected'}, true); + } + + function setState(id, val, ack) { + if (node.idChecked) { + if (val !== '__create__') { + adapter.setState(id, {val: val, ack: ack}); + } + } else { + checkState(node, id, {val: val, ack: ack}); + } + } + + node.on('input', function(msg) { + var id = node.topic || msg.topic; + if (!ready) { + nodeSets.push({'node': node, 'msg': msg}); + //log('Message for "' + id + '" queued because ioBroker connection not initialized'); + return; + } + if (id) { + id = id.replace(/\//g, '.'); + + // Create variable if not exists + if (node.autoCreate && !node.idChecked) { + id = id.replace(/\//g, '.'); + // If no wildchars and belongs to this adapter + if (id.indexOf('*') === -1 && (node.regex.test(id) || id.indexOf('.') !== -1)) { + checkState(node, id); + } + } + + // If not this adapter state + if (!node.regex.test(id) && id.indexOf('.') !== -1) { + // Check if state exists + adapter.getForeignState(id, function (err, state) { + if (!err && state) { + adapter.setForeignState(id, {val: msg.payload, ack: node.ack}); + node.status({fill: 'green', shape: 'dot', text: msg.payload.toString() }); + } else { + log('State "' + id + '" does not exist in the ioBroker'); + } + }); + } else { + if (id.indexOf('*') !== -1) { + log('Invalid topic name "' + id + '" for ioBroker'); + } else { + setState(id, msg.payload, node.ack); + node.status({fill: 'green', shape: 'dot', text: msg.payload.toString() }); + } + } + } else { + node.warn('No key or topic set'); + } + }); + + if (!ready) { + nodes.push(node); + } + + //node.on("close", function() { +// +// }); + + } + RED.nodes.registerType('ioBroker out', IOBrokerOutNode); + + function IOBrokerGetNode(n) { + var node = this; + RED.nodes.createNode(node,n); + node.topic = (typeof n.topic=== 'string' && n.topic.length > 0 ? n.topic.replace(/\//g, '.') : null) ; + + // If no adapter prefix, add own adapter prefix + if (node.topic && node.topic.indexOf('.') === -1) { + node.topic = adapter.namespace + '.' + node.topic; + } + + node.regex = new RegExp('^node-red\\.' + instance + '\\.'); + //node.regex = getRegex(this.topic); + node.payloadType = n.payloadType; + node.attrname = n.attrname; + + if (node.topic) { + var id = node.topic; + // If no wildchars and belongs to this adapter + if (id.indexOf('*') === -1 && (node.regex.test(id) || id.indexOf('.') !== -1)) { + checkState(node, id); + } + } + + if (ready) { + node.status({fill: 'green', shape: 'dot', text: 'connected'}); + } else { + node.status({fill: 'red', shape: 'ring', text: 'disconnected'}, true); + } + + node.getStateValue = function (msg) { + return function (err, state) { + if (!err && state) { + msg[node.attrname] = (node.payloadType === 'object') ? state : ((state.val === null || state.val === undefined) ? '' : (valueConvert ? state.val.toString() : state.val)); + msg.acknowledged = state.ack; + msg.timestamp = state.ts; + msg.lastchange = state.lc; + node.status({ + fill: 'green', + shape: 'dot', + text: (node.payloadType === 'object') ? JSON.stringify(state) : ((state.val === null || state.val === undefined) ? '' : state.val.toString()) + }); + node.send(msg); + } else { + log('State "' + id + '" does not exist in the ioBroker'); + } + }; + }; + + node.on('input', function(msg) { + var id = node.topic || msg.topic; + if (!ready) { + nodeSets.push({'node': node, 'msg': msg}); + //log('Message for "' + id + '" queued because ioBroker connection not initialized'); + return; + } + if (id) { + id = id.replace(/\//g, '.'); + // If not this adapter state + if (!node.regex.test(id) && id.indexOf('.') !== -1) { + // Check if state exists + adapter.getForeignState(id, node.getStateValue(msg)); + } else { + if (id.indexOf('*') !== -1) { + log('Invalid topic name "' + id + '" for ioBroker'); + } else { + adapter.getState(id, node.getStateValue(msg)); + } + } + } else { + node.warn('No key or topic set'); + } + }); + + if (!ready) { + nodes.push(node); + } + + } + RED.nodes.registerType('ioBroker get', IOBrokerGetNode); +}; diff --git a/package.json b/package.json new file mode 100644 index 0000000..43a1fc5 --- /dev/null +++ b/package.json @@ -0,0 +1,53 @@ +{ + "name": "iobroker.node-red", + "description": "This adapter uses node-red as a service of ioBroker. No additional node-red instance required.", + "version": "1.7.1", + "author": { + "name": "bluefox", + "email": "dogafox@gmail.com" + }, + "contributors": [ + { + "name": "bluefox", + "email": "dogafox@gmail.com" + } + ], + "homepage": "https://github.com/ioBroker/ioBroker.node-red", + "license": "Apache-2.0", + "keywords": [ + "ioBroker", + "node-red", + "home automation" + ], + "repository": { + "type": "git", + "url": "https://github.com/ioBroker/ioBroker.node-red" + }, + "optionalDependencies": { + "js2xmlparser": "^3.0.0", + "fs.notify": "^0.0.4", + "feedparser": "^2.2.9", + "mongodb": "^3.1.3" + }, + "dependencies": { + "node-red": "^0.19.4", + "node-red-contrib-os": "^0.1.7", + "node-red-dashboard": "^2.9.8", + "node-red-contrib-aggregator": "^1.3.0", + "node-red-contrib-polymer": "^0.0.21" + }, + "devDependencies": { + "gulp": "^3.9.1", + "request": "^2.88.0", + "mocha": "^5.2.0", + "chai": "^4.1.2" + }, + "bugs": { + "url": "https://github.com/ioBroker/ioBroker.node-red/issues" + }, + "main": "main.js", + "scripts": { + "test": "node node_modules/mocha/bin/mocha --exit" + }, + "readmeFilename": "README.md" +} diff --git a/public/schedule/LICENSE.txt b/public/schedule/LICENSE.txt new file mode 100644 index 0000000..d0381d6 --- /dev/null +++ b/public/schedule/LICENSE.txt @@ -0,0 +1,176 @@ +Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS diff --git a/public/schedule/index.html b/public/schedule/index.html new file mode 100644 index 0000000..195f6a2 --- /dev/null +++ b/public/schedule/index.html @@ -0,0 +1,68 @@ + + + + + + + Node-RED Schedule + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + +
NameHourMinDayMonthDay of WeekTopicPayload
+
+ + + diff --git a/public/schedule/style.css b/public/schedule/style.css new file mode 100644 index 0000000..705751f --- /dev/null +++ b/public/schedule/style.css @@ -0,0 +1,28 @@ +body { + font: 14px "Helvetica" !important; +} + +a.brand { + line-height: 16px; +} + +a.brand span { + vertical-align: middle; + font-size: 16px !important; +} + +a.brand:hover span.red { + color: rgb(197, 112, 112) !important; +} + +a.brand:hover { + color: #bbb !important; +} + +a.brand img { + height: 16px; +} + +#schedule { + margin-top: 20px; +} diff --git a/public/selectID.js b/public/selectID.js new file mode 100644 index 0000000..997bfe7 --- /dev/null +++ b/public/selectID.js @@ -0,0 +1,2465 @@ +/* + Copyright 2014-2017 bluefox + + version: 1.0.2 (2017.04.13) + + To use this dialog as standalone in ioBroker environment include: + + + + + + + + + + + + + + + To use as part, just + + + + + Interface: + + init(options) - init select ID dialog. Following options are supported + { + currentId: '', // Current ID or empty if nothing preselected + objects: null, // All objects that should be shown. It can be empty if connCfg used. + states: null, // All states of objects. It can be empty if connCfg used. If objects are set and no states, states will no be shown. + filter: null, // filter + imgPath: 'lib/css/fancytree/', // Path to images device.png, channel.png and state.png + connCfg: null, // configuration for dialog, ti read objects itself: {socketUrl: socketUrl, socketSession: socketSession} + onSuccess: null, // callback function to be called if user press "Select". Can be overwritten in "show" - function (newId, oldId, newObj) + onChange: null, // called every time the new object selected - function (newId, oldId, newObj) + noDialog: false, // do not make dialog + noMultiselect: false, // do not make multiselect + buttons: null, // array with buttons, that should be shown in last column + // if array is not empty it can has following fields + // [{ + // text: false, // same as jquery button + // icons: { // same as jquery button + // primary: 'ui-icon-gear' + // }, + // click: function (id) { + // // do on click + // }, + // match: function (id) { + // // you have here object "this" pointing to $('button') + // }, + // width: 26, // same as jquery button + // height: 20 // same as jquery button + // }], + panelButtons: null, // array with buttons, that should be shown at the top of dialog (near expand all) + list: false, // tree view or list view + name: null, // name of the dialog to store filter settings + noCopyToClipboard: false, // do not show button for copy to clipboard + root: null, // root node, e.g. "script.js" + useNameAsId: false, // use name of object as ID + noColumnResize: false, // do not allow column resize + firstMinWidth: null, // width if ID column, default 400 + showButtonsForNotExistingObjects: false, + webServer: null, // link to webserver, by default ":8082" + texts: { + select: 'Select', + cancel: 'Cancel', + all: 'All', + id: 'ID', + name: 'Name', + role: 'Role', + type: 'Type', + room: 'Room', + 'function': 'Function', + enum: 'Members', + value: 'Value', + selectid: 'Select ID', + from: 'From', + lc: 'Last changed', + ts: 'Time stamp', + ack: 'Acknowledged', + expand: 'Expand all nodes', + collapse: 'Collapse all nodes', + refresh: 'Rebuild tree', + edit: 'Edit', + ok: 'Ok', + push: 'Trigger event' + wait: 'Processing...', + list: 'Show list view', + tree: 'Show tree view', + selectAll: 'Select all', + unselectAll: 'Unselect all', + invertSelection: 'Invert selection', + copyToClipboard: 'Copy to clipboard', + expertMode: 'Toggle expert mode' + }, + columns: ['image', 'name', 'type', 'role', 'enum', 'room', 'function', 'value', 'button'], + // some elements of columns could be an object {name: field, data: function (id, name){}, title: function (id, name) {}} + widths: null, // array with width for every column + editEnd: null, // function (id, newValues) for edit lines (only id and name can be edited) + editStart: null, // function (id, $inputs) called after edit start to correct input fields (inputs are jquery objects), + zindex: null, // z-index of dialog or table + customButtonFilter: null, // if in the filter over the buttons some specific button must be shown. It has type like {icons:{primary: 'ui-icon-close'}, text: false, callback: function ()} + expertModeRegEx: null // list of regex with objects, that will be shown only in expert mode, like /^system\.|^iobroker\.|^_|^[\w-]+$|^enum\.|^[\w-]+\.admin/ + quickEdit: null, // list of fields with edit on click. Elements can be just names from standard list or objects like: + // {name: 'field', options: {a1: 'a111_Text', a2: 'a22_Text'}}, options can be a function (id, name), that give back such an object + quickEditCallback: null // function (id, attr, newValue, oldValue) + } + + show(currentId, filter, callback) - all arguments are optional if set by "init" + + clear() - clear object tree to read and build anew (used only if objects set by "init") + + getInfo(id) - get information about ID + + getTreeInfo(id) - get {id, parent, children, object} + + state(id, val) - update states in tree + + object(id, obj) - update object info in tree + + reinit() - draw tree anew + */ +(function ($) { + 'use strict'; + + if ($.fn.selectId) return; + + var instance = 0; + + function formatDate(dateObj) { + //return dateObj.getFullYear() + '-' + + // ('0' + (dateObj.getMonth() + 1).toString(10)).slice(-2) + '-' + + // ('0' + (dateObj.getDate()).toString(10)).slice(-2) + ' ' + + // ('0' + (dateObj.getHours()).toString(10)).slice(-2) + ':' + + // ('0' + (dateObj.getMinutes()).toString(10)).slice(-2) + ':' + + // ('0' + (dateObj.getSeconds()).toString(10)).slice(-2); + // Following implementation is 5 times faster + if (!dateObj) return ''; + + var text = dateObj.getFullYear(); + var v = dateObj.getMonth() + 1; + if (v < 10) { + text += '-0' + v; + } else { + text += '-' + v; + } + + v = dateObj.getDate(); + if (v < 10) { + text += '-0' + v; + } else { + text += '-' + v; + } + + v = dateObj.getHours(); + if (v < 10) { + text += ' 0' + v; + } else { + text += ' ' + v; + } + v = dateObj.getMinutes(); + if (v < 10) { + text += ':0' + v; + } else { + text += ':' + v; + } + + v = dateObj.getSeconds(); + if (v < 10) { + text += ':0' + v; + } else { + text += ':' + v; + } + + v = dateObj.getMilliseconds(); + if (v < 10) { + text += '.00' + v; + } else if (v < 100) { + text += '.0' + v; + } else { + text += '.' + v; + } + + return text; + } + + function filterId(data, id) { + if (data.rootExp) { + if (!data.rootExp.test(id)) return false; + } + + if (data.filter) { + if (data.filter.type && data.filter.type !== data.objects[id].type) return false; + + if (data.filter.common && data.filter.common.custom) { + if (!data.objects[id].common) return false; + // todo: remove history sometime 09.2016 + var custom = data.objects[id].common.custom || data.objects[id].common.history; + + if (!custom) return false; + if (data.filter.common.custom === true) { + return true; + } else { + if (!custom[data.filter.common.custom]) return false; + } + } + } + return true; + } + + function getAllStates(data) { + var objects = data.objects; + var isType = data.columns.indexOf('type') !== -1; + var isRoom = data.columns.indexOf('room') !== -1; + var isFunc = data.columns.indexOf('function') !== -1; + var isRole = data.columns.indexOf('role') !== -1; + var isHist = data.columns.indexOf('button') !== -1; + + data.tree = {title: '', children: [], count: 0, root: true}; + data.roomEnums = []; + data.funcEnums = []; + + for (var id in objects) { + + if (isRoom && objects[id].type === 'enum' && data.regexEnumRooms.test(id)) data.roomEnums.push(id); + if (isFunc && objects[id].type === 'enum' && data.regexEnumFuncs.test(id)) data.funcEnums.push(id); + if ((isRoom || isFunc) && objects[id].enums) { + for (var e in objects[id].enums) { + if (isRoom && data.regexEnumRooms.test(e)) { + if (data.roomEnums.indexOf(e) === -1) data.roomEnums.push(e); + + if (!objects[e]) { + objects[e] = { + _id: e, + common: { + name: objects[id].enums[e], + members: [id] + } + }; + } else if (objects[e].common.members.indexOf(id) === -1) { + objects[e].common.members.push(id); + } + } else if (isFunc && data.regexEnumFuncs.test(e)) { + if (data.funcEnums.indexOf(e) === -1) data.funcEnums.push(e); + if (!objects[e]) { + objects[e] = { + _id: e, + common: { + name: objects[id].enums[e], + members: [id] + } + }; + } else if (objects[e].common.members.indexOf(id) === -1) { + objects[e].common.members.push(id); + } + } + } + } + + if (isType && objects[id].type && data.types.indexOf(objects[id].type) === -1) data.types.push(objects[id].type); + + if (isRole && objects[id].common && objects[id].common.role) { + try { + var parts = objects[id].common.role.split('.'); + var role = ''; + for (var u = 0; u < parts.length; u++) { + role += (role ? '.' : '') + parts[u]; + if (data.roles.indexOf(role) === -1) data.roles.push(role); + } + } catch (e) { + console.error('Cannot parse role "' + objects[id].common.role + '" by ' + id); + } + } + if (isHist && objects[id].type === 'instance' && (objects[id].common.type === 'storage' || objects[id].common.supportCustoms)) { + var h = id.substring('system.adapter.'.length); + if (data.histories.indexOf(h) === -1) data.histories.push(h); + } + + // ignore system objects in expert mode + if (data.expertModeRegEx && !data.expertMode && data.expertModeRegEx.test(id)) continue; + + if (!filterId(data, id)) continue; + + treeInsert(data, id, data.currentId === id); + + if (objects[id].enums) { + for (var e in objects[id].enums) { + if (objects[e] && + objects[e].common && + objects[e].common.members && + objects[e].common.members.indexOf(id) === -1) { + objects[e].common.members.push(id); + } + } + } + } + data.inited = true; + data.roles.sort(); + data.types.sort(); + data.roomEnums.sort(); + data.funcEnums.sort(); + data.histories.sort(); + } + + function treeSplit(data, id) { + if (!id) return null; + if (data.root) { + id = id.substring(data.root.length); + } + + var parts = id.split('.'); + if (data.regexSystemAdapter.test(id)) { + if (parts.length > 3) { + parts[0] = 'system.adapter.' + parts[2] + '.' + parts[3]; + parts.splice(1, 3); + } else { + parts[0] = 'system.adapter.' + parts[2]; + parts.splice(1, 2); + } + } else if (data.regexSystemHost.test(id)) { + parts[0] = 'system.host.' + parts[2]; + parts.splice(1, 2); + } else if (parts.length > 1 && !data.root) { + parts[0] = parts[0] + '.' + parts[1]; + parts.splice(1, 1); + } + + /*if (optimized) { + parts = treeOptimizePath(parts); + }*/ + + return parts; + } + + function _deleteTree(node, deletedNodes) { + if (node.parent) { + if (deletedNodes && node.id) deletedNodes.push(node); + var p = node.parent; + if (p.children.length <= 1) { + _deleteTree(node.parent); + } else { + for (var z = 0; z < p.children.length; z++) { + if (node.key === p.children[z].key) { + p.children.splice(z, 1); + break; + } + } + } + } else { + //error + } + } + + function deleteTree(data, id, deletedNodes) { + var node = findTree(data, id); + if (!node) { + console.log('Id ' + id + ' not found'); + return; + } + _deleteTree(node, deletedNodes); + } + + function findTree(data, id) { + return _findTree(data.tree, treeSplit(data, id, false), 0); + } + function _findTree(tree, parts, index) { + var num = -1; + for (var j = 0; j < tree.children.length; j++) { + if (tree.children[j].title === parts[index]) { + num = j; + break; + } + if (tree.children[j].title > parts[index]) break; + } + + if (num === -1) return null; + + if (parts.length - 1 === index) { + return tree.children[num]; + } else { + return _findTree(tree.children[num], parts, index + 1); + } + } + + function treeInsert(data, id, isExpanded, addedNodes) { + return _treeInsert(data.tree, data.list ? [id] : treeSplit(data, id, false), id, 0, isExpanded, addedNodes, data); + } + function _treeInsert(tree, parts, id, index, isExpanded, addedNodes, data) { + index = index || 0; + + if (!parts) { + console.error('Empty object ID!'); + return; + } + + var num = -1; + var j; + for (j = 0; j < tree.children.length; j++) { + if (tree.children[j].title === parts[index]) { + num = j; + break; + } + if (tree.children[j].title > parts[index]) break; + } + + if (num === -1) { + tree.folder = true; + tree.expanded = isExpanded; + + var fullName = ''; + for (var i = 0; i <= index; i++) { + fullName += ((fullName) ? '.' : '') + parts[i]; + } + var obj = { + key: (data.root || '') + fullName, + children: [], + title: parts[index], + folder: false, + expanded: false, + parent: tree + }; + if (j === tree.children.length) { + num = tree.children.length; + tree.children.push(obj); + } else { + num = j; + tree.children.splice(num, 0, obj); + } + if (addedNodes) { + addedNodes.push(tree.children[num]); + } + } + if (parts.length - 1 === index) { + tree.children[num].id = id; + } else { + tree.children[num].expanded = tree.children[num].expanded || isExpanded; + _treeInsert(tree.children[num], parts, id, index + 1, isExpanded, addedNodes, data); + } + } + + function showActive($dlg, scrollIntoView) { + var data = $dlg.data('selectId'); + // Select current element + if (data.selectedID) { + data.$tree.fancytree('getTree').visit(function (node) { + if (node.key === data.selectedID) { + try { + node.setActive(); + node.makeVisible({scrollIntoView: scrollIntoView || false}); + } catch (err) { + console.error(err); + } + return false; + } + }); + } + } + + function syncHeader($dlg) { + // read width of data.$tree and set the same width for header + var data = $dlg.data('selectId'); + var $header = $('#selectID_header_' + data.instance); + var thDest = $header.find('>colgroup>col'); //if table headers are specified in its semantically correct tag, are obtained + var thSrc = data.$tree.find('>thead>tr>th'); + for (var i = 1; i < thSrc.length; i++) { + $(thDest[i]).attr('width', $(thSrc[i]).width()); + } + } + + function findRoomsForObject(data, id, withParentInfo, rooms) { + rooms = rooms || []; + for (var i = 0; i < data.roomEnums.length; i++) { + if (data.objects[data.roomEnums[i]].common.members.indexOf(id) !== -1 && + rooms.indexOf(data.objects[data.roomEnums[i]].common.name) === -1) { + if (!withParentInfo) { + rooms.push(data.objects[data.roomEnums[i]].common.name); + } else { + rooms.push({name: data.objects[data.roomEnums[i]].common.name, origin: id}); + } + } + } + var parts = id.split('.'); + parts.pop(); + id = parts.join('.'); + if (data.objects[id]) findRoomsForObject(data, id, withParentInfo, rooms); + + return rooms; + } + + function findRoomsForObjectAsIds(data, id, rooms) { + rooms = rooms || []; + for (var i = 0; i < data.roomEnums.length; i++) { + if (data.objects[data.roomEnums[i]].common.members.indexOf(id) !== -1 && + rooms.indexOf(data.roomEnums[i]) === -1) { + rooms.push(data.roomEnums[i]); + } + } + return rooms; + } + + function findFunctionsForObject(data, id, withParentInfo, funcs) { + funcs = funcs || []; + for (var i = 0; i < data.funcEnums.length; i++) { + if (data.objects[data.funcEnums[i]].common.members.indexOf(id) !== -1 && + funcs.indexOf(data.objects[data.funcEnums[i]].common.name) === -1) { + if (!withParentInfo) { + funcs.push(data.objects[data.funcEnums[i]].common.name); + } else { + funcs.push({name: data.objects[data.funcEnums[i]].common.name, origin: id}); + } + } + } + var parts = id.split('.'); + parts.pop(); + id = parts.join('.'); + if (data.objects[id]) findFunctionsForObject(data, id, withParentInfo, funcs); + + return funcs; + } + + function findFunctionsForObjectAsIds(data, id, funcs) { + funcs = funcs || []; + for (var i = 0; i < data.funcEnums.length; i++) { + if (data.objects[data.funcEnums[i]].common.members.indexOf(id) !== -1 && + funcs.indexOf(data.funcEnums[i]) === -1) { + funcs.push(data.funcEnums[i]); + } + } + + return funcs; + } + + function clippyCopy(e) { + var $temp = $(''); + //$('body').append($temp); + $(this).append($temp); + $temp.val($(this).parent().data('clippy')).select(); + document.execCommand('copy'); + $temp.remove(); + e.preventDefault(); + e.stopPropagation(); + } + + function clippyShow(e) { + if ($(this).hasClass('clippy')) { + var text = ''; + + $(this).append(text); + $(this).find('.clippy-button').click(clippyCopy); + } + } + + function clippyHide(e) { + $(this).find('.clippy-button').remove(); + } + + function installColResize(data, $dlg) { + if (data.noColumnResize || !$.fn.colResizable) return; + + var data = $dlg.data('selectId'); + if (data.$tree.is(':visible')) { + data.$tree.colResizable({ + liveDrag: true, + onResize: function (event) { + syncHeader($dlg); + } + }); + } else { + setTimeout(function () { + installColResize(data, $dlg); + }, 400) + } + } + + function getStates(data, id) { + var states; + if (data.objects[id] && + data.objects[id].common && + data.objects[id].common.states) { + states = data.objects[id].common.states; + } + if (states) { + if (typeof states === 'string' && states[0] === '{') { + try { + states = JSON.parse(states); + } catch (ex) { + console.error('Cannot parse states: ' + states); + states = null; + } + } else + // if odl format val1:text1;val2:text2 + if (typeof states === 'string') { + var parts = states.split(';'); + states = {}; + for (var p = 0; p < parts.length; p++) { + var s = parts[p].split(':'); + states[s[0]] = s[1]; + } + } + } + return states; + } + + function onQuickEditField(e) { + var $this = $(this); + var id = $this.data('id'); + var attr = $this.data('name'); + var data = $this.data('selectId'); + var type = $this.data('type'); + var clippy = $this.hasClass('clippy'); + var options = $this.data('options'); + var oldVal = $this.data('old-value'); + var states = null; + + if (clippy) $this.removeClass('clippy'); + + $this.unbind('click').removeClass('select-id-quick-edit').css('position', 'relative'); + + var css = 'cursor: pointer; position: absolute;width: 16px; height: 16px; top: 2px; border-radius: 6px; z-index: 3; background-color: lightgray'; + if (type === 'boolean') { + type = 'checkbox'; + } else { + type = 'text'; + } + var text; + + if (attr === 'value') { + states = getStates(data, id); + if (states) { + text = ''; + } + } else if (attr === 'room') { + states = findRoomsForObjectAsIds(data, id) || []; + text = ''; + } else if (attr === 'function') { + states = findFunctionsForObjectAsIds(data, id) || []; + text = ''; + } else if (options) { + if (typeof options === 'function') { + states = options(id, attr); + } else { + states = options; + } + if (states) { + text = ''; + } else if (states === false) { + return; + } + } + text = text || ''; + + var timeout = null; + + $this.html(text + + '
' + + '
'); + + var $input = (attr === 'function' || attr === 'room' || states) ? $this.find('select') : $this.find('input'); + + if (attr === 'room' || attr === 'function') { + $input.multiselect({ + autoOpen: true, + close: function () { + $input.trigger('blur'); + } + }); + } else if (attr === 'role') { + $input.autocomplete({ + minLength: 0, + source: data.roles + }).on('focus', function () { + $(this).autocomplete('search', ''); + }); + } + + $this.find('.select-id-quick-edit-cancel').click(function (e) { + if (timeout) clearTimeout(timeout); + timeout = null; + e.preventDefault(); + e.stopPropagation(); + var old = $this.data('old-value'); + if (old === undefined) old = ''; + $this.html(old).click(onQuickEditField).addClass('select-id-quick-edit'); + if (clippy) $this.addClass('clippy'); + }); + + $this.find('.select-id-quick-edit-ok').click(function () { + var _$input = (attr === 'function' || attr === 'room' || states) ? $this.find('select') : $this.find('input'); + _$input.trigger('blur'); + }); + if (type === 'checkbox') { + $input.prop('checked', oldVal); + } else { + if (attr !== 'room' && attr !== 'function') $input.val(oldVal); + } + + $input.blur(function () { + if (timeout) clearTimeout(timeout); + timeout = setTimeout(function () { + var _oldText = $this.data('old-value'); + var val = $(this).attr('type') === 'checkbox' ? $(this).prop('checked') : $(this).val(); + if ((attr === 'room' || attr === 'function') && !val) val = []; + + if (attr === 'value' || JSON.stringify(val) !== JSON.stringify(_oldText)) { + data.quickEditCallback(id, attr, val, _oldText); + + _oldText = '' + _oldText + ''; + } + if (clippy) $this.addClass('clippy'); + $this.html(_oldText).click(onQuickEditField).addClass('select-id-quick-edit'); + }.bind(this), 100); + }).keyup(function (e) { + if (e.which === 13) $(this).trigger('blur'); + if (e.which === 27) { + if (clippy) $this.addClass('clippy'); + var old = $this.data('old-value'); + if (old === undefined) old = ''; + $this.html(old).click(onQuickEditField).addClass('select-id-quick-edit'); + } + }); + + if (typeof e === 'object') { + e.preventDefault(); + e.stopPropagation(); + } + + setTimeout(function () { + $input.focus().select(); + }, 100); + } + + function quality2text(q) { + if (!q) return 'ok'; + var custom = q & 0xFFFF0000; + var text = ''; + if (q & 0x40) text += 'device'; + if (q & 0x80) text += 'sensor'; + if (q & 0x01) text += ' bad'; + if (q & 0x02) text += ' not connected'; + if (q & 0x04) text += ' error'; + + return text + (custom ? '|0x' + (custom >> 16).toString(16).toUpperCase() : '') + ' [0x' + q.toString(16).toUpperCase() + ']'; + } + + function initTreeDialog($dlg) { + var c; + var data = $dlg.data('selectId'); + //var noStates = (data.objects && !data.states); + var multiselect = (!data.noDialog && !data.noMultiselect); + + // load expert mode flag + if (typeof Storage !== 'undefined' && data.name && data.expertModeRegEx) { + data.expertMode = window.localStorage.getItem(data.name + '-expert'); + data.expertMode = (data.expertMode === true || data.expertMode === 'true'); + } + + // Get all states + getAllStates(data); + + if (!data.noDialog && !data.buttonsDlg) { + data.buttonsDlg = [ + { + id: data.instance + '-button-ok', + text: data.texts.select, + click: function () { + var _data = $dlg.data('selectId'); + if (_data && _data.onSuccess) _data.onSuccess(_data.selectedID, _data.currentId, _data.objects[_data.selectedID]); + _data.currentId = _data.selectedID; + storeSettings(data); + $dlg.dialog('close'); + } + }, + { + id: data.instance + '-button-cancel', + text: data.texts.cancel, + click: function () { + storeSettings(data); + $(this).dialog('close'); + } + } + ]; + + $dlg.dialog({ + autoOpen: false, + modal: true, + width: '90%', + close: function () { + storeSettings(data); + }, + height: 500, + buttons: data.buttonsDlg + }); + if (data.zindex !== null) { + $('div[aria-describedby="' + $dlg.attr('id') + '"]').css({'z-index': data.zindex}) + } + } + + // Store current filter + var filter = {ID: $('#filter_ID_' + data.instance).val()}; + for (var u = 0; u < data.columns.length; u++) { + var name = data.columns[u]; + if (typeof name === 'object') name = name.name; + filter[name] = $('#filter_' + name + '_' + data.instance).val(); + } + + var textRooms; + if (data.columns.indexOf('room') !== -1) { + textRooms = ''; + } else { + if (data.rooms) delete data.rooms; + if (data.roomsColored) delete data.roomsColored; + } + + var textFuncs; + if (data.columns.indexOf('function') !== -1) { + textFuncs = ''; + } else { + if (data.funcs) delete data.funcs; + if (data.funcsColored) delete data.funcsColored; + } + + var textRoles; + if (data.columns.indexOf('role') !== -1) { + textRoles = ''; + } + + var textTypes; + if (data.columns.indexOf('type') !== -1) { + textTypes = ''; + } + + var text = '
'; + text += ''; + text += ' '; + text += ' '; + + for (c = 0; c < data.columns.length; c++) { + var name = data.columns[c]; + if (typeof name === 'object') name = name.name; + if (name === 'image') { + text += ''; + } else if (name === 'name') { + text += ''; + } else if (name === 'type') { + text += ''; + } else if (name === 'role') { + text += ''; + } else if (name === 'room') { + text += ''; + } else if (name === 'function') { + text += ''; + } else if (name === 'value') { + text += ''; + } else if (name === 'button') { + text += ''; + } else if (name === 'enum') { + text += ''; + } else { + text += ''; + } + } + + text += ' '; // TODO calculate width of scroll bar + text += ' '; + text += ' '; + text += ' '; + + for (c = 0; c < data.columns.length; c++) { + var _name = data.columns[c]; + if (typeof _name === 'object') _name = name.name; + text += ''; + } + + text += ''; + text += ' '; + text += ' '; + text += ' '; + text += ' '; + + for (c = 0; c < data.columns.length; c++) { + var name = data.columns[c]; + if (typeof name === 'object') name = name.name; + if (name === 'image') { + text += ''; + } else if (name === 'name' || name === 'value' || name === 'enum') { + text += ''; + } else if (name === 'type') { + text += ''; + } else if (name== 'role') { + text += ''; + } else if (name === 'room') { + text += ''; + } else if (name === 'function') { + text += ''; + } else if (name === 'button') { + text += ''; + } else { + text += ''; + } + } + + text += ' '; + text += ' '; + text += '
'; + text += ''; + text += ''; + text += ''; + text += ''; + if (data.filter && data.filter.type === 'state' && multiselect) { + text += ''; + text += ''; + text += ''; + } + if (data.expertModeRegEx) { + text += ''; + } + + if (data.panelButtons) { + text += ''; + for (c = 0; c < data.panelButtons.length; c++) { + text += ''; + } + } + + text += '
  ' + data.texts.id + '
' + (data.texts[_name] || '') + '
' + textTypes + '' + textRoles + '' + textRooms + '' + textFuncs + ''; + if (data.customButtonFilter) { + var t = ''; + + text += '' + '
' + t + '
' + } + text += '
'; + + //text += '
'; + text += '
'; + text +=' '; + text += ' '; + text += ' '; + text += ' '; + + for (c = 0; c < data.columns.length; c++) { + var name = data.columns[c]; + if (typeof name === 'object') name = name.name; + if (name === 'image') { + text += ''; + } else if (name === 'name') { + text += ''; + } else if (name === 'type') { + text += ''; + } else if (name === 'role') { + text += ''; + } else if (name === 'room') { + text += ''; + } else if (name === 'function') { + text += ''; + } else if (name === 'value') { + text += ''; + } else if (name === 'button') { + text += ''; + } else if (name === 'enum') { + text += ''; + } else { + text += ''; + } + } + + text += ' '; + text += ' '; + text += ' '; + for (c = 0; c < data.columns.length; c++) { + text += ''; + } + text += ''; + text += ' '; + text += ' '; + text += ' '; + text += '
'; + + $dlg.html(text); + + data.$tree = $('#selectID_' + data.instance); + data.$tree[0]._onChange = data.onSuccess || data.onChange; + + var foptions = { + titlesTabbable: true, // Add all node titles to TAB chain + quicksearch: true, + source: data.tree.children, + extensions: ["table", "gridnav", "filter", "themeroller"], + checkbox: multiselect, + table: { + indentation: 20, + nodeColumnIdx: 1 + }, + gridnav: { + autofocusInput: false, + handleCursorKeys: true + }, + filter: { + mode: 'hide', + autoApply: true + }, + activate: function (event, data) { + // A node was activated: display its title: + // On change + //var $dlg = $('#' + data.instance + '-dlg'); + if (!multiselect) { + var _data = $dlg.data('selectId'); + var newId = data.node.key; + + if (_data.onChange) _data.onChange(newId, _data.selectedID, _data.objects[newId]); + + _data.selectedID = newId; + if (!_data.noDialog) { + // Set title of dialog box + if (_data.objects[newId] && _data.objects[newId].common && _data.objects[newId].common.name) { + $dlg.dialog('option', 'title', _data.texts.selectid + ' - ' + (_data.objects[newId].common.name || ' ')); + } else { + $dlg.dialog('option', 'title', _data.texts.selectid + ' - ' + (newId || ' ')); + } + // Enable/ disable "Select" button + if (_data.objects[newId]) { // && _data.objects[newId].type === 'state') { + $('#' + _data.instance + '-button-ok').removeClass('ui-state-disabled'); + } else { + $('#' + _data.instance + '-button-ok').addClass('ui-state-disabled'); + } + + } + } + }, + select: function(event, data) { + var _data = $dlg.data('selectId'); + var newIds = []; + var selectedNodes = data.tree.getSelectedNodes(); + for (var i = 0; i < selectedNodes.length; i++) { + newIds.push(selectedNodes[i].key); + } + + if (_data.onChange) _data.onChange(newIds, _data.selectedID); + + _data.selectedID = newIds; + + // Enable/ disable "Select" button + if (newIds.length > 0) { + $('#' + _data.instance + '-button-ok').removeClass('ui-state-disabled'); + } else { + $('#' + _data.instance + '-button-ok').addClass('ui-state-disabled'); + } + }, + renderColumns: function (event, _data) { + var node = _data.node; + var $tr = $(node.tr); + var $tdList = $tr.find('>td'); + + var isCommon = data.objects[node.key] && data.objects[node.key].common; + var $firstTD = $tdList.eq(1); + $firstTD.css({'overflow': 'hidden'}); + var base = 2; + + // hide checkbox if only states should be selected + if (data.filter && data.filter.type === 'state' && (!data.objects[node.key] || data.objects[node.key].type !== 'state')) { + $firstTD.find('.fancytree-checkbox').hide(); + } + + // special case for javascript scripts + if (data.objects[node.key] && (node.key.match(/^script\.js\./) || node.key.match(/^enum\.[\w\d_-]+$/))) { + if (data.objects[node.key].type !== 'script') { + // force folder icon and change color + if (node.key !== 'script.js.global') { + $firstTD.find('.fancytree-title').css({'font-weight': 'bold', color: '#000080'}); + } else { + $firstTD.find('.fancytree-title').css({'font-weight': 'bold', color: '#078a0c'}); + } + $firstTD.addClass('fancytree-force-folder'); + } + } + + if (!data.noCopyToClipboard) { + $firstTD + .addClass('clippy') + .data('clippy', node.key) + .css({position: 'relative'}) + .data('copyToClipboard', data.texts.copyToClipboard || data.texts.copyTpClipboard) + .mouseenter(clippyShow) + .mouseleave(clippyHide); + } + + if (data.useNameAsId && data.objects[node.key] && data.objects[node.key].common && data.objects[node.key].common.name) { + $firstTD.find('.fancytree-title').html(data.objects[node.key].common.name); + } + var $elem; + var val; + for (var c = 0; c < data.columns.length; c++) { + var name = data.columns[c]; + if (typeof name === 'object') name = name.name; + if (name === 'image') { + var icon = ''; + var alt = ''; + var _id_ = 'system.adapter.' + node.key; + if (data.objects[_id_] && data.objects[_id_].common && data.objects[_id_].common.icon) { + icon = '/adapter/' + data.objects[_id_].common.name + '/' + data.objects[_id_].common.icon; + } else + if (isCommon) { + if (data.objects[node.key].common.icon) { + var instance; + if (data.objects[node.key].type === 'instance') { + icon = '/adapter/' + data.objects[node.key].common.name + '/' + data.objects[node.key].common.icon; + } else if (node.key.match(/^system\.adapter\./)) { + instance = node.key.split('.', 3); + if (data.objects[node.key].common.icon[0] === '/') { + instance[2] += data.objects[node.key].common.icon; + } else { + instance[2] += '/' + data.objects[node.key].common.icon; + } + icon = '/adapter/' + instance[2]; + } else { + instance = node.key.split('.', 2); + if (data.objects[node.key].common.icon[0] === '/') { + instance[0] += data.objects[node.key].common.icon; + } else { + instance[0] += '/' + data.objects[node.key].common.icon; + } + icon = '/adapter/' + instance[0]; + } + } else if (data.objects[node.key].type === 'device') { + icon = data.imgPath + 'device.png'; + alt = 'device'; + } else if (data.objects[node.key].type === 'channel') { + icon = data.imgPath + 'channel.png'; + alt = 'channel'; + } else if (data.objects[node.key].type === 'state') { + icon = data.imgPath + 'state.png'; + alt = 'state'; + } + } + if (icon) { + $tdList.eq(base).html('' + alt + ''); + } else { + $tdList.eq(base).text(''); + } + base++; + } else + if (name === 'name') { + $elem = $tdList.eq(base); + $elem.text(isCommon ? data.objects[node.key].common.name : '').css({overflow: 'hidden', 'white-space': 'nowrap', 'text-overflow': 'ellipsis'}).attr('title', isCommon ? data.objects[node.key].common.name : ''); + if (data.quickEdit && data.objects[node.key] && data.quickEdit.indexOf('name') !== -1) { + $elem.data('old-value', isCommon ? data.objects[node.key].common.name : ''); + $elem.click(onQuickEditField).data('id', node.key).data('name', 'name').data('selectId', data).addClass('select-id-quick-edit'); + } + base++; + } else + if (name === 'type') { + $tdList.eq(base++).text(data.objects[node.key] ? data.objects[node.key].type: ''); + } else + if (name === 'role') { + $elem = $tdList.eq(base); + val = isCommon ? data.objects[node.key].common.role : ''; + $elem.text(val); + + if (data.quickEdit && data.objects[node.key] && data.quickEdit.indexOf('role') !== -1) { + $elem.data('old-value', val); + $elem.click(onQuickEditField).data('id', node.key).data('name', 'role').data('selectId', data).addClass('select-id-quick-edit'); + } + base++; + } else + if (name === 'room') { + $elem = $tdList.eq(base); + // Try to find room + if (data.roomsColored) { + if (!data.roomsColored[node.key]) data.roomsColored[node.key] = findRoomsForObject(data, node.key, true); + val = data.roomsColored[node.key].map(function (e) {return e.name;}).join(', '); + if (data.roomsColored[node.key].length && data.roomsColored[node.key][0].origin !== node.key) { + $elem.css({color: 'gray'}).attr('title', data.roomsColored[node.key][0].origin); + } else { + $elem.css({color: 'inherit'}).attr('title', null); + } + } else { + val = ''; + } + $elem.text(val); + + if (data.quickEdit && data.objects[node.key] && data.quickEdit.indexOf('room') !== -1) { + $elem.data('old-value', val); + $elem.click(onQuickEditField) + .data('id', node.key) + .data('name', 'room') + .data('selectId', data) + .addClass('select-id-quick-edit'); + } + base++; + } else + if (name === 'function') { + $elem = $tdList.eq(base); + // Try to find function + if (data.funcsColored) { + if (!data.funcsColored[node.key]) data.funcsColored[node.key] = findFunctionsForObject(data, node.key, true); + val = data.funcsColored[node.key].map(function (e) {return e.name;}).join(', '); + if (data.funcsColored[node.key].length && data.funcsColored[node.key][0].origin !== node.key) { + $elem.css({color: 'gray'}).attr('title', data.funcsColored[node.key][0].origin); + } else { + $elem.css({color: 'inherit'}).attr('title', null); + } + } else { + val = ''; + } + $elem.text(val); + + if (data.quickEdit && data.objects[node.key] && data.quickEdit.indexOf('function') !== -1) { + $elem.data('old-value', val); + $elem.click(onQuickEditField) + .data('id', node.key) + .data('name', 'function') + .data('selectId', data) + .addClass('select-id-quick-edit'); + } + base++; + } else + if (name === 'value') { + $elem = $tdList.eq(base); + var common = data.objects[node.key] ? data.objects[node.key].common || {} : {}; + if (data.states && (data.states[node.key] || data.states[node.key + '.val'] !== undefined)) { + var $elem = $tdList.eq(base); + var state = data.states[node.key]; + var states = getStates(data, node.key); + if (!state) { + state = { + val: data.states[node.key + '.val'], + ts: data.states[node.key + '.ts'], + lc: data.states[node.key + '.lc'], + from: data.states[node.key + '.from'], + ack: (data.states[node.key + '.ack'] === undefined) ? '' : data.states[node.key + '.ack'], + q: (data.states[node.key + '.q'] === undefined) ? 0 : data.states[node.key + '.q'] + }; + } else { + state = JSON.parse(JSON.stringify(state)); + } + + if (common.role === 'value.time') { + state.val = state.val ? (new Date(state.val)).toString() : state.val; + } + if (states && states[state.val] !== undefined) { + state.val = states[state.val] + '(' + state.val + ')'; + } + + var fullVal; + if (state.val === undefined) { + state.val = ''; + } else { + // if less 2000.01.01 00:00:00 + if (state.ts < 946681200000) state.ts *= 1000; + if (state.lc < 946681200000) state.lc *= 1000; + + if (isCommon && common.unit) state.val += ' ' + common.unit; + fullVal = data.texts.value + ': ' + state.val; + fullVal += '\x0A' + data.texts.ack + ': ' + state.ack; + fullVal += '\x0A' + data.texts.ts + ': ' + (state.ts ? formatDate(new Date(state.ts)) : ''); + fullVal += '\x0A' + data.texts.lc + ': ' + (state.lc ? formatDate(new Date(state.lc)) : ''); + fullVal += '\x0A' + data.texts.from + ': ' + (state.from || ''); + fullVal += '\x0A' + data.texts.quality + ': ' + quality2text(state.q || 0); + } + + $elem.html('' + state.val + '') + .attr('title', fullVal) + .css({position: 'relative'}); + + $elem.css({color: state.ack ? (state.q ? 'orange' : '') : 'red'}); + + if (!data.noCopyToClipboard && data.objects[node.key] && data.objects[node.key].type === 'state' && common.type !== 'file') { + $elem.data('clippy', state.val) + .addClass('clippy') + .data('copyToClipboard', data.texts.copyToClipboard || data.texts.copyTpClipboard) + .mouseenter(clippyShow) + .mouseleave(clippyHide); + } + + } else { + $elem.text('') + .attr('title', '') + .removeClass('clippy'); + } + $elem.dblclick(function (e) { + e.preventDefault(); + }); + + if (data.quickEdit && + data.objects[node.key] && + data.objects[node.key].type === 'state' && + data.quickEdit.indexOf('value') !== -1 && + (data.expertMode || data.objects[node.key].common.write !== false) + ) { + if (data.objects[node.key].common.role === 'button' && !data.expertMode) { + $tdList.eq(base).html(''); + } else + if (!data.objects[node.key].common || data.objects[node.key].common.type !== 'file') { + var val = data.states[node.key]; + val = val ? val.val : ''; + $elem.data('old-value', val).data('type', common.type || typeof val); + + $elem.click(onQuickEditField) + .data('id', node.key) + .data('name', 'value') + .data('selectId', data) + .addClass('select-id-quick-edit'); + } + + $tr.find('.select-button-push[data-id="' + node.key + '"]').button({ + text: false, + icons: { + primary: 'ui-icon-arrowthickstop-1-s' + } + }).click(function () { + var id = $(this).data('id'); + data.quickEditCallback(id, 'value', true); + }).attr('title', data.texts.push).css({width: 26, height: 20}); + } + + if (common.type === 'file') { + data.webServer = data.webServer || (window.location.protocol + '//' + window.location.hostname + ':8082'); + + // link + $elem.html('' + data.webServer + '/state/' + node.key + '') + .attr('title', data.texts.linkToFile); + } + + base++; + } else + if (name === 'button') { + // Show buttons + var text; + if (data.buttons) { + if (data.objects[node.key] || data.showButtonsForNotExistingObjects) { + text = ''; + if (data.editEnd) { + text += '' + + '' + + ''; + } + + for (var j = 0; j < data.buttons.length; j++) { + text += ''; + } + + $tdList.eq(base).html(text); + + for (var p = 0; p < data.buttons.length; p++) { + var btn = $tr.find('.select-button-' + p + '[data-id="' + node.key + '"]').button(data.buttons[p]).click(function () { + var cb = $(this).data('callback'); + if (cb) cb.call($(this), $(this).attr('data-id')); + }).data('callback', data.buttons[p].click).attr('title', data.buttons[p].title || ''); + if (data.buttons[p].width) btn.css({width: data.buttons[p].width}); + if (data.buttons[p].height) btn.css({height: data.buttons[p].height}); + if (data.buttons[p].match) data.buttons[p].match.call(btn, node.key); + } + } else { + $tdList.eq(base).text(''); + } + } else if (data.editEnd) { + text = '' + + '' + + ''; + } + + if (data.editEnd) { + $tr.find('.select-button-edit[data-id="' + node.key + '"]').button({ + text: false, + icons: { + primary:'ui-icon-pencil' + } + }).click(function () { + $(this).data('node').editStart(); + }).attr('title', data.texts.edit).data('node', node).css({width: 26, height: 20}); + + $tr.find('.select-button-ok[data-id="' + node.key + '"]').button({ + text: false, + icons: { + primary: 'ui-icon-check' + } + }).click(function () { + var node = $(this).data('node'); + node.editFinished = true; + node.editEnd(true); + }).attr('title', data.texts.ok).data('node', node).hide().css({width: 26, height: 20}); + + $tr.find('.select-button-cancel[data-id="' + node.key + '"]').button({ + text: false, + icons: { + primary: 'ui-icon-close' + } + }).click(function () { + var node = $(this).data('node'); + node.editFinished = true; + node.editEnd(false); + }).attr('title', data.texts.cancel).data('node', node).hide().css({width: 26, height: 20}); + } + + base++; + } else + if (name === 'enum') { + if (isCommon && data.objects[node.key].common.members && data.objects[node.key].common.members.length > 0) { + if (data.objects[node.key].common.members.length < 4) { + $tdList.eq(base).text('(' + data.objects[node.key].common.members.length + ')' + data.objects[node.key].common.members.join(', ')); + } else { + $tdList.eq(base).text(data.objects[node.key].common.members.length); + } + $tdList.eq(base).attr('title', data.objects[node.key].common.members.join('\x0A')); + } else { + $tdList.eq(base).text(''); + $tdList.eq(base).attr('title', ''); + } + base++; + } else + if (typeof data.columns[c].data === 'function') { + $elem = $tdList.eq(base); + var val = data.columns[c].data(node.key, data.columns[c].name); + var title = ''; + if (data.columns[c].title) title = data.columns[c].title(node.key, data.columns[c].name); + $elem.html(val).attr('title', title); + if (data.quickEdit && data.objects[node.key]) { + for (var q = 0; q < data.quickEdit.length; q++) { + if (data.quickEdit[q] === data.columns[c].name || + data.quickEdit[q].name === data.columns[c].name) { + $elem.data('old-value', val).data('type', typeof val); + + $elem.click(onQuickEditField) + .data('id', node.key) + .data('name', data.columns[c].name) + .data('selectId', data) + .data('options', data.quickEdit[q].options) + .addClass('select-id-quick-edit'); + + break; + } + } + } + base++; + } + } + }, + dblclick: function (event, _data) { + if (data.buttonsDlg && !data.quickEditCallback) { + if (_data && _data.node && !_data.node.folder) { + data.buttonsDlg[0].click(); + } + } else if (data.dblclick) { + var tree = data.$tree.fancytree('getTree'); + + var node = tree.getActiveNode(); + if (node) { + data.dblclick(node.key); + } + } + } + }; + if (data.editEnd) { + foptions.extensions.push('edit'); + foptions.edit = { + triggerStart: ['f2', 'dblclick', 'shift+click', 'mac+enter'], + triggerStop: ['esc'], + beforeEdit: function (event, _data) { + // Return false to prevent edit mode + if (!data.objects[_data.node.key]) return false; + }, + edit: function (event, _data) { + $dlg.find('.select-button-edit[data-id="' + _data.node.key + '"]').hide(); + $dlg.find('.select-button-cancel[data-id="' + _data.node.key + '"]').show(); + $dlg.find('.select-button-ok[data-id="' + _data.node.key + '"]').show(); + $dlg.find('.select-button-custom[data-id="' + _data.node.key + '"]').hide(); + + var node = _data.node; + var $tdList = $(node.tr).find('>td'); + // Editor was opened (available as data.input) + var inputs = {id: _data.input}; + + for (var c = 0; c < data.columns.length; c++) { + var name = data.columns[c]; + if (typeof name === 'object') name = name.name; + + if (name === 'name') { + $tdList.eq(2 + c).html(''); + inputs[name] = $dlg.find('#select_edit_' + name); + } + } + for (var i in inputs) { + inputs[i].keyup(function (e) { + var node; + if (e.which === 13) { + // end edit + node = $(this).data('node'); + node.editFinished = true; + node.editEnd(true); + } else if (e.which === 27) { + // end edit + node = $(this).data('node'); + node.editFinished = true; + node.editEnd(false); + } + }).data('node', node); + } + + if (data.editStart) data.editStart(_data.node.key, inputs); + node.editFinished = false; + }, + beforeClose: function (event, _data) { + // Return false to prevent cancel/save (data.input is available) + return _data.node.editFinished; + }, + save: function (event, _data) { + var editValues = {id: _data.input.val()}; + + for (var c = 0; c < data.columns.length; c++) { + var name = data.columns[c]; + if (typeof name === 'object') name = name.name; + if (name === 'name') { + editValues[name] = $dlg.find('#select_edit_' + name).val(); + } + } + + // Save data.input.val() or return false to keep editor open + if (data.editEnd) data.editEnd(_data.node.key, editValues); + _data.node.render(true); + + // We return true, so ext-edit will set the current user input + // as title + return true; + }, + close: function (event, _data) { + $dlg.find('.select-button-edit[data-id="' + _data.node.key + '"]').show(); + $dlg.find('.select-button-cancel[data-id="' + _data.node.key + '"]').hide(); + $dlg.find('.select-button-ok[data-id="' + _data.node.key + '"]').hide(); + $dlg.find('.select-button-custom[data-id="' + _data.node.key + '"]').show(); + if (_data.node.editFinished !== undefined) delete _data.node.editFinished; + // Editor was removed + if (data.save) { + // Since we started an async request, mark the node as preliminary + $(data.node.span).addClass('pending'); + } + } + }; + } + + data.$tree.fancytree(foptions).on('nodeCommand', function (event, data) { + // Custom event handler that is triggered by keydown-handler and + // context menu: + var refNode; + var tree = $(this).fancytree('getTree'); + var node = tree.getActiveNode(); + + switch (data.cmd) { + case 'moveUp': + node.moveTo(node.getPrevSibling(), 'before'); + node.setActive(); + break; + case 'moveDown': + node.moveTo(node.getNextSibling(), 'after'); + node.setActive(); + break; + case 'indent': + refNode = node.getPrevSibling(); + node.moveTo(refNode, 'child'); + refNode.setExpanded(); + node.setActive(); + break; + case 'outdent': + node.moveTo(node.getParent(), 'after'); + node.setActive(); + break; + /*case 'copy': + CLIPBOARD = { + mode: data.cmd, + data: node.toDict(function (n) { + delete n.key; + }) + }; + break; + case 'clear': + CLIPBOARD = null; + break;*/ + default: + alert('Unhandled command: ' + data.cmd); + return; + } + + }).on('keydown', function (e) { + var c = String.fromCharCode(e.which); + var cmd = null; + + if (e.which === 'c' && e.ctrlKey) { + cmd = 'copy'; + }else if (e.which === $.ui.keyCode.UP && e.ctrlKey) { + cmd = 'moveUp'; + } else if (e.which === $.ui.keyCode.DOWN && e.ctrlKey) { + cmd = 'moveDown'; + } else if (e.which === $.ui.keyCode.RIGHT && e.ctrlKey) { + cmd = 'indent'; + } else if (e.which === $.ui.keyCode.LEFT && e.ctrlKey) { + cmd = 'outdent'; + } + if (cmd) { + $(this).trigger('nodeCommand', {cmd: cmd}); + return false; + } + }); + + function customFilter(node) { + if (node.parent && node.parent.match) return true; + + // Read all filter settings + if (data.filterVals === null) { + data.filterVals = {length: 0}; + var value = $('#filter_ID_' + data.instance).val().toLowerCase(); + if (value) { + data.filterVals.ID = value; + data.filterVals.length++; + } + + for (var c = 0; c < data.columns.length; c++) { + var name = data.columns[c]; + if (typeof name === 'object') name = name.name; + if (name === 'image') { + //continue; + } else if (name === 'role' || name === 'type' || name === 'room' || name === 'function') { + value = $('#filter_' + name + '_' + data.instance).val(); + if (value) { + data.filterVals[name] = value; + data.filterVals.length++; + } + } else { + value = $('#filter_' + name + '_' + data.instance).val(); + if (value) { + value = value.toLowerCase(); + data.filterVals[name] = value; + data.filterVals.length++; + } + } + } + // if no clear "close" event => store on change + if (data.noDialog) storeSettings(data); + } + + var isCommon = null; + + for (var f in data.filterVals) { + if (f === 'length') continue; + + if (isCommon === null) isCommon = data.objects[node.key] && data.objects[node.key].common; + + if (f === 'ID') { + if (node.key.toLowerCase().indexOf(data.filterVals[f]) === -1) return false; + } else + if (f === 'name' || f === 'enum') { + if (!isCommon || data.objects[node.key].common[f] === undefined || data.objects[node.key].common[f].toLowerCase().indexOf(data.filterVals[f]) === -1) return false; + } else + if (f === 'role') { + if (!isCommon || data.objects[node.key].common[f] === undefined || data.objects[node.key].common[f].indexOf(data.filterVals[f]) === -1) return false; + } else + if (f === 'type') { + if (!data.objects[node.key] || data.objects[node.key][f] === undefined || data.objects[node.key][f] !== data.filterVals[f]) return false; + } else + if (f === 'value') { + if (!data.states[node.key] || data.states[node.key].val === undefined || data.states[node.key].val === null || data.states[node.key].val.toString().toLowerCase().indexOf(data.filterVals[f]) === -1) return false; + } else + if (f === 'button') { + if (data.filterVals[f] === 'true') { + if (!isCommon || !data.objects[node.key].common.custom || data.objects[node.key].common.custom.enabled === false) return false; + } else if (data.filterVals[f] === 'false') { + if (!isCommon || data.objects[node.key].type !== 'state' || data.objects[node.key].common.custom) return false; + } else if (data.filterVals[f]) { + if (!isCommon || !data.objects[node.key].common.custom || !data.objects[node.key].common.custom[data.filterVals[f]]) return false; + } + } else + if (f === 'room') { + if (!data.objects[node.key]) return false; + + // Try to find room + if (!data.rooms[node.key]) data.rooms[node.key] = findRoomsForObject(data, node.key); + if (data.rooms[node.key].indexOf(data.filterVals[f]) === -1) return false; + } else + if (f === 'function') { + if (!data.objects[node.key]) return false; + + // Try to find functions + if (!data.funcs[node.key]) data.funcs[node.key] = findFunctionsForObject(data, node.key); + if (data.funcs[node.key].indexOf(data.filterVals[f]) === -1) return false; + } + } + + return true; + } + + $('.filter_' + data.instance).change(function () { + data.filterVals = null; + $('#process_running_' + data.instance).show(); + data.$tree.fancytree('getTree').filterNodes(customFilter, false); + $('#process_running_' + data.instance).hide(); + }).keyup(function () { + var tree = data.$tree[0]; + if (tree._timer) tree._timer = clearTimeout(tree._timer); + + var that = this; + tree._timer = setTimeout(function () { + $(that).trigger('change'); + }, 200); + }); + + $('.filter_btn_' + data.instance).button({icons: {primary: 'ui-icon-close'}, text: false}).css({width: 18, height: 18}).click(function () { + $('#' + $(this).attr('data-id')).val('').trigger('change'); + }); + + $('#btn_collapse_' + data.instance).button({icons: {primary: 'ui-icon-folder-collapsed'}, text: false}).css({width: 18, height: 18}).click(function () { + $('#process_running_' + data.instance).show(); + setTimeout(function () { + data.$tree.fancytree('getRootNode').visit(function (node) { + if (!data.filterVals.length || node.match || node.subMatch) node.setExpanded(false); + }); + $('#process_running_' + data.instance).hide(); + }, 100); + }).attr('title', data.texts.collapse); + + $('#btn_expand_' + data.instance).button({icons: {primary: 'ui-icon-folder-open'}, text: false}).css({width: 18, height: 18}).click(function () { + $('#process_running_' + data.instance).show(); + setTimeout(function () { + data.$tree.fancytree('getRootNode').visit(function (node) { + if (!data.filterVals.length || node.match || node.subMatch) + node.setExpanded(true); + }); + $('#process_running_' + data.instance).hide(); + }, 100); + }).attr('title', data.texts.expand); + + $('#btn_list_' + data.instance).button({icons: {primary: 'ui-icon-grip-dotted-horizontal'}, text: false}).css({width: 18, height: 18}).click(function () { + $('#process_running_' + data.instance).show(); + data.list = !data.list; + if (data.list) { + $('#btn_list_' + data.instance).addClass('ui-state-error'); + $('#btn_expand_' + data.instance).hide(); + $('#btn_collapse_' + data.instance).hide(); + $(this).attr('title', data.texts.list); + } else { + $('#btn_list_' + data.instance).removeClass('ui-state-error'); + $('#btn_expand_' + data.instance).show(); + $('#btn_collapse_' + data.instance).show(); + $(this).attr('title', data.texts.tree); + } + $('#process_running_' + data.instance).show(); + setTimeout(function () { + data.inited = false; + initTreeDialog(data.$dlg); + $('#process_running_' + data.instance).hide(); + }, 200); + }).attr('title', data.texts.tree); + + if (data.list) { + $('#btn_list_' + data.instance).addClass('ui-state-error'); + $('#btn_expand_' + data.instance).hide(); + $('#btn_collapse_' + data.instance).hide(); + $('#btn_list_' + data.instance).attr('title', data.texts.list); + } + + $('#btn_refresh_' + data.instance).button({icons: {primary: 'ui-icon-refresh'}, text: false}).css({width: 18, height: 18}).click(function () { + $('#process_running_' + data.instance).show(); + setTimeout(function () { + data.inited = false; + initTreeDialog(data.$dlg); + $('#process_running_' + data.instance).hide(); + }, 100); + }).attr('title', data.texts.refresh); + + $('#btn_select_all_' + data.instance).button({icons: {primary: 'ui-icon-circle-check'}, text: false}).css({width: 18, height: 18}).click(function () { + $('#process_running_' + data.instance).show(); + setTimeout(function () { + data.$tree.fancytree('getRootNode').visit(function (node) { + if (!data.filterVals.length || node.match || node.subMatch) { + // hide checkbox if only states should be selected + if (data.objects[node.key] && data.objects[node.key].type === 'state') { + node.setSelected(true); + } + } + }); + $('#process_running_' + data.instance).hide(); + }, 100); + }).attr('title', data.texts.selectAll); + + if (data.expertModeRegEx) { + $('#btn_expert_' + data.instance).button({icons: {primary: 'ui-icon-person'}, text: false}).css({width: 18, height: 18}).click(function () { + $('#process_running_' + data.instance).show(); + + data.expertMode = !data.expertMode; + if (data.expertMode) { + $('#btn_expert_' + data.instance).addClass('ui-state-error'); + } else { + $('#btn_expert_' + data.instance).removeClass('ui-state-error'); + } + storeSettings(data, true); + + setTimeout(function () { + data.inited = false; + initTreeDialog(data.$dlg); + $('#process_running_' + data.instance).hide(); + }, 200); + }).attr('title', data.texts.expertMode); + + if (data.expertMode) $('#btn_expert_' + data.instance).addClass('ui-state-error'); + } + + $('#btn_unselect_all_' + data.instance).button({icons: {primary: 'ui-icon-circle-close'}, text: false}).css({width: 18, height: 18}).click(function () { + $('#process_running_' + data.instance).show(); + setTimeout(function () { + data.$tree.fancytree('getRootNode').visit(function (node) { + node.setSelected(false); + }); + $('#process_running_' + data.instance).hide(); + }, 100); + }).attr('title', data.texts.unselectAll); + + $('#btn_invert_selection_' + data.instance).button({icons: {primary: 'ui-icon-transferthick-e-w'}, text: false}).css({width: 18, height: 18}).click(function () { + $('#process_running_' + data.instance).show(); + setTimeout(function () { + data.$tree.fancytree('getRootNode').visit(function (node) { + if (!data.filterVals.length || node.match || node.subMatch){ + if (data.objects[node.key] && data.objects[node.key].type === 'state') { + node.toggleSelected(); + } + } + }); + $('#process_running_' + data.instance).hide(); + }, 100); + }).attr('title', data.texts.invertSelection); + + for (var f in filter) { + try { + if (f) $('#filter_' + f + '_' + data.instance).val(filter[f]).trigger('change'); + } catch (err) { + console.error('Cannot apply filter: ' + err) + } + } + + if (data.panelButtons) { + for (var z = 0; z < data.panelButtons.length; z++) { + $('#btn_custom_' + data.instance + '_' + z).button(data.panelButtons[z]).css({width: 18, height: 18}).click(data.panelButtons[z].click).attr('title', data.panelButtons[z].title || ''); + text += ''; + } + } + + if (data.customButtonFilter) { + $('#filter_button_' + data.instance + '_btn').button(data.customButtonFilter).css({width: 18, height: 18}).click(data.customButtonFilter.callback); + } + + showActive($dlg); + loadSettings(data); + installColResize(data, $dlg); + + // set preset filters + for (var field in data.filterPresets) { + if (!data.filterPresets[field]) continue; + if (typeof data.filterPresets[field] === 'object') { + $('#filter_' + field + '_' + data.instance).val(data.filterPresets[field][0]).trigger('change'); + } else { + $('#filter_' + field + '_' + data.instance).val(data.filterPresets[field]).trigger('change'); + } + } + } + + function storeSettings(data, force) { + if (typeof Storage === 'undefined' || !data.name) return; + + if (data.timer) clearTimeout(data.timer); + + if (force) { + window.localStorage.setItem(data.name + '-filter', JSON.stringify(data.filterVals)); + window.localStorage.setItem(data.name + '-expert', JSON.stringify(data.expertMode)); + data.timer = null; + } else { + data.timer = setTimeout(function () { + window.localStorage.setItem(data.name + '-filter', JSON.stringify(data.filterVals)); + window.localStorage.setItem(data.name + '-expert', JSON.stringify(data.expertMode)); + }, 500); + } + } + + function loadSettings(data) { + if (typeof Storage !== 'undefined' && data.name) { + var f = window.localStorage.getItem(data.name + '-filter'); + if (f) { + try{ + f = JSON.parse(f); + for (var field in f) { + if (field === 'length') continue; + if (data.filterPresets[field]) continue; + $('#filter_' + field + '_' + data.instance).val(f[field]).trigger('change'); + } + } catch(e) { + console.error('Cannot parse settings: ' + e); + } + } else if (!data.filter) { + // set default filter: state + $('#filter_type_' + data.instance).val('state').trigger('change'); + } + } + } + + var methods = { + init: function (options) { + // done, just to show possible settings, this is not required + var settings = $.extend({ + currentId: '', + objects: null, + states: null, + filter: null, + imgPath: 'lib/css/fancytree/', + connCfg: null, + onSuccess: null, + onChange: null, + zindex: null, + list: false, + name: null, + columns: ['image', 'name', 'type', 'role', 'enum', 'room', 'function', 'value', 'button'] + }, options); + + settings.texts = settings.texts || {}; + settings.texts = $.extend({ + select: 'Select', + cancel: 'Cancel', + all: 'All', + id: 'ID', + name: 'Name', + role: 'Role', + type: 'Type', + room: 'Room', + 'function': 'Function', + enum: 'Members', + value: 'Value', + selectid: 'Select ID', + from: 'From', + quality: 'Quality', + lc: 'Last changed', + ts: 'Time stamp', + ack: 'Acknowledged', + expand: 'Expand all nodes', + collapse: 'Collapse all nodes', + refresh: 'Rebuild tree', + edit: 'Edit', + ok: 'Ok', + push: 'Trigger event', + wait: 'Processing...', + list: 'Show list view', + tree: 'Show tree view', + selectAll: 'Select all', + unselectAll: 'Unselect all', + invertSelection: 'Invert selection', + copyToClipboard: 'Copy to clipboard' + }, settings.texts); + + var that = this; + for (var i = 0; i < this.length; i++) { + var dlg = this[i]; + var $dlg = $(dlg); + var data = $dlg.data('selectId'); + // Init data + if (!data) { + data = { + tree: {title: '', children: [], count: 0, root: true}, + roomEnums: [], + rooms: {}, + roomsColored: {}, + funcEnums: [], + funcs: {}, + funcsColored: {}, + roles: [], + histories: [], + types: [], + regexSystemAdapter: new RegExp('^system\\.adapter\\.'), + regexSystemHost: new RegExp('^system\\.host\\.'), + regexEnumRooms: new RegExp('^enum\\.rooms\\.'), + regexEnumFuncs: new RegExp('^enum\\.functions\\.'), + instance: instance++, + inited: false, + filterPresets: {} + }; + $dlg.data('selectId', data); + } + if (data.inited) { + // Re-init tree if filter or selectedID changed + if ((data.filter && !settings.filter && settings.filter !== undefined) || + (!data.filter && settings.filter) || + (data.filter && settings.filter && JSON.stringify(data.filter) !== JSON.stringify(settings.filter))) { + data.inited = false; + } + if (data.inited && settings.currentId !== undefined && (data.currentId !== settings.currentId)) { + // Deactivate current line + var tree = data.$tree.fancytree('getTree'); + tree.visit(function (node) { + if (node.key === data.currentId) { + node.setActive(false); + return false; + } + }); + } + } + + data = $.extend(data, settings); + + data.rootExp = data.root ? new RegExp('^' + data.root.replace('.', '\\.')) : null; + + data.selectedID = data.currentId; + + // make a copy of filter + data.filter = JSON.parse(JSON.stringify(data.filter)); + + if (!data.objects && data.connCfg) { + // Read objects and states + data.socketURL = ''; + data.socketSESSION = ''; + if (typeof data.connCfg.socketUrl !== 'undefined') { + data.socketURL = data.connCfg.socketUrl; + if (data.socketURL && data.socketURL[0] === ':') { + data.socketURL = location.protocol + '//' + location.hostname + data.socketURL; + } + data.socketSESSION = data.connCfg.socketSession; + data.socketUPGRADE = data.connCfg.upgrade; + data.socketRememberUpgrade = data.connCfg.rememberUpgrade; + data.socketTransports = data.connCfg.transports; + } + + var connectTimeout = setTimeout(function () { + if (!$('#select-id-dialog').length) { + $('body').append('
' + (data.texts.noconnection || 'No connection to server') + '
'); + } + $('#select-id-dialog').dialog({ + modal: true + }); + }, 5000); + + data.socket = io.connect(data.socketURL, { + query: 'key=' + data.socketSESSION, + 'reconnection limit': 10000, + 'max reconnection attempts': Infinity, + upgrade: data.socketUPGRADE, + rememberUpgrade: data.socketRememberUpgrade, + transports: data.socketTransports + }); + + data.socket.on('connect', function () { + if (connectTimeout) clearTimeout(connectTimeout); + this.emit('name', data.connCfg.socketName || 'selectId'); + this.emit('getObjects', function (err, res) { + data.objects = res; + data.socket.emit('getStates', function (err, res) { + data.states = res; + }); + }); + }); + data.socket.on('stateChange', function (id, obj) { + that.selectId('state', id, obj); + }); + data.socket.on('objectChange', function (id, obj) { + that.selectId('object', id, obj); + }); + } + + $dlg.data('selectId', data); + } + + return this; + }, + show: function (currentId, filter, onSuccess) { + if (typeof filter === 'function') { + onSuccess = filter; + filter = undefined; + } + if (typeof currentId === 'function') { + onSuccess = currentId; + currentId = undefined; + } + + for (var i = 0; i < this.length; i++) { + var dlg = this[i]; + var $dlg = $(dlg); + var data = $dlg.data('selectId'); + if (!data) continue; + if (data.inited) { + // Re-init tree if filter or selectedID changed + if ((data.filter && !filter && filter !== undefined) || + (!data.filter && filter) || + (data.filter && filter && JSON.stringify(data.filter) !== JSON.stringify(filter))) { + data.inited = false; + } + + if (data.inited && currentId !== undefined && (data.currentId !== currentId)) { + // Deactivate current line + var tree = data.$tree.fancytree('getTree'); + tree.visit(function (node) { + if (node.key === data.currentId) { + node.setActive(false); + return false; + } + }); + } + } + if (currentId !== undefined) data.currentId = currentId; + if (filter !== undefined) data.filter = JSON.parse(JSON.stringify(filter)); + if (onSuccess !== undefined) { + data.onSuccess = onSuccess; + data.$tree = $('#selectID_' + data.instance); + if (data.$tree[0]) data.$tree[0]._onSuccess = data.onSuccess; + } + data.selectedID = data.currentId; + + if (!data.inited || !data.noDialog) { + data.$dlg = $dlg; + initTreeDialog($dlg); + } else { + if (data.selectedID) { + var tree = data.$tree.fancytree('getTree'); + tree.visit(function (node) { + if (node.key === data.selectedID) { + node.setActive(); + node.makeVisible({scrollIntoView: false}); + return false; + } + }); + } + } + if (!data.noDialog) { + $dlg.dialog('option', 'title', data.texts.selectid + ' - ' + (data.currentId || ' ')); + if (data.currentId) { + if (data.objects[data.currentId] && data.objects[data.currentId].common && data.objects[data.currentId].common.name) { + $dlg.dialog('option', 'title', data.texts.selectid + ' - ' + (data.objects[data.currentId].common.name || ' ')); + } else { + $dlg.dialog('option', 'title', data.texts.selectid + ' - ' + (data.currentId || ' ')); + } + } else { + $('#' + data.instance + '-button-ok').addClass('ui-state-disabled'); + } + + $dlg.dialog('open'); + showActive($dlg, true); + } else { + $dlg.show(); + showActive($dlg, true); + } + } + + return this; + }, + hide: function () { + for (var i = 0; i < this.length; i++) { + var dlg = this[i]; + var $dlg = $(dlg); + var data = $dlg.data('selectId'); + if (data && !data.noDialog) { + $dlg.dialog('hide'); + } else { + $dlg.hide(); + } + } + return this; + }, + clear: function () { + for (var i = 0; i < this.length; i++) { + var dlg = this[i]; + var $dlg = $(dlg); + var data = $dlg.data('selectId'); + // Init data + if (data) { + data.tree = {title: '', children: [], count: 0, root: true}; + data.rooms = {}; + data.roomEnums = []; + data.funcs = {}; + data.funcEnums = []; + data.roles = []; + data.types = []; + data.histories = []; + } + } + return this; + }, + getInfo: function (id) { + for (var i = 0; i < this.length; i++) { + var dlg = this[i]; + var $dlg = $(dlg); + var data = $dlg.data('selectId'); + if (data && data.objects) { + return data.objects[id]; + } + } + return null; + }, + getTreeInfo: function (id) { + for (var i = 0; i < this.length; i++) { + var dlg = this[i]; + var $dlg = $(dlg); + var data = $dlg.data('selectId'); + if (!data || !data.$tree) continue; + + var tree = data.$tree.fancytree('getTree'); + var node = null; + tree.visit(function (n) { + if (n.key === id) { + node = n; + return false; + } + }); + var result = { + id: id, + parent: (node && node.parent && node.parent.parent) ? node.parent.key : null, + children: null, + obj: data.objects ? data.objects[id] : null + }; + if (node && node.children) { + result.children = []; + for (var t = 0; t < node.children.length; t++) { + result.children.push(node.children[t].key); + } + if (!result.children.length) delete result.children; + + } + return result; + } + return null; + }, + destroy: function () { + for (var i = 0; i < this.length; i++) { + var dlg = this[i]; + var $dlg = $(dlg); + $dlg.data('selectId', null); + $('#' + data.instance + '-div')[0].innerHTML(''); + } + return this; + }, + reinit: function () { + for (var i = 0; i < this.length; i++) { + var dlg = this[i]; + var $dlg = $(dlg); + var data = $dlg.data('selectId'); + if (data) { + data.inited = false; + initTreeDialog(data.$dlg); + } + } + return this; + }, + // update states + state: function (id, state) { + for (var i = 0; i < this.length; i++) { + var dlg = this[i]; + var $dlg = $(dlg); + var data = $dlg.data('selectId'); + if (!data || !data.states || !data.$tree) continue; + if (data.states[id] && + state && + data.states[id].val === state.val && + data.states[id].ack === state.ack && + data.states[id].q === state.q && + data.states[id].from === state.from && + data.states[id].ts === state.ts + ) return; + + data.states[id] = state; + var tree = data.$tree.fancytree('getTree'); + var node = null; + tree.visit(function (n) { + if (n.key === id) { + node = n; + return false; + } + }); + if (node) node.render(true); + } + return this; + }, + // update objects + object: function (id, obj) { + for (var k = 0; k < this.length; k++) { + var dlg = this[k]; + var $dlg = $(dlg); + var data = $dlg.data('selectId'); + if (!data || !data.$tree || !data.objects) continue; + + if (id.match(/^enum\.rooms/)) { + data.rooms = {}; + data.roomsColored = {}; + } + if (id.match(/^enum\.functions/)) { + data.funcs = {}; + data.funcsColored = {}; + } + + var tree = data.$tree.fancytree('getTree'); + var node = null; + tree.visit(function (n) { + if (n.key === id) { + node = n; + return false; + } + }); + + // If new node + if (!node && obj) { + // Filter it + + data.objects[id] = obj; + var addedNodes = []; + + if (!filterId(data, id)) return; + + treeInsert(data, id, false, addedNodes); + + for (var i = 0; i < addedNodes.length; i++) { + if (!addedNodes[i].parent.root) { + tree.visit(function (n) { + if (n.key === addedNodes[i].parent.key) { + node = n; + return false; + } + }); + + } else { + node = data.$tree.fancytree('getRootNode'); + } + // if no children + if (!node.children || !node.children.length) { + // add + node.addChildren(addedNodes[i]); + node.folder = true; + node.expanded = false; + node.render(true); + node.children[0].match = true; + } else { + var c; + for (c = 0; c < node.children.length; c++) { + if (node.children[c].key > addedNodes[i].key) break; + } + // if some found greater than new one + if (c !== node.children.length) { + node.addChildren(addedNodes[i], node.children[c]); + node.children[c].match = true; + node.render(true); + } else { + // just add + node.addChildren(addedNodes[i]); + node.children[node.children.length - 1].match = true; + node.render(true); + } + } + } + } else if (!obj) { + // object deleted + delete data.objects[id]; + deleteTree(data, id); + if (node) { + if (node.children && node.children.length) { + if (node.children.length === 1) { + node.folder = false; + node.expanded = false; + } + node.render(true); + } else { + if (node.parent && node.parent.children.length === 1) { + node.parent.folder = false; + node.parent.expanded = false; + node.parent.render(true); + } + node.remove(); + } + } + } else { + // object updated + if (node) node.render(true); + } + } + return this; + }, + option: function (name, value) { + for (var k = 0; k < this.length; k++) { + var dlg = this[k]; + var $dlg = $(dlg); + var data = $dlg.data('selectId'); + if (!data) continue; + + if (data[name] !== undefined) { + data[name] = value; + } else { + console.error('Unknown options for selectID: ' + name); + } + } + }, + objectAll: function (id, obj) { + $('.select-id-dialog-marker').selectId('object', id, obj); + }, + stateAll: function (id, state) { + $('.select-id-dialog-marker').selectId('state', id, state); + }, + getFilteredIds: function () { + for (var k = 0; k < this.length; k++) { + var dlg = this[k]; + var $dlg = $(dlg); + var data = $dlg.data('selectId'); + if (!data || !data.$tree || !data.objects) continue; + + var tree = data.$tree.fancytree('getTree'); + var nodes = []; + tree.visit(function (n) { + if (n.match) nodes.push(n.key); + }); + return nodes; + } + return null; + }, + getActual: function () { + //for (var k = 0; k < this.length; k++) { + // + //} + var dlg = this[0]; + var $dlg = $(dlg); + var data = $dlg.data('selectId'); + return data ? data.selectedID : null; + } + }; + + $.fn.selectId = function (method) { + if (methods[method]) { + return methods[method].apply(this, Array.prototype.slice.call(arguments, 1)); + } else if (typeof method === 'object' || !method) { + return methods.init.apply(this, arguments); + } else { + $.error('Method "' + method + '" not found in jQuery.selectId'); + } + }; +})(jQuery); diff --git a/public/vendor/icons.gif b/public/vendor/icons.gif new file mode 100644 index 0000000..65719cc Binary files /dev/null and b/public/vendor/icons.gif differ diff --git a/public/vendor/jquery.fancytree-all.min.js b/public/vendor/jquery.fancytree-all.min.js new file mode 100644 index 0000000..fb189e7 --- /dev/null +++ b/public/vendor/jquery.fancytree-all.min.js @@ -0,0 +1,27 @@ +/*! jQuery Fancytree Plugin - 2.6.0 - 2014-11-29T08:33 + * https://github.com/mar10/fancytree + * Copyright (c) 2014 Martin Wendt; Licensed MIT */ +(function( factory ) { + if (jQuery.fn.fancytree) return; + // this does not work in node-red + if ( false && typeof define === "function" && define.amd ) { + define( [ "jquery" ], factory ); + } else { + factory( jQuery ); + } +}(function( $ ) { + !function(a,b,c,d){"use strict";function e(b,c){b||(c=c?": "+c:"",a.error("Fancytree assertion failed"+c))}function f(a,c){var d,e,f=b.console?b.console[a]:null;if(f)try{f.apply(b.console,c)}catch(g){for(e="",d=0;de;return!0}function i(a,b,c,d,e){var f=function(){var c=b[a],f=d[a],g=b.ext[e],h=function(){return c.apply(b,arguments)};return function(){var a=b._local,c=b._super;try{return b._local=g,b._super=h,f.apply(b,arguments)}finally{b._local=a,b._super=c}}}();return f}function j(b,c,d,e){for(var f in d)"function"==typeof d[f]?"function"==typeof b[f]?b[f]=i(f,b,c,d,e):"_"===f.charAt(0)?b.ext[e][f]=i(f,b,c,d,e):a.error("Could not override tree."+f+". Use prefix '_' to create tree."+e+"._"+f):"options"!==f&&(b.ext[e][f]=d[f])}function k(b,c){return b===d?a.Deferred(function(){this.resolve()}).promise():a.Deferred(function(){this.resolveWith(b,c)}).promise()}function l(b,c){return b===d?a.Deferred(function(){this.reject()}).promise():a.Deferred(function(){this.rejectWith(b,c)}).promise()}function m(a,b){return function(){a.resolveWith(b)}}function n(b){var c=a.extend({},b.data()),d=c.json;return delete c.fancytree,d&&(delete c.json,c=a.extend(c,d)),c}function o(a){return a=a.toLowerCase(),function(b){return b.title.toLowerCase().indexOf(a)>=0}}function p(a){var b=new RegExp("^"+a,"i");return function(a){return b.test(a.title)}}function q(b,c){var d,f,g,h;for(this.parent=b,this.tree=b.tree,this.ul=null,this.li=null,this.statusNodeType=null,this._isLoading=!1,this._error=null,this.data={},d=0,f=x.length;f>d;d++)g=x[d],this[g]=c[g];c.data&&a.extend(this.data,c.data);for(g in c)y[g]||a.isFunction(c[g])||z[g]||(this.data[g]=c[g]);null==this.key?this.tree.options.defaultKey?(this.key=this.tree.options.defaultKey(this),e(this.key,"defaultKey() must return a unique key")):this.key="_"+t._nextNodeKey++:this.key=""+this.key,c.active&&(e(null===this.tree.activeNode,"only one active node allowed"),this.tree.activeNode=this),c.selected&&(this.tree.lastSelectedNode=this),this.children=null,h=c.children,h&&h.length&&this._setChildren(h),this.tree._callHook("treeRegisterNode",this.tree,!0,this)}function r(b){this.widget=b,this.$div=b.element,this.options=b.options,this.options&&a.isFunction(this.options.lazyload)&&(a.isFunction(this.options.lazyLoad)||(this.options.lazyLoad=function(){t.warn("The 'lazyload' event is deprecated since 2014-02-25. Use 'lazyLoad' (with uppercase L) instead."),b.options.lazyload.apply(this,arguments)})),this.options&&a.isFunction(this.options.loaderror)&&a.error("The 'loaderror' event was renamed since 2014-07-03. Use 'loadError' (with uppercase E) instead."),this.ext={},this.data=n(this.$div),this._id=a.ui.fancytree._nextId++,this._ns=".fancytree-"+this._id,this.activeNode=null,this.focusNode=null,this._hasFocus=null,this.lastSelectedNode=null,this.systemFocusElement=null,this.lastQuicksearchTerm="",this.lastQuicksearchTime=0,this.statusClassPropName="span",this.ariaPropName="li",this.nodeContainerAttrName="li",this.$div.find(">ul.fancytree-container").remove();var c,d={tree:this};this.rootNode=new q(d,{title:"root",key:"root_"+this._id,children:null,expanded:!0}),this.rootNode.parent=null,c=a("
    ",{"class":"ui-fancytree fancytree-container"}).appendTo(this.$div),this.$container=c,this.rootNode.ul=c[0],null==this.options.debugLevel&&(this.options.debugLevel=t.debugLevel),this.$container.attr("tabindex",this.options.tabbable?"0":"-1"),this.options.aria&&this.$container.attr("role","tree").attr("aria-multiselectable",!0)}if(a.ui&&a.ui.fancytree)return void a.ui.fancytree.warn("Fancytree: ignored duplicate include");e(a.ui,"Fancytree requires jQuery UI (http://jqueryui.com)");var s,t=null,u={"&":"&","<":"<",">":">",'"':""","'":"'","/":"/"},v="active expanded focus folder hideCheckbox lazy selected unselectable".split(" "),w={},x="expanded extraClasses folder hideCheckbox key lazy refKey selected title tooltip unselectable".split(" "),y={},z={active:!0,children:!0,data:!0,focus:!0};for(s=0;sb;b++)if(d[b].key===a)return d[b]}else{if("number"==typeof a)return this.children[a];if(a.parent===this)return a}return null},_setChildren:function(a){e(a&&(!this.children||0===this.children.length),"only init supported"),this.children=[];for(var b=0,c=a.length;c>b;b++)this.children.push(new q(this,a[b]))},addChildren:function(b,c){var d,f,g,h=null,i=[];for(a.isPlainObject(b)&&(b=[b]),this.children||(this.children=[]),d=0,f=b.length;f>d;d++)i.push(new q(this,b[d]));return h=i[0],null==c?this.children=this.children.concat(i):(c=this._findDirectChild(c),g=a.inArray(c,this.children),e(g>=0,"insertBefore must be an existing child"),this.children.splice.apply(this.children,[g,0].concat(i))),(!this.parent||this.parent.ul||this.tr)&&this.render(),3===this.tree.options.selectMode&&this.fixSelection3FromEndNodes(),h},addNode:function(a,b){switch((b===d||"over"===b)&&(b="child"),b){case"after":return this.getParent().addChildren(a,this.getNextSibling());case"before":return this.getParent().addChildren(a,this);case"firstChild":var c=this.children?this.children[0]:null;return this.addChildren(a,c);case"child":case"over":return this.addChildren(a)}e(!1,"Invalid mode: "+b)},appendSibling:function(a){return this.addNode(a,"after")},applyPatch:function(b){if(null===b)return this.remove(),k(this);var c,d,e,f={children:!0,expanded:!0,parent:!0};for(c in b)e=b[c],f[c]||a.isFunction(e)||(y[c]?this[c]=e:this.data[c]=e);return b.hasOwnProperty("children")&&(this.removeChildren(),b.children&&this._setChildren(b.children)),this.isVisible()&&(this.renderTitle(),this.renderStatus()),d=b.hasOwnProperty("expanded")?this.setExpanded(b.expanded):k(this)},collapseSiblings:function(){return this.tree._callHook("nodeCollapseSiblings",this)},copyTo:function(a,b,c){return a.addNode(this.toDict(!0,c),b)},countChildren:function(a){var b,c,d,e=this.children;if(!e)return 0;if(d=e.length,a!==!1)for(b=0,c=d;c>b;b++)d+=e[b].countChildren();return d},debug:function(){this.tree.options.debugLevel>=2&&(Array.prototype.unshift.call(arguments,this.toString()),f("log",arguments))},discard:function(){return this.warn("FancytreeNode.discard() is deprecated since 2014-02-16. Use .resetLazy() instead."),this.resetLazy()},findAll:function(b){b=a.isFunction(b)?b:o(b);var c=[];return this.visit(function(a){b(a)&&c.push(a)}),c},findFirst:function(b){b=a.isFunction(b)?b:o(b);var c=null;return this.visit(function(a){return b(a)?(c=a,!1):void 0}),c},_changeSelectStatusAttrs:function(a){var b=!1;switch(a){case!1:b=this.selected||this.partsel,this.selected=!1,this.partsel=!1;break;case!0:b=!this.selected||!this.partsel,this.selected=!0,this.partsel=!0;break;case d:b=this.selected||!this.partsel,this.selected=!1,this.partsel=!0;break;default:e(!1,"invalid state: "+a)}return b&&this.renderStatus(),b},fixSelection3AfterClick:function(){var a=this.isSelected();this.visit(function(b){b._changeSelectStatusAttrs(a)}),this.fixSelection3FromEndNodes()},fixSelection3FromEndNodes:function(){function a(b){var c,e,f,g,h,i,j,k=b.children;if(k&&k.length){for(i=!0,j=!1,c=0,e=k.length;e>c;c++)f=k[c],g=a(f),g!==!1&&(j=!0),g!==!0&&(i=!1);h=i?!0:j?d:!1}else h=!!b.selected;return b._changeSelectStatusAttrs(h),h}e(3===this.tree.options.selectMode,"expected selectMode 3"),a(this),this.visitParents(function(a){var b,c,e,f,g=a.children,h=!0,i=!1;for(b=0,c=g.length;c>b;b++)e=g[b],(e.selected||e.partsel)&&(i=!0),e.unselectable||e.selected||(h=!1);f=h?!0:i?d:!1,a._changeSelectStatusAttrs(f)})},fromDict:function(b){for(var c in b)y[c]?this[c]=b[c]:"data"===c?a.extend(this.data,b.data):a.isFunction(b[c])||z[c]||(this.data[c]=b[c]);b.children&&(this.removeChildren(),this.addChildren(b.children)),this.renderTitle()},getChildren:function(){return this.hasChildren()===d?d:this.children},getFirstChild:function(){return this.children?this.children[0]:null},getIndex:function(){return a.inArray(this,this.parent.children)},getIndexHier:function(b){b=b||".";var c=[];return a.each(this.getParentList(!1,!0),function(a,b){c.push(b.getIndex()+1)}),c.join(b)},getKeyPath:function(a){var b=[],c=this.tree.options.keyPathSeparator;return this.visitParents(function(a){a.parent&&b.unshift(a.key)},!a),c+b.join(c)},getLastChild:function(){return this.children?this.children[this.children.length-1]:null},getLevel:function(){for(var a=0,b=this.parent;b;)a++,b=b.parent;return a},getNextSibling:function(){if(this.parent){var a,b,c=this.parent.children;for(a=0,b=c.length-1;b>a;a++)if(c[a]===this)return c[a+1]}return null},getParent:function(){return this.parent},getParentList:function(a,b){for(var c=[],d=b?this:this.parent;d;)(a||d.parent)&&c.unshift(d),d=d.parent;return c},getPrevSibling:function(){if(this.parent){var a,b,c=this.parent.children;for(a=1,b=c.length;b>a;a++)if(c[a]===this)return c[a-1]}return null},hasChildren:function(){return this.lazy?null==this.children?d:0===this.children.length?!1:1===this.children.length&&this.children[0].isStatusNode()?d:!0:!(!this.children||!this.children.length)},hasFocus:function(){return this.tree.hasFocus()&&this.tree.focusNode===this},info:function(){this.tree.options.debugLevel>=1&&(Array.prototype.unshift.call(arguments,this.toString()),f("info",arguments))},isActive:function(){return this.tree.activeNode===this},isChildOf:function(a){return this.parent&&this.parent===a},isDescendantOf:function(a){if(!a||a.tree!==this.tree)return!1;for(var b=this.parent;b;){if(b===a)return!0;b=b.parent}return!1},isExpanded:function(){return!!this.expanded},isFirstSibling:function(){var a=this.parent;return!a||a.children[0]===this},isFolder:function(){return!!this.folder},isLastSibling:function(){var a=this.parent;return!a||a.children[a.children.length-1]===this},isLazy:function(){return!!this.lazy},isLoaded:function(){return!this.lazy||this.hasChildren()!==d},isLoading:function(){return!!this._isLoading},isRoot:function(){return this.isRootNode()},isRootNode:function(){return this.tree.rootNode===this},isSelected:function(){return!!this.selected},isStatusNode:function(){return!!this.statusNodeType},isTopLevel:function(){return this.tree.rootNode===this.parent},isUndefined:function(){return this.hasChildren()===d},isVisible:function(){var a,b,c=this.getParentList(!1,!1);for(a=0,b=c.length;b>a;a++)if(!c[a].expanded)return!1;return!0},lazyLoad:function(a){return this.warn("FancytreeNode.lazyLoad() is deprecated since 2014-02-16. Use .load() instead."),this.load(a)},load:function(a){var b,c,d=this;return e(this.isLazy(),"load() requires a lazy node"),a||this.isUndefined()?(this.isLoaded()&&this.resetLazy(),c=this.tree._triggerNodeEvent("lazyLoad",this),c===!1?k(this):(e("boolean"!=typeof c,"lazyLoad event must return source in data.result"),b=this.tree._callHook("nodeLoadChildren",this,c),this.expanded&&b.always(function(){d.render()}),b)):k(this)},makeVisible:function(b){var c,d=this,e=[],f=new a.Deferred,g=this.getParentList(!1,!1),h=g.length,i=!(b&&b.noAnimation===!0),j=!(b&&b.scrollIntoView===!1);for(c=h-1;c>=0;c--)e.push(g[c].setExpanded(!0,b));return a.when.apply(a,e).done(function(){j?d.scrollIntoView(i).done(function(){f.resolve()}):f.resolve()}),f.promise()},moveTo:function(b,c,f){(c===d||"over"===c)&&(c="child");var g,h=this.parent,i="child"===c?b:b.parent;if(this!==b){if(!this.parent)throw"Cannot move system root";if(i.isDescendantOf(this))throw"Cannot move a node to its own descendant";if(1===this.parent.children.length){if(this.parent===i)return;this.parent.children=this.parent.lazy?[]:null,this.parent.expanded=!1}else g=a.inArray(this,this.parent.children),e(g>=0),this.parent.children.splice(g,1);if(this.parent=i,i.hasChildren())switch(c){case"child":i.children.push(this);break;case"before":g=a.inArray(b,i.children),e(g>=0),i.children.splice(g,0,this);break;case"after":g=a.inArray(b,i.children),e(g>=0),i.children.splice(g+1,0,this);break;default:throw"Invalid mode "+c}else i.children=[this];f&&b.visit(f,!0),this.tree!==b.tree&&(this.warn("Cross-tree moveTo is experimantal!"),this.visit(function(a){a.tree=b.tree},!0)),h.isDescendantOf(i)||h.render(),i.isDescendantOf(h)||i===h||i.render()}},navigate:function(b,c){function d(d){if(d){try{d.makeVisible()}catch(e){}return a(d.span).is(":visible")?c===!1?d.setFocus():d.setActive():(d.debug("Navigate: skipping hidden node"),void d.navigate(b,c))}}var e,f,g=!0,h=a.ui.keyCode,i=null;switch(b){case h.BACKSPACE:this.parent&&this.parent.parent&&d(this.parent);break;case h.LEFT:this.expanded?(this.setExpanded(!1),d(this)):this.parent&&this.parent.parent&&d(this.parent);break;case h.RIGHT:this.expanded||!this.children&&!this.lazy?this.children&&this.children.length&&d(this.children[0]):(this.setExpanded(),d(this));break;case h.UP:for(i=this.getPrevSibling();i&&!a(i.span).is(":visible");)i=i.getPrevSibling();for(;i&&i.expanded&&i.children&&i.children.length;)i=i.children[i.children.length-1];!i&&this.parent&&this.parent.parent&&(i=this.parent),d(i);break;case h.DOWN:if(this.expanded&&this.children&&this.children.length)i=this.children[0];else for(f=this.getParentList(!1,!0),e=f.length-1;e>=0;e--){for(i=f[e].getNextSibling();i&&!a(i.span).is(":visible");)i=i.getNextSibling();if(i)break}d(i);break;default:g=!1}},remove:function(){return this.parent.removeChild(this)},removeChild:function(a){return this.tree._callHook("nodeRemoveChild",this,a)},removeChildren:function(){return this.tree._callHook("nodeRemoveChildren",this)},render:function(a,b){return this.tree._callHook("nodeRender",this,a,b)},renderTitle:function(){return this.tree._callHook("nodeRenderTitle",this)},renderStatus:function(){return this.tree._callHook("nodeRenderStatus",this)},resetLazy:function(){this.removeChildren(),this.expanded=!1,this.lazy=!0,this.children=d,this.renderStatus()},scheduleAction:function(a,b){this.tree.timer&&clearTimeout(this.tree.timer),this.tree.timer=null;var c=this;switch(a){case"cancel":break;case"expand":this.tree.timer=setTimeout(function(){c.tree.debug("setTimeout: trigger expand"),c.setExpanded(!0)},b);break;case"activate":this.tree.timer=setTimeout(function(){c.tree.debug("setTimeout: trigger activate"),c.setActive(!0)},b);break;default:throw"Invalid mode "+a}},scrollIntoView:function(f,h){h!==d&&g(h)&&(this.warn("scrollIntoView() with 'topNode' option is deprecated since 2014-05-08. Use 'options.topNode' instead."),h={topNode:h});var i,j,k,l,m=a.extend({effects:f===!0?{duration:200,queue:!1}:f,scrollOfs:this.tree.options.scrollOfs,scrollParent:this.tree.options.scrollParent||this.tree.$container,topNode:null},h),n=new a.Deferred,o=this,p=a(this.span).height(),q=a(m.scrollParent),r=m.scrollOfs.top||0,s=m.scrollOfs.bottom||0,t=q.height(),u=q.scrollTop(),v=q,w=q[0]===b,x=m.topNode||null,y=null;return e(a(this.span).is(":visible"),"scrollIntoView node is invisible"),w?(j=a(this.span).offset().top,i=x&&x.span?a(x.span).offset().top:0,v=a("html,body")):(e(q[0]!==c&&q[0]!==c.body,"scrollParent should be an simple element or `window`, not document or body."),l=q.offset().top,j=a(this.span).offset().top-l+u,i=x?a(x.span).offset().top-l+u:0,k=Math.max(0,q.innerHeight()-q[0].clientHeight),t-=k),u+r>j?y=j-r:j+p>u+t-s&&(y=j+p-t+s,x&&(e(x.isRoot()||a(x.span).is(":visible"),"topNode must be visible"),y>i&&(y=i-r))),null!==y?m.effects?(m.effects.complete=function(){n.resolveWith(o)},v.stop(!0).animate({scrollTop:y},m.effects)):(v[0].scrollTop=y,n.resolveWith(this)):n.resolveWith(this),n.promise()},setActive:function(a,b){return this.tree._callHook("nodeSetActive",this,a,b)},setExpanded:function(a,b){return this.tree._callHook("nodeSetExpanded",this,a,b)},setFocus:function(a){return this.tree._callHook("nodeSetFocus",this,a)},setSelected:function(a){return this.tree._callHook("nodeSetSelected",this,a)},setStatus:function(a,b,c){return this.tree._callHook("nodeSetStatus",this,a,b,c)},setTitle:function(a){this.title=a,this.renderTitle()},sortChildren:function(a,b){var c,d,e=this.children;if(e){if(a=a||function(a,b){var c=a.title.toLowerCase(),d=b.title.toLowerCase();return c===d?0:c>d?1:-1},e.sort(a),b)for(c=0,d=e.length;d>c;c++)e[c].children&&e[c].sortChildren(a,"$norender$");"$norender$"!==b&&this.render()}},toDict:function(b,c){var d,e,f,g={},h=this;if(a.each(x,function(a,b){(h[b]||h[b]===!1)&&(g[b]=h[b])}),a.isEmptyObject(this.data)||(g.data=a.extend({},this.data),a.isEmptyObject(g.data)&&delete g.data),c&&c(g),b&&this.hasChildren())for(g.children=[],d=0,e=this.children.length;e>d;d++)f=this.children[d],f.isStatusNode()||g.children.push(f.toDict(!0,c));return g},toggleExpanded:function(){return this.tree._callHook("nodeToggleExpanded",this)},toggleSelected:function(){return this.tree._callHook("nodeToggleSelected",this)},toString:function(){return""},visit:function(a,b){var c,d,e=!0,f=this.children;if(b===!0&&(e=a(this),e===!1||"skip"===e))return e;if(f)for(c=0,d=f.length;d>c&&(e=f[c].visit(a,!0),e!==!1);c++);return e},visitAndLoad:function(b,c,d){var e,f,g,h=this;return b&&c===!0&&(f=b(h),f===!1||"skip"===f)?d?f:k():h.children||h.lazy?(e=new a.Deferred,g=[],h.load().done(function(){for(var c=0,d=h.children.length;d>c;c++){if(f=h.children[c].visitAndLoad(b,!0,!0),f===!1){e.reject();break}"skip"!==f&&g.push(f)}a.when.apply(this,g).then(function(){e.resolve()})}),e.promise()):k()},visitParents:function(a,b){if(b&&a(this)===!1)return!1;for(var c=this.parent;c;){if(a(c)===!1)return!1;c=c.parent}return!0},warn:function(){Array.prototype.unshift.call(arguments,this.toString()),f("warn",arguments)}},r.prototype={_makeHookContext:function(b,c,e){var f,g;return b.node!==d?(c&&b.originalEvent!==c&&a.error("invalid args"),f=b):b.tree?(g=b.tree,f={node:b,tree:g,widget:g.widget,options:g.widget.options,originalEvent:c}):b.widget?f={node:null,tree:b,widget:b.widget,options:b.widget.options,originalEvent:c}:a.error("invalid args"),e&&a.extend(f,e),f},_callHook:function(b,c){var d=this._makeHookContext(c),e=this[b],f=Array.prototype.slice.call(arguments,2);return a.isFunction(e)||a.error("_callHook('"+b+"') is not a function"),f.unshift(d),e.apply(this,f)},_requireExtension:function(b,c,d,f){d=!!d;var g=this._local.name,h=this.options.extensions,i=a.inArray(b,h)d;d++)f=b[d],e(2===f.length,"patchList must be an array of length-2-arrays"),g=f[0],h=f[1],i=null===g?this.rootNode:this.getNodeByKey(g),i?(c=new a.Deferred,k.push(c),i.applyPatch(h).always(m(c,i))):this.warn("could not find node with key '"+g+"'");return a.when.apply(a,k).promise()},count:function(){return this.rootNode.countChildren()},debug:function(){this.options.debugLevel>=2&&(Array.prototype.unshift.call(arguments,this.toString()),f("log",arguments))},findNextNode:function(b,c){var d=null,e=c.parent.children,f=null,g=function(a,b,c){var d,e,f=a.children,h=f.length,i=f[b];if(i&&c(i)===!1)return!1;if(i&&i.children&&i.expanded&&g(i,0,c)===!1)return!1;for(d=b+1;h>d;d++)if(g(a,d,c)===!1)return!1;return e=a.parent,e?g(e,e.children.indexOf(a)+1,c):g(a,0,c)};return b="string"==typeof b?p(b):b,c=c||this.getFirstChild(),g(c.parent,e.indexOf(c),function(e){return e===d?!1:(d=d||e,a(e.span).is(":visible")?b(e)&&(f=e,f!==c)?!1:void 0:void e.debug("quicksearch: skipping hidden node"))}),f},generateFormElements:function(b,c){var d,e=b!==!1?"ft_"+this._id+"[]":b,f=c!==!1?"ft_"+this._id+"_active":c,g="fancytree_result_"+this._id,h=a("#"+g);h.length?h.empty():h=a("
    ",{id:g}).hide().insertAfter(this.$container),e&&(d=this.getSelectedNodes(3===this.options.selectMode),a.each(d,function(b,c){h.append(a("",{type:"checkbox",name:e,value:c.key,checked:!0}))})),f&&this.activeNode&&h.append(a("",{type:"radio",name:f,value:this.activeNode.key,checked:!0}))},getActiveNode:function(){return this.activeNode},getFirstChild:function(){return this.rootNode.getFirstChild()},getFocusNode:function(){return this.focusNode},getNodeByKey:function(a,b){var d,e;return!b&&(d=c.getElementById(this.options.idPrefix+a))?d.ftnode?d.ftnode:null:(b=b||this.rootNode,e=null,b.visit(function(b){return b.key===a?(e=b,!1):void 0},!0),e)},getRootNode:function(){return this.rootNode},getSelectedNodes:function(a){var b=[];return this.rootNode.visit(function(c){return c.selected&&(b.push(c),a===!0)?"skip":void 0}),b},hasFocus:function(){return!!this._hasFocus},info:function(){this.options.debugLevel>=1&&(Array.prototype.unshift.call(arguments,this.toString()),f("info",arguments))},loadKeyPath:function(b,c,e){function f(a,b,d){c.call(r,b,"loading"),b.load().done(function(){r.loadKeyPath.call(r,l[a],c,b).always(m(d,r))}).fail(function(){r.warn("loadKeyPath: error loading: "+a+" (parent: "+o+")"),c.call(r,b,"error"),d.reject()})}var g,h,i,j,k,l,n,o,p,q=this.options.keyPathSeparator,r=this;for(a.isArray(b)||(b=[b]),l={},i=0;i"},_triggerNodeEvent:function(a,b,c,e){var f=this._makeHookContext(b,c,e),g=this.widget._trigger(a,c,f);return g!==!1&&f.result!==d?f.result:g},_triggerTreeEvent:function(a,b,c){var e=this._makeHookContext(this,b,c),f=this.widget._trigger(a,b,e);return f!==!1&&e.result!==d?e.result:f},visit:function(a){return this.rootNode.visit(a,!1)},warn:function(){Array.prototype.unshift.call(arguments,this.toString()),f("warn",arguments)}},a.extend(r.prototype,{nodeClick:function(a){var b,c,d=a.targetType,e=a.node;if("expander"===d)this._callHook("nodeToggleExpanded",a);else if("checkbox"===d)this._callHook("nodeToggleSelected",a),a.options.focusOnSelect&&this._callHook("nodeSetFocus",a,!0);else{if(c=!1,b=!0,e.folder)switch(a.options.clickFolderMode){case 2:c=!0,b=!1;break;case 3:b=!0,c=!0}b&&(this.nodeSetFocus(a),this._callHook("nodeSetActive",a,!0)),c&&this._callHook("nodeToggleExpanded",a)}},nodeCollapseSiblings:function(a,b){var c,d,e,f=a.node;if(f.parent)for(c=f.parent.children,d=0,e=c.length;e>d;d++)c[d]!==f&&c[d].expanded&&this._callHook("nodeSetExpanded",c[d],!1,b)},nodeDblclick:function(a){"title"===a.targetType&&4===a.options.clickFolderMode&&this._callHook("nodeToggleExpanded",a),"title"===a.targetType&&a.originalEvent.preventDefault()},nodeKeydown:function(b){var c,d,e,f=b.originalEvent,g=b.node,h=b.tree,i=b.options,j=f.which,k=String.fromCharCode(j),l=!(f.altKey||f.ctrlKey||f.metaKey||f.shiftKey),m=a(f.target),n=!0,o=!(f.ctrlKey||!i.autoActivate),p=a.ui.keyCode;if(g||(this.getFirstChild().setFocus(),g=b.node=this.focusNode,g.debug("Keydown force focus on first node")),i.quicksearch&&l&&/\w/.test(k)&&!m.is(":input:enabled"))return d=(new Date).getTime(),d-h.lastQuicksearchTime>500&&(h.lastQuicksearchTerm=""),h.lastQuicksearchTime=d,h.lastQuicksearchTerm+=k,c=h.findNextNode(h.lastQuicksearchTerm,h.getActiveNode()),c&&c.setActive(),void f.preventDefault();switch(j){case p.NUMPAD_ADD:case 187:h.nodeSetExpanded(b,!0);break;case p.NUMPAD_SUBTRACT:case 189:h.nodeSetExpanded(b,!1);break;case p.SPACE:i.checkbox?h.nodeToggleSelected(b):h.nodeSetActive(b,!0);break;case p.ENTER:h.nodeSetActive(b,!0);break;case p.BACKSPACE:case p.LEFT:case p.RIGHT:case p.UP:case p.DOWN:e=g.navigate(f.which,o);break;default:n=!1}n&&f.preventDefault()},nodeLoadChildren:function(b,c){var d,f,g,h=b.tree,i=b.node;return a.isFunction(c)&&(c=c()),c.url&&(d=a.extend({},b.options.ajax,c),d.debugDelay?(f=d.debugDelay,a.isArray(f)&&(f=f[0]+Math.random()*(f[1]-f[0])),i.debug("nodeLoadChildren waiting debug delay "+Math.round(f)+"ms"),d.debugDelay=!1,g=a.Deferred(function(b){setTimeout(function(){a.ajax(d).done(function(){b.resolveWith(this,arguments)}).fail(function(){b.rejectWith(this,arguments)})},f)})):g=a.ajax(d),c=new a.Deferred,g.done(function(d){var e,f;if("string"==typeof d&&a.error("Ajax request returned a string (did you get the JSON dataType wrong?)."),b.options.postProcess){if(f=h._triggerNodeEvent("postProcess",b,b.originalEvent,{response:d,error:null,dataType:this.dataType}),f.error)return e=a.isPlainObject(f.error)?f.error:{message:f.error},e=h._makeHookContext(i,null,e),void c.rejectWith(this,[e]);d=a.isArray(f)?f:d}else d&&d.hasOwnProperty("d")&&b.options.enableAspx&&(d="string"==typeof d.d?a.parseJSON(d.d):d.d);c.resolveWith(this,[d])}).fail(function(a,b,d){var e=h._makeHookContext(i,null,{error:a,args:Array.prototype.slice.call(arguments),message:d,details:a.status+": "+d});c.rejectWith(this,[e])})),a.isFunction(c.promise)&&(e(!i.isLoading()),h.nodeSetStatus(b,"loading"),c.done(function(){h.nodeSetStatus(b,"ok")}).fail(function(a){var c;c=a.node&&a.error&&a.message?a:h._makeHookContext(i,null,{error:a,args:Array.prototype.slice.call(arguments),message:a?a.message||a.toString():""}),h._triggerNodeEvent("loadError",c,null)!==!1&&h.nodeSetStatus(b,"error",c.message,c.details)})),a.when(c).done(function(b){var c;a.isPlainObject(b)&&(e(a.isArray(b.children),"source must contain (or be) an array of children"),e(i.isRoot(),"source may only be an object for root nodes"),c=b,b=b.children,delete c.children,a.extend(h.data,c)),e(a.isArray(b),"expected array of children"),i._setChildren(b),h._triggerNodeEvent("loadChildren",i)})},nodeLoadKeyPath:function(){},nodeRemoveChild:function(b,c){var d,f=b.node,g=b.options,h=a.extend({},b,{node:c}),i=f.children;return 1===i.length?(e(c===i[0]),this.nodeRemoveChildren(b)):(this.activeNode&&(c===this.activeNode||this.activeNode.isDescendantOf(c))&&this.activeNode.setActive(!1),this.focusNode&&(c===this.focusNode||this.focusNode.isDescendantOf(c))&&(this.focusNode=null),this.nodeRemoveMarkup(h),this.nodeRemoveChildren(h),d=a.inArray(c,i),e(d>=0),c.visit(function(a){a.parent=null},!0),this._callHook("treeRegisterNode",this,!1,c),g.removeNode&&g.removeNode.call(b.tree,{type:"removeNode"},h),void i.splice(d,1))},nodeRemoveChildMarkup:function(b){var c=b.node;c.ul&&(c.isRoot()?a(c.ul).empty():(a(c.ul).remove(),c.ul=null),c.visit(function(a){a.li=a.ul=null}))},nodeRemoveChildren:function(b){var c,d=b.tree,e=b.node,f=e.children,g=b.options;f&&(this.activeNode&&this.activeNode.isDescendantOf(e)&&this.activeNode.setActive(!1),this.focusNode&&this.focusNode.isDescendantOf(e)&&(this.focusNode=null),this.nodeRemoveChildMarkup(b),c=a.extend({},b),e.visit(function(a){a.parent=null,d._callHook("treeRegisterNode",d,!1,a),g.removeNode&&(c.node=a,g.removeNode.call(b.tree,{type:"removeNode"},c))}),e.children=e.lazy?[]:null,this.nodeRenderStatus(b))},nodeRemoveMarkup:function(b){var c=b.node;c.li&&(a(c.li).remove(),c.li=null),this.nodeRemoveChildMarkup(b)},nodeRender:function(b,d,f,g,h){var i,j,k,l,m,n,o,p=b.node,q=b.tree,r=b.options,s=r.aria,t=!1,u=p.parent,v=!u,w=p.children;if(v||u.ul){if(e(v||u.ul,"parent UL must exist"),v||(p.li&&(d||p.li.parentNode!==p.parent.ul)&&(p.li.parentNode!==p.parent.ul&&this.warn("unlink "+p+" (must be child of "+p.parent+")"),this.nodeRemoveMarkup(b)),p.li?this.nodeRenderStatus(b):(t=!0,p.li=c.createElement("li"),p.li.ftnode=p,p.key&&r.generateIds&&(p.li.id=r.idPrefix+p.key),p.span=c.createElement("span"),p.span.className="fancytree-node",s&&a(p.span).attr("aria-labelledby","ftal_"+p.key),p.li.appendChild(p.span),this.nodeRenderTitle(b),r.createNode&&r.createNode.call(q,{type:"createNode"},b)),r.renderNode&&r.renderNode.call(q,{type:"renderNode"},b)),w){if(v||p.expanded||f===!0){for(p.ul||(p.ul=c.createElement("ul"),(g===!0&&!h||!p.expanded)&&(p.ul.style.display="none"),s&&a(p.ul).attr("role","group"),p.li?p.li.appendChild(p.ul):p.tree.$div.append(p.ul)),l=0,m=w.length;m>l;l++)o=a.extend({},b,{node:w[l]}),this.nodeRender(o,d,f,!1,!0);for(i=p.ul.firstChild;i;)k=i.ftnode,k&&k.parent!==p?(p.debug("_fixParent: remove missing "+k,i),n=i.nextSibling,i.parentNode.removeChild(i),i=n):i=i.nextSibling;for(i=p.ul.firstChild,l=0,m=w.length-1;m>l;l++)j=w[l],k=i.ftnode,j!==k?p.ul.insertBefore(j.li,k.li):i=i.nextSibling}}else p.ul&&(this.warn("remove child markup for "+p),this.nodeRemoveChildMarkup(b));v||t&&u.ul.appendChild(p.li)}},nodeRenderTitle:function(a,b){var c,e,f,g,h,i,j=a.node,k=a.tree,l=a.options,m=l.aria,n=j.getLevel(),o=[],p=j.data.icon;b!==d&&(j.title=b),j.span&&(n1&&o.push(m?"":"")):o.push(m?"":""),l.checkbox&&j.hideCheckbox!==!0&&!j.isStatusNode()&&o.push(m?"":""),g=m?" role='img'":"",(p===!0||p!==!1&&l.icons!==!1)&&(p&&"string"==typeof p?(p="/"===p.charAt(0)?p:(l.imagePath||"")+p,o.push("")):(e=l.iconClass&&l.iconClass.call(k,j,a)||j.data.iconclass||null,o.push(e?"":""))),f="",l.renderTitle&&(f=l.renderTitle.call(k,{type:"renderTitle"},a)||""),f||(i=j.tooltip?" title='"+t.escapeHtml(j.tooltip)+"'":"",c=m?" id='ftal_"+j.key+"'":"",g=m?" role='treeitem'":"",h=l.titlesTabbable?" tabindex='0'":"",f=""+j.title+""),o.push(f),j.span.innerHTML=o.join(""),this.nodeRenderStatus(a))},nodeRenderStatus:function(b){var c=b.node,d=b.tree,e=b.options,f=c.hasChildren(),g=c.isLastSibling(),h=e.aria,i=a(c.span).find(".fancytree-title"),j=e._classNames,k=[],l=c[d.statusClassPropName];l&&(k.push(j.node),d.activeNode===c&&k.push(j.active),d.focusNode===c?(k.push(j.focused),h&&i.attr("aria-activedescendant",!0)):h&&i.removeAttr("aria-activedescendant"),c.expanded?(k.push(j.expanded),h&&i.attr("aria-expanded",!0)):h&&i.removeAttr("aria-expanded"),c.folder&&k.push(j.folder),f!==!1&&k.push(j.hasChildren),g&&k.push(j.lastsib),c.lazy&&null==c.children&&k.push(j.lazy),c.partsel&&k.push(j.partsel),c.unselectable&&k.push(j.unselectable),c._isLoading&&k.push(j.loading),c._error&&k.push(j.error),c.selected?(k.push(j.selected),h&&i.attr("aria-selected",!0)):h&&i.attr("aria-selected",!1),c.extraClasses&&k.push(c.extraClasses),k.push(f===!1?j.combinedExpanderPrefix+"n"+(g?"l":""):j.combinedExpanderPrefix+(c.expanded?"e":"c")+(c.lazy&&null==c.children?"d":"")+(g?"l":"")),k.push(j.combinedIconPrefix+(c.expanded?"e":"c")+(c.folder?"f":"")),l.className=k.join(" "),c.li&&(c.li.className=g?j.lastsib:"")) + },nodeSetActive:function(b,c,d){d=d||{};var f,g=b.node,h=b.tree,i=b.options,j=d.noEvents===!0,m=g===h.activeNode;return c=c!==!1,m===c?k(g):c&&!j&&this._triggerNodeEvent("beforeActivate",g,b.originalEvent)===!1?l(g,["rejected"]):void(c?(h.activeNode&&(e(h.activeNode!==g,"node was active (inconsistency)"),f=a.extend({},b,{node:h.activeNode}),h.nodeSetActive(f,!1),e(null===h.activeNode,"deactivate was out of sync?")),i.activeVisible&&g.makeVisible({scrollIntoView:!1}),h.activeNode=g,h.nodeRenderStatus(b),h.nodeSetFocus(b),j||h._triggerNodeEvent("activate",g,b.originalEvent)):(e(h.activeNode===g,"node was not active (inconsistency)"),h.activeNode=null,this.nodeRenderStatus(b),j||b.tree._triggerNodeEvent("deactivate",g,b.originalEvent)))},nodeSetExpanded:function(b,c,e){e=e||{};var f,g,h,i,j,m,n=b.node,o=b.tree,p=b.options,q=e.noAnimation===!0,r=e.noEvents===!0;if(c=c!==!1,n.expanded&&c||!n.expanded&&!c)return k(n);if(c&&!n.lazy&&!n.hasChildren())return k(n);if(!c&&n.getLevel()h;h++)this._callHook("nodeCollapseSiblings",j[h],e)}finally{p.autoCollapse=m}}return g.done(function(){c&&p.autoScroll&&!q?n.getLastChild().scrollIntoView(!0,{topNode:n}).always(function(){r||b.tree._triggerNodeEvent(c?"expand":"collapse",b)}):r||b.tree._triggerNodeEvent(c?"expand":"collapse",b)}),f=function(d){var e,f,g,h;if(n.expanded=c,o._callHook("nodeRender",b,!1,!1,!0),n.ul)if(g="none"!==n.ul.style.display,h=!!n.expanded,g===h)n.warn("nodeSetExpanded: UL.style.display already set");else{if(p.fx&&!q)return e=p.fx.duration||200,f=p.fx.easing,void a(n.ul).animate(p.fx,e,f,function(){d()});n.ul.style.display=n.expanded||!parent?"":"none"}d()},c&&n.lazy&&n.hasChildren()===d?n.load().done(function(){g.notifyWith&&g.notifyWith(n,["loaded"]),f(function(){g.resolveWith(n)})}).fail(function(a){f(function(){g.rejectWith(n,["load failed ("+a+")"])})}):f(function(){g.resolveWith(n)}),g.promise()},nodeSetFocus:function(b,c){var d,e=b.tree,f=b.node;if(c=c!==!1,e.focusNode){if(e.focusNode===f&&c)return;d=a.extend({},b,{node:e.focusNode}),e.focusNode=null,this._triggerNodeEvent("blur",d),this._callHook("nodeRenderStatus",d)}c&&(this.hasFocus()||(f.debug("nodeSetFocus: forcing container focus"),this._callHook("treeSetFocus",b,!0,!0)),f.makeVisible({scrollIntoView:!1}),e.focusNode=f,this._triggerNodeEvent("focus",b),b.options.autoScroll&&f.scrollIntoView(),this._callHook("nodeRenderStatus",b))},nodeSetSelected:function(a,b){var c=a.node,d=a.tree,e=a.options;if(b=b!==!1,c.debug("nodeSetSelected("+b+")",a),!c.unselectable){if(c.selected&&b||!c.selected&&!b)return!!c.selected;if(this._triggerNodeEvent("beforeSelect",c,a.originalEvent)===!1)return!!c.selected;b&&1===e.selectMode?d.lastSelectedNode&&d.lastSelectedNode.setSelected(!1):3===e.selectMode&&(c.selected=b,c.fixSelection3AfterClick()),c.selected=b,this.nodeRenderStatus(a),d.lastSelectedNode=b?c:null,d._triggerNodeEvent("select",a)}},nodeSetStatus:function(b,c,d,e){function f(){var a=h.children?h.children[0]:null;if(a&&a.isStatusNode()){try{h.ul&&(h.ul.removeChild(a.li),a.li=null)}catch(b){}1===h.children.length?h.children=[]:h.children.shift()}}function g(b,c){var d=h.children?h.children[0]:null;return d&&d.isStatusNode()?(a.extend(d,b),i._callHook("nodeRenderTitle",d)):(b.key="_statusNode",h._setChildren([b]),h.children[0].statusNodeType=c,i.render()),h.children[0]}var h=b.node,i=b.tree;switch(c){case"ok":f(),h._isLoading=!1,h._error=null,h.renderStatus();break;case"loading":h.parent||g({title:i.options.strings.loading+(d?" ("+d+") ":""),tooltip:e,extraClasses:"fancytree-statusnode-wait"},c),h._isLoading=!0,h._error=null,h.renderStatus();break;case"error":g({title:i.options.strings.loadError+(d?" ("+d+") ":""),tooltip:e,extraClasses:"fancytree-statusnode-error"},c),h._isLoading=!1,h._error={message:d,details:e},h.renderStatus();break;default:a.error("invalid node status "+c)}},nodeToggleExpanded:function(a){return this.nodeSetExpanded(a,!a.node.expanded)},nodeToggleSelected:function(a){return this.nodeSetSelected(a,!a.node.selected)},treeClear:function(a){var b=a.tree;b.activeNode=null,b.focusNode=null,b.$div.find(">ul.fancytree-container").empty(),b.rootNode.children=null},treeCreate:function(){},treeDestroy:function(){},treeInit:function(a){this.treeLoad(a)},treeLoad:function(b,c){var d,e,f,g=b.tree,h=b.widget.element,i=a.extend({},b,{node:this.rootNode});if(g.rootNode.children&&this.treeClear(b),c=c||this.options.source)"string"==typeof c&&a.error("Not implemented");else switch(d=h.data("type")||"html"){case"html":e=h.find(">ul:first"),e.addClass("ui-fancytree-source ui-helper-hidden"),c=a.ui.fancytree.parseHtml(e),this.data=a.extend(this.data,n(e));break;case"json":c=a.parseJSON(h.text()),c.children&&(c.title&&(g.title=c.title),c=c.children);break;default:a.error("Invalid data-type: "+d)}return f=this.nodeLoadChildren(i,c).done(function(){g.render(),3===b.options.selectMode&&g.rootNode.fixSelection3FromEndNodes(),g._triggerTreeEvent("init",null,{status:!0})}).fail(function(){g.render(),g._triggerTreeEvent("init",null,{status:!1})})},treeRegisterNode:function(){},treeSetFocus:function(a,b){b=b!==!1,b!==this.hasFocus()&&(this._hasFocus=b,this.$container.toggleClass("fancytree-treefocus",b),this._triggerTreeEvent(b?"focusTree":"blurTree"))}}),a.widget("ui.fancytree",{options:{activeVisible:!0,ajax:{type:"GET",cache:!1,dataType:"json"},aria:!1,autoActivate:!0,autoCollapse:!1,autoScroll:!1,checkbox:!1,clickFolderMode:4,debugLevel:null,disabled:!1,enableAspx:!0,extensions:[],fx:{height:"toggle",duration:200},generateIds:!1,icons:!0,idPrefix:"ft_",focusOnSelect:!1,keyboard:!0,keyPathSeparator:"/",minExpandLevel:1,quicksearch:!1,scrollOfs:{top:0,bottom:0},scrollParent:null,selectMode:2,strings:{loading:"Loading…",loadError:"Load error!"},tabbable:!0,titlesTabbable:!1,_classNames:{node:"fancytree-node",folder:"fancytree-folder",combinedExpanderPrefix:"fancytree-exp-",combinedIconPrefix:"fancytree-ico-",hasChildren:"fancytree-has-children",active:"fancytree-active",selected:"fancytree-selected",expanded:"fancytree-expanded",lazy:"fancytree-lazy",focused:"fancytree-focused",partsel:"fancytree-partsel",unselectable:"fancytree-unselectable",lastsib:"fancytree-lastsib",loading:"fancytree-loading",error:"fancytree-error"},lazyLoad:null,postProcess:null},_create:function(){this.tree=new r(this),this.$source=this.source||"json"===this.element.data("type")?this.element:this.element.find(">ul:first");var b,c,f,g=this.options.extensions,h=this.tree;for(f=0;f"),d&&a.Widget.prototype._setOption.apply(this,arguments),e&&this.tree.render(!0,!1)},destroy:function(){this._unbind(),this.tree._callHook("treeDestroy",this.tree),this.tree.$div.find(">ul.fancytree-container").remove(),this.$source&&this.$source.removeClass("ui-helper-hidden"),a.Widget.prototype.destroy.call(this)},_unbind:function(){var b=this.tree._ns;this.element.unbind(b),this.tree.$container.unbind(b),a(c).unbind(b)},_bind:function(){var a=this,b=this.options,c=this.tree,d=c._ns;this._unbind(),c.$container.on("focusin"+d+" focusout"+d,function(a){var b=t.getNode(a),d="focusin"===a.type;b?c._callHook("nodeSetFocus",b,d):c._callHook("treeSetFocus",c,d)}).on("selectstart"+d,"span.fancytree-title",function(a){a.preventDefault()}).on("keydown"+d,function(a){if(b.disabled||b.keyboard===!1)return!0;var d,e=c.focusNode,f=c._makeHookContext(e||c,a),g=c.phase;try{return c.phase="userEvent",d=e?c._triggerNodeEvent("keydown",e,a):c._triggerTreeEvent("keydown",a),"preventNav"===d?d=!0:d!==!1&&(d=c._callHook("nodeKeydown",f)),d}finally{c.phase=g}}).on("click"+d+" dblclick"+d,function(c){if(b.disabled)return!0;var d,e=t.getEventTarget(c),f=e.node,g=a.tree,h=g.phase;if(!f)return!0;d=g._makeHookContext(f,c);try{switch(g.phase="userEvent",c.type){case"click":return d.targetType=e.type,g._triggerNodeEvent("click",d,c)===!1?!1:g._callHook("nodeClick",d);case"dblclick":return d.targetType=e.type,g._triggerNodeEvent("dblclick",d,c)===!1?!1:g._callHook("nodeDblclick",d)}}finally{g.phase=h}})},getActiveNode:function(){return this.tree.activeNode},getNodeByKey:function(a){return this.tree.getNodeByKey(a)},getRootNode:function(){return this.tree.rootNode},getTree:function(){return this.tree}}),t=a.ui.fancytree,a.extend(a.ui.fancytree,{version:"2.6.0",buildType: "production",debugLevel: 1,_nextId:1,_nextNodeKey:1,_extensions:{},_FancytreeClass:r,_FancytreeNodeClass:q,jquerySupports:{positionMyOfs:h(a.ui.version,1,9)},assert:function(a,b){return e(a,b)},debounce:function(a,b,c,d){var e;return 3===arguments.length&&"boolean"!=typeof c&&(d=c,c=!1),function(){var f=arguments;d=d||this,c&&!e&&b.apply(d,f),clearTimeout(e),e=setTimeout(function(){c||b.apply(d,f),e=null},a)}},debug:function(){a.ui.fancytree.debugLevel>=2&&f("log",arguments)},error:function(){f("error",arguments)},escapeHtml:function(a){return(""+a).replace(/[&<>"'\/]/g,function(a){return u[a]})},unescapeHtml:function(a){var b=c.createElement("div");return b.innerHTML=a,0===b.childNodes.length?"":b.childNodes[0].nodeValue},getEventTargetType:function(a){return this.getEventTarget(a).type},getEventTarget:function(b){var c=b&&b.target?b.target.className:"",e={node:this.getNode(b.target),type:d};return/\bfancytree-title\b/.test(c)?e.type="title":/\bfancytree-expander\b/.test(c)?e.type=e.node.hasChildren()===!1?"prefix":"expander":/\bfancytree-checkbox\b/.test(c)||/\bfancytree-radio\b/.test(c)?e.type="checkbox":/\bfancytree-icon\b/.test(c)?e.type="icon":/\bfancytree-node\b/.test(c)?e.type="title":b&&b.target&&a(b.target).closest(".fancytree-title").length&&(e.type="title"),e},getNode:function(a){if(a instanceof q)return a;for(a.selector!==d?a=a[0]:a.originalEvent!==d&&(a=a.target);a;){if(a.ftnode)return a.ftnode;a=a.parentNode}return null},info:function(){a.ui.fancytree.debugLevel>=1&&f("info",arguments)},parseHtml:function(b){var c,e,f,g,h,i,j,k,l=b.find(">li"),m=[];return l.each(function(){var l,o=a(this),p=o.find(">span:first",this),q=p.length?null:o.find(">a:first"),r={tooltip:null,data:{}};for(p.length?r.title=p.html():q&&q.length?(r.title=q.html(),r.data.href=q.attr("href"),r.data.target=q.attr("target"),r.tooltip=q.attr("title")):(r.title=o.html(),g=r.title.search(/
      =0&&(r.title=r.title.substring(0,g))),r.title=a.trim(r.title),e=0,f=v.length;f>e;e++)r[v[e]]=d;for(j=this.className.split(" "),c=[],e=0,f=j.length;f>e;e++)k=j[e],w[k]?r[k]=!0:c.push(k);if(r.extraClasses=c.join(" "),h=o.attr("title"),h&&(r.tooltip=h),h=o.attr("id"),h&&(r.key=h),l=n(o),l&&!a.isEmptyObject(l)){for(e=0,f=x.length;f>e;e++)h=x[e],i=l[h],null!=i&&(delete l[h],r[h]=i);a.extend(r.data,l)}b=o.find(">ul:first"),r.children=b.length?a.ui.fancytree.parseHtml(b):r.lazy?d:null,m.push(r)}),m},registerExtension:function(b){e(null!=b.name,"extensions must have a `name` property."),e(null!=b.version,"extensions must have a `version` property."),a.ui.fancytree._extensions[b.name]=b},warn:function(){f("warn",arguments)}})}(jQuery,window,document); + + /*! Extension 'jquery.fancytree.filter.min.js' */ + !function(a){"use strict";function b(a){return(a+"").replace(/([.?*+\^\$\[\]\\(){}|-])/g,"\\$1")}a.ui.fancytree._FancytreeClass.prototype._applyFilterImpl=function(a,c,d){var e,f,g=0,h="hide"===this.options.filter.mode;return d=!!d&&!c,"string"==typeof a&&(e=b(a),f=new RegExp(".*"+e+".*","i"),a=function(a){return!!f.exec(a.title)}),this.enableFilter=!0,this.lastFilterArgs=arguments,this.$div.addClass("fancytree-ext-filter"),this.$div.addClass(h?"fancytree-ext-filter-hide":"fancytree-ext-filter-dimm"),this.visit(function(a){delete a.match,delete a.subMatch}),this.visit(function(b){return d&&null!=b.children||!a(b)||(g++,b.match=!0,b.visitParents(function(a){a.subMatch=!0}),!c)?void 0:(b.visit(function(a){a.match=!0}),"skip")}),this.render(),g},a.ui.fancytree._FancytreeClass.prototype.filterNodes=function(a,b){return this._applyFilterImpl(a,!1,b)},a.ui.fancytree._FancytreeClass.prototype.applyFilter=function(){return this.warn("Fancytree.applyFilter() is deprecated since 2014-05-10. Use .filterNodes() instead."),this.filterNodes.apply(this,arguments)},a.ui.fancytree._FancytreeClass.prototype.filterBranches=function(a){return this._applyFilterImpl(a,!0,null)},a.ui.fancytree._FancytreeClass.prototype.clearFilter=function(){this.visit(function(a){delete a.match,delete a.subMatch}),this.enableFilter=!1,this.lastFilterArgs=null,this.$div.removeClass("fancytree-ext-filter fancytree-ext-filter-dimm fancytree-ext-filter-hide"),this.render()},a.ui.fancytree.registerExtension({name:"filter",version:"0.3.0",options:{autoApply:!0,mode:"dimm"},treeInit:function(a){this._super(a)},nodeLoadChildren:function(a,b){return this._super(a,b).done(function(){a.tree.enableFilter&&a.tree.lastFilterArgs&&a.options.filter.autoApply&&a.tree._applyFilterImpl.apply(a.tree,a.tree.lastFilterArgs)})},nodeRenderStatus:function(b){var c,d=b.node,e=b.tree,f=a(d[e.statusClassPropName]);return c=this._super(b),f.length&&e.enableFilter?(f.toggleClass("fancytree-match",!!d.match).toggleClass("fancytree-submatch",!!d.subMatch).toggleClass("fancytree-hide",!(d.match||d.subMatch)),c):c}})}(jQuery,window,document); + + /*! Extension 'jquery.fancytree.gridnav.min.js' */ + !function(a){"use strict";function b(b,c){var d,e=c.get(0),f=0;return b.children().each(function(){return this===e?!1:(d=a(this).prop("colspan"),void(f+=d?d:1))}),f}function c(b,c){var d,e=null,f=0;return b.children().each(function(){return f>=c?(e=a(this),!1):(d=a(this).prop("colspan"),void(f+=d?d:1))}),e}function d(a,d){var f,g,h=a.closest("td"),i=null;switch(d){case e.LEFT:i=h.prev();break;case e.RIGHT:i=h.next();break;case e.UP:case e.DOWN:for(f=h.parent(),g=b(f,h);;){if(f=d===e.UP?f.prev():f.next(),!f.length)break;if(!f.is(":hidden")&&(i=c(f,g),i&&i.find(":input").length))break}}return i}var e=a.ui.keyCode,f={text:[e.UP,e.DOWN],checkbox:[e.UP,e.DOWN,e.LEFT,e.RIGHT],radiobutton:[e.UP,e.DOWN,e.LEFT,e.RIGHT],"select-one":[e.LEFT,e.RIGHT],"select-multiple":[e.LEFT,e.RIGHT]};a.ui.fancytree.registerExtension({name:"gridnav",version:"0.0.1",options:{autofocusInput:!1,handleCursorKeys:!0},treeInit:function(b){this._requireExtension("table",!0,!0),this._super(b),this.$container.addClass("fancytree-ext-gridnav"),this.$container.on("focusin",function(c){var d,e=a.ui.fancytree.getNode(c.target);e&&!e.isActive()&&(d=b.tree._makeHookContext(e,c),b.tree._callHook("nodeSetActive",d,!0))})},nodeSetActive:function(b,c){var d,e=b.options.gridnav,f=b.node,g=b.originalEvent||{},h=a(g.target).is(":input");c=c!==!1,this._super(b,c),c&&(b.options.titlesTabbable?(h||(a(f.span).find("span.fancytree-title").focus(),f.setFocus()),b.tree.$container.attr("tabindex","-1")):e.autofocusInput&&!h&&(d=a(f.tr||f.span),d.find(":input:enabled:first").focus()))},nodeKeydown:function(b){var c,e,g,h=b.options.gridnav,i=b.originalEvent,j=a(i.target);return c=j.is(":input:enabled")?j.prop("type"):null,c&&h.handleCursorKeys?(e=f[c],e&&a.inArray(i.which,e)>=0&&(g=d(j,i.which),g&&g.length)?(g.find(":input:enabled").focus(),!1):!0):(b.tree.debug("ext-gridnav NOT HANDLED",i,c),this._super(b))}})}(jQuery,window,document); + + /*! Extension 'jquery.fancytree.table.min.js' */ + !function(a,b,c){"use strict";function d(b,c){c=c||"",b||a.error("Assertion failed "+c)}function e(a,b){a.parentNode.insertBefore(b,a.nextSibling)}function f(a,b){a.visit(function(a){var c=a.tr;return c&&(c.style.display=a.hide||!b?"none":""),a.expanded?void 0:"skip"})}function g(b){var c,e,f,g=b.parent,h=g?g.children:null;if(h&&h.length>1&&h[0]!==b)for(c=a.inArray(b,h),f=h[c-1],d(f.tr);f.children&&(e=f.children[f.children.length-1],e.tr);)f=e;else f=g;return f}a.ui.fancytree.registerExtension({name:"table",version:"0.2.0",options:{checkboxColumnIdx:null,customStatus:!1,indentation:16,nodeColumnIdx:0},treeInit:function(b){var d,e,f,g=b.tree,h=g.widget.element;for(h.addClass("fancytree-container fancytree-ext-table"),g.tbody=h.find("> tbody")[0],g.columnCount=a("thead >tr >th",h).length,a(g.tbody).empty(),g.rowFragment=c.createDocumentFragment(),e=a(""),f="",b.options.aria&&(e.attr("role","row"),f=" role='gridcell'"),d=0;d":"");g.rowFragment.appendChild(e.get(0)),g.statusClassPropName="tr",g.ariaPropName="tr",this.nodeContainerAttrName="tr",this._super(b),a(g.rootNode.ul).remove(),g.rootNode.ul=null,g.$container=h,this.$container.attr("tabindex",this.options.tabbable?"0":"-1"),this.options.aria&&g.$container.attr("role","treegrid").attr("aria-readonly",!0)},nodeRemoveChildMarkup:function(b){var c=b.node;c.visit(function(b){b.tr&&(a(b.tr).remove(),b.tr=null)})},nodeRemoveMarkup:function(b){var c=b.node;c.tr&&(a(c.tr).remove(),c.tr=null),this.nodeRemoveChildMarkup(b)},nodeRender:function(b,c,h,i,j){var k,l,m,n,o,p,q,r,s=b.tree,t=b.node,u=b.options,v=!t.parent;if(j||(b.hasCollapsedParents=t.parent&&!t.parent.expanded),!v)if(t.tr)c?this.nodeRenderTitle(b):this.nodeRenderStatus(b);else{if(b.hasCollapsedParents)return void t.debug("nodeRender ignored due to unrendered parent");o=s.rowFragment.firstChild.cloneNode(!0),p=g(t),d(p),i===!0&&j?o.style.display="none":h&&b.hasCollapsedParents&&(o.style.display="none"),p.tr?e(p.tr,o):(d(!p.parent,"prev. row must have a tr, or is system root"),s.tbody.appendChild(o)),t.tr=o,t.key&&u.generateIds&&(t.tr.id=u.idPrefix+t.key),t.tr.ftnode=t,u.aria&&a(t.tr).attr("aria-labelledby","ftal_"+t.key),t.span=a("span.fancytree-node",t.tr).get(0),this.nodeRenderTitle(b),u.createNode&&u.createNode.call(s,{type:"createNode"},b)}if(u.renderNode&&u.renderNode.call(s,{type:"renderNode"},b),k=t.children,k&&(v||h||t.expanded))for(m=0,n=k.length;n>m;m++)r=a.extend({},b,{node:k[m]}),r.hasCollapsedParents=r.hasCollapsedParents||!t.expanded,this.nodeRender(r,c,h,i,!0);k&&!j&&(q=t.tr||null,l=s.tbody.firstChild,t.visit(function(a){if(a.tr){if(a.parent.expanded||"none"===a.tr.style.display||(a.tr.style.display="none",f(a,!1)),a.tr.previousSibling!==q){t.debug("_fixOrder: mismatch at node: "+a);var b=q?q.nextSibling:l;s.tbody.insertBefore(a.tr,b)}q=a.tr}}))},nodeRenderTitle:function(b){var c,d=b.node,e=b.options;this._super(b),e.checkbox&&null!=e.table.checkboxColumnIdx&&(c=a("span.fancytree-checkbox",d.span).detach(),a(d.tr).find("td:first").html(c)),d.isRoot()||this.nodeRenderStatus(b),!e.table.customStatus&&d.isStatusNode()||e.renderColumns&&e.renderColumns.call(b.tree,{type:"renderColumns"},b)},nodeRenderStatus:function(b){var c,d=b.node,e=b.options;this._super(b),a(d.tr).removeClass("fancytree-node"),c=(d.getLevel()-1)*e.table.indentation,a(d.span).css({marginLeft:c+"px"})},nodeSetExpanded:function(b,c,d){function e(a){c=c!==!1,f(b.node,c),a?c&&b.options.autoScroll&&!d.noAnimation&&b.node.hasChildren()?b.node.getLastChild().scrollIntoView(!0,{topNode:b.node}).always(function(){d.noEvents||b.tree._triggerNodeEvent(c?"expand":"collapse",b),g.resolveWith(b.node)}):(d.noEvents||b.tree._triggerNodeEvent(c?"expand":"collapse",b),g.resolveWith(b.node)):(d.noEvents||b.tree._triggerNodeEvent(c?"expand":"collapse",b),g.rejectWith(b.node))}var g=new a.Deferred,h=a.extend({},d,{noEvents:!0,noAnimation:!0});return d=d||{},this._super(b,c,h).done(function(){e(!0)}).fail(function(){e(!1)}),g.promise()},nodeSetStatus:function(b,c,d,e){if("ok"===c){var f=b.node,g=f.children?f.children[0]:null;g&&g.isStatusNode()&&a(g.tr).remove()}this._super(b,c,d,e)},treeClear:function(a){return this.nodeRemoveChildMarkup(this._makeHookContext(this.rootNode)),this._super(a)}})}(jQuery,window,document); + + /*! Extension 'jquery.fancytree.themeroller.min.js' */ + !function(a){"use strict";a.ui.fancytree.registerExtension({name:"themeroller",version:"0.0.1",options:{activeClass:"ui-state-active",foccusClass:"ui-state-focus",hoverClass:"ui-state-hover",selectedClass:"ui-state-highlight"},treeInit:function(b){this._super(b);var c=b.widget.element;"TABLE"===c[0].nodeName?(c.addClass("ui-widget ui-corner-all"),c.find(">thead tr").addClass("ui-widget-header"),c.find(">tbody").addClass("ui-widget-conent")):c.addClass("ui-widget ui-widget-content ui-corner-all"),c.delegate(".fancytree-node","mouseenter mouseleave",function(b){var c=a.ui.fancytree.getNode(b.target),d="mouseenter"===b.type;c.debug("hover: "+d),a(c.span).toggleClass("ui-state-hover ui-corner-all",d)})},treeDestroy:function(a){this._super(a),a.widget.element.removeClass("ui-widget ui-widget-content ui-corner-all")},nodeRenderStatus:function(b){var c=b.node,d=a(c.span);this._super(b),d.toggleClass("ui-state-active",c.isActive()),d.toggleClass("ui-state-focus",c.hasFocus()),d.toggleClass("ui-state-highlight",c.isSelected())}})}(jQuery,window,document); +})); diff --git a/public/vendor/ui.fancytree.min.css b/public/vendor/ui.fancytree.min.css new file mode 100644 index 0000000..dd2a653 --- /dev/null +++ b/public/vendor/ui.fancytree.min.css @@ -0,0 +1,8 @@ +/* --------------------------- ui.fancytree.min.css ----------------------------------------------------- */ +/*! + * Fancytree "Win7" skin. + * + * DON'T EDIT THE CSS FILE DIRECTLY, since it is automatically generated from + * the LESS templates. + */ +.ui-helper-hidden{display:none}ul.fancytree-container{font-family:tahoma,arial,helvetica;font-size:10pt;white-space:nowrap;padding:3px;margin:0;background-color:#fff;border:1px dotted gray;overflow:auto;min-height:0;position:relative}ul.fancytree-container ul{padding:0 0 0 16px;margin:0}ul.fancytree-container li{list-style-image:none;list-style-position:outside;list-style-type:none;-moz-background-clip:border;-moz-background-inline-policy:continuous;-moz-background-origin:padding;background-attachment:scroll;background-color:transparent;background-position:0 0;background-repeat:repeat-y;background-image:none;margin:0;padding:1px 0 0}ul.fancytree-container li.fancytree-lastsib,ul.fancytree-no-connector>li{background-image:none}.ui-fancytree-disabled ul.fancytree-container{opacity:.5;background-color:silver}#fancytree-drop-marker,span.fancytree-checkbox,span.fancytree-drag-helper-img,span.fancytree-empty,span.fancytree-expander,span.fancytree-icon,span.fancytree-radio,span.fancytree-vline{width:16px;height:16px;display:inline-block;vertical-align:top;background-repeat:no-repeat;background-image:url(icons.gif);background-position:0 0}span.fancytree-checkbox,span.fancytree-custom-icon,span.fancytree-icon,span.fancytree-radio{margin-top:1px}span.fancytree-custom-icon{display:inline-block}img.fancytree-icon{width:16px;height:16px;margin-left:3px;margin-top:1px;vertical-align:top;border-style:none}span.fancytree-expander{cursor:pointer}.fancytree-exp-n span.fancytree-expander,.fancytree-exp-nl span.fancytree-expander{background-image:none;cursor:default}.fancytree-exp-n span.fancytree-expander,.fancytree-exp-n span.fancytree-expander:hover{background-position:0 -64px}.fancytree-exp-nl span.fancytree-expander,.fancytree-exp-nl span.fancytree-expander:hover{background-position:-16px -64px}.fancytree-exp-c span.fancytree-expander{background-position:0 -80px}.fancytree-exp-c span.fancytree-expander:hover{background-position:-16px -80px}.fancytree-exp-cl span.fancytree-expander{background-position:0 -96px}.fancytree-exp-cl span.fancytree-expander:hover{background-position:-16px -96px}.fancytree-exp-cd span.fancytree-expander{background-position:-64px -80px}.fancytree-exp-cd span.fancytree-expander:hover{background-position:-80px -80px}.fancytree-exp-cdl span.fancytree-expander{background-position:-64px -96px}.fancytree-exp-cdl span.fancytree-expander:hover{background-position:-80px -96px}.fancytree-exp-e span.fancytree-expander,.fancytree-exp-ed span.fancytree-expander{background-position:-32px -80px}.fancytree-exp-e span.fancytree-expander:hover,.fancytree-exp-ed span.fancytree-expander:hover{background-position:-48px -80px}.fancytree-exp-edl span.fancytree-expander,.fancytree-exp-el span.fancytree-expander{background-position:-32px -96px}.fancytree-exp-edl span.fancytree-expander:hover,.fancytree-exp-el span.fancytree-expander:hover{background-position:-48px -96px}span.fancytree-checkbox{margin-left:3px;background-position:0 -32px}span.fancytree-checkbox:hover{background-position:-16px -32px}.fancytree-partsel span.fancytree-checkbox{background-position:-64px -32px}.fancytree-partsel span.fancytree-checkbox:hover{background-position:-80px -32px}.fancytree-selected span.fancytree-checkbox{background-position:-32px -32px}.fancytree-selected span.fancytree-checkbox:hover{background-position:-48px -32px}.fancytree-unselectable span.fancytree-checkbox,.fancytree-unselectable span.fancytree-checkbox:hover{opacity:.4;filter:alpha(opacity=40);background-position:0 -32px}.fancytree-radio span.fancytree-checkbox{background-position:0 -48px}.fancytree-radio span.fancytree-checkbox:hover{background-position:-16px -48px}.fancytree-radio .fancytree-partsel span.fancytree-checkbox{background-position:-64px -48px}.fancytree-radio .fancytree-partsel span.fancytree-checkbox:hover{background-position:-80px -48px}.fancytree-radio .fancytree-selected span.fancytree-checkbox{background-position:-32px -48px}.fancytree-radio .fancytree-selected span.fancytree-checkbox:hover{background-position:-48px -48px}.fancytree-radio .fancytree-unselectable span.fancytree-checkbox,.fancytree-radio .fancytree-unselectable span.fancytree-checkbox:hover{background-position:0 -48px}span.fancytree-icon{margin-left:3px;background-position:0 0}.fancytree-ico-c span.fancytree-icon:hover{background-position:-16px 0}.fancytree-has-children.fancytree-ico-c span.fancytree-icon{background-position:-32px 0}.fancytree-has-children.fancytree-ico-c span.fancytree-icon:hover{background-position:-48px 0}.fancytree-ico-e span.fancytree-icon{background-position:-64px 0}.fancytree-ico-e span.fancytree-icon:hover{background-position:-80px 0}.fancytree-ico-cf span.fancytree-icon{background-position:0 -16px}.fancytree-ico-cf span.fancytree-icon:hover{background-position:-16px -16px}.fancytree-has-children.fancytree-ico-cf span.fancytree-icon{background-position:-32px -16px}.fancytree-has-children.fancytree-ico-cf span.fancytree-icon:hover{background-position:-48px -16px}.fancytree-ico-ef span.fancytree-icon{background-position:-64px -16px}.fancytree-ico-ef span.fancytree-icon:hover{background-position:-80px -16px}.fancytree-loading span.fancytree-expander,.fancytree-loading span.fancytree-expander:hover,.fancytree-statusnode-wait span.fancytree-icon,.fancytree-statusnode-wait span.fancytree-icon:hover{background-image:url(loading.gif);background-position:0 0}.fancytree-statusnode-error span.fancytree-icon,.fancytree-statusnode-error span.fancytree-icon:hover{background-position:0 -112px}span.fancytree-node{display:inherit;width:100%;border:solid rgba(0,0,0,0) 1px}span.fancytree-title{display:inline-block;padding-left:3px;padding-right:3px;color:#000;vertical-align:top;margin:0;margin-left:3px;cursor:pointer}span.fancytree-node.fancytree-error span.fancytree-title{color:red}div.fancytree-drag-helper a{border:1px solid gray;background-color:#fff;padding-left:5px;padding-right:5px;opacity:.8}div.fancytree-drag-helper.fancytree-drop-reject{border-color:red}div.fancytree-drop-accept span.fancytree-drag-helper-img{background-position:-32px -112px}div.fancytree-drop-reject span.fancytree-drag-helper-img{background-position:-16px -112px}#fancytree-drop-marker{width:32px;position:absolute;background-position:0 -128px;margin:0}#fancytree-drop-marker.fancytree-drop-after,#fancytree-drop-marker.fancytree-drop-before{width:64px;background-position:0 -144px}#fancytree-drop-marker.fancytree-drop-copy{background-position:-64px -128px}#fancytree-drop-marker.fancytree-drop-move{background-position:-32px -128px}span.fancytree-drag-source{background-color:#e0e0e0}span.fancytree-drag-source span.fancytree.title{color:gray}span.fancytree-drop-target.fancytree-drop-accept a{background-color:#3169C6!important;color:#fff!important;text-decoration:none}table.fancytree-ext-table{border-collapse:collapse}table.fancytree-ext-table span.fancytree-node{display:inline-block}table.fancytree-ext-columnview tbody tr td{position:relative;border:1px solid gray;vertical-align:top;overflow:auto}table.fancytree-ext-columnview tbody tr td>ul{padding:0}table.fancytree-ext-columnview tbody tr td>ul li{list-style-image:none;list-style-position:outside;list-style-type:none;-moz-background-clip:border;-moz-background-inline-policy:continuous;-moz-background-origin:padding;background-attachment:scroll;background-color:transparent;background-position:0 0;background-repeat:repeat-y;background-image:none;margin:0;padding:1px 0 0}table.fancytree-ext-columnview span.fancytree-node{position:relative;display:inline-block}table.fancytree-ext-columnview span.fancytree-node.fancytree-expanded{background-color:#CBE8F6}table.fancytree-ext-columnview .fancytree-has-children span.fancytree-cv-right{position:absolute;right:3px;background-position:0 -80px}table.fancytree-ext-columnview .fancytree-has-children span.fancytree-cv-right:hover{background-position:-16px -80px}.fancytree-ext-filter-dimm span.fancytree-node span.fancytree-title{color:silver;font-weight:lighter}.fancytree-ext-filter-dimm span.fancytree-node.fancytree-submatch span.fancytree-title,.fancytree-ext-filter-dimm tr.fancytree-submatch span.fancytree-title{color:#000;font-weight:400}.fancytree-ext-filter-dimm span.fancytree-node.fancytree-match span.fancytree-title,.fancytree-ext-filter-dimm tr.fancytree-match span.fancytree-title{color:#000;font-weight:700}.fancytree-ext-filter-hide span.fancytree-node.fancytree-hide,.fancytree-ext-filter-hide tr.fancytree-hide{display:none}.fancytree-ext-filter-hide span.fancytree-node.fancytree-submatch span.fancytree-title,.fancytree-ext-filter-hide tr.fancytree-submatch span.fancytree-title{color:silver;font-weight:lighter}.fancytree-ext-filter-hide span.fancytree-node.fancytree-match span.fancytree-title,.fancytree-ext-filter-hide tr.fancytree-match span.fancytree-title{color:#000;font-weight:400}ul.fancytree-ext-wide span.fancytree-node>span{position:relative;z-index:2}ul.fancytree-ext-wide span.fancytree-node span.fancytree-title{position:relative;z-index:1;width:100%;padding-left:503px;margin-left:-500px}span.fancytree-title{border:1px solid transparent;border-radius:3px}span.fancytree-title:hover{border-color:#d8f0fa;color:inherit;background:-moz-linear-gradient(top,#f8fcfe 0,#eff9fe 100%);background:-webkit-gradient(linear,left top,left bottom,color-stop(0%,#f8fcfe),color-stop(100%,#eff9fe));background:-webkit-linear-gradient(top,#f8fcfe 0,#eff9fe 100%);background:-o-linear-gradient(top,#f8fcfe 0,#eff9fe 100%);background:-ms-linear-gradient(top,#f8fcfe 0,#eff9fe 100%);background:linear-gradient(to bottom,#f8fcfe 0,#eff9fe 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#f8fcfe', endColorstr='#eff9fe', GradientType=0)}span.fancytree-focused span.fancytree-title{outline:1px dotted #000}span.fancytree-active .fancytree-title{border-color:#d9d9d9;color:inherit;background:-moz-linear-gradient(top,#fafafb 0,#e5e5e5 100%);background:-webkit-gradient(linear,left top,left bottom,color-stop(0%,#fafafb),color-stop(100%,#e5e5e5));background:-webkit-linear-gradient(top,#fafafb 0,#e5e5e5 100%);background:-o-linear-gradient(top,#fafafb 0,#e5e5e5 100%);background:-ms-linear-gradient(top,#fafafb 0,#e5e5e5 100%);background:linear-gradient(to bottom,#fafafb 0,#e5e5e5 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fafafb', endColorstr='#e5e5e5', GradientType=0)}.fancytree-treefocus span.fancytree-active .fancytree-title,span.fancytree-selected .fancytree-title{border-color:#99defd;color:inherit;background:-moz-linear-gradient(top,#f6fbfd 0,#d5effc 100%);background:-webkit-gradient(linear,left top,left bottom,color-stop(0%,#f6fbfd),color-stop(100%,#d5effc));background:-webkit-linear-gradient(top,#f6fbfd 0,#d5effc 100%);background:-o-linear-gradient(top,#f6fbfd 0,#d5effc 100%);background:-ms-linear-gradient(top,#f6fbfd 0,#d5effc 100%);background:linear-gradient(to bottom,#f6fbfd 0,#d5effc 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#f6fbfd', endColorstr='#d5effc', GradientType=0)}span.fancytree-active .fancytree-title:hover,span.fancytree-active.fancytree-focused .fancytree-title,span.fancytree-selected .fancytree-title:hover,span.fancytree-selected.fancytree-focused .fancytree-title{border-color:#b6e6fb;color:inherit;background:-moz-linear-gradient(top,#f2f9fd 0,#c4e8fa 100%);background:-webkit-gradient(linear,left top,left bottom,color-stop(0%,#f2f9fd),color-stop(100%,#c4e8fa));background:-webkit-linear-gradient(top,#f2f9fd 0,#c4e8fa 100%);background:-o-linear-gradient(top,#f2f9fd 0,#c4e8fa 100%);background:-ms-linear-gradient(top,#f2f9fd 0,#c4e8fa 100%);background:linear-gradient(to bottom,#f2f9fd 0,#c4e8fa 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#f2f9fd', endColorstr='#c4e8fa', GradientType=0)}.fancytree-selected .fancytree-title{font-style:italic}table.fancytree-ext-table tbody tr td{border:1px solid #EDEDED}table.fancytree-ext-table tbody tr:hover{border-color:inherit;color:inherit;background:-moz-linear-gradient(top,#f8fcfe 0,#eff9fe 100%);background:-webkit-gradient(linear,left top,left bottom,color-stop(0%,#f8fcfe),color-stop(100%,#eff9fe));background:-webkit-linear-gradient(top,#f8fcfe 0,#eff9fe 100%);background:-o-linear-gradient(top,#f8fcfe 0,#eff9fe 100%);background:-ms-linear-gradient(top,#f8fcfe 0,#eff9fe 100%);background:linear-gradient(to bottom,#f8fcfe 0,#eff9fe 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#f8fcfe', endColorstr='#eff9fe', GradientType=0);outline:1px solid #D8F0FA}table.fancytree-ext-table tbody tr.fancytree-focused{outline:1px dotted #090402}table.fancytree-ext-table tbody span.fancytree-focused span.fancytree-title{outline:solid dotted #000}table.fancytree-ext-table tbody span.fancytree-title:hover{border:1px solid transparent;background:inherit;background:0 0;background:0 0;filter:none}table.fancytree-ext-table tbody tr.fancytree-active:hover,table.fancytree-ext-table tbody tr.fancytree-selected:hover{border-color:inherit;color:inherit;background:-moz-linear-gradient(top,#f2f9fd 0,#c4e8fa 100%);background:-webkit-gradient(linear,left top,left bottom,color-stop(0%,#f2f9fd),color-stop(100%,#c4e8fa));background:-webkit-linear-gradient(top,#f2f9fd 0,#c4e8fa 100%);background:-o-linear-gradient(top,#f2f9fd 0,#c4e8fa 100%);background:-ms-linear-gradient(top,#f2f9fd 0,#c4e8fa 100%);background:linear-gradient(to bottom,#f2f9fd 0,#c4e8fa 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#f2f9fd', endColorstr='#c4e8fa', GradientType=0);outline:1px solid #B6E6FB}table.fancytree-ext-table tbody tr.fancytree-active,table.fancytree-ext-table tbody tr.fancytree-selected{border-color:inherit;color:inherit;background:-moz-linear-gradient(top,#f6fbfd 0,#d5effc 100%);background:-webkit-gradient(linear,left top,left bottom,color-stop(0%,#f6fbfd),color-stop(100%,#d5effc));background:-webkit-linear-gradient(top,#f6fbfd 0,#d5effc 100%);background:-o-linear-gradient(top,#f6fbfd 0,#d5effc 100%);background:-ms-linear-gradient(top,#f6fbfd 0,#d5effc 100%);background:linear-gradient(to bottom,#f6fbfd 0,#d5effc 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#f6fbfd', endColorstr='#d5effc', GradientType=0);outline:1px solid #99DEFD} diff --git a/settings.js b/settings.js new file mode 100644 index 0000000..8e213cb --- /dev/null +++ b/settings.js @@ -0,0 +1,144 @@ +/** + * Copyright 2013 IBM Corp. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + **/ + +// The `https` setting requires the `fs` module. Uncomment the following +// to make it available: +//var fs = require("fs"); + +module.exports = { + // the tcp port that the Node-RED web server is listening on + uiPort: '%%port%%', + uiHost: '%%bind%%', + + // By default, the Node-RED UI accepts connections on all IPv4 interfaces. + // The following property can be used to listen on a specific interface. For + // example, the following would only allow connections from the local machine. + //uiHost: "127.0.0.1", + iobrokerInstance: '%%instance%%', + iobrokerConfig: '%%config%%', + + // Retry time in milliseconds for MQTT connections + mqttReconnectTime: 15000, + + // Retry time in milliseconds for Serial port connections + serialReconnectTime: 15000, + + // Retry time in milliseconds for TCP socket connections + //socketReconnectTime: 10000, + + // Timeout in milliseconds for TCP server socket connections + // defaults to no timeout + //socketTimeout: 120000, + + // Maximum number of lines in debug window before pruning + debugMaxLength: 1000, + + // The file containing the flows. If not set, it defaults to flows_.json + flowFile: 'flows.json', + + // To enabled pretty-printing of the flow within the flow file, set the following + // property to true: + flowFilePretty: true, + + // By default, all user data is stored in the Node-RED install directory. To + // use a different location, the following property can be used + userDir: __dirname + '/', + + // Node-RED scans the `nodes` directory in the install directory to find nodes. + // The following property can be used to specify an additional directory to scan. + nodesDir: '%%nodesdir%%', + + // By default, the Node-RED UI is available at http://localhost:1880/ + // The following property can be used to specify a different root path. + // If set to false, this is disabled. + //httpAdminRoot: '/admin', + + // You can protect the user interface with a userid and password by using the following property. + // The password must be an md5 hash eg.. 5f4dcc3b5aa765d61d8327deb882cf99 ('password') + httpAdminAuth: '%%auth%%', + + // Some nodes, such as HTTP In, can be used to listen for incoming http requests. + // By default, these are served relative to '/'. The following property + // can be used to specifiy a different root path. If set to false, this is + // disabled. + //httpNodeRoot: '/nodes', + + // To password protect the node-defined HTTP endpoints, the following property + // can be used. + // The password must be an md5 hash eg.. 5f4dcc3b5aa765d61d8327deb882cf99 ('password') + //httpNodeAuth: {user:"user",pass:"5f4dcc3b5aa765d61d8327deb882cf99"}, + + // When httpAdminRoot is used to move the UI to a different root path, the + // following property can be used to identify a directory of static content + // that should be served at http://localhost:1880/. + //httpStatic: '/home/nol/node-red-dashboard/', + + // To password protect the static content, the following property can be used. + // The password must be an md5 hash eg.. 5f4dcc3b5aa765d61d8327deb882cf99 ('password') + //httpStaticAuth: {user:"user",pass:"5f4dcc3b5aa765d61d8327deb882cf99"}, + + // The following property can be used in place of 'httpAdminRoot' and 'httpNodeRoot', + // to apply the same root to both parts. + httpRoot: "'%%httpRoot%%'", + + // The following property can be used in place of 'httpAdminAuth' and 'httpNodeAuth', + // to apply the same authentication to both parts. + //httpAuth: {user:"user",pass:"5f4dcc3b5aa765d61d8327deb882cf99"}, + + // The following property can be used to disable the editor. The admin API + // is not affected by this option. To disable both the editor and the admin + // API, use either the httpRoot or httpAdminRoot properties + //disableEditor: false, + + // The following property can be used to enable HTTPS + // See http://nodejs.org/api/https.html#https_https_createserver_options_requestlistener + // for details on its contents. + // See the comment at the top of this file on how to load the `fs` module used by + // this setting. + // + //https: { + // key: fs.readFileSync('privatekey.pem'), + // cert: fs.readFileSync('certificate.pem') + //}, + + // The following property can be used to configure cross-origin resource sharing + // in the HTTP nodes. + // See https://github.com/troygoode/node-cors#configuration-options for + // details on its contents. The following is a basic permissive set of options: + //httpNodeCors: { + // origin: "*", + // methods: "GET,PUT,POST,DELETE" + //}, + + // Anything in this hash is globally available to all functions. + // It is accessed as context.global. + // eg: + // functionGlobalContext: { os:require('os') } + // can be accessed in a function block as: + // context.global.os + + valueConvert: '%%valueConvert%%', + + credentialSecret: "'%%credentialSecret%%'", + + functionGlobalContext: { + //'%%functionGlobalContext%%' + // os:require('os'), + // bonescript:require('bonescript'), + // arduino:require('duino') + } + +}; diff --git a/tasks/jscs.js b/tasks/jscs.js new file mode 100644 index 0000000..588b6f2 --- /dev/null +++ b/tasks/jscs.js @@ -0,0 +1,17 @@ +var srcDir = __dirname + "/../"; + +module.exports = { + all: { + src: [ + srcDir + "*.js", + srcDir + "lib/*.js", + srcDir + "adapter/example/*.js", + srcDir + "tasks/**/*.js", + srcDir + "www/**/*.js", + '!' + srcDir + "www/lib/**/*.js", + '!' + srcDir + 'node_modules/**/*.js', + '!' + srcDir + 'adapter/*/node_modules/**/*.js' + ], + options: require('./jscsRules.js') + } +}; \ No newline at end of file diff --git a/tasks/jscsRules.js b/tasks/jscsRules.js new file mode 100644 index 0000000..ded301d --- /dev/null +++ b/tasks/jscsRules.js @@ -0,0 +1,36 @@ +module.exports = { + force: true, + "requireCurlyBraces": ["else", "for", "while", "do", "try", "catch"], /*"if",*/ + "requireSpaceAfterKeywords": ["if", "else", "for", "while", "do", "switch", "return", "try", "catch"], + "requireSpaceBeforeBlockStatements": true, + "requireParenthesesAroundIIFE": true, + "disallowSpacesInFunctionDeclaration": {"beforeOpeningRoundBrace": true}, + "disallowSpacesInNamedFunctionExpression": {"beforeOpeningRoundBrace": true}, + "requireSpacesInFunctionExpression": {"beforeOpeningCurlyBrace": true}, + "requireSpacesInAnonymousFunctionExpression": {"beforeOpeningRoundBrace": true, "beforeOpeningCurlyBrace": true}, + "requireSpacesInNamedFunctionExpression": {"beforeOpeningCurlyBrace": true}, + "requireSpacesInFunctionDeclaration": {"beforeOpeningCurlyBrace": true}, + "disallowMultipleVarDecl": true, + "requireBlocksOnNewline": true, + "disallowEmptyBlocks": true, + "disallowSpacesInsideObjectBrackets": true, + "disallowSpacesInsideArrayBrackets": true, + "disallowSpaceAfterObjectKeys": true, + "disallowSpacesInsideParentheses": true, + "requireCommaBeforeLineBreak": true, + //"requireAlignedObjectValues": "all", + "requireOperatorBeforeLineBreak": ["?", "+", "-", "/", "*", "=", "==", "===", "!=", "!==", ">", ">=", "<", "<="], +// "disallowLeftStickedOperators": ["?", "+", "/", "*", "=", "==", "===", "!=", "!==", ">", ">=", "<", "<="], +// "requireRightStickedOperators": ["!"], +// "requireSpaceAfterBinaryOperators": ["?", "+", "/", "*", ":", "=", "==", "===", "!=", "!==", ">", ">=", "<", "<="], + //"disallowSpaceAfterBinaryOperators": [","], + "disallowSpaceAfterPrefixUnaryOperators": ["++", "--", "+", "-", "~", "!"], + "disallowSpaceBeforePostfixUnaryOperators": ["++", "--"], + "requireSpaceBeforeBinaryOperators": ["+", "-", "/", "*", "=", "==", "===", "!=", "!=="], + "requireSpaceAfterBinaryOperators": ["?", ">", ",", ">=", "<=", "<", "+", "-", "/", "*", "=", "==", "===", "!=", "!=="], + //"validateIndentation": 4, + //"validateQuoteMarks": { "mark": "\"", "escape": true }, + "disallowMixedSpacesAndTabs": true, + "disallowKeywordsOnNewLine": ["else", "catch"] + +}; diff --git a/tasks/jshint.js b/tasks/jshint.js new file mode 100644 index 0000000..f823ebc --- /dev/null +++ b/tasks/jshint.js @@ -0,0 +1,17 @@ +var srcDir = __dirname + "/../"; + +module.exports = { + options: { + force: true + }, + all: [ + srcDir + "*.js", + srcDir + "lib/*.js", + srcDir + "adapter/example/*.js", + srcDir + "tasks/**/*.js", + srcDir + "www/**/*.js", + '!' + srcDir + "www/lib/**/*.js", + '!' + srcDir + 'node_modules/**/*.js', + '!' + srcDir + 'adapter/*/node_modules/**/*.js' + ] +}; \ No newline at end of file diff --git a/test/lib/setup.js b/test/lib/setup.js new file mode 100644 index 0000000..16857ed --- /dev/null +++ b/test/lib/setup.js @@ -0,0 +1,728 @@ +/* jshint -W097 */// jshint strict:false +/*jslint node: true */ +// check if tmp directory exists +var fs = require('fs'); +var path = require('path'); +var child_process = require('child_process'); +var rootDir = path.normalize(__dirname + '/../../'); +var pkg = require(rootDir + 'package.json'); +var debug = typeof v8debug === 'object'; +pkg.main = pkg.main || 'main.js'; + +var adapterName = path.normalize(rootDir).replace(/\\/g, '/').split('/'); +adapterName = adapterName[adapterName.length - 2]; +var adapterStarted = false; + +function getAppName() { + var parts = __dirname.replace(/\\/g, '/').split('/'); + return parts[parts.length - 3].split('.')[0]; +} + +var appName = getAppName().toLowerCase(); + +var objects; +var states; + +var pid = null; + +function copyFileSync(source, target) { + + var targetFile = target; + + //if target is a directory a new file with the same name will be created + if (fs.existsSync(target)) { + if ( fs.lstatSync( target ).isDirectory() ) { + targetFile = path.join(target, path.basename(source)); + } + } + + try { + fs.writeFileSync(targetFile, fs.readFileSync(source)); + } + catch (err) { + console.log("file copy error: " +source +" -> " + targetFile + " (error ignored)"); + } +} + +function copyFolderRecursiveSync(source, target, ignore) { + var files = []; + + var base = path.basename(source); + if (base === adapterName) { + base = pkg.name; + } + //check if folder needs to be created or integrated + var targetFolder = path.join(target, base); + if (!fs.existsSync(targetFolder)) { + fs.mkdirSync(targetFolder); + } + + //copy + if (fs.lstatSync(source).isDirectory()) { + files = fs.readdirSync(source); + files.forEach(function (file) { + if (ignore && ignore.indexOf(file) !== -1) { + return; + } + + var curSource = path.join(source, file); + var curTarget = path.join(targetFolder, file); + if (fs.lstatSync(curSource).isDirectory()) { + // ignore grunt files + if (file.indexOf('grunt') !== -1) return; + if (file === 'chai') return; + if (file === 'mocha') return; + copyFolderRecursiveSync(curSource, targetFolder, ignore); + } else { + copyFileSync(curSource, curTarget); + } + }); + } +} + +if (!fs.existsSync(rootDir + 'tmp')) { + fs.mkdirSync(rootDir + 'tmp'); +} + +function storeOriginalFiles() { + console.log('Store original files...'); + var dataDir = rootDir + 'tmp/' + appName + '-data/'; + + var f = fs.readFileSync(dataDir + 'objects.json'); + var objects = JSON.parse(f.toString()); + if (objects['system.adapter.admin.0'] && objects['system.adapter.admin.0'].common) { + objects['system.adapter.admin.0'].common.enabled = false; + } + if (objects['system.adapter.admin.1'] && objects['system.adapter.admin.1'].common) { + objects['system.adapter.admin.1'].common.enabled = false; + } + + fs.writeFileSync(dataDir + 'objects.json.original', JSON.stringify(objects)); + try { + f = fs.readFileSync(dataDir + 'states.json'); + fs.writeFileSync(dataDir + 'states.json.original', f); + } + catch (err) { + console.log('no states.json found - ignore'); + } +} + +function restoreOriginalFiles() { + console.log('restoreOriginalFiles...'); + var dataDir = rootDir + 'tmp/' + appName + '-data/'; + + var f = fs.readFileSync(dataDir + 'objects.json.original'); + fs.writeFileSync(dataDir + 'objects.json', f); + try { + f = fs.readFileSync(dataDir + 'states.json.original'); + fs.writeFileSync(dataDir + 'states.json', f); + } + catch (err) { + console.log('no states.json.original found - ignore'); + } + +} + +function checkIsAdapterInstalled(cb, counter, customName) { + customName = customName || pkg.name.split('.').pop(); + counter = counter || 0; + var dataDir = rootDir + 'tmp/' + appName + '-data/'; + console.log('checkIsAdapterInstalled...'); + + try { + var f = fs.readFileSync(dataDir + 'objects.json'); + var objects = JSON.parse(f.toString()); + if (objects['system.adapter.' + customName + '.0']) { + console.log('checkIsAdapterInstalled: ready!'); + setTimeout(function () { + if (cb) cb(); + }, 100); + return; + } else { + console.warn('checkIsAdapterInstalled: still not ready'); + } + } catch (err) { + + } + + if (counter > 20) { + console.error('checkIsAdapterInstalled: Cannot install!'); + if (cb) cb('Cannot install'); + } else { + console.log('checkIsAdapterInstalled: wait...'); + setTimeout(function() { + checkIsAdapterInstalled(cb, counter + 1); + }, 1000); + } +} + +function checkIsControllerInstalled(cb, counter) { + counter = counter || 0; + var dataDir = rootDir + 'tmp/' + appName + '-data/'; + + console.log('checkIsControllerInstalled...'); + try { + var f = fs.readFileSync(dataDir + 'objects.json'); + var objects = JSON.parse(f.toString()); + if (objects['system.adapter.admin.0']) { + console.log('checkIsControllerInstalled: installed!'); + setTimeout(function () { + if (cb) cb(); + }, 100); + return; + } + } catch (err) { + + } + + if (counter > 20) { + console.log('checkIsControllerInstalled: Cannot install!'); + if (cb) cb('Cannot install'); + } else { + console.log('checkIsControllerInstalled: wait...'); + setTimeout(function() { + checkIsControllerInstalled(cb, counter + 1); + }, 1000); + } +} + +function installAdapter(customName, cb) { + if (typeof customName === 'function') { + cb = customName; + customName = null; + } + customName = customName || pkg.name.split('.').pop(); + console.log('Install adapter...'); + var startFile = 'node_modules/' + appName + '.js-controller/' + appName + '.js'; + // make first install + if (debug) { + child_process.execSync('node ' + startFile + ' add ' + customName + ' --enabled false', { + cwd: rootDir + 'tmp', + stdio: [0, 1, 2] + }); + checkIsAdapterInstalled(function (error) { + if (error) console.error(error); + console.log('Adapter installed.'); + if (cb) cb(); + }); + } else { + // add controller + var _pid = child_process.fork(startFile, ['add', customName, '--enabled', 'false'], { + cwd: rootDir + 'tmp', + stdio: [0, 1, 2, 'ipc'] + }); + + waitForEnd(_pid, function () { + checkIsAdapterInstalled(function (error) { + if (error) console.error(error); + console.log('Adapter installed.'); + if (cb) cb(); + }); + }); + } +} + +function waitForEnd(_pid, cb) { + if (!_pid) { + cb(-1, -1); + return; + } + _pid.on('exit', function (code, signal) { + if (_pid) { + _pid = null; + cb(code, signal); + } + }); + _pid.on('close', function (code, signal) { + if (_pid) { + _pid = null; + cb(code, signal); + } + }); +} + +function installJsController(cb) { + console.log('installJsController...'); + if (!fs.existsSync(rootDir + 'tmp/node_modules/' + appName + '.js-controller') || + !fs.existsSync(rootDir + 'tmp/' + appName + '-data')) { + // try to detect appName.js-controller in node_modules/appName.js-controller + // travis CI installs js-controller into node_modules + if (fs.existsSync(rootDir + 'node_modules/' + appName + '.js-controller')) { + console.log('installJsController: no js-controller => copy it from "' + rootDir + 'node_modules/' + appName + '.js-controller"'); + // copy all + // stop controller + console.log('Stop controller if running...'); + var _pid; + if (debug) { + // start controller + _pid = child_process.exec('node ' + appName + '.js stop', { + cwd: rootDir + 'node_modules/' + appName + '.js-controller', + stdio: [0, 1, 2] + }); + } else { + _pid = child_process.fork(appName + '.js', ['stop'], { + cwd: rootDir + 'node_modules/' + appName + '.js-controller', + stdio: [0, 1, 2, 'ipc'] + }); + } + + waitForEnd(_pid, function () { + // copy all files into + if (!fs.existsSync(rootDir + 'tmp')) fs.mkdirSync(rootDir + 'tmp'); + if (!fs.existsSync(rootDir + 'tmp/node_modules')) fs.mkdirSync(rootDir + 'tmp/node_modules'); + + if (!fs.existsSync(rootDir + 'tmp/node_modules/' + appName + '.js-controller')){ + console.log('Copy js-controller...'); + copyFolderRecursiveSync(rootDir + 'node_modules/' + appName + '.js-controller', rootDir + 'tmp/node_modules/'); + } + + console.log('Setup js-controller...'); + var __pid; + if (debug) { + // start controller + _pid = child_process.exec('node ' + appName + '.js setup first --console', { + cwd: rootDir + 'tmp/node_modules/' + appName + '.js-controller', + stdio: [0, 1, 2] + }); + } else { + __pid = child_process.fork(appName + '.js', ['setup', 'first', '--console'], { + cwd: rootDir + 'tmp/node_modules/' + appName + '.js-controller', + stdio: [0, 1, 2, 'ipc'] + }); + } + waitForEnd(__pid, function () { + checkIsControllerInstalled(function () { + // change ports for object and state DBs + var config = require(rootDir + 'tmp/' + appName + '-data/' + appName + '.json'); + config.objects.port = 19001; + config.states.port = 19000; + fs.writeFileSync(rootDir + 'tmp/' + appName + '-data/' + appName + '.json', JSON.stringify(config, null, 2)); + console.log('Setup finished.'); + + copyAdapterToController(); + + installAdapter(function () { + storeOriginalFiles(); + if (cb) cb(true); + }); + }); + }); + }); + } else { + // check if port 9000 is free, else admin adapter will be added to running instance + var client = new require('net').Socket(); + client.connect(9000, '127.0.0.1', function() { + console.error('Cannot initiate fisrt run of test, because one instance of application is running on this PC. Stop it and repeat.'); + process.exit(0); + }); + + setTimeout(function () { + client.destroy(); + if (!fs.existsSync(rootDir + 'tmp/node_modules/' + appName + '.js-controller')) { + console.log('installJsController: no js-controller => install from git'); + + child_process.execSync('npm install https://github.com/' + appName + '/' + appName + '.js-controller/tarball/master --prefix ./ --production', { + cwd: rootDir + 'tmp/', + stdio: [0, 1, 2] + }); + } else { + console.log('Setup js-controller...'); + var __pid; + if (debug) { + // start controller + child_process.exec('node ' + appName + '.js setup first', { + cwd: rootDir + 'tmp/node_modules/' + appName + '.js-controller', + stdio: [0, 1, 2] + }); + } else { + child_process.fork(appName + '.js', ['setup', 'first'], { + cwd: rootDir + 'tmp/node_modules/' + appName + '.js-controller', + stdio: [0, 1, 2, 'ipc'] + }); + } + } + + // let npm install admin and run setup + checkIsControllerInstalled(function () { + var _pid; + + if (fs.existsSync(rootDir + 'node_modules/' + appName + '.js-controller/' + appName + '.js')) { + _pid = child_process.fork(appName + '.js', ['stop'], { + cwd: rootDir + 'node_modules/' + appName + '.js-controller', + stdio: [0, 1, 2, 'ipc'] + }); + } + + waitForEnd(_pid, function () { + // change ports for object and state DBs + var config = require(rootDir + 'tmp/' + appName + '-data/' + appName + '.json'); + config.objects.port = 19001; + config.states.port = 19000; + fs.writeFileSync(rootDir + 'tmp/' + appName + '-data/' + appName + '.json', JSON.stringify(config, null, 2)); + + copyAdapterToController(); + + installAdapter(function () { + storeOriginalFiles(); + if (cb) cb(true); + }); + }); + }); + }, 1000); + } + } else { + setTimeout(function () { + console.log('installJsController: js-controller installed'); + if (cb) cb(false); + }, 0); + } +} + +function copyAdapterToController() { + console.log('Copy adapter...'); + // Copy adapter to tmp/node_modules/appName.adapter + copyFolderRecursiveSync(rootDir, rootDir + 'tmp/node_modules/', ['.idea', 'test', 'tmp', '.git', appName + '.js-controller']); + console.log('Adapter copied.'); +} + +function clearControllerLog() { + var dirPath = rootDir + 'tmp/log'; + var files; + try { + if (fs.existsSync(dirPath)) { + console.log('Clear controller log...'); + files = fs.readdirSync(dirPath); + } else { + console.log('Create controller log directory...'); + files = []; + fs.mkdirSync(dirPath); + } + } catch(e) { + console.error('Cannot read "' + dirPath + '"'); + return; + } + if (files.length > 0) { + try { + for (var i = 0; i < files.length; i++) { + var filePath = dirPath + '/' + files[i]; + fs.unlinkSync(filePath); + } + console.log('Controller log cleared'); + } catch (err) { + console.error('cannot clear log: ' + err); + } + } +} + +function clearDB() { + var dirPath = rootDir + 'tmp/iobroker-data/sqlite'; + var files; + try { + if (fs.existsSync(dirPath)) { + console.log('Clear sqlite DB...'); + files = fs.readdirSync(dirPath); + } else { + console.log('Create controller log directory...'); + files = []; + fs.mkdirSync(dirPath); + } + } catch(e) { + console.error('Cannot read "' + dirPath + '"'); + return; + } + if (files.length > 0) { + try { + for (var i = 0; i < files.length; i++) { + var filePath = dirPath + '/' + files[i]; + fs.unlinkSync(filePath); + } + console.log('Clear sqlite DB'); + } catch (err) { + console.error('cannot clear DB: ' + err); + } + } +} + +function setupController(cb) { + installJsController(function (isInited) { + clearControllerLog(); + clearDB(); + + if (!isInited) { + restoreOriginalFiles(); + copyAdapterToController(); + } + // read system.config object + var dataDir = rootDir + 'tmp/' + appName + '-data/'; + + var objs; + try { + objs = fs.readFileSync(dataDir + 'objects.json'); + objs = JSON.parse(objs); + } + catch (e) { + console.log('ERROR reading/parsing system configuration. Ignore'); + objs = {'system.config': {}}; + } + if (!objs || !objs['system.config']) { + objs = {'system.config': {}}; + } + + if (cb) cb(objs['system.config']); + }); +} + +function startAdapter(objects, states, callback) { + if (adapterStarted) { + console.log('Adapter already started ...'); + if (callback) callback(objects, states); + return; + } + adapterStarted = true; + console.log('startAdapter...'); + if (fs.existsSync(rootDir + 'tmp/node_modules/' + pkg.name + '/' + pkg.main)) { + try { + if (debug) { + // start controller + pid = child_process.exec('node node_modules/' + pkg.name + '/' + pkg.main + ' --console silly', { + cwd: rootDir + 'tmp', + stdio: [0, 1, 2] + }); + } else { + // start controller + pid = child_process.fork('node_modules/' + pkg.name + '/' + pkg.main, ['--console', 'silly'], { + cwd: rootDir + 'tmp', + stdio: [0, 1, 2, 'ipc'] + }); + } + } catch (error) { + console.error(JSON.stringify(error)); + } + } else { + console.error('Cannot find: ' + rootDir + 'tmp/node_modules/' + pkg.name + '/' + pkg.main); + } + if (callback) callback(objects, states); +} + +function startController(isStartAdapter, onObjectChange, onStateChange, callback) { + if (typeof isStartAdapter === 'function') { + callback = onStateChange; + onStateChange = onObjectChange; + onObjectChange = isStartAdapter; + isStartAdapter = true; + } + + if (onStateChange === undefined) { + callback = onObjectChange; + onObjectChange = undefined; + } + + if (pid) { + console.error('Controller is already started!'); + } else { + console.log('startController...'); + adapterStarted = false; + var isObjectConnected; + var isStatesConnected; + + var Objects = require(rootDir + 'tmp/node_modules/' + appName + '.js-controller/lib/objects/objectsInMemServer'); + objects = new Objects({ + connection: { + "type" : "file", + "host" : "127.0.0.1", + "port" : 19001, + "user" : "", + "pass" : "", + "noFileCache": false, + "connectTimeout": 2000 + }, + logger: { + silly: function (msg) { + console.log(msg); + }, + debug: function (msg) { + console.log(msg); + }, + info: function (msg) { + console.log(msg); + }, + warn: function (msg) { + console.warn(msg); + }, + error: function (msg) { + console.error(msg); + } + }, + connected: function () { + isObjectConnected = true; + if (isStatesConnected) { + console.log('startController: started!'); + if (isStartAdapter) { + startAdapter(objects, states, callback); + } else { + if (callback) { + callback(objects, states); + callback = null; + } + } + } + }, + change: onObjectChange + }); + + // Just open in memory DB itself + var States = require(rootDir + 'tmp/node_modules/' + appName + '.js-controller/lib/states/statesInMemServer'); + states = new States({ + connection: { + type: 'file', + host: '127.0.0.1', + port: 19000, + options: { + auth_pass: null, + retry_max_delay: 15000 + } + }, + logger: { + silly: function (msg) { + console.log(msg); + }, + debug: function (msg) { + console.log(msg); + }, + info: function (msg) { + console.log(msg); + }, + warn: function (msg) { + console.log(msg); + }, + error: function (msg) { + console.log(msg); + } + }, + connected: function () { + isStatesConnected = true; + if (isObjectConnected) { + console.log('startController: started!!'); + if (isStartAdapter) { + startAdapter(objects, states, callback); + } else { + if (callback) { + callback(objects, states); + callback = null; + } + } + } + }, + change: onStateChange + }); + } +} + +function stopAdapter(cb) { + if (!pid) { + console.error('Controller is not running!'); + if (cb) { + setTimeout(function () { + cb(false); + }, 0); + } + } else { + adapterStarted = false; + pid.on('exit', function (code, signal) { + if (pid) { + console.log('child process terminated due to receipt of signal ' + signal); + if (cb) cb(); + pid = null; + } + }); + + pid.on('close', function (code, signal) { + if (pid) { + if (cb) cb(); + pid = null; + } + }); + + pid.kill('SIGTERM'); + } +} + +function _stopController() { + if (objects) { + objects.destroy(); + objects = null; + } + if (states) { + states.destroy(); + states = null; + } +} + +function stopController(cb) { + var timeout; + if (objects) { + console.log('Set system.adapter.' + pkg.name + '.0'); + objects.setObject('system.adapter.' + pkg.name + '.0', { + common:{ + enabled: false + } + }); + } + + stopAdapter(function () { + if (timeout) { + clearTimeout(timeout); + timeout = null; + } + + _stopController(); + + if (cb) { + cb(true); + cb = null; + } + }); + + timeout = setTimeout(function () { + timeout = null; + console.log('child process NOT terminated'); + + _stopController(); + + if (cb) { + cb(false); + cb = null; + } + pid = null; + }, 5000); +} + +// Setup the adapter +function setAdapterConfig(common, native, instance) { + var objects = JSON.parse(fs.readFileSync(rootDir + 'tmp/' + appName + '-data/objects.json').toString()); + var id = 'system.adapter.' + adapterName.split('.').pop() + '.' + (instance || 0); + if (common) objects[id].common = common; + if (native) objects[id].native = native; + fs.writeFileSync(rootDir + 'tmp/' + appName + '-data/objects.json', JSON.stringify(objects)); +} + +// Read config of the adapter +function getAdapterConfig(instance) { + var objects = JSON.parse(fs.readFileSync(rootDir + 'tmp/' + appName + '-data/objects.json').toString()); + var id = 'system.adapter.' + adapterName.split('.').pop() + '.' + (instance || 0); + return objects[id]; +} + +if (typeof module !== undefined && module.parent) { + module.exports.getAdapterConfig = getAdapterConfig; + module.exports.setAdapterConfig = setAdapterConfig; + module.exports.startController = startController; + module.exports.stopController = stopController; + module.exports.setupController = setupController; + module.exports.stopAdapter = stopAdapter; + module.exports.startAdapter = startAdapter; + module.exports.installAdapter = installAdapter; + module.exports.appName = appName; + module.exports.adapterName = adapterName; + module.exports.adapterStarted = adapterStarted; +} diff --git a/test/testFunctions.js b/test/testFunctions.js new file mode 100644 index 0000000..fc37111 --- /dev/null +++ b/test/testFunctions.js @@ -0,0 +1,105 @@ +var expect = require('chai').expect; +var setup = require(__dirname + '/lib/setup'); +var request = require('request'); + +var objects = null; +var states = null; +var onStateChanged = null; +var onObjectChanged = null; +var port = 18888; + +function checkConnectionOfAdapter(cb, counter) { + counter = counter || 0; + if (counter > 20) { + cb && cb('Cannot check connection'); + return; + } + + states.getState('system.adapter.node-red.0.alive', function (err, state) { + if (err) console.error(err); + if (state && state.val) { + cb && cb(); + } else { + setTimeout(function () { + checkConnectionOfAdapter(cb, counter + 1); + }, 1000); + } + }); +} + +function checkValueOfState(id, value, cb, counter) { + counter = counter || 0; + if (counter > 20) { + cb && cb('Cannot check value Of State ' + id); + return; + } + + states.getState(id, function (err, state) { + if (err) console.error(err); + if (value === null && !state) { + cb && cb(); + } else + if (state && (value === undefined || state.val === value)) { + cb && cb(); + } else { + setTimeout(function () { + checkValueOfState(id, value, cb, counter + 1); + }, 500); + } + }); +} + +describe('Test node-red', function() { + before('Test node-red: Start js-controller', function (_done) { + this.timeout(600000); // because of first install from npm + + setup.setupController(function () { + var config = setup.getAdapterConfig(); + // enable adapter + config.common.enabled = true; + config.common.loglevel = 'debug'; + config.native.port = port; + + setup.setAdapterConfig(config.common, config.native); + + setup.startController(true, function (id, obj) { + if (onObjectChanged) onObjectChanged(id, obj); + }, function (id, state) { + if (onStateChanged) onStateChanged(id, state); + }, + function (_objects, _states) { + objects = _objects; + states = _states; + states.subscribe('*'); + _done(); + }); + }); + }); + + it('Test node-red: Check if adapter started', function (done) { + this.timeout(5000); + checkConnectionOfAdapter(done); + }); + + it('Test node-red: check creation of state', function (done) { + this.timeout(20000); + // check if node-red is running + + setTimeout(function () { + request('http://localhost:' + port, function (error, response, body) { + expect(error).to.be.not.ok; + expect(body).to.be.ok; + done(); + }); + }, 5000); + }); + + after('Test node-red: Stop js-controller', function (done) { + this.timeout(6000); + + setup.stopController(function (normalTerminated) { + console.log('Adapter normal terminated: ' + normalTerminated); + done(); + }); + }); +}); \ No newline at end of file diff --git a/test/testPackageFiles.js b/test/testPackageFiles.js new file mode 100644 index 0000000..c600a60 --- /dev/null +++ b/test/testPackageFiles.js @@ -0,0 +1,91 @@ +/* jshint -W097 */ +/* jshint strict:false */ +/* jslint node: true */ +/* jshint expr: true */ +var expect = require('chai').expect; +var fs = require('fs'); + +describe('Test package.json and io-package.json', function() { + it('Test package files', function (done) { + console.log(); + + var fileContentIOPackage = fs.readFileSync(__dirname + '/../io-package.json', 'utf8'); + var ioPackage = JSON.parse(fileContentIOPackage); + + var fileContentNPMPackage = fs.readFileSync(__dirname + '/../package.json', 'utf8'); + var npmPackage = JSON.parse(fileContentNPMPackage); + + expect(ioPackage).to.be.an('object'); + expect(npmPackage).to.be.an('object'); + + expect(ioPackage.common.version, 'ERROR: Version number in io-package.json needs to exist').to.exist; + expect(npmPackage.version, 'ERROR: Version number in package.json needs to exist').to.exist; + + expect(ioPackage.common.version, 'ERROR: Version numbers in package.json and io-package.json needs to match').to.be.equal(npmPackage.version); + + if (!ioPackage.common.news || !ioPackage.common.news[ioPackage.common.version]) { + console.log('WARNING: No news entry for current version exists in io-package.json, no rollback in Admin possible!'); + console.log(); + } + + expect(npmPackage.author, 'ERROR: Author in package.json needs to exist').to.exist; + expect(ioPackage.common.authors, 'ERROR: Authors in io-package.json needs to exist').to.exist; + + if (ioPackage.common.name.indexOf('template') !== 0) { + if (Array.isArray(ioPackage.common.authors)) { + expect(ioPackage.common.authors.length, 'ERROR: Author in io-package.json needs to be set').to.not.be.equal(0); + if (ioPackage.common.authors.length === 1) { + expect(ioPackage.common.authors[0], 'ERROR: Author in io-package.json needs to be a real name').to.not.be.equal('my Name '); + } + } + else { + expect(ioPackage.common.authors, 'ERROR: Author in io-package.json needs to be a real name').to.not.be.equal('my Name '); + } + } + else { + console.log('WARNING: Testing for set authors field in io-package skipped because template adapter'); + console.log(); + } + expect(fs.existsSync(__dirname + '/../README.md'), 'ERROR: README.md needs to exist! Please create one with description, detail information and changelog. English is mandatory.').to.be.true; + if (!ioPackage.common.titleLang || typeof ioPackage.common.titleLang !== 'object') { + console.log('WARNING: titleLang is not existing in io-package.json. Please add'); + console.log(); + } + if ( + ioPackage.common.title.indexOf('iobroker') !== -1 || + ioPackage.common.title.indexOf('ioBroker') !== -1 || + ioPackage.common.title.indexOf('adapter') !== -1 || + ioPackage.common.title.indexOf('Adapter') !== -1 + ) { + console.log('WARNING: title contains Adapter or ioBroker. It is clear anyway, that it is adapter for ioBroker.'); + console.log(); + } + + if (ioPackage.common.name.indexOf('vis-') !== 0) { + if (!ioPackage.common.materialize || !fs.existsSync(__dirname + '/../admin/index_m.html') || !fs.existsSync(__dirname + '/../gulpfile.js')) { + console.log('WARNING: Admin3 support is missing! Please add it'); + console.log(); + } + if (ioPackage.common.materialize) { + expect(fs.existsSync(__dirname + '/../admin/index_m.html'), 'Admin3 support is enabled in io-package.json, but index_m.html is missing!').to.be.true; + } + } + + var licenseFileExists = fs.existsSync(__dirname + '/../LICENSE'); + var fileContentReadme = fs.readFileSync(__dirname + '/../README.md', 'utf8'); + if (fileContentReadme.indexOf('## Changelog') === -1) { + console.log('Warning: The README.md should have a section ## Changelog'); + console.log(); + } + expect((licenseFileExists || fileContentReadme.indexOf('## License') !== -1), 'A LICENSE must exist as LICENSE file or as part of the README.md').to.be.true; + if (!licenseFileExists) { + console.log('Warning: The License should also exist as LICENSE file'); + console.log(); + } + if (fileContentReadme.indexOf('## License') === -1) { + console.log('Warning: The README.md should also have a section ## License to be shown in Admin3'); + console.log(); + } + done(); + }); +});