config-editor-card.js 9.3 KB

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