diff --git a/src/snek/static/njet.js b/src/snek/static/njet.js new file mode 100644 index 0000000..a23aeb6 --- /dev/null +++ b/src/snek/static/njet.js @@ -0,0 +1,473 @@ + + +class RestClient { + constructor({ baseURL = '', headers = {} } = {}) { + this.baseURL = baseURL; + this.headers = { ...headers }; + + // Interceptor containers + this.interceptors = { + request: { + handlers: [], + use: (fn) => { + this.interceptors.request.handlers.push(fn); + } + }, + response: { + handlers: [], + use: (successFn, errorFn) => { + this.interceptors.response.handlers.push({ success: successFn, error: errorFn }); + } + } + }; + } + + // Core request method + request(config) { + // Merge defaults + const cfg = { + method: 'GET', + url: '', + data: null, + headers: {}, + ...config + }; + cfg.headers = { ...this.headers, ...cfg.headers }; + + // Apply request interceptors + let chain = Promise.resolve(cfg); + this.interceptors.request.handlers.forEach((fn) => { + chain = chain.then(fn); + }); + + // Perform fetch + chain = chain.then((c) => { + const url = this.baseURL + c.url; + const options = { method: c.method, headers: c.headers }; + if (c.data != null) { + options.body = JSON.stringify(c.data); + if (!options.headers['Content-Type']) { + options.headers['Content-Type'] = 'application/json'; + } + } + return fetch(url, options).then(async (response) => { + const text = await response.text(); + let data; + try { + data = JSON.parse(text); + } catch (e) { + data = text; + } + const result = { + data, + status: response.status, + statusText: response.statusText, + headers: RestClient._parseHeaders(response.headers), + config: c, + request: response + }; + if (!response.ok) { + return Promise.reject(result); + } + return result; + }); + }); + + // Apply response interceptors + this.interceptors.response.handlers.forEach(({ success, error }) => { + chain = chain.then(success, error); + }); + + return chain; + } + + // Helper methods for HTTP verbs + get(url, config) { + return this.request({ ...config, method: 'GET', url }); + } + delete(url, config) { + return this.request({ ...config, method: 'DELETE', url }); + } + head(url, config) { + return this.request({ ...config, method: 'HEAD', url }); + } + options(url, config) { + return this.request({ ...config, method: 'OPTIONS', url }); + } + post(url, data, config) { + return this.request({ ...config, method: 'POST', url, data }); + } + put(url, data, config) { + return this.request({ ...config, method: 'PUT', url, data }); + } + patch(url, data, config) { + return this.request({ ...config, method: 'PATCH', url, data }); + } + + // Utility to parse Fetch headers into an object + static _parseHeaders(headers) { + const result = {}; + for (const [key, value] of headers.entries()) { + result[key] = value; + } + return result; + } +} + + + + + + +class Njet extends HTMLElement { + static _root = null + static showDialog = null + + get isRoot() { + return Njet._root === this + } + + get root() { + return Njet._root + } + + get rest() { + return Njet._root._rest + } + + _subscriptions = {} + _elements = [] + _rest = null + + match(args) { + return Object.entries(args).every(([key, value]) => this[key] === value); + } + + set(key, value) { + this.dataset[key] = value + } + + get(key, defaultValue) { + if (this.dataset[key]) { + return this.dataset[key] + } + if (defaultValue === undefined) { + return + } + this.dataset[key] = defaultValue + return this.dataset[key] + } + + showDialog(args){ + + // const dialog = this.createComponent('njet-dialog', args) + // dialog.show() + // return dialog() + } + + find(args) { + for (let element of this.root._elements) { + if (element.match(args)) { + return element + } + } + return null + } + + findAll(args) { + return this.root._elements.filter(element => element.match(args)) + } + + subscribe(event, callback) { + if (!this.root._subscriptions[event]) { + this.root._subscriptions[event] = [] + } + this.root._subscriptions[event].push(callback) + } + + publish(event, data) { + if (this.root._subscriptions[event]) { + this.root._subscriptions[event].forEach(callback => callback(data)) + } + } + + static registerComponent(name, component) { + customElements.define(name, component); + } + + constructor(config = {}) { + super(); + if (!Njet._root) { + Njet._root = this + Njet._rest = new RestClient({ baseURL: config.baseURL || null }) + } + this.root._elements.push(this) + this.classList.add('njet'); + this.config = config; + this.render.call(this); + this.initProps(config); + if (typeof this.construct === 'function') + this.construct.call(this) + } + + initProps(config) { + const props = Object.keys(config) + props.forEach(prop => { + if (config[prop] !== undefined) { + this[prop] = config[prop]; + } + }); + if (config.classes) { + this.classList.add(...config.classes); + } + } + + duplicate() { + const duplicatedConfig = { ...this.config }; + if (duplicatedConfig.items) { + duplicatedConfig.items = duplicatedConfig.items.map(item => { + return typeof item.duplicate === 'function' ? item.duplicate() : item; + }); + } + return new this.constructor(duplicatedConfig); + } + + set width(val) { + this.style.width = typeof val === 'number' ? `${val}px` : val; + } + + get width() { return this.style.width; } + + set height(val) { + this.style.height = typeof val === 'number' ? `${val}px` : val; + } + + get height() { return this.style.height; } + + set left(val) { + this.style.position = 'absolute'; + this.style.left = typeof val === 'number' ? `${val}px` : val; + } + + get left() { return this.style.left; } + + set top(val) { + this.style.position = 'absolute'; + this.style.top = typeof val === 'number' ? `${val}px` : val; + } + + get top() { return this.style.top; } + + set opacity(val) { this.style.opacity = val; } + get opacity() { return this.style.opacity; } + + set disabled(val) { this.toggleAttribute('disabled', !!val); } + get disabled() { return this.hasAttribute('disabled'); } + + set visible(val) { this.style.display = val ? '' : 'none'; } + get visible() { return this.style.display !== 'none'; } + + render() {} +} + +class Component extends Njet {} + +class NjetPanel extends Component { + render() { + this.innerHTML = ''; + const { title, items = [] } = this.config; + this.style.border = '1px solid #ccc'; + this.style.padding = '10px'; + if (title) { + const header = document.createElement('h3'); + header.textContent = title; + this.appendChild(header); + } + items.forEach(item => this.appendChild(item)); + } +} +Njet.registerComponent('njet-panel', NjetPanel); + +class NjetButton extends Component { + render() { + this.classList.add('njet-button'); + this.innerHTML = ''; + const button = document.createElement('button'); + button.textContent = this.config.text || 'Button'; + if (typeof this.config.handler === 'function') { + button.addEventListener('click', (event) => this.config.handler.call(this)); + } + const observer = new MutationObserver(() => { + button.disabled = this.disabled; + }); + observer.observe(this, { attributes: true, attributeFilter: ['disabled'] }); + button.disabled = this.disabled; + this.appendChild(button); + } +} +Njet.registerComponent('njet-button', NjetButton); + +class NjetDialog extends Component { + render() { + this.innerHTML = ''; + const { title, content, primaryButton, secondaryButton } = this.config; + this.classList.add('njet-dialog'); + this.style.position = 'fixed'; + this.style.top = '50%'; + this.style.left = '50%'; + this.style.transform = 'translate(-50%, -50%)'; + this.style.padding = '20px'; + this.style.border = '1px solid #444'; + this.style.backgroundColor = '#fff'; + this.style.boxShadow = '0 4px 8px rgba(0, 0, 0, 0.2)'; + this.style.minWidth = '300px'; + if (title) { + const header = document.createElement('h2'); + header.textContent = title; + this.appendChild(header); + } + if (content) { + const body = document.createElement('div'); + body.innerHTML = content; + this.appendChild(body); + } + const buttonContainer = document.createElement('div'); + buttonContainer.style.marginTop = '20px'; + buttonContainer.style.display = 'flex'; + buttonContainer.style.justifyContent = 'flenjet-end'; + buttonContainer.style.gap = '10px'; + if (secondaryButton) { + const secondary = new NjetButton(secondaryButton); + buttonContainer.appendChild(secondary); + } + if (primaryButton) { + const primary = new NjetButton(primaryButton); + buttonContainer.appendChild(primary); + } + this.appendChild(buttonContainer); + } + + show(){ + document.body.appendChild(this) + } +} +Njet.registerComponent('njet-dialog', NjetDialog); + +class NjetGrid extends Component { + render() { + this.classList.add('njet-grid'); + this.innerHTML = ''; + const table = document.createElement('table'); + table.style.width = '100%'; + table.style.borderCollapse = 'collapse'; + const data = this.config.data || []; + data.forEach(row => { + const tr = document.createElement('tr'); + Object.values(row).forEach(cell => { + const td = document.createElement('td'); + td.textContent = cell; + td.style.border = '1px solid #ddd'; + td.style.padding = '4px'; + tr.appendChild(td); + }); + table.appendChild(tr); + }); + this.appendChild(table); + } +} +Njet.registerComponent('njet-grid', NjetGrid); +/* +const button = new NjetButton({ + classes: ['my-button'], + text: 'Shared', + tag: 'shared', + width: 120, + height: 30, + handler() { + this.root.findAll({ tag: 'shared' }).forEach(e => { + e.disabled = !e.disabled; + }); + } +}); + +const button2 = new NjetButton({ + classes: ['my-button'], + text: 'Single', + iValue: 0, + width: 120, + height: 30, + handler() { + this.iValue++; + const panel = this.closest('njet-panel'); + if (panel) { + const h3 = panel.querySelector('h3'); + if (h3) h3.textContent = `Click ${this.iValue}`; + } + this.publish("btn2Click", `Click ${this.iValue}`); + } +}); + +const grid = new NjetGrid({ + data: [ + { id: 1, name: 'John' }, + { id: 2, name: 'Jane' } + ], + width: '100%', + visible: true +}); + +const panel = new NjetPanel({ + title: 'My Panel', + items: [button, grid, button2], + left: 50, + top: 50, + construct: function () { + this.subscribe('btn2Click', (data) => { + this._title = data + }); + } +}); + +document.body.appendChild(panel); + +const panelClone = panel.duplicate(); +const panell = panelClone.duplicate(); +panell.left = 120; +panell.width = 300; +panelClone.appendChild(panell); +panelClone.left = 300; +panelClone.top = 50; +document.body.appendChild(panelClone); + +const dialog = new NjetDialog({ + title: 'Confirm Action', + content: 'Are you sure you want to continue?', + primaryButton: { + text: 'Yes', + handler: function () { + alert('Confirmed'); + this.closest('njet-dialog').remove(); + } + }, + secondaryButton: { + text: 'Cancel', + handler: function () { + this.closest('njet-dialog').remove(); + } + } +}); + +document.body.appendChild(dialog); +*/ + +class NjetComponent extends Component {} + const njet = Njet; +njet.showDialog = function(args){ + const dialog = new NjetDialog(args) + dialog.show() + return dialog +} + +window.njet = njet + +export { Njet, NjetButton, NjetPanel, NjetDialog, NjetGrid, NjetComponent, njet};