From e3d7529a96640c858809dbb151402ff13395a50f Mon Sep 17 00:00:00 2001 From: Joeylene <23741509+jorenrui@users.noreply.github.com> Date: Sat, 10 Feb 2024 23:53:53 +0800 Subject: [PATCH 01/10] refactor: relocate events logic to events class --- lib/entity.js | 146 ++----------------------------------------- lib/entity/events.js | 139 ++++++++++++++++++++++++++++++++++++++++ lib/main.js | 4 +- 3 files changed, 146 insertions(+), 143 deletions(-) create mode 100644 lib/entity/events.js diff --git a/lib/entity.js b/lib/entity.js index b50be84..2abe9cf 100644 --- a/lib/entity.js +++ b/lib/entity.js @@ -1,6 +1,7 @@ import { Interpreter, ClassInterpreter } from './generators/interpreter' import { Lexer } from './generators/lexer' import { escapeHTML } from './helpers/sanitize' +import { Events } from './entity/events' export default class Entity { constructor(el) { @@ -10,10 +11,11 @@ export default class Entity { className: el.className, } this.variables = [] - this.listener = {} this.dynamicAttributes = [] this.id = this.generateEntityUUID() + this.events = new Events(this) + this._getDynamicAttributes() if (MiniJS.debug) this.element.dataset.entityId = this.id @@ -203,7 +205,7 @@ export default class Entity { async init() { this.getVariables() - this.applyEventBindings() + this.events.apply() await this.evaluateAll() MiniJS.elements.push(this) @@ -225,11 +227,6 @@ export default class Entity { } } - async evaluateEventAction(attrName) { - const attrVal = this.element.getAttribute(attrName) - await this._interpret(attrVal) - } - async evaluateClass() { const expr = this.element.getAttribute(':class') if (!expr) return @@ -240,12 +237,6 @@ export default class Entity { }) } - async evaluateLoadEvents() { - const loadExpr = this.element.getAttribute(':load') - if (!loadExpr) return - await this.evaluateEventAction(':load') - } - async evaluateEach() { const eachExpr = this.element.getAttribute(':each') @@ -329,137 +320,10 @@ export default class Entity { } } - applyEventBindings() { - this.removeEventBindings() - - const el = this.element - - // Change binding - if (el.hasAttribute(':change')) { - this.listener[':change'] = { - el, - eventName: - el.type == 'checkbox' || el.tagName == 'select' ? 'change' : 'input', - event: () => { - this.evaluateEventAction(':change') - }, - } - } - - if (el.hasAttribute(':clickout')) { - this.listener[':clickout'] = { - el: document, - eventName: 'click', - event: (e) => { - if (!document.documentElement.contains(e.target)) return - if (el.contains(e.target)) return - this.evaluateEventAction(':clickout') - }, - } - } - - if (el.hasAttribute(':press')) { - this.listener[':press'] = [] - this.listener[':press'].push({ - el, - eventName: 'keyup', - event: (e) => { - if (e.target !== el) return - if (!['Enter', 'Space'].includes(e.code)) return - if (e.code == 'Space') e.preventDefault() - this.evaluateEventAction(':press') - }, - }) - - this.listener[':press'].push({ - el, - eventName: 'click', - event: (e) => { - this.evaluateEventAction(':press') - }, - }) - - this.listener[':press'].push({ - el, - eventName: 'touchstart', - event: (e) => { - this.evaluateEventAction(':press') - }, - }) - } - - // Other Event Bindings - Array.from(el.attributes).forEach((attr) => { - if ( - attr.name.startsWith(':') && - !MiniJS.allCustomBindings.includes(attr.name) - ) { - const nativeEventName = attr.name.substring(1) - this.listener[attr.name] = { - el, - eventName: nativeEventName, - event: () => { - this.evaluateEventAction(attr.name) - }, - } - } else if (attr.name.startsWith(':keyup.')) { - const [event, keycode] = attr.name.split('.') - const nativeEventName = event.substring(1) - - let key = keycode[0].toUpperCase() + keycode.slice(1) - - if (['up', 'down', 'left', 'right'].includes(keycode)) { - key = 'Arrow' + key - } else if (!['enter', 'space'].includes(keycode)) { - return - } - - this.listener[attr.name] = { - el, - eventName: nativeEventName, - event: (e) => { - if (e.target !== el) return - if (e.key !== key) return - this.evaluateEventAction(attr.name) - }, - } - } - }) - - // Add event listeners - Object.keys(this.listener).forEach((key) => { - const listener = this.listener[key] - - if (Array.isArray(listener)) { - listener.forEach(({ el, eventName, event }) => { - el.addEventListener(eventName, event) - }) - } else { - const { el, eventName, event } = listener - el.addEventListener(eventName, event) - } - }) - } - hasAttribute(attr) { return !!this.element.getAttribute(attr) } - removeEventBindings() { - Object.keys(this.listener).forEach((key) => { - const listener = this.listener[key] - - if (Array.isArray(listener)) { - listener.forEach(({ el, eventName, event }) => { - el.removeEventListener(eventName, event) - }) - } else { - const { el, eventName, event } = listener - el.removeEventListener(eventName, event) - } - }) - } - dispose() { const elements = [this.element, ...this.element.querySelectorAll('*')] const variables = [] @@ -474,7 +338,7 @@ export default class Entity { if (!entity) continue variables.push(...entity.variables) - entity.removeEventBindings() + entity.events.dispose() } // Remove disposed elements diff --git a/lib/entity/events.js b/lib/entity/events.js new file mode 100644 index 0000000..dbbb03a --- /dev/null +++ b/lib/entity/events.js @@ -0,0 +1,139 @@ +export class Events { + constructor(base) { + this.base = base + this.listener = {} + } + + apply() { + this.dispose() + + const el = this.base.element + + // Change binding + if (el.hasAttribute(':change')) { + this.listener[':change'] = { + el, + eventName: + el.type == 'checkbox' || el.tagName == 'select' ? 'change' : 'input', + event: () => { + this.evaluate(':change') + }, + } + } + + if (el.hasAttribute(':clickout')) { + this.listener[':clickout'] = { + el: document, + eventName: 'click', + event: (e) => { + if (!document.documentElement.contains(e.target)) return + if (el.contains(e.target)) return + this.evaluate(':clickout') + }, + } + } + + if (el.hasAttribute(':press')) { + this.listener[':press'] = [] + this.listener[':press'].push({ + el, + eventName: 'keyup', + event: (e) => { + if (e.target !== el) return + if (!['Enter', 'Space'].includes(e.code)) return + if (e.code == 'Space') e.preventDefault() + this.evaluate(':press') + }, + }) + + this.listener[':press'].push({ + el, + eventName: 'click', + event: (e) => { + this.evaluate(':press') + }, + }) + + this.listener[':press'].push({ + el, + eventName: 'touchstart', + event: (e) => { + this.evaluate(':press') + }, + }) + } + + // Other Event Bindings + Array.from(el.attributes).forEach((attr) => { + if ( + attr.name.startsWith(':') && + !MiniJS.allCustomBindings.includes(attr.name) + ) { + const nativeEventName = attr.name.substring(1) + this.listener[attr.name] = { + el, + eventName: nativeEventName, + event: () => { + this.evaluate(attr.name) + }, + } + } else if (attr.name.startsWith(':keyup.')) { + const [event, keycode] = attr.name.split('.') + const nativeEventName = event.substring(1) + + let key = keycode[0].toUpperCase() + keycode.slice(1) + + if (['up', 'down', 'left', 'right'].includes(keycode)) { + key = 'Arrow' + key + } else if (!['enter', 'space'].includes(keycode)) { + return + } + + this.listener[attr.name] = { + el, + eventName: nativeEventName, + event: (e) => { + if (e.target !== el) return + if (e.key !== key) return + this.evaluate(attr.name) + }, + } + } + }) + + // Add event listeners + Object.keys(this.listener).forEach((key) => { + const listener = this.listener[key] + + if (Array.isArray(listener)) { + listener.forEach(({ el, eventName, event }) => { + el.addEventListener(eventName, event) + }) + } else { + const { el, eventName, event } = listener + el.addEventListener(eventName, event) + } + }) + } + + async evaluate(attr) { + const value = this.base.element.getAttribute(attr) + if (!value) return + await this.base._interpret(value) + } + + dispose() { + Object.keys(this.listener).forEach((key) => { + const listener = this.listener[key] + + if (Array.isArray(listener)) { + listener.forEach(({ el, eventName, event }) => { + el.removeEventListener(eventName, event) + }) + } else { + const { el, eventName, event } = listener + el.removeEventListener(eventName, event) + } + }) + } +} diff --git a/lib/main.js b/lib/main.js index 66d12bb..cf23812 100644 --- a/lib/main.js +++ b/lib/main.js @@ -172,7 +172,7 @@ const MiniJS = (() => { function _evaluateLoadEvents() { _elements.forEach((entity) => { - entity.evaluateLoadEvents() + entity.events.evaluate(':load') }) } @@ -192,7 +192,7 @@ const MiniJS = (() => { function _applyBindings() { _elements.forEach((entity) => { - entity.applyEventBindings() + entity.events.apply() }) } From d03ae2b3cfab61951be428c0aa8f9446b441752b Mon Sep 17 00:00:00 2001 From: Joeylene <23741509+jorenrui@users.noreply.github.com> Date: Sun, 11 Feb 2024 00:13:16 +0800 Subject: [PATCH 02/10] refactor: let events class manage possible events and is valid event --- lib/entity.js | 15 ++++----------- lib/entity/events.js | 39 +++++++++++++++++++++++++++++++++++++++ lib/main.js | 35 ++--------------------------------- 3 files changed, 45 insertions(+), 44 deletions(-) diff --git a/lib/entity.js b/lib/entity.js index 2abe9cf..49dee62 100644 --- a/lib/entity.js +++ b/lib/entity.js @@ -140,21 +140,14 @@ export default class Entity { } get allEvents() { - const allMainEvents = MiniJS.allEvents - const eventsSet = new Set(allMainEvents) const attributeNames = Array.from(this.element.attributes).map( (attr) => attr.name ) + const validEvents = attributeNames.filter((value) => + Events.isValidEvent(value) + ) - const intersections = attributeNames.filter((value) => { - if (eventsSet.has(value)) return true - if (!value.startsWith(':')) return false - - const nativeEventName = `on${value.substring(1)}` - return eventsSet.has(nativeEventName) - }) - - return intersections + return validEvents } get baseClasses() { diff --git a/lib/entity/events.js b/lib/entity/events.js index dbbb03a..2c082fd 100644 --- a/lib/entity/events.js +++ b/lib/entity/events.js @@ -1,4 +1,43 @@ +const ELEMENTS = [ + 'div', + 'a', + 'input', + 'textarea', + 'select', + 'button', + 'video', + 'audio', + 'img', + 'form', + 'details', + 'iframe', + 'canvas', +] + export class Events { + static CUSTOM_EVENTS = [':change', ':clickout', ':press'] + static CUSTOM_KEY_EVENTS = [':keyup', ':keydown', ':keypress'] + + static initValidEvents() { + const events = new Set() + + ELEMENTS.forEach((tag) => { + const el = document.createElement(tag) + for (const name in el) { + if (name.startsWith('on')) events.add(`:${name.substring(2)}`) + } + }) + + Events.validEvents = [...events, ...Events.CUSTOM_EVENTS] + } + + static isValidEvent(event) { + return ( + Events.validEvents.includes(event) || + Events.CUSTOM_KEY_EVENTS.some((key) => event.startsWith(key + '.')) + ) + } + constructor(base) { this.base = base this.listener = {} diff --git a/lib/main.js b/lib/main.js index cf23812..f2453d9 100644 --- a/lib/main.js +++ b/lib/main.js @@ -2,6 +2,7 @@ import Entity from './entity' import MiniArray from './helpers/array' import { Lexer } from './generators/lexer' import { observeDOM } from './generators/observer' +import { Events } from './entity/events' let nativeProps = Object.getOwnPropertyNames(window) @@ -12,7 +13,6 @@ const MiniJS = (() => { let _ignoredVariables = [] let _elements = [] let _variables = [] - let _allEvents = [] let _customStatements = [':each'] let _customProperties = [':text', ':class', ':value', ':checked'] let _customEvents = [ @@ -49,34 +49,6 @@ const MiniJS = (() => { }, } - function _getAllPossibleEventNames() { - const elements = [ - 'div', - 'a', - 'input', - 'textarea', - 'select', - 'button', - 'video', - 'audio', - 'img', - 'form', - 'details', - 'iframe', - 'canvas', - ] - const allEvents = new Set() - - elements.forEach((tag) => { - const ele = document.createElement(tag) - for (let name in ele) { - if (name.startsWith('on')) allEvents.add(name) - } - }) - - _allEvents = [...allEvents] - } - async function init() { // Automatically initialize when the script is loaded await _domReady() @@ -84,7 +56,7 @@ const MiniJS = (() => { let startTime = performance.now() _setDebugMode() _setProxyWindow() - _getAllPossibleEventNames() + Events.initValidEvents() _findElements() _initializeGlobalVariables() _addMethodsToVariables() @@ -251,9 +223,6 @@ const MiniJS = (() => { get allCustomBindings() { return [..._customProperties, ..._customEvents, ..._customStatements] }, - get allEvents() { - return [..._allEvents, ..._customEvents] - }, get window() { return proxyWindow }, From 9f20ad1dc930a81abf67518d1a7e12b6b2c87b1d Mon Sep 17 00:00:00 2001 From: Joeylene <23741509+jorenrui@users.noreply.github.com> Date: Sun, 11 Feb 2024 00:24:21 +0800 Subject: [PATCH 03/10] refactor: relocate custom event checker logic --- lib/entity.js | 1 + lib/entity/events.js | 33 +++++++++++++++++++-------------- lib/main.js | 13 +------------ 3 files changed, 21 insertions(+), 26 deletions(-) diff --git a/lib/entity.js b/lib/entity.js index 49dee62..2f47f4a 100644 --- a/lib/entity.js +++ b/lib/entity.js @@ -32,6 +32,7 @@ export default class Entity { isDynamicAttribute(attr) { if (!attr.startsWith(':')) return false + if (Events.isValidEvent(attr)) return false if (MiniJS.allCustomBindings.includes(attr)) return false if (this.allEvents.includes(attr)) return false return true diff --git a/lib/entity/events.js b/lib/entity/events.js index 2c082fd..38e0a54 100644 --- a/lib/entity/events.js +++ b/lib/entity/events.js @@ -15,8 +15,13 @@ const ELEMENTS = [ ] export class Events { - static CUSTOM_EVENTS = [':change', ':clickout', ':press'] static CUSTOM_KEY_EVENTS = [':keyup', ':keydown', ':keypress'] + static CUSTOM_EVENTS = [ + ':change', + ':clickout', + ':press', + ...Events.CUSTOM_KEY_EVENTS, + ] static initValidEvents() { const events = new Set() @@ -104,19 +109,9 @@ export class Events { // Other Event Bindings Array.from(el.attributes).forEach((attr) => { - if ( - attr.name.startsWith(':') && - !MiniJS.allCustomBindings.includes(attr.name) - ) { - const nativeEventName = attr.name.substring(1) - this.listener[attr.name] = { - el, - eventName: nativeEventName, - event: () => { - this.evaluate(attr.name) - }, - } - } else if (attr.name.startsWith(':keyup.')) { + if (!Events.isValidEvent(attr.name)) return + + if (attr.name.startsWith(':keyup.')) { const [event, keycode] = attr.name.split('.') const nativeEventName = event.substring(1) @@ -137,6 +132,16 @@ export class Events { this.evaluate(attr.name) }, } + } else if (!Events.CUSTOM_EVENTS.includes(attr.name)) { + const nativeEventName = attr.name.substring(1) + + this.listener[attr.name] = { + el, + eventName: nativeEventName, + event: () => { + this.evaluate(attr.name) + }, + } } }) diff --git a/lib/main.js b/lib/main.js index f2453d9..bdece41 100644 --- a/lib/main.js +++ b/lib/main.js @@ -15,17 +15,6 @@ const MiniJS = (() => { let _variables = [] let _customStatements = [':each'] let _customProperties = [':text', ':class', ':value', ':checked'] - let _customEvents = [ - ':change', - ':clickout', - ':keyup.up', - ':keyup.left', - ':keyup.down', - ':keyup.right', - ':keyup.enter', - ':keyup.space', - ':press', - ] const watchHandler = { set: function (target, property, value) { @@ -221,7 +210,7 @@ const MiniJS = (() => { _ignoredVariables = ignoredVariables }, get allCustomBindings() { - return [..._customProperties, ..._customEvents, ..._customStatements] + return [..._customProperties, ..._customStatements] }, get window() { return proxyWindow From 553f1e0e119b2073f3cffe506ec7b568fcfab32c Mon Sep 17 00:00:00 2001 From: Joeylene <23741509+jorenrui@users.noreply.github.com> Date: Sun, 11 Feb 2024 00:25:27 +0800 Subject: [PATCH 04/10] refactor: relocate apply event bindings --- lib/entity/events.js | 6 ++++++ lib/main.js | 8 +------- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/lib/entity/events.js b/lib/entity/events.js index 38e0a54..4822416 100644 --- a/lib/entity/events.js +++ b/lib/entity/events.js @@ -36,6 +36,12 @@ export class Events { Events.validEvents = [...events, ...Events.CUSTOM_EVENTS] } + static applyEvents(entities) { + entities.forEach((entity) => { + entity.events.apply() + }) + } + static isValidEvent(event) { return ( Events.validEvents.includes(event) || diff --git a/lib/main.js b/lib/main.js index bdece41..6b9784d 100644 --- a/lib/main.js +++ b/lib/main.js @@ -49,7 +49,7 @@ const MiniJS = (() => { _findElements() _initializeGlobalVariables() _addMethodsToVariables() - _applyBindings() + Events.applyEvents(_elements) updateStates() _listenToDOMChanges() // Temporarily commented out - to be reviewed @@ -151,12 +151,6 @@ const MiniJS = (() => { } } - function _applyBindings() { - _elements.forEach((entity) => { - entity.events.apply() - }) - } - function _findElements() { const elems = document.body.getElementsByTagName('*') From 536c729b09fa89639261fa68e6b641e0625b06d9 Mon Sep 17 00:00:00 2001 From: Joeylene <23741509+jorenrui@users.noreply.github.com> Date: Sun, 11 Feb 2024 00:39:07 +0800 Subject: [PATCH 05/10] refactor: relocate tracked events to events class --- lib/entity.js | 16 +-- lib/entity/events.js | 264 ++++++++++++++++++++++++++----------------- 2 files changed, 162 insertions(+), 118 deletions(-) diff --git a/lib/entity.js b/lib/entity.js index 2f47f4a..6f99350 100644 --- a/lib/entity.js +++ b/lib/entity.js @@ -32,9 +32,8 @@ export default class Entity { isDynamicAttribute(attr) { if (!attr.startsWith(':')) return false - if (Events.isValidEvent(attr)) return false if (MiniJS.allCustomBindings.includes(attr)) return false - if (this.allEvents.includes(attr)) return false + if (this.events.trackedEvents.includes(attr)) return false return true } @@ -87,7 +86,7 @@ export default class Entity { _getVariablesFromEvents() { const RESERVED_KEYWORDS = ['event', '$', 'window', 'document', 'console'] - this.allEvents.forEach((event) => { + this.events.trackedEvents.forEach((event) => { const expr = this.element.getAttribute(event) const lexer = new Lexer(expr, { @@ -140,17 +139,6 @@ export default class Entity { }) } - get allEvents() { - const attributeNames = Array.from(this.element.attributes).map( - (attr) => attr.name - ) - const validEvents = attributeNames.filter((value) => - Events.isValidEvent(value) - ) - - return validEvents - } - get baseClasses() { return this.initialState.className.split(' ') } diff --git a/lib/entity/events.js b/lib/entity/events.js index 4822416..c8e0b7b 100644 --- a/lib/entity/events.js +++ b/lib/entity/events.js @@ -54,136 +54,192 @@ export class Events { this.listener = {} } + get trackedEvents() { + const attributeNames = Array.from(this.base.element.attributes).map( + (attr) => attr.name + ) + const validEvents = attributeNames.filter((value) => + Events.isValidEvent(value) + ) + + return validEvents + } + apply() { this.dispose() + this.setChangeEvent() + this.setClickoutEvent() + this.setPressEvent() + const el = this.base.element - // Change binding - if (el.hasAttribute(':change')) { - this.listener[':change'] = { - el, - eventName: - el.type == 'checkbox' || el.tagName == 'select' ? 'change' : 'input', - event: () => { - this.evaluate(':change') - }, - } - } + // Other Event Bindings + Array.from(el.attributes).forEach((attr) => { + if (!Events.isValidEvent(attr.name)) return - if (el.hasAttribute(':clickout')) { - this.listener[':clickout'] = { - el: document, - eventName: 'click', - event: (e) => { - if (!document.documentElement.contains(e.target)) return - if (el.contains(e.target)) return - this.evaluate(':clickout') - }, + const isKeyEvent = Events.CUSTOM_KEY_EVENTS.some((keyType) => + attr.name.startsWith(keyType + '.') + ) + + if (isKeyEvent) { + this.setKeyEvent(attr.name) + } else if (!Events.CUSTOM_EVENTS.includes(attr.name)) { + this.setEvent(attr.name) } - } + }) - if (el.hasAttribute(':press')) { - this.listener[':press'] = [] - this.listener[':press'].push({ - el, - eventName: 'keyup', - event: (e) => { - if (e.target !== el) return - if (!['Enter', 'Space'].includes(e.code)) return - if (e.code == 'Space') e.preventDefault() - this.evaluate(':press') - }, - }) + // Add event listeners + Object.keys(this.listener).forEach((attr) => { + this.applyEvent(attr) + }) + } - this.listener[':press'].push({ - el, - eventName: 'click', - event: (e) => { - this.evaluate(':press') - }, - }) + applyEvent(attr) { + const listener = this.listener[attr] + if (!listener) return - this.listener[':press'].push({ - el, - eventName: 'touchstart', - event: (e) => { - this.evaluate(':press') - }, + if (Array.isArray(listener)) { + listener.forEach(({ el, eventName, event }) => { + el.addEventListener(eventName, event) }) + } else { + const { el, eventName, event } = listener + el.addEventListener(eventName, event) } + } - // Other Event Bindings - Array.from(el.attributes).forEach((attr) => { - if (!Events.isValidEvent(attr.name)) return + setChangeEvent() { + const el = this.base.element - if (attr.name.startsWith(':keyup.')) { - const [event, keycode] = attr.name.split('.') - const nativeEventName = event.substring(1) - - let key = keycode[0].toUpperCase() + keycode.slice(1) - - if (['up', 'down', 'left', 'right'].includes(keycode)) { - key = 'Arrow' + key - } else if (!['enter', 'space'].includes(keycode)) { - return - } - - this.listener[attr.name] = { - el, - eventName: nativeEventName, - event: (e) => { - if (e.target !== el) return - if (e.key !== key) return - this.evaluate(attr.name) - }, - } - } else if (!Events.CUSTOM_EVENTS.includes(attr.name)) { - const nativeEventName = attr.name.substring(1) - - this.listener[attr.name] = { - el, - eventName: nativeEventName, - event: () => { - this.evaluate(attr.name) - }, - } - } + if (!el.hasAttribute(':change')) return + if (this.listener[':change']) this.disposeEvent(':change') + + this.listener[':change'] = { + el, + eventName: + el.type == 'checkbox' || el.tagName == 'select' ? 'change' : 'input', + event: () => { + this.evaluate(':change') + }, + } + } + + setClickoutEvent() { + const el = this.base.element + + if (!el.hasAttribute(':clickout')) return + if (this.listener[':clickout']) this.disposeEvent(':clickout') + + this.listener[':clickout'] = { + el: document, + eventName: 'click', + event: (e) => { + if (!document.documentElement.contains(e.target)) return + if (el.contains(e.target)) return + this.evaluate(':clickout') + }, + } + } + + setPressEvent() { + const el = this.base.element + + if (!el.hasAttribute(':press')) return + if (this.listener[':press']) this.disposeEvent(':press') + + this.listener[':press'] = [] + this.listener[':press'].push({ + el, + eventName: 'keyup', + event: (e) => { + if (e.target !== el) return + if (!['Enter', 'Space'].includes(e.code)) return + if (e.code == 'Space') e.preventDefault() + this.evaluate(':press') + }, }) - // Add event listeners - Object.keys(this.listener).forEach((key) => { - const listener = this.listener[key] - - if (Array.isArray(listener)) { - listener.forEach(({ el, eventName, event }) => { - el.addEventListener(eventName, event) - }) - } else { - const { el, eventName, event } = listener - el.addEventListener(eventName, event) - } + this.listener[':press'].push({ + el, + eventName: 'click', + event: (e) => { + this.evaluate(':press') + }, + }) + + this.listener[':press'].push({ + el, + eventName: 'touchstart', + event: (e) => { + this.evaluate(':press') + }, }) } + setKeyEvent(attr) { + const [event, keycode] = attr.split('.') + + if (!Events.CUSTOM_KEY_EVENTS.includes(event)) return + + const el = this.base.element + + if (!el.hasAttribute(attr)) return + if (this.listener[attr]) this.disposeEvent(attr) + + let key = keycode[0].toUpperCase() + keycode.slice(1) + + if (['up', 'down', 'left', 'right'].includes(keycode)) { + key = 'Arrow' + key + } else if (!['enter', 'space'].includes(keycode)) { + return + } + + this.listener[attr] = { + el, + eventName: event.substring(1), + event: (e) => { + if (e.target !== el) return + if (e.key !== key) return + this.evaluate(attr) + }, + } + } + + setEvent(attr) { + const el = this.base.element + const nativeEventName = attr.substring(1) + + this.listener[attr] = { + el, + eventName: nativeEventName, + event: () => { + this.evaluate(attr) + }, + } + } + async evaluate(attr) { const value = this.base.element.getAttribute(attr) if (!value) return await this.base._interpret(value) } - dispose() { - Object.keys(this.listener).forEach((key) => { - const listener = this.listener[key] - - if (Array.isArray(listener)) { - listener.forEach(({ el, eventName, event }) => { - el.removeEventListener(eventName, event) - }) - } else { - const { el, eventName, event } = listener + disposeEvent(attr) { + const listener = this.listener[attr] + if (Array.isArray(listener)) { + listener.forEach(({ el, eventName, event }) => { el.removeEventListener(eventName, event) - } + }) + } else { + const { el, eventName, event } = listener + el.removeEventListener(eventName, event) + } + } + + dispose() { + Object.keys(this.listener).forEach((attr) => { + this.disposeEvent(attr) }) } } From 77e332265d7f5963f079cca87c5367a16e49e521 Mon Sep 17 00:00:00 2001 From: Joeylene <23741509+jorenrui@users.noreply.github.com> Date: Sun, 11 Feb 2024 00:54:01 +0800 Subject: [PATCH 06/10] refactor: update tracked events to dynamic events --- lib/entity.js | 2 +- lib/entity/events.js | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/lib/entity.js b/lib/entity.js index 6f99350..0430376 100644 --- a/lib/entity.js +++ b/lib/entity.js @@ -86,7 +86,7 @@ export default class Entity { _getVariablesFromEvents() { const RESERVED_KEYWORDS = ['event', '$', 'window', 'document', 'console'] - this.events.trackedEvents.forEach((event) => { + this.events.dynamicEvents.forEach((event) => { const expr = this.element.getAttribute(event) const lexer = new Lexer(expr, { diff --git a/lib/entity/events.js b/lib/entity/events.js index c8e0b7b..73d08cf 100644 --- a/lib/entity/events.js +++ b/lib/entity/events.js @@ -43,6 +43,7 @@ export class Events { } static isValidEvent(event) { + if (!event.startsWith(':')) return false return ( Events.validEvents.includes(event) || Events.CUSTOM_KEY_EVENTS.some((key) => event.startsWith(key + '.')) @@ -52,9 +53,12 @@ export class Events { constructor(base) { this.base = base this.listener = {} + this.dynamicEvents = [] + + this._getDynamicEvents() } - get trackedEvents() { + _getDynamicEvents() { const attributeNames = Array.from(this.base.element.attributes).map( (attr) => attr.name ) From c68aa52260f3adbddef730c5c66f30162a49ed0f Mon Sep 17 00:00:00 2001 From: Joeylene <23741509+jorenrui@users.noreply.github.com> Date: Sun, 11 Feb 2024 01:29:23 +0800 Subject: [PATCH 07/10] fix: variables from events not being tracked --- lib/entity/events.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/lib/entity/events.js b/lib/entity/events.js index 73d08cf..d5b6c73 100644 --- a/lib/entity/events.js +++ b/lib/entity/events.js @@ -62,11 +62,10 @@ export class Events { const attributeNames = Array.from(this.base.element.attributes).map( (attr) => attr.name ) - const validEvents = attributeNames.filter((value) => + + this.dynamicEvents = attributeNames.filter((value) => Events.isValidEvent(value) ) - - return validEvents } apply() { From 455f67f3955a235deef88986557d9dd21ea151ff Mon Sep 17 00:00:00 2001 From: Joeylene <23741509+jorenrui@users.noreply.github.com> Date: Sun, 11 Feb 2024 01:29:45 +0800 Subject: [PATCH 08/10] refactor: relocate logic for entity attributes --- lib/entity.js | 130 ++--------------------------------- lib/entity/attributes.js | 143 +++++++++++++++++++++++++++++++++++++++ lib/main.js | 10 +-- 3 files changed, 149 insertions(+), 134 deletions(-) create mode 100644 lib/entity/attributes.js diff --git a/lib/entity.js b/lib/entity.js index 0430376..a3f4cea 100644 --- a/lib/entity.js +++ b/lib/entity.js @@ -1,7 +1,7 @@ import { Interpreter, ClassInterpreter } from './generators/interpreter' import { Lexer } from './generators/lexer' -import { escapeHTML } from './helpers/sanitize' import { Events } from './entity/events' +import { Attributes } from './entity/attributes' export default class Entity { constructor(el) { @@ -11,12 +11,10 @@ export default class Entity { className: el.className, } this.variables = [] - this.dynamicAttributes = [] this.id = this.generateEntityUUID() this.events = new Events(this) - - this._getDynamicAttributes() + this.attributes = new Attributes(this) if (MiniJS.debug) this.element.dataset.entityId = this.id } @@ -30,22 +28,6 @@ export default class Entity { return !!this.uuid } - isDynamicAttribute(attr) { - if (!attr.startsWith(':')) return false - if (MiniJS.allCustomBindings.includes(attr)) return false - if (this.events.trackedEvents.includes(attr)) return false - return true - } - - _getDynamicAttributes() { - for (let i = 0; i < this.element.attributes.length; i++) { - const attr = this.element.attributes[i] - if (!this.isDynamicAttribute(attr.name)) continue - if (this.dynamicAttributes.includes(attr.name)) continue - this.dynamicAttributes.push(attr.name) - } - } - getVariables() { this._getVariablesFromAttributes() this._getVariablesFromEvents() @@ -54,11 +36,8 @@ export default class Entity { _getVariablesFromAttributes() { const RESERVED_KEYWORDS = ['$', 'window', 'document', 'console'] - const CUSTOM_ATTRIBUTES = [':each', ':class', ':text', ':value', ':checked'] - const attributes = [...this.dynamicAttributes, ...CUSTOM_ATTRIBUTES] - - attributes.forEach((name) => { + this.attributes.dynamicAttributes.forEach((name) => { const attr = this.element.attributes[name] if (!attr) return @@ -139,10 +118,6 @@ export default class Entity { }) } - get baseClasses() { - return this.initialState.className.split(' ') - } - async _interpret(expr, options = {}) { const Engine = options.isClass ? ClassInterpreter : Interpreter const engine = new Engine(expr, options) @@ -188,7 +163,7 @@ export default class Entity { async init() { this.getVariables() this.events.apply() - await this.evaluateAll() + await this.attributes.evaluate() MiniJS.elements.push(this) } @@ -209,103 +184,6 @@ export default class Entity { } } - async evaluateClass() { - const expr = this.element.getAttribute(':class') - if (!expr) return - - this.element.className = await this._interpret(expr, { - base: this.baseClasses, - isClass: true, - }) - } - - async evaluateEach() { - const eachExpr = this.element.getAttribute(':each') - - if (eachExpr) { - const [args, iterable] = eachExpr.split(' in ') - const [variable, indexName] = args.split(',').map((v) => v.trim()) - const items = await this._interpret(iterable) - this.childClone ||= this.element.innerHTML - - let newHTML = '' - - items.forEach((item, index) => { - // TODO: Use the lexer to replace the variables - newHTML += this.childClone - .replaceAll(indexName, index) - .replaceAll(variable, `'${escapeHTML(item)}'`) - }) - - // ObserveDOM will be called for updated DOM to initialize the entities - this.element.innerHTML = newHTML - } - } - - async evaluateAttribute(attribute) { - if (attribute === ':class') this.evaluateClass() - else if (attribute === ':text') this.evaluateText() - else if ([':value', ':checked'].includes(attribute)) this.evaluateValue() - else if (attribute === ':each') this.evaluateEach() - else if (this.isDynamicAttribute(attribute)) { - if (!this.dynamicAttributes.includes(attribute)) - this.dynamicAttributes.push(attribute) - this.evaluateDynamicAttributes() - } - } - - async evaluateAll() { - await this.evaluateValue() - await this.evaluateClass() - await this.evaluateText() - await this.evaluateDynamicAttributes() - } - - async evaluateDynamicAttributes() { - for (const attr of this.dynamicAttributes) { - const expr = this.element.getAttribute(attr) - if (!expr) return - - const newValue = await this._interpret(expr) - const nativeAttr = attr.slice(1) - - if (this.element[nativeAttr] !== newValue && newValue != null) - this.element[nativeAttr] = newValue - } - } - - async evaluateText() { - const textExpr = this.element.getAttribute(':text') - if (!textExpr) return - - const newText = await this._interpret(textExpr) - - if (newText || newText == '') this.element.innerText = newText - } - - async evaluateValue() { - const valueExpr = this.element.getAttribute(':value') - - if (valueExpr) { - const newValue = await this._interpret(valueExpr) - - if (this.element.value !== newValue && newValue != null) - this.element.value = newValue - } - - const checkedExpr = this.element.getAttribute(':checked') - - if (checkedExpr) { - const newValue = await this._interpret(checkedExpr) - - if (newValue) this.element.checked = newValue - } - } - - hasAttribute(attr) { - return !!this.element.getAttribute(attr) - } - dispose() { const elements = [this.element, ...this.element.querySelectorAll('*')] const variables = [] diff --git a/lib/entity/attributes.js b/lib/entity/attributes.js new file mode 100644 index 0000000..8547c4e --- /dev/null +++ b/lib/entity/attributes.js @@ -0,0 +1,143 @@ +import { Events } from './events' +import { escapeHTML } from '../helpers/sanitize' + +export class Attributes { + static CUSTOM_ATTRIBUTES = [':class', ':text', ':value', ':checked', ':each'] + + static isValidAttribute(attribute, element) { + if (!attribute.startsWith(':')) return false + if (Events.isValidEvent(attribute)) return false + if (Attributes.CUSTOM_ATTRIBUTES.includes(attribute)) return true + + const [nativeAttr] = attribute.replace(':', '').split('.') + if (element[nativeAttr] !== undefined) return false + + return true + } + + constructor(base) { + this.base = base + this.dynamicAttributes = [] + this.childClone = null + this.initialState = { className: this.base.element.className } + + this._getDynamicAttributes() + } + + _getDynamicAttributes() { + const el = this.base.element + + for (let i = 0; i < el.attributes.length; i++) { + const attr = el.attributes[i] + if (!Attributes.isValidAttribute(attr.name, el)) continue + if (!this.dynamicAttributes.includes(attr.name)) + this.dynamicAttributes.push(attr.name) + } + } + + get baseClasses() { + return this.initialState.className.split(' ') + } + + async evaluate() { + await this.evaluateClass() + await this.evaluateText() + await this.evaluateValue() + + for (const attr of this.dynamicAttributes) { + if (Attributes.CUSTOM_ATTRIBUTES.includes(attr)) continue + await this.evaluateAttribute(attr) + } + + await this.evaluateEach() + } + + async evaluateAttribute(attr) { + if (!Attributes.isValidAttribute(attr, this.base.element)) return + if (attr === ':class') await this.evaluateClass() + else if (attr === ':text') await this.evaluateText() + else if ([':value', ':checked'].includes(attr)) await this.evaluateValue() + else if (attr === ':each') await this.evaluateEach() + else { + if (!this.dynamicAttributes.includes(attr)) + this.dynamicAttributes.push(attr) + await this.evaluateOtherAttributes() + } + } + + async evaluateClass() { + const expr = this.base.element.getAttribute(':class') + if (!expr) return + + this.base.element.className = await this.base._interpret(expr, { + base: this.baseClasses, + isClass: true, + }) + } + + async evaluateText() { + const textExpr = this.base.element.getAttribute(':text') + if (!textExpr) return + + const newText = await this.base._interpret(textExpr) + + if (newText || newText == '') this.base.element.innerText = newText + } + + async evaluateValue() { + const valueExpr = this.base.element.getAttribute(':value') + + if (valueExpr) { + const newValue = await this.base._interpret(valueExpr) + + if (this.base.element.value !== newValue && newValue != null) + this.base.element.value = newValue + } + + const checkedExpr = this.base.element.getAttribute(':checked') + + if (checkedExpr) { + const newValue = await this.base._interpret(checkedExpr) + + if (newValue) this.base.element.checked = newValue + } + } + + async evaluateOtherAttributes() { + for (const attr of this.dynamicAttributes) { + if (Attributes.CUSTOM_ATTRIBUTES.includes(attr)) continue + + const expr = this.base.element.getAttribute(attr) + if (!expr) return + + const newValue = await this.base._interpret(expr) + const nativeAttr = attr.slice(1) + + if (this.base.element[nativeAttr] !== newValue && newValue != null) + this.base.element[nativeAttr] = newValue + } + } + + async evaluateEach() { + const eachExpr = this.base.element.getAttribute(':each') + + if (eachExpr == null) return + + const [args, iterable] = eachExpr.split(' in ') + const [variable, indexName] = args.split(',').map((v) => v.trim()) + const items = await this.base._interpret(iterable) + this.childClone ||= this.base.element.innerHTML + + let newHTML = '' + + items.forEach((item, index) => { + // TODO: Use the lexer to replace the variables + newHTML += this.childClone + .replaceAll(indexName, index) + .replaceAll(variable, `'${escapeHTML(item)}'`) + }) + + // ObserveDOM will be called for updated DOM to initialize the entities + this.base.element.innerHTML = newHTML + } +} diff --git a/lib/main.js b/lib/main.js index 6b9784d..958e940 100644 --- a/lib/main.js +++ b/lib/main.js @@ -13,8 +13,6 @@ const MiniJS = (() => { let _ignoredVariables = [] let _elements = [] let _variables = [] - let _customStatements = [':each'] - let _customProperties = [':text', ':class', ':value', ':checked'] const watchHandler = { set: function (target, property, value) { @@ -69,7 +67,7 @@ const MiniJS = (() => { const entity = _elements.find( (entity) => entity.element === record.target ) - entity?.evaluateAttribute(record.attributeName) + entity?.attributes.evaluateAttribute(record.attributeName) } record.removedNodes.forEach((node) => { @@ -145,8 +143,7 @@ const MiniJS = (() => { entity.uuid == property || entity.parent?.uuid == property ) { - await entity.evaluateEach() - await entity.evaluateAll() + await entity.attributes.evaluate() } } } @@ -203,9 +200,6 @@ const MiniJS = (() => { set ignore(ignoredVariables) { _ignoredVariables = ignoredVariables }, - get allCustomBindings() { - return [..._customProperties, ..._customStatements] - }, get window() { return proxyWindow }, From 97c0776b1754cc7c0f77b9afa70ea531b52b5084 Mon Sep 17 00:00:00 2001 From: Joeylene <23741509+jorenrui@users.noreply.github.com> Date: Sun, 11 Feb 2024 01:30:31 +0800 Subject: [PATCH 09/10] refactor: remove unused values --- lib/entity.js | 3 --- lib/main.js | 7 ------- 2 files changed, 10 deletions(-) diff --git a/lib/entity.js b/lib/entity.js index a3f4cea..ff82aaa 100644 --- a/lib/entity.js +++ b/lib/entity.js @@ -7,9 +7,6 @@ export default class Entity { constructor(el) { this.element = el this.tagName = el.tagName - this.initialState = { - className: el.className, - } this.variables = [] this.id = this.generateEntityUUID() diff --git a/lib/main.js b/lib/main.js index 958e940..508f769 100644 --- a/lib/main.js +++ b/lib/main.js @@ -10,7 +10,6 @@ const MiniJS = (() => { window.proxyWindow = null let _debug = false - let _ignoredVariables = [] let _elements = [] let _variables = [] @@ -194,12 +193,6 @@ const MiniJS = (() => { set variables(newVarList) { _variables = newVarList }, - get ignore() { - return _ignoredVariables - }, - set ignore(ignoredVariables) { - _ignoredVariables = ignoredVariables - }, get window() { return proxyWindow }, From 6495dda3055d7c27accb99e1b048d80d9eb5d001 Mon Sep 17 00:00:00 2001 From: Joeylene <23741509+jorenrui@users.noreply.github.com> Date: Mon, 12 Feb 2024 13:09:28 +0800 Subject: [PATCH 10/10] Revert "Commit fully compiled files" This reverts commit 820c75355e18363e3e21d26d389f31e71e799f06. --- .gitignore | 1 + dist/minijs.es.js | 306 --------------------------------------------- dist/minijs.umd.js | 1 - 3 files changed, 1 insertion(+), 307 deletions(-) delete mode 100644 dist/minijs.es.js delete mode 100644 dist/minijs.umd.js diff --git a/.gitignore b/.gitignore index 0435f00..3db6100 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,5 @@ node_modules yarn.lock yarn-error.log +dist .DS_STORE \ No newline at end of file diff --git a/dist/minijs.es.js b/dist/minijs.es.js deleted file mode 100644 index 383eb4e..0000000 --- a/dist/minijs.es.js +++ /dev/null @@ -1,306 +0,0 @@ -class Entity { - constructor(e) { - this.element = e, this.tagName = e.tagName, this.initialState = { - className: e.className - }, this.variables = [], this.id = this.generateEntityUUID(); - } - setAsParent() { - this.uuid = this.id, this.element.dataset.uuid = this.uuid; - } - isParent() { - return !!this.uuid; - } - getVariablesFromEvents() { - this.allEvents.forEach((e) => { - const t = this.element.getAttribute(e); - if (t) { - const a = /(\$?\w+(\.\w+)?)\s*=/g; - let s; - for (; (s = a.exec(t)) !== null; ) - if (s && !window.hasOwnProperty(s[1]) && !s[1].includes("this.")) { - if (s[1].includes("el.")) { - const c = s[1].replace("el.", ""); - this.setAsParent(), window[this.uuid] || (window[this.uuid] = {}), window[this.uuid][c] = MiniJS.tryFromLocal(s[1].replace("el.", this.uuid)), MiniJS.variables.push(this.uuid); - } else - window[s[1]] = MiniJS.tryFromLocal(s[1]); - MiniJS.variables.push(s[1]); - } - } - }); - } - getVariables() { - const e = MiniJS.variables, t = Array.from(this.element.attributes).map((s) => s.value), a = [...new Set(e.filter((s) => t.find((c) => c.includes(s))))]; - for (let s of a) { - if (typeof window[s] == "function") { - const c = e.filter((h) => window[s].toString().includes(h)); - a.concat(c); - } - s.includes("el.") && !this.parent && (this.parent = this.getParent()); - } - this.variables = a; - } - get allEvents() { - const e = MiniJS.allEvents, t = new Set(e); - return Array.from(this.element.attributes).map((c) => c.name).filter((c) => t.has(c)); - } - get baseClasses() { - return this.initialState.className.split(" "); - } - _eventAction(e) { - const t = this.element.getAttribute(e); - return this._sanitizeExpression(t); - } - _sanitizeExpression(expr) { - return console.log(expr, this.variables), this.variables.forEach((variable) => { - const exp = expr.split(";").find((e) => e.includes(variable)); - if (exp) - if (exp.includes("el.")) { - window.temp = eval(`proxyWindow['${this.parent.uuid}']`); - let tempExpr = exp; - tempExpr = tempExpr.replaceAll(variable, `temp['${variable.replace("el.", "")}']`), eval(tempExpr); - const newVal = JSON.stringify(window.temp), newExpr = exp.replace(exp, `proxyWindow.${this.parent.uuid} = ${newVal};`); - expr = expr.replace(exp, newExpr); - } else - expr = expr.replace(variable, `proxyWindow.${variable}`); - }), expr = expr.replace("this", "this.element"), expr; - } - _sanitizeContentExpression(e) { - if (e.includes("el.")) { - let t = this.parent; - this.variables.forEach((a) => { - if (a.includes("el.")) { - const s = `proxyWindow.${t.uuid}['${a.replace("el.", "")}']`; - e = e.replace(a, s); - } - }); - } - return e; - } - getParent() { - if (this.isParent()) - return this; - { - let e = this.element, t = e.parentNode; - for (; !t.dataset.uuid; ) - e = t, t = e.parentNode; - return MiniJS.elements.find((s) => s.uuid == t.dataset.uuid); - } - } - generateEntityUUID() { - return "Entity" + Date.now() + Math.floor(Math.random() * 1e4); - } - evaluateEventAction(attrName) { - eval(this._eventAction(attrName)); - } - evaluateClass() { - const classExpr = this.element.getAttribute(":class"); - if (classExpr) { - const newClassNames = eval(this._sanitizeContentExpression(classExpr)), classesCombined = [...this.baseClasses, ...newClassNames.split(" ")].join(" "); - this.element.className = classesCombined; - } - } - evaluateEach() { - const eachExpr = this.element.getAttribute(":each"); - if (eachExpr) { - const [variable, iterable] = eachExpr.split(" in "), items = eval(iterable); - this.childClone || (this.childClone = this.element.innerHTML); - let newHTML = ""; - items.forEach((e) => { - newHTML += this.childClone.replaceAll(variable, `'${e}'`); - }), this.element.innerHTML = newHTML; - const elements = this.element.querySelectorAll("*"); - for (let e = 0; e < elements.length; e++) { - const t = new Entity(elements[e]); - t.getVariablesFromEvents(), t.getVariables(), t.applyEventBindings(), t.evaluateAll(), MiniJS.elements.push(t); - } - } - } - evaluateAll() { - this.evaluateValue(), this.evaluateClass(), this.evaluateText(); - } - evaluateText() { - const textExpr = this.element.getAttribute(":text"); - if (textExpr) { - const newText = eval(this._sanitizeContentExpression(textExpr)); - (newText || newText == "") && (this.element.innerText = newText); - } - } - evaluateValue() { - const valueExpr = this.element.getAttribute(":value"); - if (valueExpr) { - const newValue = eval(valueExpr); - newValue && (this.element.value = newValue); - } - const checkedExpr = this.element.getAttribute(":checked"); - if (checkedExpr) { - const newValue = eval(checkedExpr); - newValue && (this.element.checked = newValue); - } - } - applyEventBindings() { - const e = this.element; - this.allEvents.forEach((t) => { - e[t] = () => { - this.evaluateEventAction(t); - }; - }), e.hasAttribute(":click") && e.addEventListener("click", (t) => { - this.evaluateEventAction(":click"); - }), e.hasAttribute(":change") && (e.type == "checkbox" || e.tagName == "select" ? e.addEventListener("change", (t) => { - this.evaluateEventAction(":change"); - }) : e.addEventListener("input", (t) => { - this.evaluateEventAction(":change"); - })), e.hasAttribute(":enter") && e.addEventListener("keypress", (t) => { - t.key == "Enter" && this.evaluateEventAction(":enter"); - }), e.hasAttribute(":keypress") && e.addEventListener("keypress", (t) => { - this.evaluateEventAction(":keypress"); - }), e.hasAttribute(":keydown") && e.addEventListener("keydown", (t) => { - this.evaluateEventAction(":keydown"); - }), e.hasAttribute(":keyup") && e.addEventListener("keyup", (t) => { - this.evaluateEventAction(":keyup"); - }), document.addEventListener("click", (t) => { - e.hasAttribute(":clickout") && !e.contains(t.target) && this.evaluateEventAction(":clickout"); - }); - } - hasAttribute(e) { - return !!this.element.getAttribute(e); - } -} -let nativeProps = Object.getOwnPropertyNames(window); -const MiniJS$1 = (() => { - window.proxyWindow = null; - let e = [], t = [], a = [], s = [":click", ":change", ":input", ":clickout"]; - const c = { - set: function(n, i, o) { - return n[i] = o, i[0] === "$" && localStorage.setItem(i, JSON.stringify(o)), t.includes(i) && (m(i), f([i])), !0; - } - }; - function h() { - const n = ["div", "a", "input", "textarea", "select", "button", "video", "audio", "img", "form", "details", "iframe", "canvas"], i = /* @__PURE__ */ new Set(); - n.forEach((o) => { - const d = document.createElement(o); - for (let p in d) - p.startsWith("on") && i.add(p); - }), a = [...i]; - } - async function A() { - await _(); - let n = performance.now(); - S(), h(), L(), W(), f(), M(), k(), m(); - const o = performance.now() - n; - console.log(`myFunction took ${o}ms to run.`); - } - function k() { - Object.defineProperty(Number.prototype, "times", { - get: function() { - return Array.from({ length: this }); - } - }); - } - function f(n = t) { - n.forEach((i) => { - if (Array.isArray(proxyWindow[i])) { - let w = function() { - return proxyWindow[i][0]; - }, E = function() { - return proxyWindow[i][proxyWindow[i].length - 1]; - }, x = function(r) { - let l; - if (Array.isArray(r)) - l = r; - else { - let u = this.indexOf(r); - u === -1 ? l = this.concat(r) : l = this.slice(0, u).concat(this.slice(u + 1)); - } - proxyWindow[i] = l; - }, v = function(r) { - let l; - this.indexOf(r) === -1 && (l = this.concat(r), proxyWindow[i] = l); - }, g = function(r) { - let l, u = this.indexOf(r); - u !== -1 && (l = this.slice(0, u).concat(this.slice(u + 1)), proxyWindow[i] = l); - }, y = function(r, l) { - const u = r.toLowerCase().split(/\s+/); - return l.filter((P) => { - const C = P.toLowerCase(); - return u.every((J) => C.includes(J)); - }); - }, b = function(r) { - return y(r, this); - }; - var o = w, d = E, p = x, $ = v, O = g, z = y, F = b; - proxyWindow[i].first = w(), proxyWindow[i].last = E(), proxyWindow[i].remove = g, proxyWindow[i].add = v, proxyWindow[i].toggle = x, proxyWindow[i].search = b; - } - }); - } - function S() { - proxyWindow = new Proxy(window, c); - } - function V() { - t = Object.getOwnPropertyNames(window).filter((i) => !nativeProps.includes(i)); - } - function W() { - V(), e.forEach((n, i) => { - n.getVariablesFromEvents(i); - }), e.forEach((n, i) => { - n.getVariables(); - }); - } - function N(n) { - return n.startsWith("$") && JSON.parse(localStorage.getItem(n)) || void 0; - } - function m(n = null) { - e.forEach((i) => { - var o; - (i.variables.includes(n) || n == null || i.uuid == n || ((o = i.parent) == null ? void 0 : o.uuid) == n) && (i.evaluateEach(), i.evaluateAll()); - }); - } - function M() { - e.forEach((n) => { - n.applyEventBindings(); - }); - } - function L() { - var n = document.body.getElementsByTagName("*"); - for (let i = 0; i < n.length; i++) { - const o = n[i], d = new Entity(o); - T(d.element.parentElement) || e.push(d); - } - } - function T(n) { - for (; n; ) { - if (n.hasAttribute && n.hasAttribute(":each")) - return !0; - n = n.parentElement; - } - return !1; - } - function _() { - return new Promise((n) => { - document.readyState == "loading" ? document.addEventListener("DOMContentLoaded", n) : n(); - }); - } - return A().catch((n) => { - console.error("Error initializing MiniJS:", n); - }), { - get elements() { - return e; - }, - set elements(n) { - return n; - }, - get variables() { - return t; - }, - set variables(n) { - t = n; - }, - get allEvents() { - return [...a, ...s]; - }, - get window() { - return proxyWindow; - }, - tryFromLocal: N - }; -})(); -window.MiniJS = MiniJS$1; diff --git a/dist/minijs.umd.js b/dist/minijs.umd.js deleted file mode 100644 index cc0eaaf..0000000 --- a/dist/minijs.umd.js +++ /dev/null @@ -1 +0,0 @@ -(function(e){typeof define=="function"&&define.amd?define(e):e()})(function(){"use strict";class Entity{constructor(e){this.element=e,this.tagName=e.tagName,this.initialState={className:e.className},this.variables=[],this.id=this.generateEntityUUID()}setAsParent(){this.uuid=this.id,this.element.dataset.uuid=this.uuid}isParent(){return!!this.uuid}getVariablesFromEvents(){this.allEvents.forEach(e=>{const t=this.element.getAttribute(e);if(t){const a=/(\$?\w+(\.\w+)?)\s*=/g;let s;for(;(s=a.exec(t))!==null;)if(s&&!window.hasOwnProperty(s[1])&&!s[1].includes("this.")){if(s[1].includes("el.")){const c=s[1].replace("el.","");this.setAsParent(),window[this.uuid]||(window[this.uuid]={}),window[this.uuid][c]=MiniJS.tryFromLocal(s[1].replace("el.",this.uuid)),MiniJS.variables.push(this.uuid)}else window[s[1]]=MiniJS.tryFromLocal(s[1]);MiniJS.variables.push(s[1])}}})}getVariables(){const e=MiniJS.variables,t=Array.from(this.element.attributes).map(s=>s.value),a=[...new Set(e.filter(s=>t.find(c=>c.includes(s))))];for(let s of a){if(typeof window[s]=="function"){const c=e.filter(p=>window[s].toString().includes(p));a.concat(c)}s.includes("el.")&&!this.parent&&(this.parent=this.getParent())}this.variables=a}get allEvents(){const e=MiniJS.allEvents,t=new Set(e);return Array.from(this.element.attributes).map(c=>c.name).filter(c=>t.has(c))}get baseClasses(){return this.initialState.className.split(" ")}_eventAction(e){const t=this.element.getAttribute(e);return this._sanitizeExpression(t)}_sanitizeExpression(expr){return console.log(expr,this.variables),this.variables.forEach(variable=>{const exp=expr.split(";").find(e=>e.includes(variable));if(exp)if(exp.includes("el.")){window.temp=eval(`proxyWindow['${this.parent.uuid}']`);let tempExpr=exp;tempExpr=tempExpr.replaceAll(variable,`temp['${variable.replace("el.","")}']`),eval(tempExpr);const newVal=JSON.stringify(window.temp),newExpr=exp.replace(exp,`proxyWindow.${this.parent.uuid} = ${newVal};`);expr=expr.replace(exp,newExpr)}else expr=expr.replace(variable,`proxyWindow.${variable}`)}),expr=expr.replace("this","this.element"),expr}_sanitizeContentExpression(e){if(e.includes("el.")){let t=this.parent;this.variables.forEach(a=>{if(a.includes("el.")){const s=`proxyWindow.${t.uuid}['${a.replace("el.","")}']`;e=e.replace(a,s)}})}return e}getParent(){if(this.isParent())return this;{let e=this.element,t=e.parentNode;for(;!t.dataset.uuid;)e=t,t=e.parentNode;return MiniJS.elements.find(s=>s.uuid==t.dataset.uuid)}}generateEntityUUID(){return"Entity"+Date.now()+Math.floor(Math.random()*1e4)}evaluateEventAction(attrName){eval(this._eventAction(attrName))}evaluateClass(){const classExpr=this.element.getAttribute(":class");if(classExpr){const newClassNames=eval(this._sanitizeContentExpression(classExpr)),classesCombined=[...this.baseClasses,...newClassNames.split(" ")].join(" ");this.element.className=classesCombined}}evaluateEach(){const eachExpr=this.element.getAttribute(":each");if(eachExpr){const[variable,iterable]=eachExpr.split(" in "),items=eval(iterable);this.childClone||(this.childClone=this.element.innerHTML);let newHTML="";items.forEach(e=>{newHTML+=this.childClone.replaceAll(variable,`'${e}'`)}),this.element.innerHTML=newHTML;const elements=this.element.querySelectorAll("*");for(let e=0;e{e[t]=()=>{this.evaluateEventAction(t)}}),e.hasAttribute(":click")&&e.addEventListener("click",t=>{this.evaluateEventAction(":click")}),e.hasAttribute(":change")&&(e.type=="checkbox"||e.tagName=="select"?e.addEventListener("change",t=>{this.evaluateEventAction(":change")}):e.addEventListener("input",t=>{this.evaluateEventAction(":change")})),e.hasAttribute(":enter")&&e.addEventListener("keypress",t=>{t.key=="Enter"&&this.evaluateEventAction(":enter")}),e.hasAttribute(":keypress")&&e.addEventListener("keypress",t=>{this.evaluateEventAction(":keypress")}),e.hasAttribute(":keydown")&&e.addEventListener("keydown",t=>{this.evaluateEventAction(":keydown")}),e.hasAttribute(":keyup")&&e.addEventListener("keyup",t=>{this.evaluateEventAction(":keyup")}),document.addEventListener("click",t=>{e.hasAttribute(":clickout")&&!e.contains(t.target)&&this.evaluateEventAction(":clickout")})}hasAttribute(e){return!!this.element.getAttribute(e)}}let nativeProps=Object.getOwnPropertyNames(window);const MiniJS$1=(()=>{window.proxyWindow=null;let e=[],t=[],a=[],s=[":click",":change",":input",":clickout"];const c={set:function(n,i,r){return n[i]=r,i[0]==="$"&&localStorage.setItem(i,JSON.stringify(r)),t.includes(i)&&(m(i),f([i])),!0}};function p(){const n=["div","a","input","textarea","select","button","video","audio","img","form","details","iframe","canvas"],i=new Set;n.forEach(r=>{const d=document.createElement(r);for(let h in d)h.startsWith("on")&&i.add(h)}),a=[...i]}async function w(){await S();let n=performance.now();x(),p(),A(),g(),f(),b(),E(),m();const r=performance.now()-n;console.log(`myFunction took ${r}ms to run.`)}function E(){Object.defineProperty(Number.prototype,"times",{get:function(){return Array.from({length:this})}})}function f(n=t){n.forEach(i=>{if(Array.isArray(proxyWindow[i])){let r=function(){return proxyWindow[i][0]},d=function(){return proxyWindow[i][proxyWindow[i].length-1]},h=function(l){let o;if(Array.isArray(l))o=l;else{let u=this.indexOf(l);u===-1?o=this.concat(l):o=this.slice(0,u).concat(this.slice(u+1))}proxyWindow[i]=o},V=function(l){let o;this.indexOf(l)===-1&&(o=this.concat(l),proxyWindow[i]=o)},W=function(l){let o,u=this.indexOf(l);u!==-1&&(o=this.slice(0,u).concat(this.slice(u+1)),proxyWindow[i]=o)},M=function(l,o){const u=l.toLowerCase().split(/\s+/);return o.filter(L=>{const T=L.toLowerCase();return u.every(_=>T.includes(_))})},N=function(l){return M(l,this)};proxyWindow[i].first=r(),proxyWindow[i].last=d(),proxyWindow[i].remove=W,proxyWindow[i].add=V,proxyWindow[i].toggle=h,proxyWindow[i].search=N}})}function x(){proxyWindow=new Proxy(window,c)}function v(){t=Object.getOwnPropertyNames(window).filter(i=>!nativeProps.includes(i))}function g(){v(),e.forEach((n,i)=>{n.getVariablesFromEvents(i)}),e.forEach((n,i)=>{n.getVariables()})}function y(n){return n.startsWith("$")&&JSON.parse(localStorage.getItem(n))||void 0}function m(n=null){e.forEach(i=>{var r;(i.variables.includes(n)||n==null||i.uuid==n||((r=i.parent)==null?void 0:r.uuid)==n)&&(i.evaluateEach(),i.evaluateAll())})}function b(){e.forEach(n=>{n.applyEventBindings()})}function A(){var n=document.body.getElementsByTagName("*");for(let i=0;i{document.readyState=="loading"?document.addEventListener("DOMContentLoaded",n):n()})}return w().catch(n=>{console.error("Error initializing MiniJS:",n)}),{get elements(){return e},set elements(n){return n},get variables(){return t},set variables(n){t=n},get allEvents(){return[...a,...s]},get window(){return proxyWindow},tryFromLocal:y}})();window.MiniJS=MiniJS$1});