config-editor-card.js 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348
  1. console.info("Config Editor 4.2");
  2. const LitElement = window.LitElement || Object.getPrototypeOf(customElements.get("hui-masonry-view") );
  3. const html = LitElement.prototype.html;
  4. const css = LitElement.prototype.css;
  5. class ConfigEditor extends LitElement {
  6. static get properties() {
  7. return {
  8. _hass: {type: Object},
  9. code: {type: String},
  10. fileList: {type: Array},
  11. openedFile: {type: String},
  12. infoLine: {type: String},
  13. alertLine: {type: String},
  14. edit: {},
  15. };
  16. }
  17. constructor() {
  18. super();
  19. this.code = '';
  20. this.fileList = [];
  21. this.openedFile = '';
  22. this.infoLine = '';
  23. this.alertLine = '';
  24. }
  25. static get styles() {
  26. return css`
  27. textarea{
  28. width:98%;
  29. height:80vh;
  30. padding:5px;
  31. overflow-wrap:normal;
  32. white-space:pre}
  33. .top{
  34. min-height:calc(95vh - var(--header-height))}
  35. .pin,.filebar{
  36. display:flex}
  37. .pin label{
  38. cursor:pointer}
  39. .right{text-align:right;
  40. flex-grow:1}
  41. .right button{
  42. font-family:Times,serif;
  43. font-weight:bold}
  44. .bar{
  45. position:-webkit-sticky;
  46. position:sticky;
  47. bottom:0;
  48. z-index:2;
  49. background:var(--app-header-background-color);
  50. color:var(--app-header-text-color,white);
  51. white-space:nowrap;
  52. overflow:hidden;
  53. text-overflow:ellipsis}
  54. .bar i{
  55. background:#ff7a81;
  56. cursor:pointer}
  57. .bar select{
  58. flex-grow:1;
  59. text-overflow:ellipsis;
  60. width:100%;
  61. overflow:hidden}
  62. `;
  63. }
  64. render(){
  65. const hver=this._hass ? this._hass.states['config_editor.version']:0;
  66. if(!hver){return html`<ha-card>Missing 'config_editor:' in configuration.yaml
  67. for github.com/htmltiger/config-editor</ha-card>`;}
  68. if(hver.state != '4'){return html`<ha-card>Please upgrade
  69. github.com/htmltiger/config-editor</ha-card>`;}
  70. if(this.fileList.length<1){
  71. this.openedFile = this.localGet('Open')||'';
  72. this.edit.ext = this.localGet('Ext')||'yaml';
  73. this.edit.basic = this.localGet('Basic')||'';
  74. if(this.fileList = JSON.parse(this.localGet('List'+this.edit.ext))){
  75. if(this.extOk(this.openedFile)){
  76. setTimeout(this.oldText, 500, this);
  77. }
  78. }else{this.List();}
  79. }
  80. return html`
  81. <ha-card>
  82. <div class="top">
  83. <div class="pin">
  84. <div class="left"><button @click="${this.reLoad}">Reload</button></div>
  85. <div class="right">
  86. <button @click="${e=>this.txtSize(0)}">-</button>
  87. <button @click="${e=>this.txtSize(2)}">A</button>
  88. <button @click="${e=>this.txtSize(1)}">+</button>
  89. <select @change=${this.extChange}>
  90. ${["yaml","py","json","conf","js","txt","log","all"].map(value =>
  91. html`<option ?selected=${value === this.edit.ext }
  92. value=${value}>${value.toUpperCase()}</option>`)}
  93. </select>
  94. <label>Basic Editor<input type="checkbox" ?checked=${this.edit.basic=='1'}
  95. name="basic" value="1" @change=${this.basicChange}></label>
  96. </div>
  97. </div>
  98. ${(this.edit.basic || this.edit.coder ) ?
  99. html`<textarea rows="10" ?readonly=${!0==this.edit.readonly}
  100. @change=${this.updateText} id="code" @keydown=${this.saveKey}>${this.code}</textarea>`:
  101. html`<ha-code-editor id="code" mode="yaml" ?readOnly=${!0==this.edit.readonly}
  102. @keydown=${this.saveKey} .hass=${this._hass} @value-changed=${this.updateText}
  103. dir="ltr" autocomplete-entities autocomplete-icons></ha-code-editor>`}
  104. </div>
  105. ${this.edit.hidefooter ? '' : html`
  106. <div class="bar">
  107. <div>${this.alertLine}</div>
  108. <div class="filebar">${!this.edit.readonly ?
  109. html`<button @click="${this.Save}">Save</button>`:''}
  110. <select @change=${this.Load}>
  111. ${[''].concat(this.fileList).map(value =>
  112. html`<option ?selected=${value === this.openedFile}
  113. value=${value}>${value}</option>`)}
  114. </select>
  115. <button @click="${this.List}">Get List</button>
  116. </div>
  117. <code>#${this.infoLine}</code>
  118. </div>`}
  119. </ha-card>
  120. `;
  121. }
  122. txtSize(e){
  123. if(e<3){
  124. if(e>1){
  125. this.edit.size=100;
  126. }else if(e>0){
  127. this.edit.size+=5;
  128. }else{
  129. this.edit.size-=5;
  130. }
  131. this.localSet('Size', this.edit.size);
  132. this.infoLine = 'size: '+this.edit.size;
  133. }
  134. this.renderRoot.querySelector('#code').style.fontSize=this.edit.size+'%';
  135. }
  136. extChange(e){
  137. this.edit.ext = e.target.value;
  138. this.localSet('Ext', this.edit.ext);
  139. this.openedFile = '';
  140. this.oldText(this);
  141. this.List();
  142. }
  143. basicChange(){
  144. this.edit.basic = this.edit.basic?'':'1';
  145. this.localSet('Basic', this.edit.basic);
  146. this.reLoad();
  147. }
  148. updateText(e) {
  149. e.stopPropagation();
  150. this.code = this.edit.basic ? e.target.value : e.detail.value;
  151. if(this.openedFile){this.localSet('Text', this.code);}
  152. }
  153. Unsave(){
  154. this.code = this.localGet('Unsaved');
  155. this.renderRoot.querySelector('#code').value=this.code;
  156. this.localSet('Unsaved','');
  157. this.alertLine = '';
  158. this.Toast("Loaded from browser",1500);
  159. }
  160. localGet(e){
  161. return localStorage.getItem('config_editor'+e);
  162. }
  163. localSet(k,v){
  164. localStorage.setItem('config_editor'+k,v);
  165. }
  166. cmd(action, data, file){
  167. return this._hass.callWS({type: "config_editor/ws", action: action,
  168. data: data, file: file, ext: this.edit.ext, depth: this.edit.depth});
  169. }
  170. saveList(){
  171. this.localSet('List'+this.edit.ext, JSON.stringify(this.fileList));
  172. }
  173. reLoad(e){
  174. this.Load({target:{value:this.openedFile},reload:1});
  175. }
  176. oldText(dhis){
  177. dhis.Load({target:{value:dhis.openedFile}});
  178. }
  179. saveKey(e) {
  180. if((e.key == 'S' || e.key == 's' ) && (e.ctrlKey || e.metaKey)){
  181. e.preventDefault();
  182. this.Save();
  183. return false;
  184. }
  185. return true;
  186. }
  187. Toast(message, duration){
  188. const e = new Event("hass-notification",
  189. {bubbles: true, cancelable: false, composed: true});
  190. e.detail = {message, duration, dismissable: true,
  191. //action: {text:"Save",action:()=>{this.sureSave();}},
  192. };
  193. document.querySelector("home-assistant").dispatchEvent(e);
  194. }
  195. //sureSave(){console.log(this.openedFile);}
  196. async Coder(){
  197. const c="ha-yaml-editor";
  198. if(!customElements.get(c)){
  199. await customElements.whenDefined("partial-panel-resolver");
  200. const p = document.createElement('partial-panel-resolver');
  201. p.hass = {panels: [{url_path: "tmp", component_name: "config"}]};
  202. p._updateRoutes();
  203. await p.routerOptions.routes.tmp.load();
  204. const d=document.createElement("ha-panel-config");
  205. await d.routerOptions.routes.automation.load();
  206. }
  207. const a=document.createElement(c);
  208. this.edit.coder=0;
  209. if(!a){
  210. this.localSet('Basic', 1);
  211. console.log('failed '+c);
  212. }
  213. this.render();
  214. }
  215. async List(){
  216. this.infoLine = 'List Loading...';
  217. const e=await this.cmd('list','','');
  218. this.infoLine = e.msg;
  219. this.fileList = e.file.slice().sort();
  220. this.saveList();
  221. if(this.extOk(this.openedFile)){
  222. setTimeout(this.oldText, 500, this);
  223. }
  224. }
  225. async Load(x) {
  226. if(x.target.value == this.openedFile && this.code && !x.hasOwnProperty('reload')){return;}
  227. if(this.edit.orgCode.trim() != this.code.trim()){
  228. if(!confirm("Switch without Saving?")){x.target.value = this.openedFile; return;}
  229. }
  230. this.code = ''; this.renderRoot.querySelector('#code').value='';this.infoLine = '';
  231. this.openedFile = x.target.value;
  232. if(this.openedFile){
  233. this.infoLine = 'Loading: '+this.openedFile;
  234. const e=await this.cmd('load','',this.openedFile);
  235. this.openedFile = e.file;
  236. this.infoLine = e.msg;
  237. this.Toast(this.infoLine,1000);
  238. const uns={f:this.localGet('Open'),
  239. d:this.localGet('Text')};
  240. if(uns.f == this.openedFile && uns.d && uns.d != e.data){
  241. this.localSet('Unsaved', uns.d);
  242. this.alertLine = html`<i @click="${this.Unsave}"> 
  243. Load unsaved from browser </i>`;
  244. }else{
  245. this.localSet('Text','');this.alertLine = '';
  246. }
  247. this.renderRoot.querySelector('#code').value=e.data;
  248. this.code = e.data;
  249. }
  250. this.edit.orgCode = this.code;
  251. this.localSet('Open', this.openedFile);
  252. this.txtSize(3);
  253. }
  254. extOk(f){
  255. if(f.length && (this.edit.ext=='all' || f.endsWith("."+this.edit.ext) )){return 1;}
  256. return 0;
  257. }
  258. async Save() {
  259. if(this.renderRoot.querySelector('#code').value != this.code || this.edit.readonly){
  260. this.infoLine='Something not right!';
  261. return;
  262. }
  263. let savenew=0;
  264. if(!this.openedFile && this.code){
  265. this.openedFile=prompt("type abc."+this.edit.ext+" or folder/abc."+this.edit.ext);
  266. savenew=1;
  267. }
  268. if(this.extOk(this.openedFile)){
  269. if(!confirm("Save?")){if(savenew){this.openedFile='';}return;}
  270. if(!this.code){this.infoLine=''; this.infoLine = 'Text is empty!'; return;}
  271. this.infoLine = 'Saving: '+this.openedFile;
  272. const e=await this.cmd('save', this.code, this.openedFile);
  273. this.infoLine = e.msg;
  274. this.Toast(this.infoLine,2000);
  275. if(e.msg.includes('Saved:')){
  276. this.localSet('Text','');
  277. if(savenew){
  278. this.fileList.unshift(this.openedFile);
  279. this.saveList();
  280. }
  281. }
  282. }else{this.openedFile='';}
  283. this.edit.orgCode = this.code;
  284. }
  285. getCardSize() {
  286. return 5;
  287. }
  288. setConfig(config) {
  289. this.edit = {file: '', hidefooter: false, readonly: false, basic: false, size: 0, depth: 2, ext: '', orgCode: '', coder:1, ...config};
  290. if(this.edit.file){
  291. const f=this.edit.file.split('.')[1];
  292. if(f){
  293. this.localSet('Open', this.edit.file);
  294. this.localSet('Ext', f);
  295. }
  296. }
  297. if(!this.edit.size){
  298. this.edit.size=Number(this.localGet('Size'))||100;
  299. }
  300. this.Coder();
  301. }
  302. set hass(hass) {
  303. this._hass = hass;
  304. }
  305. shouldUpdate(changedProps) {
  306. for(const e of ['code','openedFile','fileList','infoLine','alertLine','edit']) {
  307. if(changedProps.has(e)){return true;}
  308. }
  309. }
  310. } customElements.define('config-editor-card', ConfigEditor);
  311. window.customCards = window.customCards || [];
  312. window.customCards.push({
  313. type: 'config-editor-card',
  314. name: 'Config Editor Card',
  315. preview: false,
  316. description: 'Basic editor for configuration.yaml'
  317. });