@dogiloki/artha-js 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Julio Villanueva
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1 @@
1
+ # artha-js
@@ -0,0 +1 @@
1
+ artha-message{width:100%;padding:10px;text-align:left;font-size:1em;cursor:default}artha-message[type=error]{background-color:#f2dede;color:#b94a48}artha-message[type=success]{background-color:#dff0d8;color:#468847}artha-message[type=warning]{background-color:#fcf8e3;color:#c09853}artha-message[type=info]{background-color:#d9edf7;color:#3a87ad}/*# sourceMappingURL=artha.min.css.map */
@@ -0,0 +1 @@
1
+ {"version":3,"sourceRoot":"","sources":["../src/scss/message.scss","../src/scss/colors.scss"],"names":[],"mappings":"AAEA,cACI,WACA,aACA,gBACA,cACA,eAEA,0BACI,iBCLqB,QDMrB,MCVe,QDYnB,4BACI,iBCRuB,QDSvB,MCbiB,QDerB,4BACI,iBCXuB,QDYvB,MChBiB,QDkBrB,yBACI,iBCdoB,QDepB,MCnBc","file":"artha.min.css"}
@@ -0,0 +1,32 @@
1
+ import Config from '../src/core/Config.js';
2
+ import Util from '../src/core/Util.js';
3
+ import EventBus from '../src/core/EventBus.js';
4
+ import TaskQueue from '../src/core/TaskQueue.js';
5
+ import XHR from '../src/core/XHR.js';
6
+ import ArthaMessage from '../src/components/artha-message.js';
7
+ import ArthaContainer from '../src/components/artha-container.js';
8
+ import ArthaForm from '../src/components/artha-form.js';
9
+
10
+ if(!customElements.get('artha-container')){
11
+ customElements.define('artha-container',ArthaContainer);
12
+ }
13
+ if(!customElements.get('artha-form')){
14
+ customElements.define('artha-form',ArthaForm);
15
+ }
16
+ if(!customElements.get('artha-message')){
17
+ customElements.define('artha-message',ArthaMessage);
18
+ }
19
+
20
+ const Artha={
21
+ version:"1.0.0",
22
+ config(options){
23
+ Config.set(options);
24
+ },
25
+ get(path,def=null){
26
+ return Config.get(path,def);
27
+ }
28
+ };
29
+
30
+ Window.Artha=Artha;
31
+
32
+ export default Artha;
package/package.json ADDED
@@ -0,0 +1,31 @@
1
+ {
2
+ "name": "@dogiloki/artha-js",
3
+ "version": "1.0.0",
4
+ "description": "",
5
+ "main": "dist/artha.min.js",
6
+ "type": "module",
7
+ "files": [
8
+ "dist",
9
+ "src"
10
+ ],
11
+ "scripts": {
12
+ "dev": "sass --watch src/scss/main.scss:dist/artha.css",
13
+ "build": "sass src/scss/main.scss dist/artha.min.css --style=compressed"
14
+ },
15
+ "keywords": [
16
+ "task-queue",
17
+ "javascript",
18
+ "notifications",
19
+ "ui"
20
+ ],
21
+ "author": "dogiloki",
22
+ "license": "MIT",
23
+ "repository": {
24
+ "type": "git",
25
+ "url": "git+https://github.com/dogiloki/artha-js.git"
26
+ },
27
+ "bugs": {
28
+ "url": "https://github.com/dogiloki/artha-js/issues"
29
+ },
30
+ "homepage": "https://github.com/dogiloki/artha-js#readme"
31
+ }
@@ -0,0 +1,402 @@
1
+ import Util from '../core/Util.js';
2
+ import EventBus from '../core/EventBus.js';
3
+ import XHR from '../core/XHR.js';
4
+ import TaskQueue from '../core/TaskQueue.js';
5
+ import ArthaMessage from './artha-message.js';
6
+
7
+ export default class ArthaContainer extends HTMLElement{
8
+
9
+ constructor(){
10
+ super();
11
+
12
+ this.onRenderItem=(element,data)=>{};
13
+ this.onRenderItemFill=(element,data,fill_element,fill_data)=>{};
14
+ this.onRenderItemIter=(element,data,iter_element,iter_data)=>{};
15
+
16
+ this.task_queue=TaskQueue.singleton();
17
+ this.items={};
18
+ this.selection_store=new SelectionStore();
19
+ this.response_type='json';
20
+ this._elements={};
21
+ this._props=[
22
+ "template","action","action_router","method",
23
+ "pagination","message","searcher","selectable","multiple"];
24
+ this._props.forEach((prop)=>{
25
+ Object.defineProperty(this,prop,{
26
+ get:()=>{
27
+ switch(prop){
28
+ case 'template':
29
+ if(!this._elements[prop] || this._elements[prop].id!==this.getAttribute(prop)){
30
+ this._elements[prop]=document.getElementById(this.getAttribute(prop));
31
+ }
32
+ return this._elements[prop];
33
+ case 'method': return this.getAttribute(prop)??'GET';
34
+ default: return this.getAttribute(prop);
35
+ }
36
+ },
37
+ set:(value)=>{
38
+ if(this.getAttribute(prop)!==value){
39
+ this.setAttribute(prop,value);
40
+ }
41
+ }
42
+ });
43
+ const attr_val=this.getAttribute(prop);
44
+ if(attr_val!==null) this[prop]=attr_val;
45
+ });
46
+ this.pagination=this.getAttribute("pagination");
47
+ this.searcher=this.hasAttribute("searcher");
48
+ this.selectable=this.hasAttribute("selectable");
49
+ this.multiple=this.hasAttribute("multiple");
50
+ this.message=this.querySelector('artha-message')??this.querySelector(this.getAttribute('message-target'))??null;
51
+
52
+ // Loader
53
+ this.loader_container=this._createLoader();
54
+
55
+ // Contenido
56
+ this.content=this.querySelector(':scope > dynamic-content') || this.appendChild(document.createElement('dynamic-content'));
57
+ this._content=this.content.children[0];
58
+
59
+ // Input de búsqueda
60
+ this.input_search=this.querySelector("input-search");
61
+ if(this.input_search){
62
+ this.input_search.addEventListener('search',(evt)=>this._handleSearch(evt));
63
+ this.refresh(this.input_search.value);
64
+ }else{
65
+ this.refresh();
66
+ }
67
+ }
68
+
69
+ _createLoader(){
70
+ return Util.createElement('div',(div)=>{
71
+ div.classList.add('loader-container');
72
+ div.setAttribute('title','Procesando...');
73
+ div.appendChild(Util.createElement('div',(div2)=>{
74
+ div2.classList.add('background-overlay');
75
+ }));
76
+ div.appendChild(Util.createElement('div',(div3)=>{
77
+ div3.classList.add('loader','active');
78
+ }));
79
+ const img_container=div.appendChild(Util.createElement('div',(div4)=>{
80
+ div4.classList.add('img-container');
81
+ }));
82
+ img_container.appendChild(Util.createElement('img',(img)=>{
83
+ img.src='/assets/logo.png';
84
+ }));
85
+ });
86
+ }
87
+
88
+ _handleSearch(evt){
89
+ if(this.action){
90
+ this.refresh(evt.detail.query);
91
+ }else{
92
+ const search=evt.detail.query?.toLowerCase()??'';
93
+ for(const item of this.items??[]){
94
+ Util.modal(item,item.textContent.toLowerCase().includes(search));
95
+ }
96
+ }
97
+ }
98
+
99
+ // value - getter/setter
100
+ get value(){
101
+ if(!this.selectable) return null;
102
+ return this.multiple?this.selection_store.toValues():this.selection_store.toValues()[0]??null;
103
+ }
104
+
105
+ set value(values){
106
+ if(!this.selectable) return;
107
+ if(!Array.isArray(values)) values=[values];
108
+
109
+ this.selection_store.clear();
110
+ let count=0;
111
+ for(const item of this.items){
112
+ const id=item.dataset.id;
113
+ if(values.includes(id)){
114
+ this.selection_store.add(id,item,item.data);
115
+ this.classList.add('selected');
116
+ if(!this.multiple && ++count>=1) break;
117
+ }else{
118
+ item.classList.remove('selected');
119
+ }
120
+ }
121
+ }
122
+
123
+ connectedCallback(){
124
+ const channels=(this.getAttribute('refresh-on')||'').split(',').map(c=>c.trim()).filter(c=>c.length>0);
125
+ this._refresh_listeners=[];
126
+ for(const channel of channels){
127
+ const listener=(evt)=>evt?.detail?this.refreshWithData(evt.detail):this.refresh();
128
+ EventBus.on(channel,listener);
129
+ this._refresh_listeners.push({channel,listener});
130
+ }
131
+ }
132
+
133
+ disconnectedCallback(){
134
+ for(const {channel,listener} of this._refresh_listeners??[]){
135
+ EventBus.off(channel,listener);
136
+ }
137
+ }
138
+
139
+ getData(search=null){
140
+ if(!this.action) return;
141
+ const query=search?{search}:{};
142
+ const id=this.getAttribute("id")??Util.numberRandom(10000,99999);
143
+ this.task_queue.loadTask(`container-${id}`,null,(task)=>{
144
+ XHR.request({
145
+ url:this.action,
146
+ method:this.method,
147
+ headers:{
148
+ 'Accept':'application/json'
149
+ },
150
+ response_type:this.response_type,
151
+ query:Object.keys(query).length?query:{},
152
+ onLoad:(xhr)=>{
153
+ this.dispatchEvent(new CustomEvent('load',{detail:xhr}));
154
+ },
155
+ onData:(xhr,data)=>{
156
+ // Respuesta procesada en en formato json
157
+ task.resolve(xhr,(json)=>{
158
+ this.dispatchEvent(new CustomEvent('resolve',{detail:json}));
159
+ if(json.message){
160
+ this.message?.show(json.message,json.status);
161
+ }
162
+ this.render(json.data);
163
+ });
164
+ },
165
+ onError:(err)=>{
166
+ this.message?.error(err??"Error de conexión");
167
+ task.onFinalize();
168
+ }
169
+ });
170
+ },{
171
+ message:this.message
172
+ });
173
+ }
174
+
175
+ refresh(search=null){
176
+ this.loader_container.remove();
177
+ if(this.template){
178
+ this.content.innerHTML="";
179
+ if(this._content) this.content.appendChild(this._content);
180
+ this.content.appendChild(this.loader_container);
181
+ }
182
+ this.getData(search);
183
+ }
184
+
185
+ refreshWithData(data){
186
+ for(const child of this.content.querySelectorAll('[data-id]')){
187
+ if(child.dataset.id==data.id) this.renderItem(data,true,child);
188
+ }
189
+ }
190
+
191
+ render(results,refresh=false,refresh_children=true){
192
+ if(refresh) this.refresh();
193
+ results=Array.isArray(results)?results:[results];
194
+ this.data=results;
195
+ this.items=[];
196
+ this.loader_container.remove();
197
+ for(const data of results) this.renderItem(data,refresh_children);
198
+ this.dispatchEvent(new CustomEvent('dynamic-content-loaded',{detail:results}));
199
+ }
200
+
201
+ renderItem(data,refresh_children=true,update=null){
202
+ const template=this.template?(this.template.tagName==='TEMPLATE'?this.template.content.cloneNode(true):this.template.cloneNode(true)):this;
203
+ const element=update?update:(this.template?template.children[0]:this);
204
+
205
+ const items=this._findWires(update?element:template);
206
+ let index=0;
207
+
208
+ for(const item of items){
209
+ const wires=item.getAttribute("data-wire").split(",");
210
+ for(let wire of wires){
211
+ const [attrib_json,attrib_element,attrib_action]=wire.split(",");
212
+ let value=attrib_json?Util.getValueByPath(data,attrib_json.replaceAll("[]","")):data[index]??"";
213
+ const append=attrib_action==="append";
214
+ const chooser=attrib_action==="chooser";
215
+ const is_value_array=attrib_json?.endsWith("[]");
216
+
217
+ if(item instanceof ArthaContainer) item.render(value,refresh_children);
218
+ else if(is_value_array) this._fillArray(item,append,chooser);
219
+ else this._setValue(item,attrib_element,value,append,chooser);
220
+ }
221
+ }
222
+
223
+ if(!update){
224
+ this.items.push(element);
225
+ element.data=data;
226
+ element.dataset.id=data.id;
227
+ if(this.selectable) this._bindSelectable(element,data);
228
+ }
229
+
230
+ this.onRenderItem(element,data);
231
+ if(this.template && !update) this.content.appendChild(element);
232
+ this.dispatchEvent(new CustomEvent('item-rendered',{detail:{item:element,data,index}}));
233
+ index++;
234
+ }
235
+
236
+ _fillArray(item,data,value){
237
+ const fill_template=item.children[0].cloneNode(true);
238
+ item.innerHTML="";
239
+ for(const fill of value??[]){
240
+ const node=fill_template.cloneNode(true);
241
+ const fill_elements=node.querySelectorAll("[fillable]");
242
+ const iter_elements=node.querySelectorAll("[iterable]");
243
+ fill_elements.forEach((element)=>{
244
+ this.onRenderItemFill(item,data,element,fill);
245
+ element.textContent=fill;
246
+ element.value=fill;
247
+ });
248
+ iter_elements.forEach((element)=>{
249
+ this.onRenderItemIter(item,data,element,fill);
250
+ });
251
+ item.appendChild(node);
252
+ }
253
+ }
254
+
255
+ _setValue(item,attrib_element,value,append,chooser){
256
+ if(attrib_element){
257
+ if(append){
258
+ switch(attrib_element){
259
+ case 'textcontent': value=item.textContent+value; break;
260
+ case 'innerhtml': value=item.innerhtml+value; break;
261
+ default: value=item.getAttribute(attrib_element)+value;
262
+ }
263
+ }
264
+ switch(attrib_element.toLowerCase()){
265
+ case 'textcontent': item.textContent=value; break;
266
+ case 'innerhtml': item.innerhtml=value; break;
267
+ case 'boolean':{
268
+ if(chooser){
269
+ let applied=false;
270
+ const templates=item.querySelectorAll('template');
271
+ for(const template of templates){
272
+ if(template.getAttribute('data-chooser-value')==value){
273
+ item.innerHTML="";
274
+ item.appendChild(template.content.cloneNode(true));
275
+ applied=true;
276
+ break;
277
+ }
278
+ }
279
+ if(!applied){
280
+ const template=item.querySelector('template[data-chooser-default]')?.content?.cloneNode(true)??document.createElement('span');
281
+ item.innerHTML="";
282
+ item.appendChild(template);
283
+ }
284
+ }else{
285
+ item.innerHTML="";
286
+ item.appendChild(Util.createElement('span',(span)=>{
287
+ span.classList.add('check-cross');
288
+ if(value){
289
+ span.classList.add('check-cross-yes');
290
+ span.textContent="✔";
291
+ }else{
292
+ span.classList.add('check-cross-no');
293
+ span.textContent="✘";
294
+ }
295
+ }));
296
+ }
297
+ break;
298
+ }
299
+ default: this.setAttribute(attrib_element,value);
300
+ }
301
+ }else{
302
+ if(append) value=item.textContent+value;
303
+ item.textContent=value;
304
+ }
305
+ }
306
+
307
+ _bindSelectable(element,data){
308
+ const id=element.dataset.id;
309
+ element.addEventListener('click',(evt)=>{
310
+ if(this.selection_store.has(id)){
311
+ const selection=this.selection_store.remove(id);
312
+ this.dispatchEvent(new CustomEvent('item-deselected',{detail:selection}));
313
+ }else{
314
+ if(!this.multiple) this.reset();
315
+ const selection=this.selection_store.add(id,element,data);
316
+ this.dispatchEvent(new CustomEvent('item-selected',{detail:selection}));
317
+ }
318
+ element.classList.toggle('selected');
319
+ });
320
+ if(this.selection_store.has(id)) element.classList.add('selected');
321
+ }
322
+
323
+ _findWires(root){
324
+ const result=[];
325
+ for(const child of root.children){
326
+ if(child instanceof ArthaContainer && child.hasAttribute('data-ignore-wire')) continue;
327
+ if(child.hasAttribute('data-wire')) result.push(child);
328
+ result.push(...this._findWires(child));
329
+ }
330
+ return result;
331
+ }
332
+
333
+ selection(){
334
+ return this.selection_store;
335
+ }
336
+
337
+ reset(){
338
+ this.selection_store.clear();
339
+ for(const item of this.items){
340
+ item.classList.remove('selected');
341
+ }
342
+ }
343
+
344
+ }
345
+
346
+ class SelectionStore{
347
+
348
+ constructor(){
349
+ this.values=new Set();
350
+ this.elements=new Map();
351
+ this.data=new Map();
352
+ }
353
+
354
+ add(value,element,data){
355
+ this.values.add(value);
356
+ this.elements.set(value,element);
357
+ this.data.set(value,data);
358
+ return {
359
+ value,element,data
360
+ };
361
+ }
362
+
363
+ remove(value){
364
+ const sel={value,element:this.elements.get(value),data:this.data.get(value)};
365
+ this.values.delete(value);
366
+ this.elements.delete(value);
367
+ this.data.delete(value);
368
+ return sel;
369
+ }
370
+
371
+ clear(){
372
+ this.values.clear();
373
+ this.elements.clear();
374
+ this.data.clear();
375
+ return this;
376
+ }
377
+
378
+ has(value){
379
+ return this.values.has(value);
380
+ }
381
+
382
+ toValues(){
383
+ return Array.from(this.values);
384
+ }
385
+
386
+ toElements(){
387
+ return Array.from(this.elements);
388
+ }
389
+
390
+ toData(){
391
+ return Array.from(this.data);
392
+ }
393
+
394
+ toArray(){
395
+ return Array.from(this.values).map(v=>({
396
+ value:v,
397
+ element:this.elements.get(v),
398
+ data:this.data.get(v)
399
+ }));
400
+ }
401
+
402
+ }
@@ -0,0 +1,146 @@
1
+ import Util from '../core/Util.js';
2
+ import XHR from '../core/XHR.js';
3
+ import TaskQueue from '../core/TaskQueue.js';
4
+ import ArthaMessage from './artha-message.js';
5
+
6
+ export default class ArthaForm extends HTMLElement{
7
+
8
+ constructor(){
9
+ super();
10
+ this.task_queue=TaskQueue.singleton();
11
+ this.response_type=this.getAttribute('response-type')??'json';
12
+ this.disable_submit=this.hasAttribute('disable-submit');
13
+ this.message=this.querySelector('artha-message')??this.querySelector(this.getAttribute('message-target'))??null;
14
+ if(!this.message){
15
+ this.message=Util.createElement('artha-message');
16
+ this.appendChild(this.message);
17
+ }
18
+ this.element_inputs=[];
19
+ this.ignored_input=[];
20
+
21
+ // Cargar inputs iniciales
22
+ this.loadInputs();
23
+
24
+ // Interceptar submit
25
+ this.addEventListener('submit',(evt)=>{
26
+ evt.preventDefault();
27
+ if(!this.disable_submit) this.submit();
28
+ });
29
+
30
+ // Tecla enter
31
+ this.addEventListener('keydown',(evt)=>{
32
+ if(this.disable_submit && evt.key==='Enter' && evt.target instanceof HTMLInputElement){
33
+ evt.preventDefault();
34
+ }
35
+ });
36
+
37
+ this._bindEvents();
38
+ }
39
+
40
+ _bindEvents(){
41
+ // Botones
42
+ this.querySelectorAll('button').forEach((btn)=>{
43
+ switch(btn.getAttribute('type')){
44
+ case 'submit': btn.addEventListener('click',(evt)=>this.submit()); break;
45
+ case 'reset': btn.addEventListener('click',(evt)=>this.reset()); break;
46
+ default: btn.addEventListener('click',(evt)=>this.submit());
47
+ }
48
+ });
49
+ }
50
+
51
+ // Cargar inputs dinámicos
52
+ loadInputs(selector="input,select,textarea"){
53
+ this.element_inputs=[];
54
+ this.querySelectorAll(selector).forEach((element)=>{
55
+ const name=element.getAttribute('name');
56
+ if(name){
57
+ this[name]=element;
58
+ this.element_inputs.push(element);
59
+ }
60
+ });
61
+ }
62
+
63
+ // Obtener valor de un input por el atributo name
64
+ getValue(name){
65
+ const element=this[name]??this.querySelector(`[name="${name}"]`);
66
+ return element?(element.type==='checkbox'?(element.checked?1:0):element.value):null;
67
+ }
68
+ input(name){
69
+ const element=this.querySelector(`[name="${name}"]`);
70
+ if(element!=null && !(name in this)){
71
+ this[name]=element;
72
+ }
73
+ return element;
74
+ }
75
+
76
+ // Reset general del formulario
77
+ reset(reset_message=true){
78
+ this.element_inputs.forEach((element)=>{
79
+ if(element.type==='checkbox') element.checked=false;
80
+ else element.value='';
81
+ });
82
+ if(reset_message) this.resetMessage();
83
+ }
84
+
85
+ // Reset al mensaje (ocultar)
86
+ resetMessage(){
87
+ if(this.message) this.message.hidden();
88
+ }
89
+
90
+ // Validar formulario
91
+ checkValidity(){
92
+ let valid=true;
93
+ for(const element of this.element_inputs){
94
+ if(typeof element.checkValidity==='function' && !element.checkValidity()){
95
+ valid=false;
96
+ break;
97
+ }
98
+ }
99
+ return valid;
100
+ }
101
+
102
+ // Enviar formulario
103
+ submit(){
104
+ const form_data={};
105
+ this.element_inputs.forEach((element)=>{
106
+ form_data[element.name]=element.type==='checkbox'?(element.checked?1:0):element.value;
107
+ })
108
+ const action=this.getAttribute('action')??'';
109
+ const method=this.getAttribute('method')??'GET';
110
+ const id=this.getAttribute('id');
111
+ this.task_queue.loadTask(`form-${id}`,null,(task)=>{
112
+ XHR.request({
113
+ url:action,
114
+ method:method,
115
+ data:form_data,
116
+ response_type:this.response_type,
117
+ onLoad:(xhr)=>{
118
+ this.dispatchEvent(new CustomEvent('load',{detail:xhr}));
119
+ },
120
+ onData:(xhr,data)=>{
121
+ // Respuesta procesada en formato json
122
+ task.resolve(xhr,(json)=>{
123
+ this.dispatchEvent(new CustomEvent('resolve',{detail:json}));
124
+ this.fillFromJson(json.data??{},false);
125
+ });
126
+ },
127
+ onError:(err)=>{
128
+ this.message.error(err??"Error de conexión");
129
+ task.onFinalize();
130
+ }
131
+ })
132
+ },{
133
+ message:this.message
134
+ });
135
+ }
136
+
137
+ // Llenar inputs desde un JSON
138
+ fillFromJson(json,reset=true){
139
+ if(reset) this.reset(false);
140
+ for(const key in json){
141
+ const element=this[key]??this.querySelector(`[name="${key}"]`);
142
+ if(element) element.value=json[key];
143
+ }
144
+ }
145
+
146
+ }
@@ -0,0 +1,61 @@
1
+ import Config from "../core/Config.js";
2
+ import Util from "../core/Util.js";
3
+
4
+ export default class ArthaMessage extends HTMLElement{
5
+
6
+ static TYPE=Object.freeze({
7
+ ERROR:{
8
+ code:-1,
9
+ name:"error"
10
+ },
11
+ INFO:{
12
+ code:0,
13
+ name:"info"
14
+ },
15
+ SUCCESS:{
16
+ code:1,
17
+ name:"suscess"
18
+ },
19
+ WARNING:{
20
+ code:2,
21
+ name:"warning"
22
+ },
23
+
24
+ });
25
+
26
+ constructor(){
27
+ super();
28
+ }
29
+
30
+ connectedCallback(){
31
+ this.type=this.getAttribute("type")||"info";
32
+ this.hidden();
33
+ }
34
+
35
+ error(message=null){
36
+ this.show(message,ArthaMessage.TYPE.ERROR);
37
+ }
38
+
39
+ info(message=null){
40
+ this.show(message,ArthaMessage.TYPE.INFO);
41
+ }
42
+
43
+ success(message=null){
44
+ this.show(message,ArthaMessage.TYPE.SUCCESS);
45
+ }
46
+
47
+ warning(message=null){
48
+ this.show(message,ArthaMessage.TYPE.WARNING);
49
+ }
50
+
51
+ show(message=null,type=null){
52
+ if(type) this.setAttribute("type",(typeof type==="string"?type:type.name)||"info");
53
+ if(message) this.innerHTML=message;
54
+ Util.modal(this,true);
55
+ }
56
+
57
+ hidden(){
58
+ Util.modal(this,false);
59
+ }
60
+
61
+ }
@@ -0,0 +1,47 @@
1
+ const DEFAULT_CONFIG={
2
+ locale:"es-MX",
3
+ currency:"MXN",
4
+ money:{
5
+ digits:2
6
+ },
7
+ xhr:{
8
+ method:"GET",
9
+ url:null,
10
+ uri:"",
11
+ headers:{},
12
+ data:{},
13
+ query:{},
14
+ files:{},
15
+ response_type:"json",
16
+ with_credentials:false,
17
+ timeout:0,
18
+ retry:false,
19
+ retry_delay:5000,
20
+ onLoad:()=>{},
21
+ onData:()=>{},
22
+ onError:()=>{},
23
+ onTimeout:()=>{},
24
+ onProgress:()=>{},
25
+ onAbort:()=>{},
26
+ onAction:()=>{},
27
+ },
28
+ task_queue:{
29
+ title:"Petición en proceso...",
30
+ close:false,
31
+ message:null
32
+ }
33
+ };
34
+
35
+ export default class Config{
36
+
37
+ static SETTINGS={...DEFAULT_CONFIG};
38
+
39
+ static set(options){
40
+ this.SETTINGS={...this.SETTINGS,...options};
41
+ }
42
+
43
+ static get(path,def=null){
44
+ return path.split(".").reduce((o,p)=>o?o[p]:def,this.SETTINGS);
45
+ }
46
+
47
+ }
@@ -0,0 +1,33 @@
1
+ const EVENT_BUS=new EventTarget();
2
+
3
+ export default class EventBus{
4
+
5
+ // Emitir evento
6
+ static emit(name,data){
7
+ EVENT_BUS.dispatchEvent(new CustomEvent(name,{detail:data}));
8
+ }
9
+
10
+ // Escuchar evento
11
+ static on(name,callback){
12
+ const handler=(evt)=>{
13
+ callback(evt.detail);
14
+ };
15
+ EVENT_BUS.addEventListener(name,callback);
16
+ return ()=>EVENT_BUS.removeEventListener(name,handler);
17
+ }
18
+
19
+ // Escucha una sola vez
20
+ static once(name,callback){
21
+ const handler=(evt)=>{
22
+ callback(evt.detail);
23
+ EVENT_BUS.removeEventListener(name,handler);
24
+ };
25
+ EVENT_BUS.addEventListener(name,callback);
26
+ }
27
+
28
+ // Remover listener manualmente
29
+ static off(name,callback){
30
+ EVENT_BUS.removeEventListener(name,callback);
31
+ }
32
+
33
+ }
@@ -0,0 +1,171 @@
1
+ import Config from "./Config.js";
2
+ import Util from "./Util.js";
3
+ import ArthaMessage from "../components/artha-message.js";
4
+
5
+ export default class TaskQueue{
6
+
7
+ static INSTNACE=null;
8
+
9
+ static singleton(){
10
+ if(!this.INSTNACE){
11
+ this.INSTNACE=new TaskQueue();
12
+ }
13
+ return this.INSTNACE;
14
+ }
15
+
16
+ constructor(){
17
+ this.queues=new Map();
18
+ }
19
+
20
+ // Crear una nueva tarea
21
+ loadTask(id,title,callback,options={}){
22
+ if(typeof options!=='object'){
23
+ options={close:options};
24
+ }
25
+ if(this.queues.has(id)){
26
+ alert("La petición ya está en proceso... Por favor espere.");
27
+ return null;
28
+ }
29
+ if(title) options.title=title;
30
+ const task=new TaskQueueItem(id,callback,options);
31
+ task.onFinalize=(remove=false)=>{
32
+ if(remove && !task.finalized){
33
+ task.message_element.warning(task.options.title);
34
+ return;
35
+ }
36
+ task.finalized=true;
37
+ this.queues.delete(id);
38
+ if(task.options.close || remove){
39
+ setTimeout(()=>task.removeElement(),task.options.close?2500:0);
40
+ }
41
+ };
42
+ this.queues.set(id,task);
43
+ return task;
44
+ }
45
+
46
+ }
47
+
48
+ class TaskQueueItem{
49
+
50
+ constructor(id,callback,options){
51
+ this.id=id;
52
+ this.callback=callback;
53
+ options={...Config.get("task_queue"),...options};
54
+ const {
55
+ title,
56
+ close,
57
+ message
58
+ }=options;
59
+ this.options=options;
60
+ this.message_element=options.message instanceof ArthaMessage?options.message:document.querySelector("#"+options.message)??null;
61
+ this.resolve_callback=null;
62
+ this.reject_callback=null;
63
+ this.finalized=false;
64
+ this.status="pending";
65
+ this.message_element?.warning(options.title);
66
+ this.onFinalize=()=>{};
67
+
68
+ // Promesa
69
+ this.promise=new Promise((resolve,reject)=>{
70
+ this._resolve=resolve;
71
+ this._reject=reject;
72
+ });
73
+
74
+ // Ejecutar callback
75
+ callback(this);
76
+
77
+ // Resolver
78
+ this.promise.then((data)=>{
79
+ this.handleResponse(data);
80
+ });
81
+
82
+ // Error
83
+ this.promise.catch((error)=>{
84
+ this.message_element?.error(error?.message||String(error));
85
+ this.status="error";
86
+ this.reject_callback?.(error);
87
+ this.onFinalize();
88
+ });
89
+ }
90
+
91
+ // Procesar respuesta
92
+ handleResponse(data){
93
+ if(!data){
94
+ this.message_element?.error("Error en la respuesta del servidor");
95
+ this.status="error";
96
+ this.onFinalize();
97
+ return;
98
+ }
99
+ let response=data?.response??data;
100
+
101
+ // Blob para descargar
102
+ if(response instanceof Blob){
103
+ this.status="success";
104
+ this.resolve_callback?.(response);
105
+ this.onFinalize();
106
+ return;
107
+ }
108
+ let json;
109
+ try{
110
+ json=(typeof response==='string')?JSON.parse(response):response;
111
+ if(!json || typeof json!=='object'){
112
+ throw new Error("Respuesta inválida del servidor");
113
+ }
114
+ }catch(ex){
115
+ this.message_element?.error(ex.message||String(ex));
116
+ this.status="error";
117
+ this.onFinalize();
118
+ return;
119
+ }
120
+ // Obtener mensaje
121
+ let message=null;
122
+ if(json.errors && typeof json.errors==='object'){
123
+ const values=Object.values(json.errors);
124
+ if(values.length>0){
125
+ const first=values[0];
126
+ message=Array.isArray(first)?first[0]:first;
127
+ }
128
+ }
129
+ message=message||json.message||"Operación completada";
130
+ if(message){
131
+ this.message_element?.show(message,json?.status??null);
132
+ }
133
+ // Validar respuesta http
134
+ if(Util.withinRange(data.status,200,299)){
135
+ if(!message){
136
+ this.message_element?.success("Operación completada");
137
+ }
138
+ this.status="success";
139
+ }else{
140
+ if(!message){
141
+ this.message_element?.error("Error en la respuesta del servidor");
142
+ }
143
+ this.status="error";
144
+ this.onFinalize();
145
+ return;
146
+ }
147
+ this.resolve_callback?.(json);
148
+ this.onFinalize();
149
+ }
150
+
151
+ // Cancelar si usa XHR
152
+ cancel(){
153
+ if(this.xhr) this.xhr.abort();
154
+ }
155
+
156
+ // Resolver manualmente
157
+ resolve(data,callback){
158
+ this.resolve_callback=callback;
159
+ this._resolve(data);
160
+ }
161
+
162
+ // Recahzar manualmente
163
+ reject(error){
164
+ this._reject(error);
165
+ }
166
+
167
+ removeElement(){
168
+
169
+ }
170
+
171
+ }
@@ -0,0 +1,86 @@
1
+ import Config from "./Config.js";
2
+
3
+ export default class Util{
4
+
5
+ static getMeta(name){
6
+ const meta=document.querySelector(`meta[name="${name}"]`);
7
+ return meta?meta.getAttribute("content"):null;
8
+ }
9
+
10
+ static getValueByPath(obj,path,default_value=null){
11
+ return path.split(".").reduce((o,p)=>o?o[p]:default_value,obj);
12
+ }
13
+
14
+ static modal(content,visible=-1){
15
+ content.style.display=(visible==-1)?
16
+ (content.style.display=="none"?"block":"none")
17
+ :(visible?"block":"none");
18
+ if(visible==-1){
19
+ content.classList.toggle("hidden");
20
+ if(content.hasAttribute("hidden")){
21
+ content.removeAttribute("hidden");
22
+ }
23
+ }else{
24
+ if(visible){
25
+ content.classList.remove("hidden");
26
+ content.removeAttribute("hidden");
27
+ }else{
28
+ content.classList.add("hidden");
29
+ content.setAttribute("hidden","");
30
+ }
31
+ }
32
+ }
33
+
34
+ static modalById(id,visible=-1){
35
+ Util.modal(document.getElementById(id),visible);
36
+ }
37
+
38
+ static formatMoney(value,options={}){
39
+ const {
40
+ locale=Config.get("locale"),
41
+ currency=Config.get("currency"),
42
+ digits=Config.get("money.digits")
43
+ }=options;
44
+ value=value.toString();
45
+ if(value.endsWith(".") && !add_decimals){
46
+ add_decimals=true;
47
+ }
48
+ let amount=Number(value.replace(/[^0-9.]/g,""));
49
+ if(isNaN(amount)){
50
+ return value;
51
+ }
52
+ let minimum=0;
53
+ return new Intl.NumberFormat(locale,{
54
+ style:"currency",
55
+ currency,
56
+ minimum,
57
+ digits
58
+ }).format(amount);
59
+ }
60
+
61
+ static numberRandom(min,max){
62
+ return Math.floor(Math.random()*(max-min+1))+min;
63
+ }
64
+
65
+ static withinRange(value,min,max){
66
+ return value>=min && value<=max;
67
+ }
68
+
69
+ static createElement(type,value=null,options={}){
70
+ const el=document.createElement(type,options);
71
+ if(value!==null){
72
+ return el;
73
+ }
74
+ if(Array.isArray(value)){
75
+ value.forEach((item)=>{
76
+ el.appendChild(item);
77
+ });
78
+ }else if(typeof value==="function"){
79
+ value(el);
80
+ }else{
81
+ el.textContent=value;
82
+ }
83
+ return el;
84
+ }
85
+
86
+ }
@@ -0,0 +1,116 @@
1
+ import Config from './Config.js';
2
+ import Util from './Util.js';
3
+
4
+ export default class XHR{
5
+
6
+ static request(options){
7
+ options={...Config.get("xhr"),...options};
8
+ const {
9
+ method,
10
+ url,
11
+ uri,
12
+ headers,
13
+ data,
14
+ query,
15
+ files,
16
+ response_type,
17
+ with_credentials,
18
+ timeout,
19
+ retry,
20
+ retry_delay,
21
+ onLoad,
22
+ onData,
23
+ onError,
24
+ onTimeout,
25
+ onProgress,
26
+ onAbort,
27
+ onAction
28
+ }=options;
29
+ url??="/"+uri;
30
+ const xhr=new XMLHttpRequest();
31
+ const query_string=Object.keys(query).length?"?"+Object.entries(query)
32
+ .filter(([_,v])=>v!=null)
33
+ .map(([k,v])=>`${encodeURIComponent(k)}=${encodeURIComponent(v)}`).join("&"):"";
34
+ xhr.open(method,url+query_string,true);
35
+ xhr.responseType=response_type;
36
+ xhr.withCredentials=with_credentials;
37
+ xhr.timeout=timeout;
38
+
39
+ // Encabezados
40
+ const token=Util.getMeta("csrf-token")??Util.getMeta("csrf_token");
41
+ if(token){
42
+ xhr.setRequestHeader("X-CSRF-Token",token);
43
+ }
44
+ for(let key in headers){
45
+ xhr.setRequestHeader(key,headers[key]);
46
+ }
47
+
48
+ // Cuerpo
49
+ let body=null;
50
+ if(method!=='GET'){
51
+ const form_data=new FormData();
52
+ if(token) form_data.append("csrf_token",token);
53
+ form_data.append("_method",method);
54
+ for(let key in data){
55
+ form_data.append(key,data[key]);
56
+ }
57
+ for(let key in files){
58
+ const value=files[key];
59
+ if(Array.isArray(value) || value instanceof FileList){
60
+ for(let index=0; index<value.length; index++){
61
+ form_data.append(`${key}[]`,value[index]);
62
+ }
63
+ }else{
64
+ form_data.append(key,value);
65
+ }
66
+ }
67
+ body=form_data;
68
+ }
69
+
70
+ // Carga con datos según la respuesta
71
+ xhr.addEventListener("load",()=>{
72
+ onLoad(xhr);
73
+ if(Util.withinRange(xhr.status,200,299)){
74
+ onData(xhr,xhr.response);
75
+ }else{
76
+ onError(xhr.response);
77
+ }
78
+ });
79
+
80
+ // Error
81
+ xhr.addEventListener("error",()=>{
82
+ if(retry){
83
+ setTimeout(()=>{
84
+ XHR.request(options);
85
+ },retry_delay);
86
+ }
87
+ onError(xhr.response);
88
+ });
89
+
90
+ // Aborto
91
+ xhr.addEventListener("abort",()=>{
92
+ onAbort(xhr.response);
93
+ });
94
+
95
+ // Tiempo de espera
96
+ xhr.addEventListener("timeout",()=>{
97
+ if(retry){
98
+ setTimeout(()=>{
99
+ XHR.request(options);
100
+ },retry_delay);
101
+ }
102
+ onTimeout(xhr.response);
103
+ });
104
+
105
+ // Progreso
106
+ xhr.addEventListener("progress",(evt)=>{
107
+ onProgress(evt,evt.loaded,evt.total);
108
+ });
109
+
110
+ // Enviar
111
+ onAction(xhr);
112
+ xhr.send(body);
113
+ return xhr;
114
+ }
115
+
116
+ }
@@ -0,0 +1,9 @@
1
+ // Colores de ventanas de mensajes
2
+ $msg-text-error-color: #b94a48;
3
+ $msg-text-success-color: #468847;
4
+ $msg-text-warning-color: #c09853;
5
+ $msg-text-info-color: #3a87ad;
6
+ $msg-background-error-color: #f2dede;
7
+ $msg-background-success-color: #dff0d8;
8
+ $msg-background-warning-color: #fcf8e3;
9
+ $msg-background-info-color: #d9edf7;
@@ -0,0 +1 @@
1
+ @use './message.scss' as *;
@@ -0,0 +1,26 @@
1
+ @use './colors.scss' as *;
2
+
3
+ artha-message{
4
+ width: 100%;
5
+ padding: 10px;
6
+ text-align: left;
7
+ font-size: 1em;
8
+ cursor: default;
9
+
10
+ &[type="error"]{
11
+ background-color: $msg_background_error_color;
12
+ color: $msg_text_error_color;
13
+ }
14
+ &[type="success"]{
15
+ background-color: $msg_background_success_color;
16
+ color: $msg_text_success_color;
17
+ }
18
+ &[type="warning"]{
19
+ background-color: $msg_background_warning_color;
20
+ color: $msg_text_warning_color;
21
+ }
22
+ &[type="info"]{
23
+ background-color: $msg_background_info_color;
24
+ color: $msg_text_info_color;
25
+ }
26
+ }