diff --git a/.eslintrc.json b/.eslintrc.json
new file mode 100644
index 000000000..240635e86
--- /dev/null
+++ b/.eslintrc.json
@@ -0,0 +1,35 @@
+{
+ "extends": "@nodutilus",
+ "env": {
+ "browser": true,
+ "node": false
+ },
+ "overrides": [
+ {
+ "files": [
+ "./test/**"
+ ],
+ "env": {
+ "browser": true,
+ "node": true
+ }
+ },
+ {
+ "files": [
+ "./webtest/index.js"
+ ],
+ "env": {
+ "browser": false,
+ "node": true
+ }
+ }
+ ],
+ "ignorePatterns": [
+ "/_deprecated/",
+ "/build/",
+ "/@notml/notml/check-compatible.min.js",
+ "/@notml/notml/core.js",
+ "/@notml/notml/core.min.js",
+ "*.d.ts"
+ ]
+}
diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
new file mode 100644
index 000000000..95e42354d
--- /dev/null
+++ b/.github/workflows/main.yml
@@ -0,0 +1,15 @@
+name: Checks ➜ Tests ➜ Publish
+
+on:
+ pull_request:
+ branches:
+ - main
+ push:
+ branches:
+ - main
+
+jobs:
+ main:
+ uses: nodutilus/project-actions/.github/workflows/main.yml@main
+ secrets:
+ NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
diff --git a/.gitignore b/.gitignore
index 67045665d..a8c9f7d75 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,103 +2,19 @@
logs
*.log
npm-debug.log*
-yarn-debug.log*
-yarn-error.log*
-lerna-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
-# Runtime data
-pids
-*.pid
-*.seed
-*.pid.lock
-
-# Directory for instrumented libs generated by jscoverage/JSCover
-lib-cov
-
-# Coverage directory used by tools like istanbul
-coverage
-*.lcov
-
-# nyc test coverage
-.nyc_output
-
-# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
-.grunt
-
-# Bower dependency directory (https://bower.io/)
-bower_components
-
-# node-waf configuration
-.lock-wscript
-
-# Compiled binary addons (https://nodejs.org/api/addons.html)
-build/Release
-
# Dependency directories
node_modules/
-jspm_packages/
-
-# TypeScript v1 declaration files
-typings/
-
-# TypeScript cache
-*.tsbuildinfo
-
-# Optional npm cache directory
-.npm
-
-# Optional eslint cache
-.eslintcache
-
-# Microbundle cache
-.rpt2_cache/
-.rts2_cache_cjs/
-.rts2_cache_es/
-.rts2_cache_umd/
-
-# Optional REPL history
-.node_repl_history
# Output of 'npm pack'
*.tgz
-# Yarn Integrity file
-.yarn-integrity
-
-# dotenv environment variables file
-.env
-.env.test
-
-# parcel-bundler cache (https://parceljs.org/)
-.cache
-
-# Next.js build output
-.next
-
-# Nuxt.js build / generate output
-.nuxt
-dist
-
-# Gatsby files
-.cache/
-# Comment in the public line in if your project uses Gatsby and *not* Next.js
-# https://nextjs.org/blog/next-9-1#public-directory-support
-# public
-
-# vuepress build output
-.vuepress/dist
-
-# Serverless directories
-.serverless/
-
-# FuseBox cache
-.fusebox/
-
-# DynamoDB Local files
-.dynamodb/
-
-# TernJS port file
-.tern-port
+# Generate output
+/coverage
+/build
+/@notml/notml/check-compatible.min.js
+/@notml/notml/core.js
+/@notml/notml/core.min.js
diff --git a/.npmrc b/.npmrc
new file mode 100644
index 000000000..971311444
--- /dev/null
+++ b/.npmrc
@@ -0,0 +1,2 @@
+save=false
+package-lock=false
diff --git a/.nycrc.json b/.nycrc.json
new file mode 100644
index 000000000..f185ff860
--- /dev/null
+++ b/.nycrc.json
@@ -0,0 +1,13 @@
+{
+ "extends": "@nodutilus/project-config/nyc",
+ "include": [
+ "@notml",
+ "test"
+ ],
+ "exclude": [
+ "@notml/notml/**",
+ "test/pre-test.js",
+ "test/mem.js",
+ "**/*.d.ts"
+ ]
+}
diff --git a/.vscode/launch.json b/.vscode/launch.json
new file mode 100644
index 000000000..9d4c049ff
--- /dev/null
+++ b/.vscode/launch.json
@@ -0,0 +1,22 @@
+{
+ "version": "0.2.0",
+ "configurations": [{
+ "type": "node",
+ "request": "launch",
+ "name": "Current file",
+ "skipFiles": [
+ "/**"
+ ],
+ "program": "${file}"
+ },
+ {
+ "type": "node",
+ "request": "launch",
+ "name": "Tests",
+ "skipFiles": [
+ "/**"
+ ],
+ "program": "${workspaceFolder}/test/index.js"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/.vscode/settings.json b/.vscode/settings.json
new file mode 100644
index 000000000..ca6425790
--- /dev/null
+++ b/.vscode/settings.json
@@ -0,0 +1,22 @@
+{
+ "editor.tabSize": 2,
+ "editor.rulers": [
+ 110
+ ],
+ "editor.detectIndentation": false,
+ "files.autoSave": "afterDelay",
+ "files.autoSaveDelay": 3000,
+ "files.trimTrailingWhitespace": true,
+ "files.eol": "\n",
+ "files.insertFinalNewline": true,
+ "[javascript]": {
+ "editor.defaultFormatter": "vscode.typescript-language-features"
+ },
+ "[json]": {
+ "editor.defaultFormatter": "vscode.json-language-features"
+ },
+ "[jsonс]": {
+ "editor.defaultFormatter": "vscode.json-language-features"
+ },
+ "javascript.validate.enable": false
+}
diff --git a/@notml/core/README.md b/@notml/core/README.md
new file mode 100644
index 000000000..3577ecba5
--- /dev/null
+++ b/@notml/core/README.md
@@ -0,0 +1,11 @@
+# NotML Core [![npm][npmbadge]][npm] [![build][badge]][actions]
+
+Not a HTML - is object-oriented modeling of HTML and CSS
+
+[npmbadge]: https://img.shields.io/npm/v/@notml/core?label=@notml/core
+
+[npm]: https://www.npmjs.com/package/@notml/core
+
+[badge]: https://github.com/nodutilus/notml/actions/workflows/main.yml/badge.svg
+
+[actions]: https://github.com/nodutilus/notml/actions
diff --git a/@notml/core/core.js b/@notml/core/core.js
new file mode 100644
index 000000000..fbd53c418
--- /dev/null
+++ b/@notml/core/core.js
@@ -0,0 +1,20 @@
+import { OOMElement } from './lib/factory.js'
+import { extendsCustomElement, defineCustomElement } from './lib/custom-elements.js'
+
+const oomOrigin = Object.assign(Object.create(null), {
+ extends: extendsCustomElement,
+ define: defineCustomElement
+})
+
+
+/** @type {import('@notml/core').OOMProxy} */
+export const oom = oomOrigin.oom = new Proxy(OOMElement, {
+ /** @type {import('@notml/core').OOMProxy.apply} */
+ apply: (_, __, args) => {
+ return OOMElement.createProxy(args)
+ },
+ get: (_, tagName, proxy) => {
+ return oomOrigin[tagName] || ((...args) => proxy(tagName, ...args))
+ },
+ set: () => false
+})
diff --git a/@notml/core/lib/custom-elements.js b/@notml/core/lib/custom-elements.js
new file mode 100644
index 000000000..2f794c17f
--- /dev/null
+++ b/@notml/core/lib/custom-elements.js
@@ -0,0 +1,202 @@
+import { OOMElement } from './factory.js'
+import { OOMStyle } from './style.js'
+
+const oomElementRedySymbol = Symbol('oomElementRedySymbol')
+const { document, DocumentFragment, HTMLElement, HTMLHeadElement, customElements, ShadowRoot } = window
+const oomCustomElementMap = new WeakMap()
+const shadowRootOOMStyleMap = new WeakMap()
+const optionsDefaultsGlobals = Object.freeze({})
+const extendsTagNameMap = new Map()
+
+
+/** @type {import('@notml/core').CustomElement.applyOOMTemplate} */
+function applyOOMTemplate(instance) {
+ const { attachShadow } = instance.constructor
+ let { template } = instance
+ const rootNode = instance.getRootNode()
+ /** @type {import('@notml/core').CustomElement | ShadowRoot} */
+ let root = instance
+
+ if (attachShadow) {
+ root = instance.attachShadow(typeof attachShadow === 'object' ? attachShadow : { mode: 'open' })
+ }
+
+ // Клонирование стилей компонента во внутрь ShadowRoot
+ if (rootNode instanceof ShadowRoot) {
+ const { style } = instance.constructor
+
+ if (style instanceof OOMElement && style.dom instanceof OOMStyle) {
+ let styleSet = shadowRootOOMStyleMap.get(rootNode)
+
+ if (!styleSet) {
+ shadowRootOOMStyleMap.set(rootNode, (styleSet = new WeakSet()))
+ }
+ if (!styleSet.has(instance.constructor)) {
+ /** @type {HTMLElement} */
+ // @ts-ignore
+ let head = rootNode.firstChild
+
+ if (!(head instanceof HTMLHeadElement)) {
+ head = document.createElement('head')
+ rootNode.prepend(head)
+ }
+
+ head.append(style.clone().dom)
+ styleSet.add(instance.constructor)
+ }
+ }
+ }
+
+ // Построение верстки компонента произвольным методом
+ // Вернет void, если функция выполнила вставку дочерних элементов
+ if (typeof instance.template === 'function' && !(template instanceof OOMElement)) {
+ // @ts-ignore - проверка на instanceof OOMElement откидывает все Proxy типы
+ template = instance.template(root)
+ }
+
+ if (template instanceof Promise) {
+ instance[OOMElement.async] = template.then(asyncTemplate => {
+ if (asyncTemplate instanceof OOMElement) {
+ root.append(asyncTemplate.dom)
+ } else if (asyncTemplate instanceof HTMLElement || asyncTemplate instanceof DocumentFragment) {
+ root.append(asyncTemplate)
+ } else if (typeof asyncTemplate === 'string') {
+ root.innerHTML += asyncTemplate
+ }
+ }).catch(error => {
+ const err = document.createElement('code')
+
+ err.textContent = String(error.stack || error)
+ root.append(err)
+ })
+ } else {
+ if (template instanceof OOMElement) {
+ root.append(template.dom)
+ } else if (template instanceof HTMLElement || template instanceof DocumentFragment) {
+ root.append(template)
+ } else if (typeof template === 'string') {
+ root.innerHTML += template
+ }
+ }
+}
+
+
+/** @type {import('@notml/core').CustomElement.resolveOptions } */
+function resolveOptions(target, source, prevSources = new WeakSet()) {
+ let result
+
+ if (source && typeof source === 'object') {
+ // Только простые объекты и массивы подвергаем копированию и заморозке,
+ // чтобы не сломать логику встроенных и пользовательских классов
+ if (source.constructor === Object || source.constructor === Array) {
+ // Для защиты от бесконечной рекурсии заменяем цикличные ссылки на undefined
+ if (prevSources.has(source)) {
+ return undefined
+ }
+ prevSources.add(source)
+ result = Object.assign(source instanceof Array ? [] : {}, target, source)
+ for (const key in result) {
+ result[key] = resolveOptions(null, result[key], prevSources)
+ }
+ result = Object.freeze(result)
+ } else {
+ result = source
+ }
+ } else {
+ result = typeof source === 'undefined' ? target : source
+ }
+
+ return result
+}
+
+
+/** @type {import('@notml/core').CustomElement.extendsCustomElement} */
+function extendsCustomElement(CustomElement, optionsDefaults) {
+ if (oomCustomElementMap.has(CustomElement) && typeof optionsDefaults === 'undefined') {
+ return oomCustomElementMap.get(CustomElement)
+ } else {
+ /** @type {import('@notml/core').CustomElement} */
+ class OOMCustomElement extends CustomElement {
+
+ /** Создание элемента по шаблону при вставке в DOM */
+ connectedCallback() {
+ if (super.connectedCallback) {
+ super.connectedCallback()
+ }
+ if (!this[oomElementRedySymbol]) {
+ this[oomElementRedySymbol] = false
+ applyOOMTemplate(this)
+ }
+ }
+
+ /** @type {import('@notml/core').CustomElement.constructor} */
+ constructor(
+ /** @type {import('@notml/core').CustomElement.Options} */
+ options
+ ) {
+ if (options && Object.isFrozen(options)) {
+ super(options)
+ } else {
+ options = resolveOptions(OOMCustomElement.optionsDefaults, options)
+ super(options)
+ if (extendsTagNameMap.has(this.constructor)) {
+ // Атрибут "is" предназначен для хранения имени тега пользовательского элемента,
+ // но он не заполняется при создании элементов из JS, поэтому проставляем его принудительно
+ this.setAttribute('is', extendsTagNameMap.get(this.constructor))
+ }
+ }
+ if (!Reflect.has(this, 'options')) {
+ Object.defineProperty(this, 'options', {
+ value: options,
+ writable: false
+ })
+ }
+ if ('className' in this.constructor) {
+ this.className = this.constructor.className
+ }
+ }
+
+ }
+
+ Object.defineProperty(OOMCustomElement, 'optionsDefaults', {
+ value: resolveOptions(CustomElement.optionsDefaults, optionsDefaults) || optionsDefaultsGlobals,
+ writable: false
+ })
+
+ if (typeof optionsDefaults === 'undefined') {
+ oomCustomElementMap.set(CustomElement, OOMCustomElement)
+ }
+
+ return OOMCustomElement
+ }
+}
+
+
+/** @type {import('@notml/core').CustomElement.defineCustomElement} */
+function defineCustomElement(...oomCustomElements) {
+ for (const CustomElement of oomCustomElements) {
+ const { tagName } = CustomElement
+
+ customElements.define(tagName, CustomElement, { extends: CustomElement.extendsTagName })
+ if (CustomElement.extendsTagName) {
+ extendsTagNameMap.set(CustomElement, tagName)
+ }
+ if (CustomElement.style instanceof OOMElement && CustomElement.style.dom instanceof OOMStyle) {
+ if (CustomElement.extendsTagName) {
+ CustomElement.style(`${CustomElement.extendsTagName}[is="${tagName}"]`)
+ } else {
+ CustomElement.style(tagName)
+ }
+ CustomElement.style.dom.setAttribute('oom-element', tagName)
+ document.head.append(CustomElement.style.dom)
+ }
+ }
+
+ return oomCustomElements
+}
+
+
+export {
+ extendsCustomElement,
+ defineCustomElement
+}
diff --git a/@notml/core/lib/factory.js b/@notml/core/lib/factory.js
new file mode 100644
index 000000000..b944a3778
--- /dev/null
+++ b/@notml/core/lib/factory.js
@@ -0,0 +1,298 @@
+import { OOMStyle } from './style.js'
+
+const { document, customElements, DocumentFragment, Element, HTMLElement, HTMLTemplateElement } = window
+const isOOMElementSymbol = Symbol('isOOMElement')
+const proxiesMap = new WeakMap()
+/** @type {import('@notml/core').base.OOMProxyConstructor} */
+const OOMProxyConstructor = Proxy
+
+/** @type {import('@notml/core').OOMElement} */
+class OOMElement {
+
+ /** @type {import('@notml/core').OOMElement.createProxy} */
+ static createProxy(
+ /** @type {import('@notml/core').OOMElement.OOMElementArgs} */
+ args
+ ) {
+ const wrapper = /* c8 ignore next */ () => { }
+ const proxy = new OOMProxyConstructor(wrapper, OOMElement.proxyHandler)
+
+ wrapper.instance = new OOMElement(...args)
+ proxiesMap.set(wrapper.instance, proxy)
+
+ return proxy
+ }
+
+ /** @type {import('@notml/core').OOMElement.proxyApply} */
+ static proxyApply(
+ /** @type {import('@notml/core').OOMElement.OOMElementWrapper} */
+ { instance }, /** @type {any} */_,
+ /** @type {import('@notml/core').OOMElement.ProxyApplyArgs} */
+ args
+ ) {
+ const proxy = proxiesMap.get(instance)
+
+ if (instance.dom instanceof OOMStyle) {
+ // @ts-ignore вызываем независимо от аргументов, чтобы упало стандартное исключение из DOM API
+ instance.dom.update(...args)
+ } else {
+ for (const arg of args) {
+ const isChild =
+ arg instanceof OOMElement ||
+ arg instanceof HTMLElement ||
+ arg instanceof DocumentFragment ||
+ typeof arg !== 'object' || !arg ||
+ arg.constructor !== Object
+
+ // Чтобы поймать стандартное исключение DOM API, игнорируем проверку на произвольные типы данных
+ if (isChild) {
+ // @ts-ignore независимо от типа добавляемого элемента вызовем вставку
+ instance.append(arg)
+ } else {
+ // @ts-ignore независимо от типа аргументов вызываем установку атрибутов
+ OOMElement.setAttributes(instance.dom, arg)
+ }
+ }
+ }
+
+ return proxy
+ }
+
+ /** @type {import('@notml/core').OOMElement.proxyGetter} */
+ static proxyGetter(
+ /** @type {import('@notml/core').OOMElement.OOMElementWrapper} */
+ { instance },
+ /** @type {import('@notml/core').OOMElement.TagName} */
+ tagName,
+ /** @type {import('@notml/core').OOMElementProxy} */
+ proxy
+ ) {
+ if (tagName in instance) {
+ if (tagName === 'then') {
+ if (instance.dom[OOMElement.async] instanceof Promise) {
+ return async (/** @type {Function} */resolve) => {
+ await instance.then()
+ resolve(proxy)
+ }
+ } else {
+ return null
+ }
+ } else if (typeof instance[tagName] === 'function') {
+ return (...args) => {
+ const result = instance[tagName](...args)
+
+ return result === instance ? proxy : result
+ }
+ } else {
+ return instance[tagName]
+ }
+ } else {
+ return (...args) => {
+ if (instance.dom instanceof DocumentFragment) {
+ instance.append(new OOMElement(tagName, ...args))
+ } else {
+ proxy = OOMElement.createProxy([
+ document.createDocumentFragment(),
+ proxy.dom,
+ new OOMElement(tagName, ...args)
+ ])
+ }
+
+ return proxy
+ }
+ }
+ }
+
+ /** @type {import('@notml/core').OOMElement.hasInstance} */
+ static [Symbol.hasInstance](
+ /** @type {import('@notml/core').OOMElementProxy} */
+ instance
+ ) {
+ // @ts-ignore https://github.com/microsoft/TypeScript/pull/44512
+ return instance && instance[isOOMElementSymbol] === true
+ }
+
+ /** @type {import('@notml/core').OOMElement.setAttribute} */
+ static setAttribute(
+ /** @type {import('@notml/core').CustomElement} */
+ instance,
+ /** @type {import('@notml/core').OOMElement.AttributeName} */
+ attrName,
+ /** @type {import('@notml/core').OOMElement.OOMAttributeValue} */
+ attrValue
+ ) {
+ switch (typeof attrValue) {
+ case 'object':
+ if (attrName === 'style') {
+ for (const name in attrValue) {
+ instance.style[name] = attrValue[name]
+ }
+ }
+ break
+ case 'function':
+ instance[attrName] = attrValue
+ break
+ case 'boolean':
+ // Пустая строка по умолчанию для логических атрибутов:
+ // https://developer.mozilla.org/en-US/docs/Web/API/Element/setAttribute
+ attrName = attrName.replace(/[A-Z]+/g, str => `-${str.toLowerCase()}`)
+ if (attrValue) {
+ // Для защиты от подмены поведения на экземпляре, используем базовый метод
+ Element.prototype.setAttribute.call(instance, attrName, '')
+ } else {
+ // Для защиты от подмены поведения на экземпляре, используем базовый метод
+ Element.prototype.removeAttribute.call(instance, attrName)
+ }
+ break
+ default:
+ attrName = attrName.replace(/[A-Z]+/g, str => `-${str.toLowerCase()}`)
+ if (attrName === 'class' && 'className' in instance.constructor) {
+ attrValue = instance.constructor.className + ' ' + attrValue
+ }
+ if (attrName === 'inner-html') {
+ instance.innerHTML = attrValue
+ break
+ }
+ // Для защиты от подмены поведения на экземпляре, используем базовый метод
+ Element.prototype.setAttribute.call(instance, attrName, attrValue)
+ break
+ }
+ }
+
+ /** @type {import('@notml/core').OOMElement.setAttributes} */
+ static setAttributes(
+ /** @type {import('@notml/core').CustomElement} */
+ instance,
+ /** @type {import('@notml/core').OOMElement.OOMAttributes} */
+ attributes = {}
+ ) {
+ for (const [attrName, attrValue] of Object.entries(attributes)) {
+ OOMElement.setAttribute(instance, attrName, attrValue)
+ }
+ }
+
+ /** @type {import('@notml/core').OOMElement.getAttribute} */
+ static getAttribute(
+ /** @type {HTMLElement} */
+ instance,
+ /** @type {import('@notml/core').OOMElement.AttributeName} */
+ attrName
+ ) {
+ if (typeof instance[attrName] === 'function') {
+ return instance[attrName]
+ }
+ if ((/[A-Z]/).test(attrName)) {
+ attrName = attrName.replace(/[A-Z]/g, str => `-${str.toLowerCase()}`)
+ }
+ if (attrName === 'style') {
+ return instance.style
+ } else {
+ return instance.getAttribute(attrName)
+ }
+ }
+
+ /** @type {import('@notml/core').OOMElement.isOOMElementSymbol} */
+ [isOOMElementSymbol] = true
+
+ /** @type {import('@notml/core').OOMElement.DOMElement} */
+ dom
+
+ /** @type {import('@notml/core').OOMElement.HTML} */
+ get html() {
+ const { dom } = this
+ let html = ''
+
+ if (dom instanceof DocumentFragment) {
+ for (const item of Array.from(dom.childNodes)) {
+ if (item instanceof HTMLElement) {
+ html += item.outerHTML
+ } else if (item.nodeType === document.TEXT_NODE) {
+ html += item.textContent
+ }
+ }
+ } else {
+ html = dom.outerHTML
+ }
+
+ return html
+ }
+
+ /** @type {import('@notml/core').OOMElement.async} */
+ static async = Symbol('asyncOOMElement')
+
+ /** @type {import('@notml/core').OOMElement.then} */
+ async then() {
+ await this.dom[OOMElement.async]
+ delete this.dom[OOMElement.async]
+ }
+
+ /** @type {import('@notml/core').OOMElement.constructor} */
+ constructor(
+ /** @type {import('@notml/core').OOMElement.OOMTagName} */
+ tagName,
+ /** @type {import('@notml/core').OOMElement.ProxyApplyArgs} */
+ ...args
+ ) {
+ if (typeof tagName === 'string') {
+ tagName = tagName.replace((/[A-Z]+/g), str => `-${str.toLowerCase()}`)
+ if (tagName === 'style') {
+ tagName = 'oom-style'
+ }
+
+ const Constructor = customElements.get(tagName)
+
+ if (Constructor) {
+ this.dom = new Constructor()
+ } else {
+ this.dom = document.createElement(tagName)
+ }
+ } else if (tagName instanceof HTMLElement || tagName instanceof DocumentFragment) {
+ this.dom = tagName
+ /* eslint-disable-next-line no-prototype-builtins */
+ } else if (HTMLElement.isPrototypeOf(tagName) || DocumentFragment.isPrototypeOf(tagName)) {
+ /* eslint-disable-next-line new-cap */
+ this.dom = new tagName()
+ } else if (typeof tagName === 'undefined') {
+ this.dom = document.createDocumentFragment()
+ } else if (tagName instanceof OOMElement) {
+ this.dom = tagName.dom
+ } else {
+ // @ts-ignore В остальных случаях createElement вернет собственную ошибку создания элемента
+ this.dom = document.createElement(tagName)
+ }
+ OOMElement.proxyApply({ instance: this }, null, args)
+ }
+
+ /** @type {import('@notml/core').OOMElement.append} */
+ append(/** @type {any} */child) {
+ let parent = this.dom
+
+ if (parent instanceof HTMLTemplateElement) {
+ parent = parent.content
+ }
+ if (child instanceof OOMElement) {
+ parent.append(child.dom)
+ } else if (typeof child !== 'undefined') {
+ parent.append(child)
+ }
+
+ return this
+ }
+
+ /** @type {import('@notml/core').OOMElement.clone} */
+ clone() {
+ const dom = document.importNode(this.dom, true)
+
+ return OOMElement.createProxy([dom])
+ }
+
+}
+
+/** @type {import('@notml/core').OOMElement.proxyHandler} */
+OOMElement.proxyHandler = {
+ apply: OOMElement.proxyApply,
+ get: OOMElement.proxyGetter,
+ set: () => false
+}
+
+export { OOMElement }
diff --git a/@notml/core/lib/style.js b/@notml/core/lib/style.js
new file mode 100644
index 000000000..1fc069e82
--- /dev/null
+++ b/@notml/core/lib/style.js
@@ -0,0 +1,94 @@
+const { document, customElements, HTMLStyleElement } = window
+/** @type {WeakMap} */
+const privateOOMStyleMap = new WeakMap()
+
+/** @type {import('@notml/core').OOMStyle} */
+class OOMStyle extends HTMLStyleElement {
+
+ /** @type {import('@notml/core').OOMStyle.updateStyle} */
+ static updateStyle(
+ /** @type {import('@notml/core').OOMStyle.Style} */
+ style,
+ /** @type {import('@notml/core').OOMStyle.StyleName} */
+ styleName,
+ /** @type {import('@notml/core').OOMStyle.StyleSource} */
+ source
+ ) {
+ let styleCollection = style.get(styleName)
+
+ for (const [propName, propValue] of Object.entries(source)) {
+ if (propValue && typeof propValue === 'object' && propValue.constructor === Object) {
+ // Рекурсивно разворачиваем вложенное описание стилей в плоский список
+ OOMStyle.updateStyle(style, styleName ? `${styleName} ${propName}` : propName, propValue)
+ } else {
+ if (!styleCollection) {
+ styleCollection = document.createElement('div')
+ style.set(styleName, styleCollection)
+ }
+ if (propName in styleCollection.style) {
+ styleCollection.style[propName] = propValue
+ } else {
+ // @ts-ignore Игнорируем типы, метод приводит любые значения к строке
+ styleCollection.style.setProperty(propName, propValue)
+ }
+ }
+ }
+ }
+
+ /** Добавляем инициализацию приватных данных класса */
+ constructor() {
+ const privateSlots = {
+ scopeName: '',
+ style: new Map()
+ }
+
+ super()
+ privateOOMStyleMap.set(this, privateSlots)
+ }
+
+ /** @type {import('@notml/core').OOMStyle.update} */
+ update(
+ /** @type {import('@notml/core').OOMStyle.ScopeName | import('@notml/core').OOMStyle.StyleSource} */
+ scopeName,
+ /** @type {Array} */
+ ...styles
+ ) {
+ const privateSlots = privateOOMStyleMap.get(this)
+
+ if (typeof scopeName === 'string') {
+ privateSlots.scopeName = scopeName
+ } else if (typeof scopeName !== 'undefined') {
+ styles.unshift(scopeName)
+ }
+ for (const style of styles) {
+ OOMStyle.updateStyle(privateSlots.style, '', style)
+ }
+ }
+
+ /** @type {import('@notml/core').OOMStyle.connectedCallback} */
+ connectedCallback() {
+ const privateSlots = privateOOMStyleMap.get(this)
+
+ if (privateSlots.style.size) {
+ let textStyle = ''
+
+ for (const [name, style] of privateSlots.style) {
+ const selector = (privateSlots.scopeName && name.startsWith(privateSlots.scopeName) && name) ||
+ (privateSlots.scopeName && name && `${privateSlots.scopeName} ${name}`) ||
+ privateSlots.scopeName || name || '*'
+
+ textStyle += `${selector}{ ${style.getAttribute('style') || ''} }`
+ }
+
+ this.innerHTML = textStyle
+ privateSlots.style.clear()
+ }
+ }
+
+}
+
+
+customElements.define('oom-style', OOMStyle, { extends: 'style' })
+
+
+export { OOMStyle }
diff --git a/@notml/core/package.json b/@notml/core/package.json
new file mode 100644
index 000000000..2ca0766fc
--- /dev/null
+++ b/@notml/core/package.json
@@ -0,0 +1,30 @@
+{
+ "name": "@notml/core",
+ "version": "0.1.0-pre.15",
+ "description": "Not a HTML - is object-oriented modeling of HTML and CSS",
+ "keywords": [
+ "web-components",
+ "custom-elements",
+ "html-in-js",
+ "css-in-js"
+ ],
+ "license": "Unlicense",
+ "repository": "github:nodutilus/notml",
+ "type": "module",
+ "engines": {
+ "node": ">=16"
+ },
+ "files": [
+ "lib",
+ "core.js",
+ "types.d.ts"
+ ],
+ "exports": {
+ ".": "./core.js"
+ },
+ "module": "./core.js",
+ "types": "./types.d.ts",
+ "publishConfig": {
+ "access": "public"
+ }
+}
diff --git a/@notml/core/types.d.ts b/@notml/core/types.d.ts
new file mode 100644
index 000000000..bf8fe9ccb
--- /dev/null
+++ b/@notml/core/types.d.ts
@@ -0,0 +1,1324 @@
+declare module '@notml/core' {
+
+ namespace base {
+ /** Набор ловушек для создания прокси-объекта для OOMElement с уточненным типом ключей */
+ interface OOMProxyHandler extends ProxyHandler {
+ get?(target: T, name: K, receiver: U): U[K]
+ set?(target: T, name: K, value: U[K], receiver: U): boolean
+ }
+
+ /** Расширенный конструктор для создания прокси-объекта для OOMElement */
+ interface OOMProxyConstructor extends ProxyConstructor {
+ new (target: T, handler: OOMProxyHandler): U
+ }
+
+ /**
+ * Модифицированный CSSStyleDeclaration из typescript/lib/lib.dom.d.ts (typescript@4.3.5)
+ * с необязательным определением атрибутов для использования в OOMAttributeValue
+ */
+ interface CSSStyleDeclaration {
+ alignContent?: string
+ alignItems?: string
+ alignSelf?: string
+ alignmentBaseline?: string
+ all?: string
+ animation?: string
+ animationDelay?: string
+ animationDirection?: string
+ animationDuration?: string
+ animationFillMode?: string
+ animationIterationCount?: string
+ animationName?: string
+ animationPlayState?: string
+ animationTimingFunction?: string
+ backfaceVisibility?: string
+ background?: string
+ backgroundAttachment?: string
+ backgroundClip?: string
+ backgroundColor?: string
+ backgroundImage?: string
+ backgroundOrigin?: string
+ backgroundPosition?: string
+ backgroundPositionX?: string
+ backgroundPositionY?: string
+ backgroundRepeat?: string
+ backgroundSize?: string
+ baselineShift?: string
+ blockSize?: string
+ border?: string
+ borderBlockEnd?: string
+ borderBlockEndColor?: string
+ borderBlockEndStyle?: string
+ borderBlockEndWidth?: string
+ borderBlockStart?: string
+ borderBlockStartColor?: string
+ borderBlockStartStyle?: string
+ borderBlockStartWidth?: string
+ borderBottom?: string
+ borderBottomColor?: string
+ borderBottomLeftRadius?: string
+ borderBottomRightRadius?: string
+ borderBottomStyle?: string
+ borderBottomWidth?: string
+ borderCollapse?: string
+ borderColor?: string
+ borderImage?: string
+ borderImageOutset?: string
+ borderImageRepeat?: string
+ borderImageSlice?: string
+ borderImageSource?: string
+ borderImageWidth?: string
+ borderInlineEnd?: string
+ borderInlineEndColor?: string
+ borderInlineEndStyle?: string
+ borderInlineEndWidth?: string
+ borderInlineStart?: string
+ borderInlineStartColor?: string
+ borderInlineStartStyle?: string
+ borderInlineStartWidth?: string
+ borderLeft?: string
+ borderLeftColor?: string
+ borderLeftStyle?: string
+ borderLeftWidth?: string
+ borderRadius?: string
+ borderRight?: string
+ borderRightColor?: string
+ borderRightStyle?: string
+ borderRightWidth?: string
+ borderSpacing?: string
+ borderStyle?: string
+ borderTop?: string
+ borderTopColor?: string
+ borderTopLeftRadius?: string
+ borderTopRightRadius?: string
+ borderTopStyle?: string
+ borderTopWidth?: string
+ borderWidth?: string
+ bottom?: string
+ boxShadow?: string
+ boxSizing?: string
+ breakAfter?: string
+ breakBefore?: string
+ breakInside?: string
+ captionSide?: string
+ caretColor?: string
+ clear?: string
+ clip?: string
+ clipPath?: string
+ clipRule?: string
+ color?: string
+ colorInterpolation?: string
+ colorInterpolationFilters?: string
+ columnCount?: string
+ columnFill?: string
+ columnGap?: string
+ columnRule?: string
+ columnRuleColor?: string
+ columnRuleStyle?: string
+ columnRuleWidth?: string
+ columnSpan?: string
+ columnWidth?: string
+ columns?: string
+ content?: string
+ counterIncrement?: string
+ counterReset?: string
+ cssFloat?: string
+ cssText?: string
+ cursor?: string
+ direction?: string
+ display?: string
+ dominantBaseline?: string
+ emptyCells?: string
+ fill?: string
+ fillOpacity?: string
+ fillRule?: string
+ filter?: string
+ flex?: string
+ flexBasis?: string
+ flexDirection?: string
+ flexFlow?: string
+ flexGrow?: string
+ flexShrink?: string
+ flexWrap?: string
+ float?: string
+ floodColor?: string
+ floodOpacity?: string
+ font?: string
+ fontFamily?: string
+ fontFeatureSettings?: string
+ fontKerning?: string
+ fontSize?: string
+ fontSizeAdjust?: string
+ fontStretch?: string
+ fontStyle?: string
+ fontSynthesis?: string
+ fontVariant?: string
+ fontVariantCaps?: string
+ fontVariantEastAsian?: string
+ fontVariantLigatures?: string
+ fontVariantNumeric?: string
+ fontVariantPosition?: string
+ fontWeight?: string
+ gap?: string
+ glyphOrientationVertical?: string
+ grid?: string
+ gridArea?: string
+ gridAutoColumns?: string
+ gridAutoFlow?: string
+ gridAutoRows?: string
+ gridColumn?: string
+ gridColumnEnd?: string
+ gridColumnGap?: string
+ gridColumnStart?: string
+ gridGap?: string
+ gridRow?: string
+ gridRowEnd?: string
+ gridRowGap?: string
+ gridRowStart?: string
+ gridTemplate?: string
+ gridTemplateAreas?: string
+ gridTemplateColumns?: string
+ gridTemplateRows?: string
+ height?: string
+ hyphens?: string
+ imageOrientation?: string
+ imageRendering?: string
+ inlineSize?: string
+ justifyContent?: string
+ justifyItems?: string
+ justifySelf?: string
+ left?: string
+ letterSpacing?: string
+ lightingColor?: string
+ lineBreak?: string
+ lineHeight?: string
+ listStyle?: string
+ listStyleImage?: string
+ listStylePosition?: string
+ listStyleType?: string
+ margin?: string
+ marginBlockEnd?: string
+ marginBlockStart?: string
+ marginBottom?: string
+ marginInlineEnd?: string
+ marginInlineStart?: string
+ marginLeft?: string
+ marginRight?: string
+ marginTop?: string
+ marker?: string
+ markerEnd?: string
+ markerMid?: string
+ markerStart?: string
+ mask?: string
+ maskComposite?: string
+ maskImage?: string
+ maskPosition?: string
+ maskRepeat?: string
+ maskSize?: string
+ maskType?: string
+ maxBlockSize?: string
+ maxHeight?: string
+ maxInlineSize?: string
+ maxWidth?: string
+ minBlockSize?: string
+ minHeight?: string
+ minInlineSize?: string
+ minWidth?: string
+ objectFit?: string
+ objectPosition?: string
+ opacity?: string
+ order?: string
+ orphans?: string
+ outline?: string
+ outlineColor?: string
+ outlineOffset?: string
+ outlineStyle?: string
+ outlineWidth?: string
+ overflow?: string
+ overflowAnchor?: string
+ overflowWrap?: string
+ overflowX?: string
+ overflowY?: string
+ overscrollBehavior?: string
+ overscrollBehaviorBlock?: string
+ overscrollBehaviorInline?: string
+ overscrollBehaviorX?: string
+ overscrollBehaviorY?: string
+ padding?: string
+ paddingBlockEnd?: string
+ paddingBlockStart?: string
+ paddingBottom?: string
+ paddingInlineEnd?: string
+ paddingInlineStart?: string
+ paddingLeft?: string
+ paddingRight?: string
+ paddingTop?: string
+ pageBreakAfter?: string
+ pageBreakBefore?: string
+ pageBreakInside?: string
+ paintOrder?: string
+ perspective?: string
+ perspectiveOrigin?: string
+ placeContent?: string
+ placeItems?: string
+ placeSelf?: string
+ pointerEvents?: string
+ position?: string
+ quotes?: string
+ resize?: string
+ right?: string
+ rotate?: string
+ rowGap?: string
+ rubyAlign?: string
+ rubyPosition?: string
+ scale?: string
+ scrollBehavior?: string
+ shapeRendering?: string
+ stopColor?: string
+ stopOpacity?: string
+ stroke?: string
+ strokeDasharray?: string
+ strokeDashoffset?: string
+ strokeLinecap?: string
+ strokeLinejoin?: string
+ strokeMiterlimit?: string
+ strokeOpacity?: string
+ strokeWidth?: string
+ tabSize?: string
+ tableLayout?: string
+ textAlign?: string
+ textAlignLast?: string
+ textAnchor?: string
+ textCombineUpright?: string
+ textDecoration?: string
+ textDecorationColor?: string
+ textDecorationLine?: string
+ textDecorationStyle?: string
+ textEmphasis?: string
+ textEmphasisColor?: string
+ textEmphasisPosition?: string
+ textEmphasisStyle?: string
+ textIndent?: string
+ textJustify?: string
+ textOrientation?: string
+ textOverflow?: string
+ textRendering?: string
+ textShadow?: string
+ textTransform?: string
+ textUnderlinePosition?: string
+ top?: string
+ touchAction?: string
+ transform?: string
+ transformBox?: string
+ transformOrigin?: string
+ transformStyle?: string
+ transition?: string
+ transitionDelay?: string
+ transitionDuration?: string
+ transitionProperty?: string
+ transitionTimingFunction?: string
+ translate?: string
+ unicodeBidi?: string
+ userSelect?: string
+ verticalAlign?: string
+ visibility?: string
+ /** @deprecated */
+ webkitAlignContent?: string
+ /** @deprecated */
+ webkitAlignItems?: string
+ /** @deprecated */
+ webkitAlignSelf?: string
+ /** @deprecated */
+ webkitAnimation?: string
+ /** @deprecated */
+ webkitAnimationDelay?: string
+ /** @deprecated */
+ webkitAnimationDirection?: string
+ /** @deprecated */
+ webkitAnimationDuration?: string
+ /** @deprecated */
+ webkitAnimationFillMode?: string
+ /** @deprecated */
+ webkitAnimationIterationCount?: string
+ /** @deprecated */
+ webkitAnimationName?: string
+ /** @deprecated */
+ webkitAnimationPlayState?: string
+ /** @deprecated */
+ webkitAnimationTimingFunction?: string
+ /** @deprecated */
+ webkitAppearance?: string
+ /** @deprecated */
+ webkitBackfaceVisibility?: string
+ /** @deprecated */
+ webkitBackgroundClip?: string
+ /** @deprecated */
+ webkitBackgroundOrigin?: string
+ /** @deprecated */
+ webkitBackgroundSize?: string
+ /** @deprecated */
+ webkitBorderBottomLeftRadius?: string
+ /** @deprecated */
+ webkitBorderBottomRightRadius?: string
+ /** @deprecated */
+ webkitBorderRadius?: string
+ /** @deprecated */
+ webkitBorderTopLeftRadius?: string
+ /** @deprecated */
+ webkitBorderTopRightRadius?: string
+ /** @deprecated */
+ webkitBoxAlign?: string
+ /** @deprecated */
+ webkitBoxFlex?: string
+ /** @deprecated */
+ webkitBoxOrdinalGroup?: string
+ /** @deprecated */
+ webkitBoxOrient?: string
+ /** @deprecated */
+ webkitBoxPack?: string
+ /** @deprecated */
+ webkitBoxShadow?: string
+ /** @deprecated */
+ webkitBoxSizing?: string
+ /** @deprecated */
+ webkitFilter?: string
+ /** @deprecated */
+ webkitFlex?: string
+ /** @deprecated */
+ webkitFlexBasis?: string
+ /** @deprecated */
+ webkitFlexDirection?: string
+ /** @deprecated */
+ webkitFlexFlow?: string
+ /** @deprecated */
+ webkitFlexGrow?: string
+ /** @deprecated */
+ webkitFlexShrink?: string
+ /** @deprecated */
+ webkitFlexWrap?: string
+ /** @deprecated */
+ webkitJustifyContent?: string
+ webkitLineClamp?: string
+ /** @deprecated */
+ webkitMask?: string
+ /** @deprecated */
+ webkitMaskBoxImage?: string
+ /** @deprecated */
+ webkitMaskBoxImageOutset?: string
+ /** @deprecated */
+ webkitMaskBoxImageRepeat?: string
+ /** @deprecated */
+ webkitMaskBoxImageSlice?: string
+ /** @deprecated */
+ webkitMaskBoxImageSource?: string
+ /** @deprecated */
+ webkitMaskBoxImageWidth?: string
+ /** @deprecated */
+ webkitMaskClip?: string
+ /** @deprecated */
+ webkitMaskComposite?: string
+ /** @deprecated */
+ webkitMaskImage?: string
+ /** @deprecated */
+ webkitMaskOrigin?: string
+ /** @deprecated */
+ webkitMaskPosition?: string
+ /** @deprecated */
+ webkitMaskRepeat?: string
+ /** @deprecated */
+ webkitMaskSize?: string
+ /** @deprecated */
+ webkitOrder?: string
+ /** @deprecated */
+ webkitPerspective?: string
+ /** @deprecated */
+ webkitPerspectiveOrigin?: string
+ webkitTapHighlightColor?: string
+ /** @deprecated */
+ webkitTextFillColor?: string
+ /** @deprecated */
+ webkitTextSizeAdjust?: string
+ /** @deprecated */
+ webkitTextStroke?: string
+ /** @deprecated */
+ webkitTextStrokeColor?: string
+ /** @deprecated */
+ webkitTextStrokeWidth?: string
+ /** @deprecated */
+ webkitTransform?: string
+ /** @deprecated */
+ webkitTransformOrigin?: string
+ /** @deprecated */
+ webkitTransformStyle?: string
+ /** @deprecated */
+ webkitTransition?: string
+ /** @deprecated */
+ webkitTransitionDelay?: string
+ /** @deprecated */
+ webkitTransitionDuration?: string
+ /** @deprecated */
+ webkitTransitionProperty?: string
+ /** @deprecated */
+ webkitTransitionTimingFunction?: string
+ /** @deprecated */
+ webkitUserSelect?: string
+ whiteSpace?: string
+ widows?: string
+ width?: string
+ willChange?: string
+ wordBreak?: string
+ wordSpacing?: string
+ wordWrap?: string
+ writingMode?: string
+ zIndex?: string
+ /** @deprecated */
+ zoom?: string
+ }
+
+ }
+
+ namespace OOMStyle {
+
+ /**
+ * Коллекция CSS селекторов и заданных для них правил,
+ * преобразуемых через атрибут style тега div из объектного представления в текстовый.
+ * Для преобразования используется особенность работы класса CSSStyleDeclaration,
+ * см. HTMLElement.style (@see https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/style)
+ */
+ type Style = Map
+
+ /**
+ * CSS селектор для указания правил
+ */
+ type StyleName = string
+
+ /**
+ * CSS селектор, который будет являться родительским для всех селекторов, описанных в коллекции
+ */
+ type ScopeName = string
+
+ /**
+ * Объект с правилами CSS определяется в формате CSSStyleDeclaration.
+ * Может содержать вложенные объекты, преобразуемые в CSS селекторы согласно пути в объекте CSS правил
+ */
+ interface StyleSource extends base.CSSStyleDeclaration {
+ [x: string]: string | StyleSource
+ }
+
+ /** Выполняет обновление коллекция CSS селекторов и их правил */
+ interface updateStyle {
+ (style: Style, styleName: StyleName, source: StyleSource): void
+ }
+
+ /**
+ * Выполняет обновление коллекция CSS селекторов и их правил.
+ * С поддержкой указания имени области действия в качестве необязательного 1го аргумента
+ */
+ interface update {
+ (scopeName?: ScopeName | StyleSource, ...styles: Array): void
+ }
+
+ /**
+ * Выполняет обновление содержимого элемента при вставке в DOM.
+ * Преобразует CSS селекторы из #style в текстовый вид CSS и записывает в innerHTML
+ */
+ interface connectedCallback {
+ (): any
+ }
+ }
+
+ /** Пользовательский элемент, наследуемый от style, для использования CSS-in-JS в шаблонах OOM */
+ class OOMStyle extends HTMLStyleElement {
+ static updateStyle: OOMStyle.updateStyle
+ #scopeName: OOMStyle.ScopeName
+ #style: OOMStyle.Style
+ update: OOMStyle.update
+ connectedCallback: OOMStyle.connectedCallback
+ }
+
+ namespace OOMElement {
+
+ /** Имя тега DOM элемента для создания элемента */
+ type TagName = string
+
+ /**
+ * Имя тега DOM элемента для создания элемента, сам DOM элемент,
+ * или его функция конструктор, на основе которого будет создан OOM элемент
+ */
+ type OOMTagName = DocumentFragment | HTMLElement | string | typeof DocumentFragment | typeof HTMLElement
+
+ /** Имя атрибута DOM элемента */
+ type AttributeName = string
+
+ /** Поддерживаемые значения атрибутов для OOMElement */
+ type OOMAttributeValue = string | boolean | Function | base.CSSStyleDeclaration
+
+ /** Справочник атрибутов для OOMElement */
+ type OOMAttributes = {
+ [x: string]: OOMAttributeValue
+ /** CSS стили DOM элемента */
+ style?: base.CSSStyleDeclaration | string
+ /** HTML разметка для вставки в DOM элемент */
+ innerHTML?: string
+ /** HTML разметка для вставки в DOM элемент */
+ ['inner-html']?: string
+ }
+
+ /** Экземпляр элемента для вставки */
+ type OOMChild = string | Node | DocumentFragment | HTMLElement | OOMElement | OOMFragmentProxy | OOMElementProxy | OOMTemplateProxy | OOMStyleProxy
+
+ /** Функция-шаблон для генерации пользовательского компонента */
+ interface TemplateFN {
+ (
+ /**
+ * Для случаев когда компонент имеет теневой DOM,
+ * this будет самим компонентом, а root корнем теневого DOM
+ */
+ root: CustomElement | ShadowRoot
+ ): Promise | OOMElement.OOMChild | void
+ }
+
+ /**
+ * Аргументы вызова OOMElementProxy элемента - объекты с атрибутами элемента, или вложенные элементы.
+ * Типы аргументов можно комбинировать в 1-ом вызове
+ */
+ type ProxyApplyArgs = Array
+
+ /** Аргументы для конструктора OOMElement */
+ type OOMElementArgs = [OOMTagName, ...ProxyApplyArgs]
+
+ /**
+ * Обертка для OOMElement, и сам элемент в instance.
+ * Объявляется как функция для корректной работы Proxy и хука apply.
+ */
+ type OOMElementWrapper = {
+ (): void
+ instance: OOMElement
+ } | { instance: OOMElement }
+
+ /** Создание внешнего Proxy для работы с OOM элементом */
+ interface createProxy {
+ (args: OOMElementArgs): OOMElementProxy
+ }
+
+ /**
+ * Обновление атрибутов или добавление вложенных элементов для OOMElement,
+ * через перехват apply внешнего Proxy.
+ * Поведение выбирается в зависимости от переданного типа аргументов
+ */
+ interface proxyApply {
+ (wrapper: OOMElementWrapper, _: any, args: ProxyApplyArgs): OOMElementProxy
+ }
+
+ /**
+ * Перехват обращений к свойствам OOM элемента.
+ * Методы и свойства объявленные в HTMLElement обеспечивают API взаимодействия с элементом.
+ * Остальные обращения, используя цепочки вызовов, создают OOM элементы на одном уровне используя DocumentFragment.
+ * Вернет метод или свойство из OOM элемента или фабрику для генерации DocumentFragment
+ */
+ interface proxyGetter {
+ (wrapper: OOMElementWrapper, tagName: TagName, proxy: OOMElementProxy): any
+ }
+
+ /** Набор ловушек для создания OOMElementProxy */
+ interface proxyHandler {
+ apply: proxyApply
+ get: proxyGetter
+ set: () => boolean
+ }
+
+ /** Проверка на соответствие экземпляру OOMElement, в т.ч. обернутый в Proxy */
+ interface hasInstance {
+ (
+ /** Экземпляр класса для проверки на соответствие OOMElement */
+ instance: OOMElementProxy
+ ): boolean
+ }
+
+ /**
+ * Установка атрибута элемента.
+ * Позволяет задавать методы, стили в виде объекта, и строковые атрибуты DOM элемента.
+ */
+ interface setAttribute {
+ (instance: HTMLElement, attrName: AttributeName, attrValue: OOMAttributeValue): void
+ }
+
+ /**
+ * Установка атрибутов элемента.
+ * Работает аналогично setAttribute, но обновляет сразу несколько атрибутов
+ */
+ interface setAttributes {
+ (instance: HTMLElement, attributes: OOMAttributes): void
+ }
+
+ /**
+ * Получение атрибута элемента.
+ * Возвращает значение аналогичное логике установки атрибутов в setAttribute
+ */
+ interface getAttribute {
+ (instance: HTMLElement, attrName: AttributeName): OOMAttributeValue
+ }
+
+ type IsOOMElementSymbol = symbol
+
+ /**
+ * Свойство экземпляра для проверки на соответствие классу OOMElement,
+ * позволяющее выполнить instanceof для Proxy-объекта через метод класса Symbol.hasInstance
+ */
+ type isOOMElementSymbol = boolean
+
+ /**
+ * Экземпляр DOM элемента, которым управляет OOM элемент
+ */
+ type DOMElement = DocumentFragment | HTMLElement | CustomElement
+
+ /**
+ * HTML код DOM элемента, привязанного к OOMElement
+ */
+ type HTML = string
+
+ /**
+ * Ключ ссылки на Promise ожидающий завершения построения асинхронного компонента
+ */
+ type async = symbol
+
+ /**
+ * Заглушка для возможности вернуть OOMElementProxy из асинхронного метода,
+ * в противном случае Promise не завершается и код перестает исполняться
+ * Вернет функция для обработки Promise,
+ * для асинхронных компонентов вернет Promise для ожидания построения шаблона,
+ * для синхронных вернет null
+ */
+ type then = () => Promise
+
+ /**
+ * Создает экземпляр OOMElement по переданному тегу DOM или классу пользовательского элемента,
+ * либо оборачивает в OOMElement существующий DOM элемент
+ */
+ interface constructor {
+ (tagName: OOMElement.OOMTagName, ...args: OOMElement.ProxyApplyArgs): OOMElement
+ }
+
+ /**
+ * Добавление дочернего элемента для OOMElement в конец списка элементов.
+ * Вернет замыкание на самого себя для использования чейнинга
+ */
+ interface append {
+ (child: OOMChild): OOMElement
+ }
+
+ /**
+ * Клонирует DOM элемент и возвращает новый экземпляр OOM, содержащий копию DOM элемента
+ */
+ interface clone {
+ (): OOMElementProxy
+ }
+
+ }
+
+ /** Базовый класс для OOM элементов */
+ class OOMElement {
+ static createProxy: OOMElement.createProxy
+ static proxyApply: OOMElement.proxyApply
+ static proxyGetter: OOMElement.proxyGetter
+ static proxyHandler: OOMElement.proxyHandler
+ static [Symbol.hasInstance]: OOMElement.hasInstance
+ static setAttribute: OOMElement.setAttribute
+ static setAttributes: OOMElement.setAttributes
+ static getAttribute: OOMElement.getAttribute
+ static async: OOMElement.async
+ // @ts-ignore https://github.com/microsoft/TypeScript/pull/44512
+ [OOMElement.IsOOMElementSymbol]: OOMElement.isOOMElementSymbol
+ /** Ссылка на оригинальный DOM элемент */
+ dom: OOMElement.DOMElement
+ /** HTML код элемента, аналогично HTMLElement.outerHTML, но работает и для DocumentFragment */
+ html: OOMElement.HTML
+ /** Заглушка для возможности вернуть OOMElementProxy из асинхронного метода */
+ then: OOMElement.then
+ constructor(tagName: OOMElement.OOMTagName, ...args: OOMElement.ProxyApplyArgs)
+ append: OOMElement.append
+ clone: OOMElement.clone
+ }
+
+ namespace CustomElement {
+
+ /** Базовый класс элемента OOM, который будет расширяться пользователем */
+ interface CustomElementClsBase {
+
+ /** Объект с опциями компонента по умолчанию */
+ readonly optionsDefaults: CustomElement.Options
+
+ new(options?: Options): CustomElement
+
+ }
+
+ /** Класс пользовательского элемента, расширенный для работы с компонентами OOM */
+ interface CustomElementCls extends CustomElementClsBase {
+
+ /** Имя тега для регистрации пользовательского элемента */
+ tagName: string
+
+ /**
+ * Встроенное имя класса компонента,
+ * устанавливается в конструкторе и сохраняется при обновлении атрибута class через oom шаблонизатор
+ */
+ className?: string
+
+ /**
+ * Имя тега встроенного DOM элемента который расширяется данным классом
+ * @see customElements.define~options.extends {@link https://developer.mozilla.org/en-US/docs/Web/API/CustomElementRegistry/define#syntax}
+ */
+ extendsTagName?: string
+
+ /**
+ * Коллекция CSS правил, описывающих стиль пользовательского компонента через oom.style.
+ * При регистрации компонента через oom.define добавляться в документ в секцию ,
+ * автоматически добавляя для стилей имя области действия соответствующее имени тега элемента.
+ */
+ style?: OOMStyleProxy
+
+ }
+
+ /** Опции пользовательского компонента */
+ type Options = {
+ readonly [K in keyof T]?: T[K]
+ }
+
+ /**
+ * Объединяет опции по умолчанию с опциями пользователя,
+ * возвращая копию защищенную от изменения (только для базовых объектов и массивов).
+ * Экземпляры пользовательских и сложных встроенных классов передаются по ссылке.
+ */
+ interface resolveOptions {
+ (
+ /** Исходный справочник опций по умолчанию */
+ target: object,
+ /** Пользовательский справочник опций */
+ source: object,
+ /**
+ * Список обработанных объектов пользовательского справочника,
+ * используемый для выявления рекурсий.
+ */
+ prevSources?: WeakSet
+ ): object
+ }
+
+ /**
+ * Создает экземпляр CustomElement
+ */
+ interface constructor {
+ /** Имя тега для регистрации пользовательского элемента */
+ tagName: string
+ /**
+ * Встроенное имя класса компонента,
+ * устанавливается в конструкторе и сохраняется при обновлении атрибута class через oom шаблонизатор
+ */
+ className?: string
+ /** Включает создание теневого DOM внутри компонента */
+ attachShadow?: boolean | ShadowRootInit
+ /**
+ * Коллекция CSS правил, описывающих стиль пользовательского компонента через oom.style.
+ */
+ style?: OOMStyleProxy
+ (options: Options): CustomElement
+ }
+
+ /**
+ * Применение OOM шаблона пользовательского элемента
+ */
+ interface applyOOMTemplate {
+ (instance: CustomElement): void
+ }
+
+ /**
+ * Расширение пользовательского элемента возможностями OOM шаблонизатора.
+ * Возвращает новый класс наследуемый от указанного базового или пользовательского класса элемента,
+ * от которого можно наследовать класс нового элемента с поддержкой OOM
+ */
+ interface extendsCustomElement {
+ (CustomElement: CustomElementCls, optionsDefaults?: CustomElement.Options): CustomElementCls
+ }
+
+ /**
+ * Регистрирует переданный набор классов пользовательских элементов в customElements.define.
+ * В качестве тега используется имя класса или `static tagName`
+ */
+ interface defineCustomElement {
+ (...oomCustomElements: Array>): Array>
+ }
+
+ }
+
+ /** Экземпляр пользовательского DOM элемента, расширенный для работы с компонентами OOM */
+ class CustomElement extends HTMLElement {
+
+ /** Имя тега для регистрации пользовательского элемента */
+ static tagName: string
+
+ /**
+ * Встроенное имя класса компонента,
+ * устанавливается в конструкторе и сохраняется при обновлении атрибута class через oom шаблонизатор
+ */
+ static className?: string
+
+ /**
+ * Имя тега встроенного DOM элемента который расширяется данным классом
+ * @see customElements.define~options.extends {@link https://developer.mozilla.org/en-US/docs/Web/API/CustomElementRegistry/define#syntax}
+ */
+ static extendsTagName?: string
+
+ /** Включает создание теневого DOM внутри компонента */
+ static attachShadow?: boolean | ShadowRootInit
+
+ /**
+ * Коллекция CSS правил, описывающих стиль пользовательского компонента через oom.style.
+ * При регистрации компонента через oom.define добавляться в документ в секцию ,
+ * автоматически добавляя для стилей имя области действия соответствующее имени тега элемента.
+ */
+ static style?: OOMStyleProxy
+
+ /** Объект с опциями компонента по умолчанию */
+ static readonly optionsDefaults: CustomElement.Options
+
+ /** Объект с опциями пользовательского компонента */
+ readonly options: CustomElement.Options
+
+ /**
+ * Создает экземпляр CustomElement
+ */// @ts-ignore
+ get constructor(): CustomElement.constructor
+
+ /**
+ * Содержимое пользовательского элемента, которое будет добавлено в его состав
+ * в момент вставки пользовательского компонента в состав документа
+ */
+ template?: Promise | OOMElement.OOMChild | OOMElement.TemplateFN | void
+
+ /**
+ * Ссылка на асинхронное состояние готовности компонента.
+ * void для синхронных компонентов, Promise, если template является асинхронной функцией
+ */
+ // @ts-ignore https://github.com/microsoft/TypeScript/pull/44512
+ [OOMElement.async]: Promise | void
+
+ /** Хук ЖЦ элемента срабатывающий при вставке элемента в DOM */
+ connectedCallback(): void
+
+ }
+
+ namespace OOMProxy {
+
+ /**
+ * Создает новый OOM элемент согласно имени запрошенного атрибута,
+ * затем добавляет его в конец списка дочерних элементов
+ *
+ * @example
+ * oom.div({ class: 'header' }, ...childs)
+ *
+ * >>
+ *
+ */
+ interface createElementProxy {
+ (...args: Array): OOMElementProxy
+ }
+ /**
+ * Создает OOM элемент, добавляя его в верстку после текущего,
+ * и возвращает новый фрагмент документа содержащий оба элемента
+ *
+ * @example
+ * const span = oom.span({ class: 'title' }, 'Link: ')
+ * const fragment = span.a({ href: 'https://test.ok' }, 'test.ok')
+ *
+ * document.body.append(span.dom)
+ *
+ * >>
+ * Link:
+ * test.ok
+ */
+ interface createElementToFragmentProxy {
+ (...args: Array): OOMFragmentProxy
+ }
+ /**
+ * Создает и добавляет к фрагменту документа еще один OOM элемент
+ *
+ * @example
+ * const fragment = oom().span({ class: 'title' }, 'Link: ')
+ *
+ * fragment.a({ href: 'https://test.ok' }, 'test.ok')
+ *
+ * document.body.append(component.dom)
+ *
+ * >>
+ * Link:
+ * test.ok
+ */
+ interface createFragmentProxy {
+ (...args: Array): OOMFragmentProxy
+ }
+
+ /** Создает новый OOM элемент и оборачивает его в Proxy */
+ interface apply {
+ (_: any, __: any, args: OOMElement.OOMElementArgs): OOMElementProxy
+ }
+
+ interface CommonOrigin {
+
+ /**
+ * Создает экземпляр пользовательского элемента OOMStyle генерирующий таблицу селекторов и их правил.
+ * OOMStyle является расширением тега style,
+ * преобразующий объектное представление CSS в текстовый, и заполняет содержимое style.
+ * Для преобразования используется особенность работы класса CSSStyleDeclaration,
+ * см. HTMLElement.style (@see https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/style)
+ *
+ * @example
+ * const style = oom.style({
+ * 'fontSize': '10px',
+ * '.my-class': { background: 'red', fontSize: '12px' }
+ * })
+ *
+ * document.head.append(style.dom)
+ *
+ * >>
+ *
+ */
+ style: (
+ scopeName?: OOMStyle.ScopeName | OOMStyle.StyleSource,
+ ...styles: Array
+ ) => OOMStyleProxy
+
+ /**
+ * Создает элемент шаблона контента - template
+ * @see https://developer.mozilla.org/ru/docs/Web/HTML/Element/template
+ *
+ * @example
+ * const tmpl = oom.template(oom.div())
+ * // Клонируем содержимое шаблона через DOM API
+ * const elm1 = oom.main(tmpl.dom.content.cloneNode(true))
+ * // Клонируем сам шаблон через OOM API
+ * const elm2 = oom.section(tmpl.clone())
+ *
+ * >>
+ * elm1.dom.outerHTML === '
'
+ * elm2.dom.outerHTML === ''
+ * tmpl.dom.outerHTML === '
'
+ */
+ template: (...args: Array) => OOMTemplateProxy
+
+ /** HTML код элемента, аналогично HTMLElement.outerHTML, но работает и для DocumentFragment */
+ html: OOMElement.HTML
+
+ }
+
+ /** Внутренний объект OOMProxy описывающий его базовые методы */
+ interface origin extends CommonOrigin {
+ /**
+ * Расширение пользовательского элемента возможностями OOM шаблонизатора.
+ * Возвращает новый класс наследуемый от указанного базового или пользовательского класса элемента,
+ * от которого можно наследовать класс нового элемента с поддержкой OOM
+ *
+ * @example
+ * const optionsDefaults = { caption: '' }
+ *
+ * class MyButton extends oom.extends(HTMLButtonElement, optionsDefaults) {
+ * static tagName = 'my-butt'
+ * static extendsTagName = 'button'
+ * static style = oom.style({
+ * 'button[is="my-butt"]': { fontSize: '12px' },
+ * 'button[is="my-butt"].active': { color: 'yellow' },
+ * '.my-butt__caption': { color: 'red' }
+ * })
+ * template = oom.span({ class: 'my-butt__caption' }, this.options.caption)
+ * }
+ *
+ * oom.define(MyButton)
+ *
+ * document.body.append(new MyButton({ caption: 'Жми тут' }))
+ *
+ * >>
+ *
+ *
+ *
+ *
+ *
+ *
+ * Жми тут
+ *
+ *
+ *
+ */
+ extends(
+ CustomElement: CustomElement.CustomElementClsBase,
+ optionsDefaults?: CustomElement.Options
+ ): CustomElement.CustomElementClsBase
+ extends(
+ CustomElement: typeof HTMLElement,
+ optionsDefaults?: CustomElement.Options
+ ): CustomElement.CustomElementClsBase
+
+ /**
+ * Регистрирует переданный набор классов пользовательских элементов в customElements.define.
+ * В качестве тега используется имя класса или `static tagName`
+ */
+ define: CustomElement.defineCustomElement
+
+ a: OOMProxy.createElementProxy
+ abbr: OOMProxy.createElementProxy
+ address: OOMProxy.createElementProxy
+ area: OOMProxy.createElementProxy
+ article: OOMProxy.createElementProxy
+ aside: OOMProxy.createElementProxy
+ audio: OOMProxy.createElementProxy
+ b: OOMProxy.createElementProxy
+ blockquote: OOMProxy.createElementProxy
+ body: OOMProxy.createElementProxy
+ br: OOMProxy.createElementProxy
+ button: OOMProxy.createElementProxy
+ canvas: OOMProxy.createElementProxy
+ caption: OOMProxy.createElementProxy
+ cite: OOMProxy.createElementProxy
+ code: OOMProxy.createElementProxy
+ col: OOMProxy.createElementProxy
+ colgroup: OOMProxy.createElementProxy
+ dialog: OOMProxy.createElementProxy
+ div: OOMProxy.createElementProxy
+ footer: OOMProxy.createElementProxy
+ form: OOMProxy.createElementProxy
+ h1: OOMProxy.createElementProxy
+ h2: OOMProxy.createElementProxy
+ h3: OOMProxy.createElementProxy
+ h4: OOMProxy.createElementProxy
+ h5: OOMProxy.createElementProxy
+ h6: OOMProxy.createElementProxy
+ head: OOMProxy.createElementProxy
+ header: OOMProxy.createElementProxy
+ hr: OOMProxy.createElementProxy
+ i: OOMProxy.createElementProxy
+ iframe: OOMProxy.createElementProxy
+ img: OOMProxy.createElementProxy
+ input: OOMProxy.createElementProxy
+ label: OOMProxy.createElementProxy
+ li: OOMProxy.createElementProxy
+ main: OOMProxy.createElementProxy
+ meta: OOMProxy.createElementProxy
+ nav: OOMProxy.createElementProxy
+ ol: OOMProxy.createElementProxy
+ optgroup: OOMProxy.createElementProxy
+ option: OOMProxy.createElementProxy
+ p: OOMProxy.createElementProxy
+ pre: OOMProxy.createElementProxy
+ progress: OOMProxy.createElementProxy
+ q: OOMProxy.createElementProxy
+ s: OOMProxy.createElementProxy
+ section: OOMProxy.createElementProxy
+ select: OOMProxy.createElementProxy
+ span: OOMProxy.createElementProxy
+ strong: OOMProxy.createElementProxy
+ sub: OOMProxy.createElementProxy
+ sup: OOMProxy.createElementProxy
+ table: OOMProxy.createElementProxy
+ tbody: OOMProxy.createElementProxy
+ td: OOMProxy.createElementProxy
+ textarea: OOMProxy.createElementProxy
+ tfoot: OOMProxy.createElementProxy
+ th: OOMProxy.createElementProxy
+ thead: OOMProxy.createElementProxy
+ title: OOMProxy.createElementProxy
+ tr: OOMProxy.createElementProxy
+ track: OOMProxy.createElementProxy
+ u: OOMProxy.createElementProxy
+ ul: OOMProxy.createElementProxy
+ video: OOMProxy.createElementProxy
+ }
+
+ interface OOMElementOrigin extends CommonOrigin {
+ /**
+ * Добавление дочернего элемента к верстке в конец списка элементов.
+ * Вернет замыкание на собственный OOMElementProxy для использования чейнинга
+ *
+ * @example
+ * const mySpan = oom.span('My element new text')
+ * const div = oom('div').append(mySpan)
+ */
+ append(child: OOMElement.OOMChild): OOMElementProxy
+ /**
+ * Клонирует элемент и возвращает новый экземпляр OOM, содержащий копию DOM элемента
+ *
+ * @example
+ * const mySpan1 = oom.span('My element new text')
+ * const mySpan2 = mySpan1.clone()
+ */
+ clone(): OOMElementProxy
+ dom: T
+ }
+
+ interface OOMFragmentOrigin extends CommonOrigin {
+ /**
+ * Добавление дочернего элемента к верстке в конец списка элементов.
+ * Вернет замыкание на собственный OOMFragmentProxy для использования чейнинга
+ *
+ * @example
+ * const mySpan = oom.span('My element').span('new text')
+ * const div = oom('div').append(mySpan)
+ */
+ append(child: OOMElement.OOMChild): OOMFragmentProxy
+ /**
+ * Клонирует фрагмент и возвращает новый экземпляр OOM фрагмента, содержащий копию DOM элементов
+ *
+ * @example
+ * const mySpan1 = oom.span('My element').span('new text')
+ * const mySpan2 = mySpan1.clone()
+ */
+ clone(): OOMFragmentProxy
+ dom: DocumentFragment
+ }
+
+ interface OOMStyleOrigin extends CommonOrigin {
+ clone(): OOMStyleProxy
+ dom: OOMStyle
+ }
+
+ }
+
+ /** Proxy для работы с OOM элементом */
+ interface OOMElementProxy extends OOMProxy.OOMElementOrigin {
+ /**
+ * Выполняет обновление атрибутов текущего элемента,
+ * а также добавление дочерних элемента к верстке в конец списка элементов.
+ *
+ * @example
+ * const div = oom.div()
+ *
+ * div({ class: 'MyClass' },
+ * oom.span('My text'),
+ * oom.span('ok'))
+ *
+ * >>
+ *
+ * My text
+ * ok
+ *
+ */
+ (...args: Array): OOMElementProxy
+ // @ts-ignore проверка типа индекса (ts 2411) не подходит, а определения типа "все кроме указанных" нет
+ [tagName: string]: OOMProxy.createElementToFragmentProxy
+ }
+
+ /**
+ * Краткая форма для объявления типа обернутого элемента
+ *
+ * @example import('@notml/core').OOM
+ */
+ interface OOM extends OOMElementProxy { }
+
+ /** Proxy для работы с элементом template */
+ // @ts-ignore переопределение dom из OOMElementOrigin
+ interface OOMTemplateProxy extends OOMElementProxy { }
+
+ interface OOMFragmentProxy extends OOMProxy.OOMFragmentOrigin {
+ /**
+ * Выполняет добавление дочерних элемента к верстке в конец фрагмента документа.
+ *
+ * @example
+ * const fragment = oom()
+ *
+ * fragment(oom.span('My text'), oom.span('ok'))
+ *
+ * >>
+ * My text
+ * ok
+ */
+ (...args: Array): OOMFragmentProxy
+ // @ts-ignore проверка типа индекса (ts 2411) не подходит, а определения типа "все кроме указанных" нет
+ [tagName: string]: OOMProxy.createFragmentProxy
+ }
+
+ /** Proxy для работы с OOMStyle элементом */
+ interface OOMStyleProxy extends OOMProxy.OOMStyleOrigin {
+ /**
+ * Выполняет обновление селекторов и их правил в элементе OOMStyle,
+ * С поддержкой указания имени области действия в качестве необязательного 1го аргумента.
+ *
+ * **!ВАЖНО:** Обновление возможно только до вставки элемента в документ,
+ * т.к. после вставки объектная модель CSS будет очищена
+ *
+ * @example
+ * const style = oom.style({ fontSize: '10px' })
+ *
+ * style({ '.my-class': { background: 'red', fontSize: '12px' } })
+ *
+ * document.body.append(style.dom)
+ *
+ * >>
+ *
+ */
+ (
+ scopeName?: OOMStyle.ScopeName | OOMStyle.StyleSource,
+ ...styles: Array
+ ): OOMStyleProxy
+ // @ts-ignore проверка типа индекса (ts 2411) не подходит, а определения типа "все кроме указанных" нет
+ [tagName: string]: OOMProxy.createElementToFragmentProxy
+ }
+
+ /** Фабрика Proxy для создания OOM элементов и сопутствующее API */
+ interface OOMProxy extends OOMProxy.origin {
+ /**
+ * Вернет новый экземпляр Proxy элемента для создания верстки
+ *
+ * @example
+ * const component = oom('div', { class: 'link' }, oom
+ * .span({ class: 'title' }, 'Link: ')
+ * .a({ href: 'https://test.ok' }, 'test.ok'))
+ *
+ * document.body.append(component.dom)
+ *
+ * >>
+ *
+ */
+ (
+ tagName: HTMLElement | string | typeof HTMLElement,
+ ...args: Array
+ ): OOMElementProxy
+ /**
+ * Вернет новый экземпляр Proxy элемента для создания верстки
+ *
+ * @example
+ * const component = oom()
+ * .span({ class: 'title' }, 'Link: ')
+ * .a({ href: 'https://test.ok' }, 'test.ok')
+ *
+ * document.body.append(component.dom)
+ *
+ * >>
+ * Link:
+ * test.ok
+ */
+ (
+ ...args: Array
+ ): OOMFragmentProxy
+ (
+ tagName: DocumentFragment | typeof DocumentFragment,
+ ...args: Array
+ ): OOMFragmentProxy
+ // @ts-ignore проверка типа индекса (ts 2411) не подходит, а определения типа "все кроме указанных" нет
+ [tagName: string]: OOMProxy.createElementProxy
+ }
+
+ /** Фабрика Proxy для создания OOM элементов и сопутствующее API */
+ export const oom: OOMProxy
+
+}
diff --git a/@notml/notml/README.md b/@notml/notml/README.md
new file mode 100644
index 000000000..215b8e1d8
--- /dev/null
+++ b/@notml/notml/README.md
@@ -0,0 +1,13 @@
+# NotML multi-package [![npm][npmbadge]][npm] [![build][badge]][actions]
+
+All-in-one NotML package for use via CDN
+
+Not a HTML - is object-oriented modeling of HTML and CSS
+
+[npmbadge]: https://img.shields.io/npm/v/notml?label=notml
+
+[npm]: https://www.npmjs.com/package/notml
+
+[badge]: https://github.com/nodutilus/notml/actions/workflows/main.yml/badge.svg
+
+[actions]: https://github.com/nodutilus/notml/actions
diff --git a/@notml/notml/check-compatible.js b/@notml/notml/check-compatible.js
new file mode 100644
index 000000000..7135a3000
--- /dev/null
+++ b/@notml/notml/check-compatible.js
@@ -0,0 +1,161 @@
+/**
+ * Выполняет проверку поддержки браузером функционала используемого библиотекой NotML
+ * В случае выявления проблемы покажет заглушку на странице
+ */
+(function compatible() {
+ /* eslint-disable no-var, no-new-func */
+ var customElements =
+ typeof window.customElements === 'object' &&
+ typeof window.customElements.define === 'function' &&
+ typeof window.HTMLElement === 'function'
+ var success = true
+ var messages = []
+
+ if (!customElements) {
+ success = false
+ messages.push('customElements are not supported')
+ }
+
+ try {
+ var testFunction1 = new Function(
+ 'class TestClass {' +
+ ' get(){};' +
+ ' set(){};' +
+ ' test(){};' +
+ ' b(){};' +
+ '}' +
+ 'const a = new TestClass()'
+ )
+ testFunction1()
+ } catch (error) {
+ success = false
+ messages.push('JS classes are not supported')
+ messages.push(error.message + '\n' + error.stack)
+ }
+
+ try {
+ var testFunction2 = new Function(
+ 'class TestClass {' +
+ ' static get(){};' +
+ ' static set(){};' +
+ ' static test(){};' +
+ ' get(){};' +
+ ' set(){};' +
+ ' test(){};' +
+ ' b(){};' +
+ '}' +
+ 'const a = new TestClass()'
+ )
+ testFunction2()
+ } catch (error) {
+ success = false
+ messages.push('Static methods are not supported in JS classes')
+ messages.push(error.message + '\n' + error.stack)
+ }
+
+ try {
+ var testFunction3 = new Function(
+ 'class TestClass {' +
+ ' test(){};' +
+ ' a = 1;' +
+ ' b(){};' +
+ '}' +
+ 'const a = new TestClass()'
+ )
+ testFunction3()
+ } catch (error) {
+ success = false
+ messages.push('Properties are not supported in JS classes')
+ messages.push(error.message + '\n' + error.stack)
+ }
+
+ try {
+ var testFunction4 = new Function(
+ 'class TestClass {' +
+ ' test(){};' +
+ ' static a = 1;' +
+ ' a = 1;' +
+ ' b(){};' +
+ '}' +
+ 'const a = new TestClass()'
+ )
+ testFunction4()
+ } catch (error) {
+ success = false
+ messages.push('Static properties are not supported in JS classes')
+ messages.push(error.message + '\n' + error.stack)
+ }
+
+
+ // Пока не везде поддерживается
+ // try {
+ // var testFunction5 = new Function(
+ // 'class TestClass {' +
+ // ' static #a = 1;' +
+ // ' #b = 1;' +
+ // '}' +
+ // 'const a = new TestClass()'
+ // )
+ // testFunction5()
+ // } catch (error) {
+ // success = false
+ // messages.push('Private properties are not supported in JS classes')
+ // messages.push(error.message + '\n' + error.stack)
+ // }
+
+ // try {
+ // var testFunction6 = new Function(
+ // 'class TestClass {' +
+ // ' #test(){};' +
+ // '}' +
+ // 'const a = new TestClass()'
+ // )
+ // testFunction6()
+ // } catch (error) {
+ // success = false
+ // messages.push('Private methods are not supported in JS classes')
+ // messages.push(error.message + '\n' + error.stack)
+ // }
+
+ if (!success) {
+ window['@notml/core:compatibility'] = false
+ window.document.head.innerHTML = ''
+ window.onload = function onload() {
+ window.document.body.innerHTML =
+ '' +
+ '' +
+ '
' +
+ '
Your browser or device is outdated and not supported.
' +
+ '
Browser compatibility or newer: Chrome 74, Firefox 90, Safari 14.1, Safari on iOS 14.5
' +
+ '
We recommend using the latest version of the Google Chrome browser
' +
+ '
Install Google Chrome
' +
+ '
' +
+ ' More details... ' +
+ '
' +
+ '
' +
+ '
' +
+ '
'
+ /** @type {HTMLElement} */
+ var moreMessages = window.document.querySelector('#moreMessages')
+ /** @type {HTMLElement} */
+ var errorMessages = window.document.querySelector('#errorMessages')
+
+ moreMessages.onclick = () => {
+ moreMessages.style.display = 'none'
+ errorMessages.innerHTML += 'userAgent: ' + window.navigator.userAgent + '\n\n' + messages.join('\n\n')
+ errorMessages.style.display = 'inline'
+ }
+ }
+ }
+})()
diff --git a/@notml/notml/core.d.ts b/@notml/notml/core.d.ts
new file mode 100644
index 000000000..bc372565e
--- /dev/null
+++ b/@notml/notml/core.d.ts
@@ -0,0 +1 @@
+export { oom } from '@notml/core'
diff --git a/@notml/notml/core.min.d.ts b/@notml/notml/core.min.d.ts
new file mode 100644
index 000000000..bc372565e
--- /dev/null
+++ b/@notml/notml/core.min.d.ts
@@ -0,0 +1 @@
+export { oom } from '@notml/core'
diff --git a/@notml/notml/package.json b/@notml/notml/package.json
new file mode 100644
index 000000000..f8c79b805
--- /dev/null
+++ b/@notml/notml/package.json
@@ -0,0 +1,32 @@
+{
+ "name": "notml",
+ "version": "0.1.0-pre.17",
+ "description": "All-in-one NotML package for use via CDN - object-oriented modeling of HTML and CSS",
+ "keywords": [
+ "web-components",
+ "custom-elements",
+ "html-in-js",
+ "css-in-js"
+ ],
+ "license": "Unlicense",
+ "repository": "github:nodutilus/notml",
+ "type": "module",
+ "engines": {
+ "node": ">=16"
+ },
+ "files": [
+ "check-compatible.min.js",
+ "check-compatible.js",
+ "core.min.js",
+ "core.js"
+ ],
+ "browser": "./core.min.js",
+ "module": "./core.js",
+ "scripts": {
+ "build": "npx rollup --config",
+ "prepack": "npx rollup --config"
+ },
+ "publishConfig": {
+ "access": "public"
+ }
+}
diff --git a/@notml/notml/rollup.config.js b/@notml/notml/rollup.config.js
new file mode 100644
index 000000000..7b4d3f7d3
--- /dev/null
+++ b/@notml/notml/rollup.config.js
@@ -0,0 +1,28 @@
+import resolve from '@rollup/plugin-node-resolve'
+import cleanup from 'rollup-plugin-cleanup'
+import { terser } from 'rollup-plugin-terser'
+
+
+export default [{
+ input: 'src/core.js',
+ output: { file: 'core.js', format: 'esm', compact: true },
+ plugins: [
+ resolve({ browser: true, preferBuiltins: false }),
+ cleanup({ comments: 'none' })
+ ]
+}, {
+ input: 'check-compatible.js',
+ output: { file: 'check-compatible.min.js', compact: true },
+ plugins: [
+ cleanup({ comments: 'none' }),
+ terser()
+ ]
+}, {
+ input: 'core.js',
+ output: { file: 'core.min.js', format: 'esm', compact: true },
+ plugins: [
+ resolve({ browser: true, preferBuiltins: false }),
+ cleanup({ comments: 'none' }),
+ terser()
+ ]
+}]
diff --git a/@notml/notml/src/core.js b/@notml/notml/src/core.js
new file mode 100644
index 000000000..b29e36d99
--- /dev/null
+++ b/@notml/notml/src/core.js
@@ -0,0 +1 @@
+export { oom } from '../../core/core.js'
diff --git a/@notml/ssr/package.json b/@notml/ssr/package.json
new file mode 100644
index 000000000..d116597ce
--- /dev/null
+++ b/@notml/ssr/package.json
@@ -0,0 +1,30 @@
+{
+ "name": "@notml/ssr",
+ "version": "0.0.1-pre.1",
+ "description": "Not a HTML - server-side rendering of HTML and CSS",
+ "keywords": [
+ "ssr",
+ "server-side-rendering",
+ "html-in-js",
+ "css-in-js"
+ ],
+ "license": "Unlicense",
+ "repository": "github:nodutilus/notml",
+ "type": "module",
+ "engines": {
+ "node": ">=16"
+ },
+ "files": [
+ "lib",
+ "ssr.js",
+ "types.d.ts"
+ ],
+ "exports": {
+ ".": "./ssr.js"
+ },
+ "module": "./ssr.js",
+ "types": "./types.d.ts",
+ "publishConfig": {
+ "access": "public"
+ }
+}
diff --git a/README.md b/README.md
index a2806db28..1a9a9d937 100644
--- a/README.md
+++ b/README.md
@@ -1,2 +1,207 @@
-# core
-Not a HTML - is object-oriented modeling of HTML
+# NotML [![build][badge]][actions]
+
+Not a HTML - is object-oriented modeling of HTML and CSS
+
+## Packages
+
+[![npm][npmbadge_notml_core]][npm_notml_core]
+[![npm][npmbadge_notml]][npm_notml]
+
+## Simple HTML-based
+
+### Example #1
+
+Simple layout. Comparison with HTML.
+
+##### NotML
+
+```js
+import { oom } from '@notml/core'
+
+const div = oom('div')
+ .div({ class: 'header' })
+ .div({ style: { borderBottom: '1px solid' } }, oom
+ .span('Name: ', { class: 'test-label' })
+ .span('Test', { class: 'test-name' }))
+ .div({ class: 'footer' })
+```
+
+##### HTML
+
+```html
+
+```
+
+##### JavaScript native DOM
+
+Code executed inside NotML
+
+```js
+const divHeader = document.createElement('div')
+const spanName = document.createElement('span')
+const spanTest = document.createElement('span')
+const divBorder = document.createElement('div')
+const divFooter = document.createElement('div')
+const domDiv = document.createElement('div')
+
+divHeader.setAttribute('class', 'header')
+spanName.setAttribute('class', 'test-label')
+spanTest.setAttribute('class', 'test-name')
+divBorder.style.borderBottom = '1px solid'
+divFooter.setAttribute('class', 'footer')
+spanName.textContent = 'Name: '
+spanTest.textContent = 'Test'
+
+domDiv.append(divHeader)
+divBorder.append(spanName)
+divBorder.append(spanTest)
+domDiv.append(divBorder)
+domDiv.append(divFooter)
+```
+
+### Example #2
+
+Reuse prepared elements
+
+##### NotML
+
+```js
+const header = oom('div', { class: 'header' })
+ .span('Test Header')
+
+const block = oom
+ .div(oom
+ .append(header.clone())
+ .div('div 1'))
+ .div(oom
+ .append(header.clone())
+ .div('div 2'))
+```
+
+##### HTML
+
+```html
+
+
+```
+
+## Based on Custom Elements
+
+### Example #3
+
+Simple `template`
+
+> A `template` instance is cloned on `connectedCallback`. Handler implemented in `oom.define`.
+
+##### NotML
+
+```js
+class MyElementExp3 extends HTMLElement {
+
+ mySpan = oom.span('My element new text')
+
+ template = oom('div', { class: 'MyElement__inner' })
+ .append(this.mySpan.clone())
+ .append(oom('br'))
+ .append(this.mySpan)
+
+}
+
+const block = oom.define(MyElementExp3).MyElementExp3()
+```
+
+##### HTML
+
+```html
+
+
+ My element new text
+
+ My element new text
+
+
+```
+
+### Example #4
+
+Reactive data-properties and attributes. (see more: `HTMLElement.dataset`)
+
+##### NotML
+
+```js
+class MyElementExp4 extends HTMLElement {
+
+ static label = oom('span', { class: 'label' })
+ static field = oom('span', { class: 'field' })
+
+ static template({ element }) {
+ return oom()
+ .append(this.label.clone()
+ .span({ class: 'text' }, label => (element._label = label)))
+ .append(this.field.clone()
+ .span({ class: 'text' }, field => (element._field = field)))
+ }
+
+ /** on 'data-field-text' attribute change */
+ dataFieldTextChanged(oldValue, newValue) {
+ this._field.textContent = newValue
+ }
+
+ /** on 'data-label-text' attribute change */
+ dataLabelTextChanged(oldValue, newValue) {
+ this._label.textContent = newValue
+ }
+
+}
+
+oom.define(MyElementExp4)
+
+const block = document.createElement('my-element-exp4')
+
+block.dataset.labelText = 'Name: '
+block.dataset.fieldText = 'Test'
+
+document.body.append(block)
+```
+
+##### HTML
+
+```html
+
+
+ Name:
+
+
+ Test
+
+
+```
+
+[npmbadge_notml_core]: https://img.shields.io/npm/v/@notml/core?label=@notml/core
+
+[npm_notml_core]: https://www.npmjs.com/package/@notml/core
+
+[npmbadge_notml]: https://img.shields.io/npm/v/notml?label=notml
+
+[npm_notml]: https://www.npmjs.com/package/notml
+
+[badge]: https://github.com/nodutilus/notml/actions/workflows/main.yml/badge.svg
+
+[actions]: https://github.com/nodutilus/notml/actions
diff --git a/_deprecated/_old_custom-elements.js b/_deprecated/_old_custom-elements.js
new file mode 100644
index 000000000..73aa4288e
--- /dev/null
+++ b/_deprecated/_old_custom-elements.js
@@ -0,0 +1,225 @@
+import { customTagNames, customElementTagName, customClasses, customOptions } from './shared-const.js'
+import { OOMElement } from '../@notml/core/lib/factory.js'
+
+const { HTMLElement, customElements } = window
+const observedAttributesSymbol = Symbol('observedAttributes')
+const attributeChangedCacheSymbol = Symbol('attributeChangedCache')
+const attributesHandler = {
+ get: OOMElement.getAttribute,
+ set: (...args) => {
+ OOMElement.setAttribute(...args)
+
+ return true
+ }
+}
+
+
+/**
+ * Подготовка списка атрибутов, изменения которых отслеживаются
+ * при помощи методов класса, содержащих 'Changed' на конце имени
+ *
+ * @param {typeof HTMLElement} proto
+ * @param {Map} setters
+ * @returns {Map|null}
+ */
+function getObservedAttributes(proto, setters) {
+ const properties = Object.getOwnPropertyNames(proto)
+ const nestedProto = Reflect.getPrototypeOf(proto)
+
+ if (Object.isPrototypeOf.call(HTMLElement, nestedProto.constructor)) {
+ getObservedAttributes(nestedProto, setters)
+ }
+
+ for (const name of properties) {
+ const { value } = Reflect.getOwnPropertyDescriptor(proto, name)
+ const isFunction = typeof value === 'function'
+ const isChanged = name.endsWith('Changed')
+ const isValidName = (/^[a-z][\w]+$/).test(name)
+
+ if (isFunction && isChanged && isValidName) {
+ const attributeName = name
+ .replace(/Changed$/, '')
+ .replace(/[A-Z]/g, str => `-${str.toLowerCase()}`)
+
+ setters.set(attributeName, name)
+ }
+ }
+
+ return setters.size > 0 ? setters : null
+}
+
+
+/**
+ * Вызов обработчиков изменения атрибутов,
+ * с кэшированием изменений до вставки элемента в DOM
+ *
+ * @param {HTMLElement} instance
+ * @param {string} name
+ * @param {string} oldValue
+ * @param {string} newValue
+ */
+function applyAttributeChangedCallback(instance, name, oldValue, newValue) {
+ const observed = instance.constructor[observedAttributesSymbol]
+
+ if (observed && observed.has(name)) {
+ if (newValue && newValue.startsWith('json::')) {
+ newValue = JSON.parse(newValue.replace('json::', ''))
+ }
+ if (instance.isConnected) {
+ instance[observed.get(name)](oldValue, newValue)
+ } else {
+ if (!(attributeChangedCacheSymbol in instance)) {
+ instance[attributeChangedCacheSymbol] = new Set()
+ }
+ instance[attributeChangedCacheSymbol].add({
+ name: observed.get(name),
+ args: [oldValue, newValue]
+ })
+ }
+ }
+}
+
+
+/**
+ * Применение OOM шаблона пользовательского элемента
+ *
+ * @param {HTMLElement} instance
+ */
+function applyOOMTemplate(instance) {
+ const attributeChanged = instance[attributeChangedCacheSymbol]
+ let staticTemplate = instance.constructor.template
+ let { shadowRootInit, template } = instance
+ let root = instance
+ let templateOptions =
+ (typeof staticTemplate === 'function' && staticTemplate.length > 0) ||
+ (typeof template === 'function' && template.length > 0) ||
+ null
+
+ // TODO: Асинхронные шаблоны
+ // TODO: Метод на экземпляре applyTemplate
+
+ if (templateOptions) {
+ templateOptions = Object.assign({}, customOptions.get(instance), {
+ element: instance,
+ attributes: new Proxy(instance, attributesHandler)
+ })
+ }
+
+ if (!(template instanceof OOMElement) && typeof template !== 'string') {
+ if (staticTemplate instanceof OOMElement) {
+ staticTemplate = staticTemplate.clone()
+ } else if (typeof staticTemplate === 'function') {
+ staticTemplate = instance.constructor.template(templateOptions)
+ }
+ if (typeof template === 'function') {
+ if (templateOptions) {
+ templateOptions.template = staticTemplate
+ }
+ template = instance.template(templateOptions) || staticTemplate
+ } else {
+ template = staticTemplate
+ }
+ }
+
+ if (shadowRootInit) {
+ root = instance.attachShadow(shadowRootInit)
+ }
+
+ if (template instanceof OOMElement) {
+ root.innerHTML = ''
+ root.append(template.dom)
+ } else if (typeof template === 'string') {
+ root.innerHTML = template
+ }
+
+ if (attributeChanged instanceof Set) {
+ for (const changed of attributeChanged) {
+ instance[changed.name](...changed.args)
+ attributeChanged.delete(changed)
+ }
+ delete instance[attributeChangedCacheSymbol]
+ }
+}
+
+
+/**
+ * @param {typeof HTMLElement} constructor
+ * @returns {typeof HTMLElement}
+ */
+function customClassFactory(constructor) {
+ /**
+ * Динамический класс пользовательского элемента, реализующий:
+ * - Использование шаблонов для верстки (template, static template)
+ * - Опции для шаблона (oom(..., { options:{} }))
+ * - Методы-обработчики изменения атрибутов (endsWith('Changed'))
+ * - JSON атрибуты с преобразованием в объект (json::)
+ */
+ class OOMCustomElement extends constructor {
+
+ /** @returns {[string]} */
+ static get observedAttributes() {
+ return this[observedAttributesSymbol]
+ ? [...this[observedAttributesSymbol].keys(), ...(super.observedAttributes || [])]
+ : super.observedAttributes
+ }
+
+ /** Передача опций элемента в пользовательский конструктор */
+ constructor() {
+ super(OOMCustomElement.options || {})
+ delete OOMCustomElement.options
+ }
+
+ /**
+ * Вызов методов класса отслеживающих изменения атрибутов
+ *
+ * @param {string} name
+ * @param {string} [oldValue]
+ * @param {string} [newValue]
+ */
+ attributeChangedCallback(name, oldValue, newValue) {
+ applyAttributeChangedCallback(this, name, oldValue, newValue)
+ if (super.attributeChangedCallback) {
+ super.attributeChangedCallback(name, oldValue, newValue)
+ }
+ }
+
+ /** Создание элемента по шаблону при вставке в DOM */
+ connectedCallback() {
+ applyOOMTemplate(this)
+ if (super.connectedCallback) {
+ super.connectedCallback()
+ }
+ }
+
+ }
+
+ OOMCustomElement[observedAttributesSymbol] = getObservedAttributes(constructor.prototype, new Map())
+
+ return OOMCustomElement
+}
+
+
+/**
+ * @typedef CustomElementsOptions
+ * @property {string} extends Имя встроенного элемента для расширения
+ */
+/**
+ * Регистрация пользовательского элемента с элементами OOM шаблонизатора
+ *
+ * @param {string} [name]
+ * @param {typeof HTMLElement} constructor
+ * @param {CustomElementsOptions} options
+ */
+export function defineCustomElement(name, constructor, options) {
+ if (Object.isPrototypeOf.call(HTMLElement, name)) {
+ [constructor, options] = [name, constructor]
+ name = constructor.tagName || OOMElement.resolveTagName(constructor.name)
+ }
+
+ const customClass = customClasses.get(constructor) || customClassFactory(constructor)
+
+ customElements.define(name, customClass, options)
+ customClasses.set(customClass, constructor)
+ customElementTagName.set(constructor, name)
+ customTagNames.add(name)
+}
diff --git a/_deprecated/_old_shared-const.js b/_deprecated/_old_shared-const.js
new file mode 100644
index 000000000..8d361e291
--- /dev/null
+++ b/_deprecated/_old_shared-const.js
@@ -0,0 +1,24 @@
+/**
+ * Список все тегов зарегистрированных пользовательских элементов
+ *
+ * @type {Set}
+ */
+export const customTagNames = new Set()
+/**
+ * Сопоставление класса пользовательского элемента и его тега
+ *
+ * @type {Map}
+ */
+export const customElementTagName = new Map()
+/**
+ * Связь динамических классов пользовательских элементов с классами определенными пользователем
+ *
+ * @type {Map}
+ */
+export const customClasses = new Map()
+/**
+ * Хранилище опций для пользовательских компонентов
+ *
+ * @type {WeakMap>}
+ */
+export const customOptions = new WeakMap()
diff --git a/_deprecated/test-oom.js b/_deprecated/test-oom.js
new file mode 100644
index 000000000..1a4216a83
--- /dev/null
+++ b/_deprecated/test-oom.js
@@ -0,0 +1,1064 @@
+import { assert, Test } from '@nodutilus/test'
+import { oom } from '@notml/core'
+
+const { HTMLElement, DocumentFragment, customElements, document } = window
+
+
+/** Тесты источника данных заказа */
+export default class TestOOM extends Test {
+
+ /** Базовый чейнинг для создания верстки */
+ ['chaining - base']() {
+ const { html } = oom
+ .html(oom
+ .body(oom
+ .div('test')))
+
+ assert.equal(html, 'test
')
+ }
+
+ /** Чейнинг методов OOMElement */
+ ['chaining - oom methods']() {
+ const div = oom('div')
+ .append(oom('span'))
+ .setAttributes({ class: 'test' })
+ .html
+
+ assert.equal(div, '
')
+ }
+
+ /** Чейнинг конструктора элементов oom из самого элемента */
+ ['chaining - oom in OOMElement']() {
+ const div = oom('div')
+ .oom('span', 'test')
+ .oom('MyElm')
+ .html
+
+ assert.equal(div, 'test
')
+ }
+
+ /** Модификация вложенных элементов, ссылка на DOM созданного элемента */
+ ['oom callback, DOM instance']() {
+ const form1 = oom('form', oom
+ .span('test', span => {
+ span.textContent += '-ok'
+ span.classList.add('test')
+ }))
+ .html
+ const form2 = oom('form')
+ .span('test', span => {
+ span.textContent += '-ok'
+ span.classList.add('test')
+ })
+ .html
+ const form3 = oom('form', form => {
+ form.append(oom.span('test', span => {
+ span.textContent += '-ok'
+ span.classList.add('test')
+ }).dom)
+ })
+ .html
+ const testForm = ''
+
+ assert.equal(form1, testForm)
+ assert.equal(form2, testForm)
+ assert.equal(form3, testForm)
+ }
+
+ /** Пустой вызов oom и обращение к атрибутам oom создают фрагмент */
+ ['create Fragment with oom']() {
+ const fr1 = oom().div1()
+ const fr2 = oom.div2()
+ const div1 = oom('div', fr1).dom.innerHTML
+ const div2 = oom('div', fr2).dom.innerHTML
+
+ assert.equal(div1, ' ')
+ assert.equal(div2, ' ')
+ assert.ok(fr1.dom instanceof DocumentFragment)
+ assert.ok(fr2.dom instanceof HTMLElement)
+ }
+
+ /** Простая запись атрибутов */
+ ['setAttributes - simple']() {
+ const div = oom('div', { id: 'test' }).html
+
+ assert.equal(div, '
')
+ }
+
+ /** Установка обработчика на элемент через атрибуты */
+ ['setAttributes - set a function']() {
+ let counter = 0
+ const div = oom('div', {
+ onclick: () => {
+ counter++
+ },
+ test: () => {
+ counter++
+ }
+ }).dom
+
+ div.click()
+ div.test()
+
+ assert.equal(counter, 2)
+ }
+
+ /** Установка объекта в качесвте атрибута */
+ ['setAttributes - object=>json']() {
+ const div = oom('div', { dataTest: [] }).dom
+
+ assert.equal(div.outerHTML, '
')
+
+ div.dataset.test = ''
+ assert.equal(div.outerHTML, '
')
+
+ delete div.dataset.test
+ assert.equal(div.outerHTML, '
')
+ }
+
+ /** Установка стилей через style в виде объекта и строки */
+ ['setAttributes - style']() {
+ const div1 = oom('div', { style: { borderBottom: '1px solid' } }).dom
+ const div2 = oom('div', { style: 'border-bottom: 1px solid;' }).dom
+ const div3 = oom('div', { style: { 'border-bottom': '1px solid' } }).dom
+
+ assert.equal(div1.outerHTML, '
')
+ assert.equal(div2.outerHTML, div1.outerHTML)
+ assert.equal(div3.outerHTML, div1.outerHTML)
+ }
+
+ /** Установка атрибута в верхнем регистре работает по аналогии dataset */
+ ['setAttributes - UpperCase']() {
+ const div = oom('div', { dataTest: 'test' }).dom
+
+ assert.equal(div.outerHTML, '
')
+ }
+
+ /** Установка атрибута опций в обычный элемент записывает их в формате JSON */
+ ['setAttributes - options']() {
+ const div = oom('div', { options: { a: 'b' } }).dom
+
+ assert.equal(div.outerHTML, '
')
+ }
+
+ /** Установка атрибутов через oom.setAttributes */
+ ['oom - setAttributes']() {
+ const div = oom('div').dom
+
+ oom.setAttributes(div, { class: 'test1' })
+ assert.equal(div.outerHTML, '
')
+
+ oom.setAttribute(div, 'class', 'test2')
+ assert.equal(div.outerHTML, '
')
+ }
+
+ /** Метод append от оом аналогичен методу от OOMElement */
+ ['oom - append']() {
+ const div11 = oom().div().append(oom('span'))
+ const div12 = oom.append(oom('div')).span()
+ const div13 = oom('div').append(oom('span'))
+
+ assert.equal(div11.html, '
')
+ assert.equal(div12.html, '
')
+ assert.equal(div13.html, '
')
+ }
+
+ /** Перетаскивание элемента между разными узлами DOM */
+ ['OOMElement - moving']() {
+ const span = oom('span', 'test')
+ const div1 = oom('div')
+ const div2 = oom('div')
+
+ div1.append(span)
+ assert.equal(span.dom.parentNode, div1.dom)
+ assert.equal(div1.dom.innerHTML, 'test ')
+ assert.equal(div2.dom.innerHTML, '')
+
+ div2.append(span)
+ assert.equal(span.dom.parentNode, div2.dom)
+ assert.equal(div1.dom.innerHTML, '')
+ assert.equal(div2.dom.innerHTML, 'test ')
+
+ assert.equal(span.html, 'test ')
+ }
+
+ /** Клонирование элемента */
+ ['OOMElement - cloning']() {
+ const span = oom('span', 'test')
+ const div1 = oom('div')
+ const div2 = oom('div')
+ const div3 = oom('div')
+
+ div1.append(span.clone())
+ assert.equal(div1.dom.innerHTML, 'test ')
+ assert.equal(div2.dom.innerHTML, '')
+
+ div2.append(span.clone())
+ assert.equal(div1.dom.innerHTML, 'test ')
+ assert.equal(div2.dom.innerHTML, 'test ')
+
+ assert.equal(span.dom.outerHTML, 'test ')
+ assert.equal(span.dom.parentNode, null)
+
+ div3.append(span)
+ assert.equal(div2.dom.innerHTML, 'test ')
+ assert.equal(div3.dom.innerHTML, 'test ')
+ assert.equal(span.dom.parentNode, div3.dom)
+ }
+
+ /** Фрагмент не перетаскивается между разными узлами DOM,
+ * его содержимое изымается и вставляется в DOM, оставляя фрагмент-пустышку */
+ ['OOMFragment - no moving']() {
+ const span = oom.span('test')
+ const div1 = oom('div')
+ const div2 = oom('div')
+
+ assert.equal(span.dom.childNodes.length, 1)
+
+ div1.append(span)
+ assert.equal(div1.dom.innerHTML, 'test ')
+ assert.equal(div2.dom.innerHTML, '')
+
+ div2.append(span)
+ assert.equal(div1.dom.innerHTML, 'test ')
+ assert.equal(div2.dom.innerHTML, '')
+
+ assert.equal(span.dom.childNodes.length, 0)
+ }
+
+ /** Клонирование фрагмента */
+ ['OOMFragment - cloning']() {
+ const span = oom.span('test')
+ const div1 = oom('div')
+ const div2 = oom('div')
+
+ assert.equal(span.dom.childNodes.length, 1)
+
+ div1.append(span.clone())
+ assert.equal(div1.dom.innerHTML, 'test ')
+ assert.equal(div2.dom.innerHTML, '')
+
+ div2.append(span.clone())
+ assert.equal(div1.dom.innerHTML, 'test ')
+ assert.equal(div2.dom.innerHTML, 'test ')
+
+ assert.equal(span.dom.childNodes.length, 1)
+ }
+
+ /** Создание и регистрация пользовательских элементов */
+ ['customElements - create']() {
+ let cCount = 0
+
+ /** Test custom element */
+ class MyElement extends HTMLElement {
+
+ /***/
+ constructor() {
+ super()
+ this.cCount = ++cCount
+ }
+
+ }
+
+ customElements.define('my-element1', MyElement)
+
+ const mye = oom('my-element1')
+ .MyElement1('test', { class: 'Test' })
+
+ assert.equal(cCount, 2)
+ assert.equal(mye.dom.cCount, 1)
+ assert.equal(mye.html, 'test ')
+ }
+
+ /** Кастомизация содержания пользовательских элементов */
+ ['customElements - connectedCallback']() {
+ /** Test custom element */
+ class MyElement extends HTMLElement {
+
+ /***/
+ connectedCallback() {
+ this.classList.add('MyElement')
+ this.append(oom('span', 'test').dom)
+ }
+
+ }
+
+ customElements.define('my-element2', MyElement)
+
+ const mye = oom.MyElement2()
+
+ assert.equal(mye.html, ' ')
+
+ document.body.innerHTML = ''
+ document.body.append(mye.dom)
+ assert.equal(document.body.innerHTML, 'test ')
+ document.body.innerHTML = ''
+ }
+
+ /** Регистрация новых элементов через oom.define */
+ ['customElements - oom.define']() {
+ /** Test custom element */
+ class MyElementDefine1 extends HTMLElement { }
+
+ /** Test custom element */
+ class MyElementDefine2 extends HTMLElement { }
+
+
+ oom.define(MyElementDefine1)
+ oom.define('m-e-d', MyElementDefine2)
+
+ assert.equal(oom.getDefined('my-element-define1'), MyElementDefine1)
+ assert.equal(oom.getDefined('m-e-d'), MyElementDefine2)
+ }
+
+ /** Регистрация новых элементов через oom.define */
+ ['customElements - static template']() {
+ /** Test custom element */
+ class MyElement3 extends HTMLElement {
+
+ static template = oom('span', 'test')
+
+ /***/
+ template() {
+ this.classList.add('MyElement')
+ }
+
+ }
+
+ const mye = oom.define(MyElement3).oom(MyElement3)
+
+ document.body.innerHTML = ''
+ document.body.append(mye.dom)
+ // Статические шаблоны клонируются для переиспользования
+ assert.equal(mye.dom.constructor.template.dom.parentNode, null)
+ assert.equal(document.body.innerHTML, 'test ')
+ document.body.innerHTML = ''
+ }
+
+ /** Статический метод шаблона */
+ ['customElements - static template function']() {
+ /** Test custom element */
+ class MyElement3x1 extends HTMLElement {
+
+ static tmp = oom('div')
+
+ /** @returns {oom} */
+ static template = () => {
+ return this.tmp.clone().span('test')
+ }
+
+ }
+
+ const mye = oom.define(MyElement3x1).oom(MyElement3x1)
+
+ document.body.innerHTML = ''
+ document.body.append(mye.dom)
+ assert.equal(mye.dom.constructor.tmp.dom.parentNode, null)
+ assert.equal(document.body.innerHTML, 'test
')
+ document.body.innerHTML = ''
+ }
+
+ /** Статический шаблон в виде текста, передается в метод на экземпляре */
+ ['customElements - template string+function']() {
+ /** Test custom element */
+ class MyElement3x2 extends HTMLElement {
+
+ static template = 'test1 '
+
+ /** @param {{template:string}} options
+ * @returns {string} */
+ template = ({ template }) => template + 'test2 '
+
+ }
+
+ const mye = oom.define(MyElement3x2).oom(MyElement3x2)
+
+ document.body.innerHTML = ''
+ document.body.append(mye.dom)
+ assert.equal(mye.dom.constructor.template, 'test1 ')
+ assert.equal(document.body.innerHTML, 'test1 test2 ')
+ document.body.innerHTML = ''
+ }
+
+ /** Шаблоны на экземпляре не клонируются,
+ * т.к. для каждого создается отдельный экземпляр шаблона */
+ ['customElements - instance template']() {
+ /** Test custom element */
+ class MyElement4 extends HTMLElement {
+
+ template = oom('span', 'test')
+
+ }
+
+ const mye = oom.define(MyElement4).oom(MyElement4)
+ const mye2 = oom(MyElement4)
+
+ document.body.innerHTML = ''
+ document.body.append(mye.dom)
+ assert.ok(mye.dom.template !== mye2.dom.template)
+ assert.ok(mye.dom.template.dom.parentNode === mye.dom)
+ assert.equal(document.body.innerHTML, 'test ')
+ document.body.innerHTML = ''
+ }
+
+ /** Шаблон неподдерживаемого типа */
+ ['customElements - instance template (bad)']() {
+ /** Test custom element */
+ class MyElement5 extends HTMLElement {
+
+ template = 123
+
+ }
+
+ const mye = oom.define(MyElement5).oom(MyElement5)
+
+ document.body.innerHTML = ''
+ document.body.append(mye.dom)
+ assert.equal(document.body.innerHTML, ' ')
+ document.body.innerHTML = ''
+ }
+
+ /** Метод шаблона на экземпляре принимает на вход статичный шаблон */
+ ['customElements - instance template function']() {
+ /** Test custom element */
+ class MyElement6 extends HTMLElement {
+
+ static template = oom('div')
+
+ /** @param {{template:oom}} options */
+ template({ template }) {
+ template.span('test')
+ }
+
+ }
+
+ const mye = oom.define(MyElement6).oom(MyElement6)
+
+ document.body.innerHTML = ''
+ document.body.append(mye.dom)
+ assert.equal(mye.dom.constructor.template.dom.parentNode, null)
+ assert.equal(document.body.innerHTML, 'test
')
+ document.body.innerHTML = ''
+ }
+
+ /** Шаблон на экземпляре в виде строки */
+ ['customElements - instance template string']() {
+ /** Test custom element */
+ class MyElement6x1 extends HTMLElement {
+
+ template = 'test
'
+
+ }
+
+ const mye = oom.define(MyElement6x1).oom(MyElement6x1)
+
+ document.body.innerHTML = ''
+ document.body.append(mye.dom)
+ assert.equal(mye.dom.template, 'test
')
+ assert.equal(document.body.innerHTML, 'test
')
+ document.body.innerHTML = ''
+ }
+
+ /** Можно использовать пользовательский connectedCallback */
+ ['customElements - oom.define, connectedCallback']() {
+ /** Test custom element */
+ class MyElement7 extends HTMLElement {
+
+ /***/
+ connectedCallback() {
+ this.classList.add('MyElement')
+ }
+
+ }
+
+ const mye = oom.define(MyElement7).oom(MyElement7)
+
+ assert.equal(mye.html, ' ')
+
+ document.body.innerHTML = ''
+ document.body.append(mye.dom)
+ assert.equal(document.body.innerHTML, ' ')
+ document.body.innerHTML = ''
+ }
+
+ /** Отслеживание изменений атрибутов */
+ ['customElements - attributeChanged']() {
+ /** Test custom element */
+ class MyElement8x0 extends HTMLElement {
+
+ /**
+ * @param {string} oldValue
+ * @param {string} newValue
+ */
+ dataClassNameChanged(oldValue, newValue) {
+ this.classList.remove(oldValue)
+ this.classList.add(newValue)
+ }
+
+ }
+
+ /** Test custom element */
+ class MyElement8 extends MyElement8x0 {
+
+ /** @returns {oom} */
+ template() {
+ return oom('div', this.dataset.myText, div => (this._div = div))
+ }
+
+ /**
+ * @param {string} oldValue
+ * @param {string} newValue
+ */
+ dataMyTextChanged(oldValue, newValue) {
+ if (this._div.textContent !== newValue) {
+ this._div.textContent = newValue
+ }
+ }
+
+ }
+
+ const mye = oom.define(MyElement8).oom(MyElement8, {
+ 'data-my-text': 'test1'
+ })
+
+ assert.equal(mye.html, ' ')
+
+ document.body.innerHTML = ''
+ document.body.append(mye.dom)
+ assert.equal(document.body.innerHTML, 'test1
')
+
+ mye.dom.dataset.myText = 'test2'
+ assert.equal(document.body.innerHTML, 'test2
')
+
+ mye.dom.dataset.className = 'CLS'
+ assert.equal(document.body.innerHTML, 'test2
')
+
+ document.body.innerHTML = ''
+ }
+
+ /** Отслеживание изменений атрибутов * 2, кэш внутри */
+ ['customElements - attributeChanged * 2, new Set']() {
+ /** Test custom element */
+ class MyElement8x1 extends HTMLElement {
+
+ /**
+ * @param {string} oldValue
+ * @param {string} newValue
+ */
+ dataMyText1Changed(oldValue, newValue) {
+ this.textContent += newValue
+ }
+
+ /**
+ * @param {string} oldValue
+ * @param {string} newValue
+ */
+ dataMyText2Changed(oldValue, newValue) {
+ this.textContent += newValue
+ }
+
+ }
+
+ const mye = oom.define(MyElement8x1).oom(MyElement8x1, {
+ 'data-my-text1': 'test1',
+ 'data-my-text2': 'test2'
+ })
+
+ assert.equal(mye.html, ' ')
+
+ document.body.innerHTML = ''
+ document.body.append(mye.dom)
+ assert.equal(document.body.innerHTML, '' +
+ 'test1test2 ')
+
+ document.body.innerHTML = ''
+ }
+
+ /** Отслеживание изменений атрибутов с пользовательским обработчиком */
+ ['customElements - attributeChanged + observedAttributes']() {
+ const changed = []
+
+ /** Test custom element */
+ class MyElement9 extends HTMLElement {
+
+ /** @returns {[string]} */
+ static get observedAttributes() {
+ return ['test']
+ }
+
+ /** @returns {oom} */
+ template() {
+ return oom('div', div => (this._div = div))
+ }
+
+ /**
+ * @param {string} oldValue
+ * @param {string} newValue
+ */
+ dataMyTextChanged(oldValue, newValue) {
+ this._div.textContent = newValue
+ }
+
+ /**
+ * @param {string} name
+ * @param {string} oldValue
+ * @param {string} newValue
+ */
+ attributeChangedCallback(name, oldValue, newValue) {
+ changed.push([name, oldValue, newValue])
+ }
+
+ }
+
+ const mye = oom.define(MyElement9).oom(MyElement9, {
+ 'data-my-text': 'test1',
+ 'test': 'test2'
+ })
+
+ assert.equal(mye.html, ' ')
+
+ document.body.innerHTML = ''
+ document.body.append(mye.dom)
+ assert.equal(document.body.innerHTML, '' +
+ 'test1
')
+
+ mye.dom.dataset.myText = 'test3'
+ mye.dom.setAttribute('test', 'test4')
+ assert.equal(document.body.innerHTML, '' +
+ 'test3
')
+
+ assert.deepEqual(changed, [
+ ['data-my-text', null, 'test1'],
+ ['test', null, 'test2'],
+ ['data-my-text', 'test1', 'test3'],
+ ['test', 'test2', 'test4']
+ ])
+ document.body.innerHTML = ''
+ }
+
+ /** JSON объекты в качестве атрибута парсятся в объект */
+ ['customElements - attributeChanged + json::*']() {
+ let result
+
+ /** Test custom element */
+ class MyElement10 extends HTMLElement {
+
+ /**
+ * @param {string} oldValue
+ * @param {string} newValue
+ */
+ testChanged(oldValue, newValue) {
+ result = newValue
+ }
+
+ }
+
+ const mye = oom.define(MyElement10).oom(MyElement10, { test: { a: 1 } })
+
+ assert.equal(mye.html, ' ')
+
+ document.body.innerHTML = ''
+ document.body.append(mye.dom)
+ assert.equal(document.body.innerHTML, ' ')
+ assert.deepEqual(result, { a: 1 })
+
+ mye.dom.setAttribute('test', '')
+ assert.equal(document.body.innerHTML, ' ')
+
+ mye.dom.removeAttribute('test')
+ assert.equal(document.body.innerHTML, ' ')
+
+ document.body.innerHTML = ''
+ }
+
+ /** Работа с атрибутами в шаблоне */
+ ['customElements - template + proxyAttributes']() {
+ let result
+
+ /** Test custom element */
+ class MyElement11 extends HTMLElement {
+
+ static template = oom('div')
+
+ /**
+ * @param {{element:HTMLElement, template:oom, attributes:Proxy}} options
+ * @returns {oom}
+ */
+ template = ({ element, template, attributes }) => {
+ attributes.onclick()
+ element._test3 = attributes.test3
+
+ return template
+ .span(attributes.test1.test2)
+ }
+
+ }
+
+ const mye = oom.define(MyElement11).oom(MyElement11, {
+ test1: {
+ test2: 'test3'
+ },
+ onclick: () => (result = 1)
+ })
+
+ assert.equal(mye.html, ' ')
+
+ document.body.innerHTML = ''
+ document.body.append(mye.dom)
+ assert.equal(result, 1)
+ assert.equal(document.body.innerHTML, 'test3
')
+ assert.equal(mye.dom._test3, null)
+
+ document.body.innerHTML = ''
+ }
+
+ /** Работа с атрибутами в статическом шаблоне */
+ ['customElements - static template + proxyAttributes']() {
+ let result
+
+ /** Test custom element */
+ class MyElement12 extends HTMLElement {
+
+ /**
+ * @param {{attributes:Proxy}} options
+ * @returns {oom}
+ */
+ static template = ({ attributes }) => {
+ attributes.onclick()
+
+ return oom('div')
+ .span(attributes.test1.test2)
+ }
+
+ /**
+ * @param {{template:oom, attributes:Proxy}} options
+ * @returns {oom}
+ */
+ template = ({ template, attributes }) => template
+ .span(attributes.test4)
+
+ }
+
+ const mye = oom.define(MyElement12).oom(MyElement12, {
+ test1: {
+ test2: 'test3'
+ },
+ test4: 'test5',
+ onclick: () => (result = 1)
+ })
+
+ assert.equal(mye.html, ' ')
+
+ document.body.innerHTML = ''
+ document.body.append(mye.dom)
+ assert.equal(result, 1)
+ assert.equal(document.body.innerHTML, '' +
+ 'test3 test5
')
+
+ document.body.innerHTML = ''
+ }
+
+ /** Работа с атрибутами в верхнем регистре как с dataset */
+ ['customElements - proxyAttributes + UpperCase']() {
+ /** Test custom element */
+ class MyElement13 extends HTMLElement {
+
+ /**
+ * @param {{attributes:Proxy}} options
+ */
+ static template = ({ attributes }) => { attributes.dataTestAttr2 = 0 }
+
+ /**
+ * @param {{attributes:Proxy}} options
+ * @returns {oom}
+ */
+ template = ({ attributes }) => oom
+ .div(attributes.dataTestAttr1, div => (this._div = div))
+
+ /**
+ * @param {string} oldValue
+ * @param {string} newValue
+ */
+ dataTestAttr1Changed(oldValue, newValue) {
+ this._div.textContent += newValue
+ }
+
+ }
+
+ const mye = oom.define(MyElement13).oom(MyElement13, {
+ dataTestAttr1: 'test1',
+ dataTestAttr2: 1
+ })
+
+ assert.equal(mye.html, ' ')
+
+ document.body.innerHTML = ''
+ document.body.append(mye.dom)
+ assert.equal(document.body.innerHTML, '' +
+ 'test1test1
')
+
+ mye.dom.dataset.testAttr1 = 'test2'
+ assert.equal(document.body.innerHTML, '' +
+ 'test1test1test2
')
+
+ document.body.innerHTML = ''
+ }
+
+ /** Особый атрибут options позволяет сохранить данные на экземпляре,
+ * но при этом его изменение не отслеживается.
+ * Концептуально опции изменяться не должны. */
+ ['customElements - options']() {
+ /** Test custom element */
+ class MyElement14 extends HTMLElement {
+
+ /** @param {{}} options */
+ constructor(options) {
+ super()
+ this._options = options
+ }
+
+ /**
+ * @param {{element:HTMLElement, attributes:Proxy, test:function}} options
+ * @returns {oom}
+ */
+ template = ({ element, attributes, test }) => oom
+ .div([
+ element._options === attributes.options,
+ element._options.test === test,
+ test()
+ ].join('-'))
+
+ }
+
+ const mye = oom.define(MyElement14).oom('my-element14', {
+ options: {
+ test: () => 'test-ok'
+ }
+ })
+
+ assert.equal(mye.html, ' ')
+
+ document.body.innerHTML = ''
+ document.body.append(mye.dom)
+ assert.equal(document.body.innerHTML, 'true-true-test-ok
')
+
+ document.body.innerHTML = ''
+ }
+
+ /** Имя тега нового элемента можно задать через статическое свойство tagName */
+ ['customElements - tagName']() {
+ /** Test custom element */
+ class MyElement15 extends HTMLElement {
+
+ static tagName = 'my-element15-custom'
+
+ }
+
+ const mye = oom.define(MyElement15).oom(MyElement15)
+
+ assert.equal(mye.html, ' ')
+ }
+
+ /** Инициализация теневого DOM */
+ ['customElements - shadowRoot init']() {
+ /** Test custom element */
+ class MyElement16 extends HTMLElement {
+
+ shadowRootInit = { mode: 'open' }
+
+ template = oom.test('ok')
+
+ }
+
+ const mye = oom.define(MyElement16).oom(MyElement16)
+
+ document.body.innerHTML = ''
+ document.body.append(mye.dom)
+ assert.equal(document.body.innerHTML, ' ')
+ assert.equal(mye.dom.shadowRoot.innerHTML, 'ok ')
+
+ document.body.innerHTML = ''
+ }
+
+ /** изменения внутренних элементов теневого DOM по ссылкам */
+ ['customElements - shadowRoot modify by link']() {
+ /** Test custom element */
+ class MyElement17 extends HTMLElement {
+
+ mySpan = oom('span', 'test')
+
+ shadowRootInit = { mode: 'open' }
+
+ /**
+ * @returns {oom}
+ */
+ template = () => oom.test(this.mySpan)
+
+ }
+
+ const mye = oom.define(MyElement17).oom(MyElement17)
+
+ document.body.innerHTML = ''
+ document.body.append(mye.dom)
+ assert.equal(document.body.innerHTML, ' ')
+ assert.equal(mye.dom.shadowRoot.innerHTML, 'test ')
+
+ mye.dom.mySpan.dom.textContent = 'ok'
+ assert.equal(mye.dom.shadowRoot.innerHTML, 'ok ')
+
+ document.body.innerHTML = ''
+ }
+
+ /** Код из примера - Простая верстка */
+ ['example in readme - Example #1']() {
+ const oomDiv = oom('div')
+ .div({ class: 'header' })
+ .div({ style: { borderBottom: '1px solid' } }, oom
+ .span('Name: ', { class: 'test-label' })
+ .span('Test', { class: 'test-name' }))
+ .div({ class: 'footer' })
+ const divHeader = document.createElement('div')
+ const spanName = document.createElement('span')
+ const spanTest = document.createElement('span')
+ const divBorder = document.createElement('div')
+ const divFooter = document.createElement('div')
+ const domDiv = document.createElement('div')
+
+ divHeader.setAttribute('class', 'header')
+ spanName.setAttribute('class', 'test-label')
+ spanTest.setAttribute('class', 'test-name')
+ divBorder.style.borderBottom = '1px solid'
+ divFooter.setAttribute('class', 'footer')
+ spanName.textContent = 'Name: '
+ spanTest.textContent = 'Test'
+
+ domDiv.append(divHeader)
+ divBorder.append(spanName)
+ divBorder.append(spanTest)
+ domDiv.append(divBorder)
+ domDiv.append(divFooter)
+
+ assert.equal(oomDiv.html, domDiv.outerHTML)
+ }
+
+ /** Код из примера - Переиспользование элементов */
+ ['example in readme - Example #2']() {
+ const header = oom('div', { class: 'header' })
+ .span('Test Header')
+ const block = oom
+ .div(oom
+ .append(header.clone())
+ .div('div 1'))
+ .div(oom
+ .append(header.clone())
+ .div('div 2'))
+
+ assert.equal(block.html,
+ '' +
+ '' +
+ '
div 1
' +
+ '
' +
+ '' +
+ '' +
+ '
div 2
' +
+ '
')
+ }
+
+ /** Код из примера - Простой шаблон */
+ ['example in readme - Example #3']() {
+ /** Test custom element */
+ class MyElementExp3 extends HTMLElement {
+
+ mySpan = oom.span('My element new text')
+
+ template = oom('div', { class: 'MyElement__inner' })
+ .append(this.mySpan.clone())
+ .append(oom('br'))
+ .append(this.mySpan)
+
+ }
+
+ const block = oom.define(MyElementExp3).MyElementExp3()
+
+ document.body.innerHTML = ''
+ document.body.append(block.dom)
+
+ assert.equal(document.body.innerHTML,
+ '' +
+ '' +
+ 'My element new text ' +
+ ' ' +
+ 'My element new text ' +
+ '
' +
+ ' ')
+
+ document.body.innerHTML = ''
+ }
+
+ /** Код из примера - Реактивные свойства */
+ ['example in readme - Example #4']() {
+ /** Test custom element */
+ class MyElementExp4 extends HTMLElement {
+
+ static label = oom('span', { class: 'label' })
+ static field = oom('span', { class: 'field' })
+
+ /**
+ * @param {{element:HTMLElement}} options
+ * @returns {oom}
+ */
+ static template({ element }) {
+ return oom()
+ .append(this.label.clone()
+ .span({ class: 'text' }, label => (element._label = label)))
+ .append(this.field.clone()
+ .span({ class: 'text' }, field => (element._field = field)))
+ }
+
+ /**
+ * on 'data-field-text' attribute change
+ *
+ * @param {string} oldValue
+ * @param {string} newValue
+ */
+ dataFieldTextChanged(oldValue, newValue) {
+ this._field.textContent = newValue
+ }
+
+ /**
+ * on 'data-label-text' attribute change
+ *
+ * @param {string} oldValue
+ * @param {string} newValue
+ */
+ dataLabelTextChanged(oldValue, newValue) {
+ this._label.textContent = newValue
+ }
+
+ }
+
+ oom.define(MyElementExp4)
+
+ const block = document.createElement('my-element-exp4')
+ const html = block.outerHTML
+
+ block.dataset.labelText = 'Name: '
+ block.dataset.fieldText = 'Test'
+
+ document.body.innerHTML = ''
+ document.body.append(block)
+
+ assert.equal(html, ' ')
+ assert.equal(document.body.innerHTML,
+ '' +
+ 'Name: ' +
+ 'Test ' +
+ ' ')
+ }
+
+}
diff --git a/package.json b/package.json
new file mode 100644
index 000000000..7b7bff7d6
--- /dev/null
+++ b/package.json
@@ -0,0 +1,19 @@
+{
+ "repository": "github:nodutilus/notml",
+ "license": "Unlicense",
+ "type": "module",
+ "devDependencies": {
+ "@nodutilus/project-config": "latest",
+ "@nodutilus/test": "latest",
+ "@rollup/plugin-node-resolve": "latest",
+ "fastify": "latest",
+ "fastify-static": "latest",
+ "jsdom": "latest",
+ "rollup": "latest",
+ "rollup-plugin-cleanup": "latest",
+ "rollup-plugin-terser": "latest"
+ },
+ "scripts": {
+ "pre-test": "node test/pre-test"
+ }
+}
diff --git a/test/1.basic-behavior.js b/test/1.basic-behavior.js
new file mode 100644
index 000000000..af65a718d
--- /dev/null
+++ b/test/1.basic-behavior.js
@@ -0,0 +1,459 @@
+// @ts-ignore
+import { assert, Test } from '@nodutilus/test'
+import { oom } from '@notml/core'
+
+const { document, HTMLDivElement, HTMLInputElement, DocumentFragment } = window
+
+
+/** Проверка базового поведения создания верстки */
+export default class BasicBehavior extends Test {
+
+ /** Защита от случайного переопределения полей для Proxy */
+ ['Отключение setter`а у Proxy OOM элемента']() {
+ const div = oom('div')
+ let trows = 0
+
+ try {
+ oom.div = null
+ } catch (error) {
+ trows++
+ }
+ try {
+ div.div = null
+ } catch (error) {
+ trows++
+ }
+
+ assert(typeof oom.div, 'function')
+ assert(typeof div.div, 'function')
+ assert(trows, 2)
+ }
+
+ /**
+ * Создание всегда начинается с единичного элемента,
+ * это делает API однозначным
+ */
+ ['Создание DOM элемента через oom']() {
+ const div1 = oom.div()
+ const div2 = oom('div')
+ const div3 = oom.oom('div')
+
+ assert.ok(div1.dom instanceof HTMLDivElement)
+ assert.ok(div2.dom instanceof HTMLDivElement)
+ assert.ok(div3.dom instanceof HTMLDivElement)
+
+ assert.equal(div1.html, '
')
+ assert.equal(div2.html, '
')
+ assert.equal(div3.html, '
')
+ }
+
+ /** Создание экземпляра OOM по готовому DOM элементу */
+ ['Использование готового DOM элемента в oom']() {
+ const div = oom(document.createElement('div'))
+
+ assert.ok(div.dom instanceof HTMLDivElement)
+
+ assert.equal(div.html, '
')
+ }
+
+ /** Создание OOM элемента с некорректным tagName завершается ошибкой */
+ ['Ошибка при вызове с некорректным tagName']() {
+ let div
+
+ try {
+ // @ts-ignore
+ div = oom(0)
+ } catch (error) {
+ assert.ok(error.message.startsWith('"0" did not match the Name production'))
+ }
+ try {
+ // @ts-ignore
+ div = oom({})
+ } catch (error) {
+ assert.ok(error.message.startsWith('"[object Object]" did not match the Name production'))
+ }
+
+ assert.equal(div, undefined)
+ }
+
+ /**
+ * Вставка выполняется вызовом как функции ранее созданного элемента.
+ * Символы "()" создают создают эффект проваливания ниже на уровень
+ */
+ ['Вложение элемента, как экземпляра oom']() {
+ const div1 = oom.div()
+ const a2 = oom('a')
+ const p3 = oom.oom('p')
+
+ div1(oom.oom('p'))
+ a2(oom.i())
+ p3(oom('div'))
+
+ assert.equal(div1.html, '')
+ assert.equal(a2.html, ' ')
+ assert.equal(p3.html, '
')
+ }
+
+ /**
+ * Вставка дочерних элементов, через вызов функции,
+ * работает и для базовых элементов DOM
+ */
+ ['Вложение элемента, как базового элемента DOM']() {
+ const div1 = oom.div()
+ const a2 = oom('a')
+ const fragment1 = document.createDocumentFragment()
+
+ fragment1.append(
+ document.createElement('p'),
+ document.createElement('b')
+ )
+
+ div1(document.createElement('a'))
+ a2(fragment1)
+
+ assert.equal(div1.html, '')
+ assert.equal(a2.html, '
')
+ }
+
+ /**
+ * При вставке в качестве дочерних элементов встроенных объектов JS,
+ * и экземпляров пользовательских классов, выполняется приведение к строке.
+ * Вставка HTML строкой также экранируется стандартным методом HTMLElement.append
+ */
+ ['Вложение элемента, встроенного объекта JS или строки']() {
+ const p1 = oom.p()
+ const p2 = oom.p()
+ const p3 = oom.p()
+ const p2Date = new Date()
+ const p2Str = p2Date + ''
+
+ // @ts-ignore
+ p1(/test/i) // RegExp - превратиться в строку
+ // @ts-ignore
+ p2(p2Date)
+ // @ts-ignore
+ p3(null, false, true, undefined)
+
+ assert.equal(p1.html, '/test/i
')
+ assert.equal(p2.html, `${p2Str}
`)
+ assert.equal(p3.html, 'nullfalsetrue
')
+ }
+
+ /**
+ * При создании элемента доп. аргументом передается объект с атрибутами,
+ * которые перекладываются на созданный экземпляр DOM.
+ * Символ " экранируется, чтобы не портить разметку
+ */
+ ['Установка атрибутов элемента при создании']() {
+ const div1 = oom.div({ class: 'test1', test: '"' })
+ const div2 = oom('div', { class: 'test2', test: '"' })
+ const div3 = oom.oom('div', { class: 'test3', test: '"' })
+
+ assert.equal(div1.html, '
')
+ assert.equal(div2.html, '
')
+ assert.equal(div3.html, '
')
+ }
+
+ /**
+ * Атрибуты можно устанавливать в camelCase, чтобы не оборачивать в кавычки,
+ * заглавные буквы автоматически заменяются с добавлением символа "-".
+ * Это классическое поведение для DataSet и CSSStyleDeclaration
+ */
+ ['Установка атрибутов в camelCase']() {
+ const div = oom.div({ camelCase: 'camelCase' })
+
+ assert.equal(div.html, '
')
+ }
+
+ /** Выполняется вызовом как функции ранее созданного элемента */
+ ['Обновление атрибутов элемента через ловушку apply']() {
+ const div1 = oom.div({ class: 'test1' })
+ const div2 = oom('div', { class: 'test2' })
+ const div3 = oom.oom('div', { class: 'test3' })
+
+ assert.equal(div1.html, '
')
+ assert.equal(div2.html, '
')
+ assert.equal(div3.html, '
')
+
+ div1({ class: 'test4' })
+ div2({ class: 'test5' })
+ div3({ class: '' })
+
+ assert.equal(div1.html, '
')
+ assert.equal(div2.html, '
')
+ assert.equal(div3.html, '
')
+ }
+
+ /**
+ * Все аргументы вызова как функции созданного элемента последовательно обрабатываются,
+ * и выполняется вставка дочерних элементов и обновление атрибутов
+ */
+ ['Вложение нескольких элементов / обновление атрибутов за 1 вызов']() {
+ const div1 = oom.div()
+ const a2 = oom('a')
+ const p3 = oom.oom('p')
+
+ div1(a2, p3, { class: 'test' }, { test: 'class', class: 'test2' })
+
+ assert.equal(div1.html, '')
+ }
+
+ /**
+ * При создании элемента все аргументы используются аналогично аргументам при вызове элемента как функции,
+ * за исключением аргумента с названием тега.
+ * А в коде используется общий метод для обновления созданного элемента
+ */
+ ['Вложение нескольких элементов / обновление атрибутов при создании экземпляра oom']() {
+ const a1 = oom('a')
+ const p1 = oom.oom('p')
+ const a2 = oom('a')
+ const p2 = oom.oom('p')
+ const div1 = oom.div(a1, p1, { class: 'test' }, { test: 'class', class: 'test2' })
+ const div2 = oom('div', a2, p2, { class: 'test' }, { test: 'class', class: 'test2' })
+
+ assert.equal(div1.html, '')
+ assert.equal(div2.html, '')
+ }
+
+ /**
+ * Для наглядной установки логических атрибутов добавим их переключение через true/false
+ */
+ ['Обновление логических атрибутов']() {
+ const div = oom.div()
+
+ assert.equal(div.html, '
')
+
+ div({ enabled: true })
+ assert.equal(div.html, '
')
+
+ div({ enabled: false })
+ assert.equal(div.html, '
')
+
+ div({ testEnabled: true })
+ assert.equal(div.html, '
')
+
+ div({ testEnabled: false })
+ assert.equal(div.html, '
')
+ }
+
+ /**
+ * Если имеется только текст верстки, то его можно задать в OOM элемент через атрибут innerHTML.
+ * Полезно если oom используется в другими библиотеками,
+ * например @primer/octicons возвращает иконки в виде текста svg-разметки.
+ */
+ ['Установка innerHTML через атрибуты OOM']() {
+ const div = oom.div()
+
+ div({ innerHTML: 'test1 ' })
+ assert.equal(div.html, 'test1
')
+
+ div({ 'inner-html': 'test2 ' })
+ assert.equal(div.html, 'test2
')
+ }
+
+ /**
+ * Создание составных компонентов можно выполнять без использования промежуточных переменных,
+ * чтобы приблизить вид к верстке через HTML.
+ */
+ ['Верстка составного компонента через аргументы']() {
+ const component1 = oom('div', { class: 'link' },
+ oom.span({ class: 'title' }, 'Link: '),
+ oom.a({ href: 'https://test.ok' }, 'test.ok')
+ )
+ const component2 = oom.div({ class: 'link' },
+ oom.span({ class: 'title' }, 'Link: '),
+ oom.a({ href: 'https://test.ok' }, 'test.ok')
+ )
+ const componentText = `
+
+ `.replace(/\s*\n+\s+/g, '')
+
+ assert.equal(component1.html, componentText)
+ assert.equal(component2.html, componentText)
+ }
+
+ /**
+ * Чтобы создать несколько элементов без вложенности на одном уровне,
+ * а затем поместить в другой элемент, в качестве контейнера используется DocumentFragment
+ */
+ ['Создание OOM элемента с DocumentFragment']() {
+ const fragment1 = oom()
+ const fragment2 = oom(document.createDocumentFragment())
+
+ fragment1(oom.div())
+ fragment2(oom.div(), oom.div())
+
+ assert.ok(fragment1.dom instanceof DocumentFragment)
+ assert.ok(fragment2.dom instanceof DocumentFragment)
+
+ assert.equal(fragment1.html, '
')
+ assert.equal(fragment2.html, '
')
+ }
+
+ /**
+ * При попытке обновления атрибутов для DocumentFragment
+ * кидается стандартная ошибка об отсутствии метода
+ */
+ ['Ошибка обновления атрибутов для DocumentFragment']() {
+ const fragment = oom()
+ let err
+
+ try {
+ // @ts-ignore Проверка на стандартное исключение DOM
+ fragment({ class: 'test' })
+ } catch (error) {
+ err = error
+ }
+
+ assert.equal(err.message, "'setAttribute' called on an object that is not a valid instance of Element.")
+ assert.equal(fragment.html, '')
+ }
+
+ /**
+ * DocumentFragment может содержать на 1ом уровне не элементы а ноды,
+ * значимой при версте через OOM является только TEXT_NODE, остальные игнорируются
+ */
+ ['DocumentFragment, Node и nodeType']() {
+ const comment = document.createComment('comment text')
+ // @ts-ignore - comment не может быть OOMChild, но при передаче в параметры он проигнорируется
+ const fragment = oom()('test text', oom.i(), comment)
+
+ assert.equal(fragment.html, 'test text ')
+ }
+
+ /**
+ * При вставке одного фрагмента DOM в другой элементы переданного фрагмента переносятся в целевой,
+ * т.к. нет необходимости хранения иерархии для DocumentFragment
+ */
+ ['DocumentFragment в DocumentFragment']() {
+ const fragment1 = oom()(oom.div('test1'))
+ const fragment2 = oom()('test2')
+
+ assert.equal(fragment2.dom.childNodes.length, 1)
+ fragment1(fragment2)
+ assert.equal(fragment2.dom.childNodes.length, 0)
+ assert.equal(fragment1.html, 'test1
test2')
+ assert.ok(fragment1.dom.firstChild instanceof HTMLDivElement)
+ }
+
+ /**
+ * Использование чейнинга на элементе создает DocumentFragment,
+ * помещая элементы последовательно
+ */
+ ['Создание последовательных элементов через чейнинг']() {
+ const fragment1 = oom
+ .div('test1')
+ .div('test2')
+ .div('test3')
+ const fragment2 = oom()
+ const div1 = oom('div', 'test2')
+
+ fragment2.div('test1')
+ fragment2(div1)
+ fragment2.div('test3')
+
+ assert.equal(fragment1.html, 'test1
test2
test3
')
+ assert.equal(fragment2.html, 'test1
test2
test3
')
+ }
+
+ /**
+ * Чейнинг работает и после вызова базового элемента для обновления или вставки дочернего элемента.
+ * Используется когда удобнее добавлять дочерние элементы через вызов функции,
+ * например при создании пользовательских элементов через `new`
+ */
+ ['Чейнинг после вызова элемента функции']() {
+ const div = oom.div({ class: 'test1' })
+ const fragment = div.clone()({ class: 'test2' }, oom.span('t1'))
+ .span('t2')
+ .span('t3')
+
+
+ assert.equal(div.html, '
')
+ assert.equal(fragment.html, 't1
t2 t3 ')
+ }
+
+ /** Что бы уменьшить кол-во кода создание составных компонентов можно выполнять с использованием чейнинга */
+ ['Верстка составного компонента через чейнинг']() {
+ const component1 = oom('div', { class: 'link' }, oom
+ .span({ class: 'title' }, 'Link: ')
+ .a({ href: 'https://test.ok' }, 'test.ok'))
+ const component2 = oom.div({ class: 'link' }, oom
+ .span({ class: 'title' }, 'Link: ')
+ .a({ href: 'https://test.ok' }, 'test.ok'))
+ const componentText = `
+
+ `.replace(/\s*\n+\s+/g, '')
+
+ assert.equal(component1.html, componentText)
+ assert.equal(component2.html, componentText)
+ }
+
+
+ /**
+ * Обработчики событий можно задавать через атрибуты или DOM свойства,
+ * в оом шаблоне оба способа работают через объект с атрибутами.
+ * Функции автоматически определяются и присваиваются как свойства объекта
+ */
+ ['Базовая обработка событий DOM элементов']() {
+ let cnt = 0
+ const div1 = oom.div({ onclick: () => cnt++ })
+ const div2 = oom.div({ onclick: `this.append('${cnt}')` })
+
+ assert.equal(cnt, 0)
+ div1.dom.click()
+ assert.equal(cnt, 1)
+
+ assert.equal(div2.html, '
')
+ div2.dom.click()
+ assert.equal(div2.html, '0
')
+ }
+
+ /**
+ * Вставка oom элемента в другой не должна завершаться ошибкой.
+ * dom элемент передается по ссылке.
+ */
+ ['Вставка oom в oom']() {
+ const div1 = oom.div()
+ const div2 = oom(div1)
+
+ assert.equal(div1.dom, div2.dom)
+ }
+
+ /**
+ * При работе с тегом template для работы с вложенными элементами нужно использовать elm.content.
+ * При этом clone из оом клонирует сам шаблон, а его содержимое клонируется через DOM API,
+ * это дает возможность удобно создавать в JS через oom шаблоны для DOM шаблонов
+ */
+ ['Работа с тегом template']() {
+ const tmpl = oom.template(oom.div())
+ // Клонируем содержимое шаблона через DOM API
+ const elm1 = oom.main(tmpl.dom.content.cloneNode(true))
+ // Клонируем сам шаблон через OOM API
+ const elm2 = oom.section(tmpl.clone())
+
+ assert.equal(elm1.dom.outerHTML, '
')
+ assert.equal(elm2.dom.outerHTML, '')
+ assert.equal(tmpl.dom.outerHTML, '
')
+ // @ts-ignore
+ assert.equal(tmpl.dom.content.firstChild.outerHTML, '
')
+ }
+
+ /**
+ * Объявлены основные html теги для упрощения обращения и автоподстановки
+ */
+ ['Типизация .dom']() {
+ const input = oom.input()
+
+ input.dom.value = 'test'
+
+ assert.equal(input.dom.value, 'test')
+ assert.ok(input.dom instanceof HTMLInputElement)
+ }
+
+}
diff --git a/test/2.basic-api.js b/test/2.basic-api.js
new file mode 100644
index 000000000..294f3f89c
--- /dev/null
+++ b/test/2.basic-api.js
@@ -0,0 +1,69 @@
+// @ts-ignore
+import { assert, Test } from '@nodutilus/test'
+import { oom } from '@notml/core'
+
+const { document } = window
+
+
+/** Проверка API OOM элементов */
+export default class BasicAPI extends Test {
+
+ /**
+ * Добавление дочернего элемента.
+ * Выполняет универсальное добавление элемента с учетом реализации OOM элементов.
+ * Добавление выполняется в конец списка детей, другое поведение нерационально
+ */
+ ['OOMElement#append - Вставка дочернего элемента']() {
+ const div = oom('div')
+ .append(oom('a'))
+ .append(document.createElement('b'))
+ .append('test')
+
+ assert.equal(div.html, '')
+ }
+
+ /**
+ * Добавление элемента в фрагмент выполняется аналогично добавлению в OOM элемент
+ */
+ ['OOMFragment#append - Вставка элемента в фрагмент']() {
+ const fragment = oom()
+ .append(oom('a'))
+ .append(document.createElement('b'))
+ .append('test')
+
+ assert.equal(fragment.html, ' test')
+ }
+
+ /**
+ * Клонирование элемента для его переиспользования.
+ * Создает копию DOM элемента и оборачивает его в новый экземпляр OOM Proxy
+ */
+ ['OOMElement#clone - Клонирование элемента']() {
+ const div1 = oom('div', { class: 'test' })
+ const div2 = div1.clone()
+
+ div1('test1')
+ div2('test2')
+
+ assert.notEqual(div1, div2)
+ assert.notEqual(div1.dom, div2.dom)
+ assert.equal(div1.html, 'test1
')
+ assert.equal(div2.html, 'test2
')
+ }
+
+ /** Фрагменты документов копируются, как и элементы, включая все вложенные элементы */
+ ['OOMFragment#clone - Клонирование фрагмента документа']() {
+ const fragment1 = oom
+ .div('test1')
+ .div('test2')
+ const fragment2 = fragment1.clone()
+
+ fragment2.dom.children[0].className = 'test3'
+
+ assert.notEqual(fragment1, fragment2)
+ assert.notEqual(fragment1.dom, fragment2.dom)
+ assert.equal(fragment1.html, 'test1
test2
')
+ assert.equal(fragment2.html, 'test1
test2
')
+ }
+
+}
diff --git a/test/3.style.js b/test/3.style.js
new file mode 100644
index 000000000..7b26fd4e2
--- /dev/null
+++ b/test/3.style.js
@@ -0,0 +1,272 @@
+// @ts-ignore
+import { assert, Test } from '@nodutilus/test'
+import { oom } from '@notml/core'
+
+const { document } = window
+
+/** Тесты генерации CSS через OOMStyle */
+export default class OOMStyle extends Test {
+
+ /**
+ * Атрибут style в oom шаблоне позволяет задавать стили объектом в формате CSSStyleDeclaration.
+ * Что позволяет последовательно обновлять inline стили, не перетирая всё значение атрибута.
+ * Также можно указать style в классическом виде, как строку
+ */
+ ['Атрибут style']() {
+ const div = oom.div({ style: { background: 'red' } })
+
+ assert.equal(div.html, '
')
+
+ div({ style: { background: 'green', fontSize: '14px' } })
+ assert.equal(div.html, '
')
+
+ div({ style: { background: 'orange' } })
+ assert.equal(div.html, '
')
+
+ // Можно указать строкой и перезаписать весь style
+ div({ style: 'background: red;' })
+ assert.equal(div.html, '
')
+
+ // Для обнуления стилей можно использовать указание пустой строки
+ div({ style: '' })
+ assert.equal(div.html, '
')
+ }
+
+ /**
+ * Генератор стилей наследуется от базового style для сохранения оригинального tagName.
+ * И все обращения к style через oom шаблонизатор создают OOMStyle, что бы использовать объектную модель
+ */
+ ['OOMStyle extends HTMLStyleElement']() {
+ let style
+
+ style = oom('style')
+ assert.equal(style.html, '')
+
+ style = oom('oom-style')
+ assert.equal(style.html, '')
+
+ style = oom.style()
+ assert.equal(style.html, '')
+
+ style = oom.oomStyle()
+ assert.equal(style.html, '')
+ }
+
+ /**
+ * У тега style общий чейнинг с другими элементами, базовая верстка через oom не нарушается
+ */
+ ['Чейнинг для style']() {
+ let style
+
+ style = oom.div().style().span()
+ assert.equal(style.html, '
')
+
+ style = oom.div(oom.style().span())
+ assert.equal(style.html, '
')
+
+ style = oom.div(oom.span().style())
+ assert.equal(style.html, '
')
+ }
+
+ /**
+ * Возможность создания встроенного style остается через document.createElement,
+ * в oom шаблоне для обычного style нет особого применения
+ */
+ ['Создание style через document.createElement']() {
+ const style = oom(document.createElement('style'))
+ const oomStyle = oom(document.createElement('style', { is: 'oom-style' }))
+
+ assert.equal(style.html, '')
+ assert.equal(oomStyle.html, '')
+ }
+
+ /**
+ * Стили указываются объектом, где ключи это CSS селекторы, а значения объект со свойствами CSS.
+ * Объект со свойствами определяется в формате CSSStyleDeclaration, как и атрибут style в oom шаблоне
+ */
+ ['Простой объект с селекторами']() {
+ const style = oom.style({
+ 'fontSize': '10px',
+ '.my-class': { background: 'red', fontSize: '12px' }
+ })
+
+ document.body.innerHTML = ''
+ document.body.append(style.dom)
+ assert.equal(document.body.innerHTML, `
+
+ `.replace(/\s*\n+\s+/g, ''))
+ document.body.innerHTML = ''
+ }
+
+ /**
+ * При ошибке в имени или значение CSS свойства оно игнорируется и не добавляется в верстку
+ */
+ ['Неверные CSS свойства']() {
+ const style = oom.style({
+ // @ts-ignore
+ '.my-class': { background: 1, fontSize: 'red', noName: 'noName' }
+ })
+
+ document.body.innerHTML = ''
+ document.body.append(style.dom)
+ assert.equal(document.body.innerHTML, `
+
+ `.replace(/\s*\n+\s+/g, ''))
+ document.body.innerHTML = ''
+ }
+
+ /**
+ * Основная фича использования стилей как объектов, это переиспользование описания стиля элементов.
+ * Можно выносить описание классов отдельно и переиспользовать для нескольких компонентов
+ */
+ ['Переиспользование описания стиля']() {
+ /** @type {import('@notml/core').OOMStyle.StyleSource} */
+ const myStyle = {
+ background: 'red',
+ fontSize: '12px'
+ }
+ const style1 = oom.style({ '.my-class1': myStyle })
+ const style2 = oom.style({ '.my-class2': { alignContent: 'center', ...myStyle } })
+ const style3 = oom.style({
+ '.my-class3': {
+ 'alignContent': 'center',
+ '.my-class1': myStyle
+ }
+ })
+
+ style3({
+ '.my-class4': {
+ '.my-class5': myStyle
+ }
+ })
+
+ document.body.innerHTML = ''
+ document.body.append(style1.dom)
+ document.body.append(style2.dom)
+ document.body.append(style3.dom)
+ assert.equal(document.body.innerHTML, `
+
+
+
+ `.replace(/\s*\n+\s+/g, ''))
+ document.body.innerHTML = ''
+ }
+
+ /**
+ * Для всех стилей в коллекции можно задать общее имен области действия.
+ * Например для пользовательских элементов использовать имя их атрибута для стилизации
+ */
+ ['Имя области действия для селекторов']() {
+ const style = oom.style('my-scope', {
+ 'alignItems': 'center',
+ '.my-class1': { background: 'red', fontSize: '11px' },
+ '.my-class2': { background: 'red', fontSize: '12px' }
+ })
+
+ document.body.innerHTML = ''
+ document.body.append(style.dom)
+ assert.equal(document.body.innerHTML, `
+
+ `.replace(/\s*\n+\s+/g, ''))
+ document.body.innerHTML = ''
+ }
+
+ /**
+ * Имя области действия в общем случае является именем пользовательского элемента,
+ * и начало селектора может начинаться с данного имени, что бы сделать модификатор стиля элемента.
+ * При это имя области не должно повторно добавляется к имени такого селектора.
+ */
+ ['Имя области действия в начале селектора']() {
+ const style = oom.style('my-scope', {
+ 'my-scope.active': { color: 'yellow' },
+ '.active': { color: 'yellow' }
+ })
+
+ document.body.innerHTML = ''
+ document.body.append(style.dom)
+ assert.equal(document.body.innerHTML, `
+
+ `.replace(/\s*\n+\s+/g, ''))
+ document.body.innerHTML = ''
+ }
+
+ /**
+ * Фактически CSS переменные задаются так же как обычные свойства.
+ * Но на данный момент поведение нельзя полностью протестировать на сервере:
+ * https://github.com/jsdom/jsdom/issues/1895
+ * https://github.com/jsdom/cssstyle/issues/89
+ * Значение свойств вида var(--...) на данный момент не присваивается в jsdom/cssstyle,
+ * но фактически в браузере все работает, см webtest/css-custom-properties
+ */
+ ['css переменные']() {
+ const style = oom.style({
+ ':root': {
+ '--my-var': 'yellow',
+ '--my-var2': 'var(--my-var)'
+ },
+ '.my-class': {
+ background: 'var(--my-var)'
+ }
+ })
+
+ document.body.innerHTML = ''
+ document.body.append(style.dom)
+ assert.equal(document.body.innerHTML, `
+
+ `.replace(/\s*\n+\s+/g, ''))
+ // Ждем реализации в jsdom/cssstyle
+ // assert.equal(document.body.innerHTML, `
+ //
+ // `.replace(/\s*\n+\s+/g, ''))
+ document.body.innerHTML = ''
+ }
+
+ /**
+ * css переменные записываются на экземпляр style,
+ * поэтому алгоритм первичной установки и обновления может отличаться.
+ * Проверим обновление работает как ожидалось и OOMStyle ничего не отломал
+ */
+ ['css переменные - перезапись']() {
+ const style = oom.style({
+ ':root': { '--my-var': 'yellow' }
+ })
+
+ style({ ':root': { '--my-var': 'red' } })
+
+ document.body.innerHTML = ''
+ document.body.append(style.dom)
+ assert.equal(document.body.innerHTML, `
+
+ `.replace(/\s*\n+\s+/g, ''))
+ document.body.innerHTML = ''
+ }
+
+}
diff --git a/test/4.custom-elements.js b/test/4.custom-elements.js
new file mode 100644
index 000000000..ee110bf4a
--- /dev/null
+++ b/test/4.custom-elements.js
@@ -0,0 +1,944 @@
+// @ts-ignore
+import { assert, Test } from '@nodutilus/test'
+import { oom } from '@notml/core'
+
+const { HTMLElement, HTMLButtonElement, customElements, document } = window
+
+
+/** Проверка расширенной работы пользовательских элементов */
+export default class CustomElements extends Test {
+
+ /** Для удобства регистрации и переиспользования все опции define перенесены на класс */
+ ['Регистрация пользовательского элемента']() {
+ /** Имя тега забирается по названию класса */
+ class MyElement1 extends oom.extends(HTMLElement) {
+
+ static tagName = 'my-element1'
+
+ }
+
+ /** Имя тега получается из статического свойства класса */
+ class MyElement2 extends oom.extends(HTMLElement) {
+
+ static tagName = 'me-2'
+
+ }
+
+ /**
+ * Несколько заклавных в названии класса подряд,
+ * при использовании его в качетстве имени тега, не разделяется
+ */
+ class OOMElement2 extends oom.extends(HTMLElement) {
+
+ static tagName = 'oom-element2'
+
+ }
+
+ /** Расширения базового тега так же выполняется через класс */
+ class MyButton1 extends oom.extends(HTMLButtonElement) {
+
+ static tagName = 'my-button1'
+ static extendsTagName = 'button'
+
+ }
+
+ oom.define(MyElement1, MyElement2, OOMElement2, MyButton1)
+
+ assert.equal(oom(new MyElement1()).html, ' ')
+ assert.equal(oom(MyElement1).html, ' ')
+ assert.equal(oom.myElement1().html, ' ')
+ assert.equal(oom('myElement1').html, ' ')
+ assert.equal(customElements.get('my-element1'), MyElement1)
+
+ assert.equal(oom(new MyElement2()).html, ' ')
+ assert.equal(oom(MyElement2).html, ' ')
+ assert.equal(oom['me-2']().html, ' ')
+ assert.equal(oom('me-2').html, ' ')
+ assert.equal(customElements.get('me-2'), MyElement2)
+
+ assert.equal(oom(new OOMElement2()).html, ' ')
+ assert.equal(oom(OOMElement2).html, ' ')
+ assert.equal(oom.oomElement2().html, ' ')
+ assert.equal(oom('oomElement2').html, ' ')
+ assert.equal(customElements.get('oom-element2'), OOMElement2)
+
+ assert.equal(oom(new MyButton1()).html, ' ')
+ assert.equal(oom(MyButton1).html, ' ')
+ assert.equal(oom.myButton1().html, ' ')
+ assert.equal(oom('myButton1').html, ' ')
+ assert.equal(customElements.get('my-button1'), MyButton1)
+
+ assert.equal((new MyButton1()).getAttribute('is'), 'my-button1')
+ }
+
+ /** Свойство template экземпляра класса используется как шаблон компонента по аналогии с одноименным тегом */
+ ['Шаблон компонента в template']() {
+ /** Класс с шаблоном */
+ class MyElement3 extends oom.extends(HTMLElement) {
+
+ static tagName = 'my-element3'
+
+ template = oom('div')
+
+ }
+
+ oom.define(MyElement3)
+
+ const myElm3 = new MyElement3()
+
+ assert.equal(myElm3.outerHTML, ' ')
+
+ document.body.innerHTML = ''
+ document.body.append(myElm3)
+ assert.equal(document.body.innerHTML, '
')
+ document.body.innerHTML = ''
+
+ assert.equal(myElm3.outerHTML, '
')
+ }
+
+ /**
+ * При вставке шаблона определяется такой порядок что бы были 2 точки взаимодействия:
+ * 1-ая в constructor до применения, 2-ая в connectedCallback, после применения шаблона
+ * Чтобы можно было гибко управлять содержимым и использовать такие элементы как slot
+ */
+ ['Работа с constructor, connectedCallback и template']() {
+ /** Шаблон + обновление верстки в конструкторе */
+ class MyElement4 extends oom.extends(HTMLElement) {
+
+ static tagName = 'my-element4'
+
+ template = oom('div')
+
+ /**
+ * На конструкторе шаблон еще не применен,
+ * и можно изменить внутреннюю верстку, аналогично работе через createElement
+ */
+ constructor() {
+ super()
+ assert.equal(this.outerHTML, ' ')
+ this.innerHTML = 'test1'
+ }
+
+ /**
+ * На обработчике вставки в DOM расширение в oom.extends вставляет шаблон,
+ * и можно работать с готовой версткой компонента
+ */
+ connectedCallback() {
+ super.connectedCallback() // Применение template
+ assert.equal(this.outerHTML, 'test1
')
+ }
+
+ }
+
+ oom.define(MyElement4)
+
+ const myElm4 = new MyElement4()
+
+ assert.equal(myElm4.outerHTML, 'test1 ')
+
+ document.body.innerHTML = ''
+ document.body.append(myElm4)
+ assert.equal(document.body.innerHTML, 'test1
')
+ document.body.innerHTML = ''
+ }
+
+ /**
+ * oom.extends может работать и от пользовательского класса, например из сторонней библиотеки.
+ * В таком случае методы из OOMCustomElement выполняются всегда раньше, сохраняя базовое поведение.
+ * Однако это создает больше классов OOMCustomElement чем наследование от базовых классов
+ * (по 1му на каждый пользовательский, вместо 1го общего на базовый)
+ */
+ ['Наследование через oom.extends от пользовательского класса']() {
+ let myE9html = ''
+
+ /** Расширяемый класс. Может быть компонентом из другой библиотеки */
+ class MyElement9 extends HTMLElement {
+
+ /** Выполниться до построения верстки в дочернем классе, но имя тега уже будет переопределено */
+ connectedCallback() {
+ myE9html = this.outerHTML
+ }
+
+ }
+
+ /** Наследуется от стороннего класса пользовательского элемента, при этом добавляя oom поведение */
+ class MyElement10 extends oom.extends(MyElement9) {
+
+ static tagName = 'my-element10'
+
+ template = 'test10'
+
+ }
+
+ oom.define(MyElement10)
+
+ document.body.innerHTML = ''
+ document.body.append(new MyElement10())
+ assert.equal(myE9html, ' ')
+ assert.equal(document.body.innerHTML, 'test10 ')
+ document.body.innerHTML = ''
+ }
+
+ /** Для разных типов шаблонов должна быть общая последовательность вставки */
+ ['Базовые типы template']() {
+ const [MyElement5, MyElement6, MyElement7, MyElement8] = oom.define(
+ class MyElement5 extends oom.extends(HTMLElement) {
+
+ static tagName = 'my-element5'
+
+ template = oom('div')
+
+ },
+ class MyElement6 extends oom.extends(HTMLElement) {
+
+ static tagName = 'my-element6'
+
+ template = '
'
+
+ },
+ class MyElement7 extends oom.extends(HTMLElement) {
+
+ static tagName = 'my-element7'
+
+ template = document.createElement('div')
+
+ },
+ class MyElement8 extends oom.extends(HTMLElement) {
+
+ static tagName = 'my-element8'
+
+ template = oom.a().b().dom
+
+ })
+ const myElm5 = new MyElement5()
+ const myElm6 = new MyElement6()
+ const myElm7 = new MyElement7()
+ const myElm8 = new MyElement8()
+
+ myElm5.innerHTML = 'test'
+ myElm6.innerHTML = 'test'
+ myElm7.innerHTML = 'test'
+ myElm8.innerHTML = 'test'
+
+ document.body.innerHTML = ''
+ document.body.append(myElm5)
+ document.body.append(myElm6)
+ document.body.append(myElm7)
+ document.body.append(myElm8)
+ assert.equal(document.body.innerHTML, `
+ test
+ test
+ test
+ test
+ `.replace(/\s*\n+\s+/g, ''))
+ document.body.innerHTML = ''
+ }
+
+ /**
+ * Шаблон элемента может быть функцией возвращающей дочерний элемент для вставки,
+ * или void, тогда предполагается что добавление дочерних элементов осуществляется внутри функции.
+ * Эта особенность позволяет создавать динамические шаблоны и влиять из функции на сам элемент.
+ */
+ ['Функция в качестве шаблона']() {
+ /** Шаблон функция */
+ class MyElement23 extends oom.extends(HTMLElement) {
+
+ static tagName = 'my-element23'
+
+ template = () => {
+ this.id = 'test-ok'
+ oom(this, oom.span('test ok'))
+ }
+
+ /** Aвтоматическая вставка в DOM */
+ constructor() {
+ super()
+ oom(document.body, this)
+ }
+
+ }
+
+ /** Шаблон функция */
+ class MyElement24 extends oom.extends(HTMLElement) {
+
+ static tagName = 'my-element24'
+
+ template = () => oom.span('test ok 2')
+
+ /** Aвтоматическая вставка в DOM */
+ constructor() {
+ super()
+ oom(document.body, this)
+ }
+
+ }
+
+ oom.define(MyElement23, MyElement24)
+ document.body.innerHTML = ''
+
+ const myElm23 = oom.myElement23().dom
+ const myElm24 = oom.myElement24().dom
+
+ assert.equal(myElm23.outerHTML, `
+
+ test ok
+
+ `.replace(/\s*\n+\s+/g, ''))
+ assert.equal(myElm24.outerHTML, `
+
+ test ok 2
+
+ `.replace(/\s*\n+\s+/g, ''))
+ assert.equal(document.body.innerHTML, `
+
+ test ok
+
+
+ test ok 2
+
+ `.replace(/\s*\n+\s+/g, ''))
+ document.body.innerHTML = ''
+ }
+
+ /**
+ * Функция построения шаблона может быть асинхронной,
+ * в этом случае содержимое компонента обновляется по завершению Promise, либо непосредственно из функции.
+ * Отображение заглушки на загрузку остается за автором пользовательского элемента.
+ * Необработанная ошибка шаблона выводится прямо в тело компонента,
+ * прикладной шаблон должен сам позаботится об отображении, а вывод является последней фатальной мерой.
+ * Асинхронная загрузка не блокирует компоненты которые строятся после данного шаблона,
+ * синхронизация состояний задача пользовательского шаблона
+ */
+ async ['Асинхронная функция в качестве шаблона']() {
+ /** Асинхроный шаблон */
+ class MyElement26 extends oom.extends(HTMLElement) {
+
+ static tagName = 'my-element26'
+
+ template = async () => {
+ const span = oom.span('async')
+
+ await new Promise(resolve => setTimeout(resolve))
+
+ return span
+ }
+
+ }
+
+ oom.define(MyElement26)
+
+ const myE26 = oom.myElement26()
+
+ document.body.innerHTML = ''
+ oom(document.body, myE26.span('sync'))
+
+ assert.equal(document.body.innerHTML, `
+
+ sync
+ `.replace(/\s*\n+\s+/g, ''))
+
+ const awaitedMyE26 = await myE26
+
+ assert.equal(awaitedMyE26, myE26)
+ assert.equal(document.body.innerHTML, `
+ async
+ sync
+ `.replace(/\s*\n+\s+/g, ''))
+
+ document.body.innerHTML = ''
+ }
+
+ /** Как и для базовых типов шаблонов, для асинхронных, должна быть общая последовательность вставки */
+ async ['Асинхронные типы template']() {
+ const [MyElement27, MyElement28, MyElement29, MyElement30] = oom.define(
+ class MyElement27 extends oom.extends(HTMLElement) {
+
+ static tagName = 'my-element27'
+
+ template = async () => {
+ await new Promise(resolve => setTimeout(resolve))
+
+ return oom('div')
+ }
+
+ },
+ class MyElement28 extends oom.extends(HTMLElement) {
+
+ static tagName = 'my-element28'
+
+ template = async () => {
+ await new Promise(resolve => setTimeout(resolve))
+
+ return '
'
+ }
+
+ },
+ class MyElement29 extends oom.extends(HTMLElement) {
+
+ static tagName = 'my-element29'
+
+ template = async () => {
+ await new Promise(resolve => setTimeout(resolve))
+
+ return document.createElement('div')
+ }
+
+ },
+ class MyElement30 extends oom.extends(HTMLElement) {
+
+ static tagName = 'my-element30'
+
+ template = async () => {
+ await new Promise(resolve => setTimeout(resolve))
+
+ return oom.a().b().dom
+ }
+
+ }
+ )
+ const myElm27 = new MyElement27()
+ const myElm28 = new MyElement28()
+ const myElm29 = new MyElement29()
+ const myElm30 = new MyElement30()
+
+
+ myElm27.innerHTML = 'test'
+ myElm28.innerHTML = 'test'
+ myElm29.innerHTML = 'test'
+ myElm30.innerHTML = 'test'
+
+ // До вставки в DOM Promise нет, т.к. компоненты еще не начали строятся
+ await Promise.all([oom(myElm27), oom(myElm28), oom(myElm29), oom(myElm30)])
+
+ document.body.innerHTML = ''
+ document.body.append(myElm27)
+ document.body.append(myElm28)
+ document.body.append(myElm29)
+ document.body.append(myElm30)
+
+ assert.equal(document.body.innerHTML, `
+ test
+ test
+ test
+ test
+ `.replace(/\s*\n+\s+/g, ''))
+
+ await Promise.all([oom(myElm27), oom(myElm28), oom(myElm29), oom(myElm30)])
+
+ assert.equal(document.body.innerHTML, `
+ test
+ test
+ test
+ test
+ `.replace(/\s*\n+\s+/g, ''))
+ document.body.innerHTML = ''
+ }
+
+ /**
+ * Ошибка в асинхронном шаблоне не должна приводить к падению построения всей страницы.
+ * Все необработанные компонентом ошибки добавляются в виде текста сообщения в вертску компонента.
+ * Это финальная проверки на ошибки, в правильном приложении все ошибки обрабатывают сами компоненты
+ */
+ async ['Ошибка в асинхронном шаблоне']() {
+ /** Компонент с ошибкой */
+ class MyAsyncError1 extends oom.extends(HTMLElement) {
+
+ static tagName = 'my-async-error1'
+
+ template = async () => {
+ await new Promise(resolve => setTimeout(resolve))
+ oom(this, oom.span('test1'))
+ throw new Error('test2')
+ }
+
+ }
+
+ /** Компонент с ошибкой (строка) */
+ class MyAsyncError2 extends oom.extends(HTMLElement) {
+
+ static tagName = 'my-async-error2'
+
+ template = async () => {
+ await new Promise(resolve => setTimeout(resolve))
+ oom(this, oom.span('test1'))
+ // eslint-disable-next-line
+ throw 'test2'
+ }
+
+ }
+
+ oom.define(MyAsyncError1, MyAsyncError2)
+
+ const myErr1 = oom.myAsyncError1()
+ const myErr2 = oom.myAsyncError2()
+
+ document.body.innerHTML = ''
+ oom(document.body, myErr1, myErr2)
+
+ assert.equal(document.body.innerHTML, `
+
+
+ `.replace(/\s*\n+\s+/g, ''))
+
+ const awaitedMyErr1 = await myErr1
+ const awaitedMyErr2 = await myErr2
+
+ assert.equal(awaitedMyErr1, myErr1)
+ assert.equal(awaitedMyErr2, myErr2)
+
+ assert.ok(myErr1.dom.innerHTML.includes('test1 Error: test2'))
+ assert.ok(myErr1.dom.innerHTML.includes('at MyAsyncError1.template'))
+ assert.equal(myErr2.dom.outerHTML.replace(/\s*\n+\s+/g, ''), `
+
+ test1
+ test2
+
+ `.replace(/\s*\n+\s+/g, ''))
+
+ document.body.innerHTML = ''
+ }
+
+ /**
+ * В качестве описания структуры опций можно использовать простые объекты и массивы.
+ * - Объекты будут клонированы и объединены с опциями по умолчанию в новые объекты.
+ * - Массивы из опций по умолчанию будет полностью заменены копиями массивов указанными в опциях.
+ * - Примитивные значения опций передаются по значению.
+ * - Сложные объекты, такие как Date, RegExp, пр., и пользовательские классы, будут переданы по ссылке.
+ * Такое поведение позволит создавать структурированные опции,
+ * и использовать в качестве опций DOM элементы для создания агрегаций
+ */
+ ['Опции: объект']() {
+ let mye11, mye12
+
+ /** Без опций по умолчанию */
+ class MyElement11 extends oom.extends(HTMLElement) {
+
+ static tagName = 'my-element11'
+
+ }
+
+ oom.define(MyElement11)
+
+ // Без указания опций
+ mye11 = new MyElement11()
+ assert.deepEqual(mye11.options, {})
+
+ // Можно передать любые опции
+ mye11 = new MyElement11({ a: 1, b11: { c11: 2 } })
+ assert.deepEqual(mye11.options, { a: 1, b11: { c11: 2 } })
+ assert.equal(mye11.options.a, 1)
+ assert.equal(mye11.options.b11.c11, 2)
+
+ /** С опцией по умолчанию */
+ class MyElement12 extends oom.extends(HTMLElement, { a: 'test', b12: { c12: 2 } }) {
+
+ static tagName = 'my-element12'
+
+ }
+
+ oom.define(MyElement12)
+
+ // Без указания опций, вернется по умолчанию
+ mye12 = new MyElement12()
+ assert.equal(mye12.options.a, 'test')
+ assert.equal(mye12.options.b12.c12, 2)
+ assert.deepEqual(MyElement12.optionsDefaults, { a: 'test', b12: { c12: 2 } })
+
+ // Обновлениен опций
+ mye12 = new MyElement12({ a: 'update', b12: { c12: 3 } })
+ assert.equal(mye12.options.a, 'update')
+ assert.equal(mye12.options.b12.c12, 3)
+ assert.deepEqual(MyElement12.optionsDefaults, { a: 'test', b12: { c12: 2 } })
+ }
+
+ /** Опции могут являться массивом, содержимое массивов и вложенные объекты копируются */
+ ['Опции: массив']() {
+ const dOptions = ['defaults']
+ const options = ['ok']
+
+ /** Опции в виде массива */
+ class MyElement15 extends oom.extends(HTMLElement, dOptions) {
+
+ static tagName = 'my-element15'
+
+ }
+
+ oom.define(MyElement15)
+
+ const myE15 = new MyElement15(options)
+
+ dOptions.push('1')
+ options.push('2')
+
+ assert.deepEqual(myE15.options, ['ok'])
+ assert.deepEqual(MyElement15.optionsDefaults, ['defaults'])
+ assert.deepEqual(dOptions, ['defaults', '1'])
+ assert.deepEqual(options, ['ok', '2'])
+ }
+
+ /**
+ * Сложные объекты считаем уникальными и передаются по ссылке, например дата или пользовательские классы.
+ * При этом сложные объекты не подвергается заморозке, чтобы не нарушить их логику работы
+ */
+ ['Опции: сложные объекты']() {
+ /** Пользовательский класс опций */
+ class COptions { c16 = new Date() }
+
+ const dOptions = { a16: null, b16: new Date() }
+ const options = { a16: new COptions() }
+
+ /** Сложные объекты в опциях */
+ class MyElement16 extends oom.extends(HTMLElement, dOptions) {
+
+ static tagName = 'my-element16'
+
+ }
+
+ oom.define(MyElement16)
+
+ const myE16 = new MyElement16(options)
+
+ assert.ok(myE16.options.a16 === options.a16)
+ assert.ok(myE16.options.a16.c16 === options.a16.c16)
+ assert.ok(myE16.options.b16 === dOptions.b16)
+
+ myE16.options.a16.e16 = 1
+ // @ts-ignore
+ myE16.options.b16.e16 = 2
+
+ assert.ok(myE16.options.b16.getTime() > 0)
+ myE16.options.b16.setTime(0)
+
+ assert.equal(myE16.options.a16.e16, 1)
+ // @ts-ignore
+ assert.equal(myE16.options.b16.e16, 2)
+ assert.equal(myE16.options.b16.getTime(), 0)
+ }
+
+
+ /** Опции запрещено редактировать */
+ ['Опции: readonly']() {
+ const optionsDefaults = { b13: { c13: 2 }, d13: [1, 2, 3] }
+
+ /** объект + объект в объекте */
+ class MyElement13 extends oom.extends(HTMLElement, optionsDefaults) {
+
+ static tagName = 'my-element13'
+
+ }
+
+ oom.define(MyElement13)
+
+ let err
+ const mye13 = new MyElement13()
+
+ try {
+ // @ts-ignore
+ mye13.options = {}
+ } catch (error) { err = error }
+ assert.equal(err.message, "Cannot assign to read only property 'options' of object '#'")
+ try {
+ // @ts-ignore
+ mye13.options.a13 = {}
+ } catch (error) { err = error }
+ assert.equal(err.message, 'Cannot add property a13, object is not extensible')
+ try {
+ // @ts-ignore
+ mye13.options.b13.c13 = {}
+ } catch (error) { err = error }
+ assert.equal(err.message, "Cannot assign to read only property 'c13' of object '#'")
+ try {
+ // @ts-ignore
+ mye13.options.b13.e13 = {}
+ } catch (error) { err = error }
+ assert.equal(err.message, 'Cannot add property e13, object is not extensible')
+ try {
+ // @ts-ignore
+ mye13.options.d13.push(4)
+ } catch (error) { err = error }
+ assert.equal(err.message, 'Cannot add property 3, object is not extensible')
+ // Опции по умолчанию также неизменяемые
+ try {
+ // @ts-ignore
+ MyElement13.optionsDefaults = {}
+ } catch (error) { err = error }
+ assert.ok(err.message.startsWith("Cannot assign to read only property 'optionsDefaults' of function 'class MyElement13"))
+ try {
+ MyElement13.optionsDefaults.b13.c13 = 4
+ } catch (error) { err = error }
+ assert.equal(err.message, "Cannot assign to read only property 'c13' of object '#'")
+ try {
+ // @ts-ignore
+ MyElement13.optionsDefaults.b13.cd13 = {}
+ } catch (error) { err = error }
+ assert.equal(err.message, 'Cannot add property cd13, object is not extensible')
+ }
+
+ /**
+ * Объект передаваемый в качестве значения опций по умолчанию или опций остается неизменным,
+ * внутри компонента опции копируются, и внешний объект можно использовать повторно
+ */
+ ['Опции: копирование']() {
+ const dOptions = { a14: 1, b14: { c14: 1 } }
+ const options = { a14: 2, d14: [{ e14: 0 }, 2, 3] }
+
+ /** объект + объект в объекте */
+ class MyElement14 extends oom.extends(HTMLElement, dOptions) {
+
+ static tagName = 'my-element14'
+
+ }
+
+ oom.define(MyElement14)
+
+ const myE14 = new MyElement14(options)
+
+ dOptions.b14.c14 = 3
+ options.a14 = 4
+ options.d14.push(4)
+ // @ts-ignore
+ options.d14[0].e14 = -1
+
+ assert.deepEqual(myE14.options, { a14: 2, b14: { c14: 1 }, d14: [{ e14: 0 }, 2, 3] })
+ assert.deepEqual(dOptions, { a14: 1, b14: { c14: 3 } })
+ assert.deepEqual(options, { a14: 4, d14: [{ e14: -1 }, 2, 3, 4] })
+ }
+
+ /**
+ * При наследовании классов расширенных через oom, опции по умолчанию тоже расширяются
+ */
+ ['Опции: наследование optionsDefaults']() {
+ /** Базовый класс */
+ class MyElement17 extends oom.extends(HTMLElement, { a17: 1, c17: 1 }) { }
+
+ /** Класс наследник */
+ class MyElement18 extends oom.extends(MyElement17, { b18: 1, c17: 2 }) { }
+
+ assert.deepEqual(MyElement17.optionsDefaults, { a17: 1, c17: 1 })
+ assert.deepEqual(MyElement18.optionsDefaults, { a17: 1, b18: 1, c17: 2 })
+ }
+
+ /**
+ * При расширении через наследование опции компонента пробрасываются в конструктор родителя.
+ * Опции собираются в первом OOMCustomElement, и в родительский конструктор прокидываются только на чтение
+ */
+ ['Опции: доступ к опциям в parent и child']() {
+ /** Базовый класс */
+ class MyElement19 extends oom.extends(HTMLElement, { name: 'testName' }) {
+
+ static tagName = 'my-element19'
+
+ template = oom.div({ class: 'field' }, this.options.name)
+
+ }
+
+ /** Класс наследник */
+ class MyElement20 extends oom.extends(MyElement19, { label: 'testLabel' }) {
+
+ static tagName = 'my-element20'
+
+ template = oom.span({ class: 'label' },
+ oom.span(this.options.label, {
+ class: 'label__title'
+ }),
+ this.template)
+
+ }
+
+ oom.define(MyElement19, MyElement20)
+
+ const myE19 = new MyElement19()
+ const myE20 = new MyElement20()
+
+ document.body.innerHTML = ''
+ document.body.append(myE19)
+ document.body.append(myE20)
+ assert.equal(document.body.innerHTML, `
+
+ testName
+
+
+
+ testLabel
+ testName
+
+
+ `.replace(/\s*\n+\s+/g, ''))
+ document.body.innerHTML = ''
+
+ const myE20v2 = new MyElement20({ label: 'label2', name: 'name2' })
+
+ document.body.append(myE20v2)
+ assert.equal(document.body.innerHTML, `
+
+
+ label2
+ name2
+
+
+ `.replace(/\s*\n+\s+/g, ''))
+ document.body.innerHTML = ''
+ }
+
+ /**
+ * При передаче зацикленных полей в options
+ * необходимо вовремя выходить из рекурсии чтобы избежать падения с ошибкой.
+ * Цикличные ссылки в опциях заменяются на undefined без изменения исходного объекта
+ */
+ ['Опции: бесконечная рекурсия в resolveOptions']() {
+ const options = { test1: {} }
+
+ /** Бесконечная рекурсия */
+ class MyElement25 extends oom.extends(HTMLElement) {
+
+ static tagName = 'my-element25'
+
+ }
+
+ oom.define(MyElement25)
+
+ options.test2 = options
+ options.test1.test3 = options
+
+ const myE25 = new MyElement25(options)
+
+ assert.equal(myE25.options.test2, undefined)
+ assert.equal(myE25.options.test1.test3, undefined)
+
+ assert.equal(options.test2, options)
+ assert.equal(options.test1.test3, options)
+
+ delete options.test2
+ delete options.test1.test3
+ }
+
+ /**
+ * Стили компонентов определяются в статическом свойстве класса style,
+ * откуда при регистрации пользовательского элемента добавляются в документ в состав .
+ * Стили автоматически привязываются к имени тега элемента, что позволяет избежать пересечений имен классов
+ */
+ ['Глобальные стили компонента']() {
+ /** Компонент с глобальными стилями */
+ class MyElement21 extends oom.extends(HTMLElement) {
+
+ static tagName = 'my-element21'
+ static style = oom.style({
+ 'my-element21': { fontSize: '12px' },
+ 'my-element21.green': { color: 'green' },
+ '.small': { fontSize: '8px' }
+ })
+
+ template = oom.span('text', { class: 'small' })
+
+ }
+
+ document.head.innerHTML = ''
+ oom.define(MyElement21)
+ document.body.innerHTML = ''
+ document.body.append(new MyElement21())
+
+ assert.equal(document.getElementsByTagName('html')[0].outerHTML, `
+
+
+
+
+
+
+ text
+
+
+
+ `.replace(/\s*\n+\s+/g, ''))
+
+ document.head.innerHTML = ''
+ document.body.innerHTML = ''
+ }
+
+ /**
+ * Для пользовательского элемента можно задать собственный класс,
+ * который не будет стираться обновлением атрибута через шаблонизатор oom.
+ * Это дает возможность удобно навешивать на элементы обязательные классы из внешних библиотек
+ */
+ ['Класс элемента в static className']() {
+ /** Компонент с собственным именем класса */
+ class MyElement22 extends oom.extends(HTMLElement) {
+
+ static tagName = 'my-element22'
+ static className = 'my-class22'
+
+ }
+
+ oom.define(MyElement22)
+
+ const mye22 = new MyElement22()
+
+ assert.equal(mye22.outerHTML, `
+
+ `.replace(/\s*\n+\s+/g, ''))
+
+ oom(mye22, { class: 'test1' })
+ assert.equal(mye22.outerHTML, `
+
+ `.replace(/\s*\n+\s+/g, ''))
+
+ oom(mye22, { class: 'test2' })
+ assert.equal(mye22.outerHTML, `
+
+ `.replace(/\s*\n+\s+/g, ''))
+ }
+
+ /** Тест примера из в extends из types.d.ts */
+ ['types.d.ts - example for extends']() {
+ /**
+ * @typedef OptionsDefaults
+ * @property {string} [caption] Надпись на кнопке
+ */
+ /** @type {OptionsDefaults} */
+ const optionsDefaults = { caption: '' }
+
+ /** Тестовая кнопка */
+ class MyButton extends oom.extends(HTMLButtonElement, optionsDefaults) {
+
+ static tagName = 'my-butt'
+ static extendsTagName = 'button'
+
+ static style = oom.style({
+ 'button[is="my-butt"]': { fontSize: '12px' },
+ 'button[is="my-butt"].active': { color: 'yellow' },
+ '.my-butt__caption': { color: 'red' }
+ })
+
+ template = oom.span({ class: 'my-butt__caption' }, this.options.caption)
+
+ }
+
+ document.head.innerHTML = ''
+ oom.define(MyButton)
+ document.body.innerHTML = ''
+ document.body.append(new MyButton({ caption: 'Жми тут' }))
+ assert.equal(document.getElementsByTagName('html')[0].outerHTML, `
+
+
+
+
+
+
+ Жми тут
+
+
+
+ `.replace(/\s*\n+\s+/g, ''))
+
+ document.head.innerHTML = ''
+ document.body.innerHTML = ''
+ }
+
+}
diff --git a/test/5.attach-shadow.js b/test/5.attach-shadow.js
new file mode 100644
index 000000000..f3a603f7e
--- /dev/null
+++ b/test/5.attach-shadow.js
@@ -0,0 +1,203 @@
+// @ts-ignore
+import { assert, Test } from '@nodutilus/test'
+import { oom } from '@notml/core'
+
+const { document, HTMLElement } = window
+
+
+/** Проверка работы ShadowRoot в составе CustomElements */
+export default class AttachShadow extends Test {
+
+ /**
+ * Включение теневого дом выполняется свойством attachShadow на классе элемента.
+ * Может быть указано как boolean | ShadowRootInit
+ */
+ ['Подключение теневого DOM']() {
+ /** Открытый теневой DOM */
+ class MyShadow1 extends oom.extends(HTMLElement) {
+
+ static tagName = 'my-shadow1'
+ static attachShadow = true
+
+ template = oom.span('MyShadow1')
+
+ }
+
+ /** Закрытый теневой DOM */
+ class MyShadow2 extends oom.extends(HTMLElement) {
+
+ static tagName = 'my-shadow2'
+ static attachShadow = { mode: 'closed' }
+
+ template = oom.span('MyShadow2')
+
+ }
+
+ oom.define(MyShadow1, MyShadow2)
+
+ const myShadow1 = new MyShadow1()
+ const myShadow2 = new MyShadow2()
+
+ document.body.innerHTML = ''
+ document.body.append(myShadow1)
+ document.body.append(myShadow2)
+
+ assert.equal(document.body.innerHTML, ' ')
+ assert.equal(myShadow1.shadowRoot.constructor.name, 'ShadowRoot')
+ assert.equal(myShadow1.shadowRoot.innerHTML, 'MyShadow1 ')
+ assert.equal(myShadow2.shadowRoot, null)
+
+ document.body.innerHTML = ''
+ }
+
+ /**
+ * Стили компонентов при вставке в теневой DOM копируются,
+ * и вставляются перед первым использованием компонента.
+ * При удалении компонента стили остаются в корне shadowRoot, аналогично стилям в основном дереве,
+ * и при повторной вставке стили не дублируются.
+ */
+ ['Копирование Style в состав shadowRoot']() {
+ /** Компонент со стилями 1 */
+ class MySpan1 extends oom.extends(HTMLElement) {
+
+ static tagName = 'my-span1'
+ static style = oom.style({
+ '.my-span1_title': { background: 'red' }
+ })
+
+ template = oom.span({ class: '.my-span1_title' })
+
+ }
+
+ /** Компонент со стилями 2 */
+ class MySpan2 extends oom.extends(HTMLElement) {
+
+ static tagName = 'my-span2'
+ static style = oom.style({
+ '.my-span2_title': { background: 'green' }
+ })
+
+ template = oom.span({ class: '.my-span2_title' })
+
+ }
+
+ /** Теневой дом содержащий внутри компонент */
+ class MyShadow3 extends oom.extends(HTMLElement) {
+
+ static tagName = 'my-shadow3'
+ static attachShadow = true
+
+ template = oom()(new MySpan1(), new MySpan2())
+
+ }
+
+ oom.define(MySpan1, MySpan2, MyShadow3)
+
+ const myShadow3 = new MyShadow3()
+
+ document.body.innerHTML = ''
+ document.body.append(myShadow3)
+
+ assert.equal(document.documentElement.innerHTML, `
+
+
+
+
+
+
+
+ `.replace(/\s*\n+\s+/g, ''))
+ assert.equal(myShadow3.shadowRoot.innerHTML, `
+
+
+
+
+
+
+
+
+
+
+ `.replace(/\s*\n+\s+/g, ''))
+
+ myShadow3.shadowRoot.lastChild.remove()
+ myShadow3.shadowRoot.lastChild.remove()
+
+ assert.equal(myShadow3.shadowRoot.innerHTML, `
+
+
+
+
+ `.replace(/\s*\n+\s+/g, ''))
+
+ oom(myShadow3.shadowRoot, new MySpan1(), new MySpan2())
+
+ assert.equal(myShadow3.shadowRoot.innerHTML, `
+
+
+
+
+
+
+
+
+
+
+ `.replace(/\s*\n+\s+/g, ''))
+
+ document.head.innerHTML = ''
+ document.body.innerHTML = ''
+ }
+
+ /**
+ * При работе с теневым DOM для добавления элементов в DOM в функцию шаблон передается корень теневого DOM.
+ * В случае с закрытым теневым DOM это позволит получить к нему доступ из функции шаблона.
+ * this сам компонент, а root теневой DOM
+ */
+ ['Функция шаблон и теневой DOM']() {
+ /** Шаблон-функция теневого DOM */
+ class MyShadow4 extends oom.extends(HTMLElement) {
+
+ static tagName = 'my-shadow4'
+ static attachShadow = true
+
+ template = (/** @type {ShadowRoot} */ root) => {
+ oom(root, oom.span('test root'))
+ }
+
+ }
+
+ oom.define(MyShadow4)
+ document.body.innerHTML = ''
+
+ const myE26 = new MyShadow4()
+
+ document.body.append(myE26)
+
+ assert.equal(myE26.shadowRoot.innerHTML, `
+ test root
+ `.replace(/\s*\n+\s+/g, ''))
+
+ assert.equal(document.body.innerHTML, `
+
+ `.replace(/\s*\n+\s+/g, ''))
+ document.body.innerHTML = ''
+ }
+
+}
diff --git a/test/6.container-elements.js b/test/6.container-elements.js
new file mode 100644
index 000000000..44522593f
--- /dev/null
+++ b/test/6.container-elements.js
@@ -0,0 +1,212 @@
+// @ts-ignore
+import { assert, Test } from '@nodutilus/test'
+import { oom } from '@notml/core'
+
+const { document, HTMLElement } = window
+
+
+/** Проверка работы контейнерных элементов */
+export default class ContainerElements extends Test {
+
+ /**
+ * В простом варианте контейнер не содержит собственной верстки
+ */
+ ['Контейнер без собственной верстки']() {
+ /** Контейнер без верстки */
+ class MyContainer1 extends oom.extends(HTMLElement) {
+
+ static tagName = 'my-container1'
+
+ }
+
+ oom.define(MyContainer1)
+
+ document.body.innerHTML = ''
+
+ oom(document.body, oom.myContainer1(oom.span('test myContainer1')))
+
+ assert.equal(document.body.innerHTML, `
+
+ test myContainer1
+
+ `.replace(/\s*\n+\s+/g, ''))
+ document.body.innerHTML = ''
+ }
+
+ /**
+ * Компонент может иметь контентные опции, используемые при описание верстки шаблона
+ */
+ ['Контентные опции']() {
+ /** Контейнер с контентными опциями */
+ class MyContainer2 extends oom.extends(HTMLElement) {
+
+ static tagName = 'my-container2'
+
+ template = oom
+ .div(this.options.span1)
+ .div(this.options.span2)
+
+ }
+
+ oom.define(MyContainer2)
+
+ document.body.innerHTML = ''
+
+ oom(document.body, new MyContainer2({
+ span1: oom.span('test myContainer2 span1'),
+ span2: oom.span('test myContainer2 span2')
+ }))
+
+ assert.equal(document.body.innerHTML, `
+
+
+ test myContainer2 span1
+
+
+ test myContainer2 span2
+
+
+ `.replace(/\s*\n+\s+/g, ''))
+ document.body.innerHTML = ''
+ }
+
+ /**
+ * В сочетании с теневым DOM можно использовать заполнение контента через слоты
+ */
+ ['Слоты теневого DOM']() {
+ /** Контейнер со слотами */
+ class MyContainer3 extends oom.extends(HTMLElement) {
+
+ static tagName = 'my-container3'
+ static attachShadow = true
+
+ template = oom
+ .div(oom.slot({ name: 'title' }))
+ .div(oom.slot({ name: 'field' }))
+
+ }
+
+ oom.define(MyContainer3)
+
+ const myC3 = new MyContainer3()
+ const myTitle = oom.span('my-title', { slot: 'title' })
+ const myField = oom.span('my-field', { slot: 'field' })
+
+ document.body.innerHTML = ''
+
+ oom(document.body, oom(myC3, myTitle, myField))
+
+ /** @type {HTMLSlotElement} */
+ // @ts-ignore
+ const slotTitle = myC3.shadowRoot.firstChild.firstChild
+ /** @type {HTMLSlotElement} */
+ // @ts-ignore
+ const slotField = myC3.shadowRoot.lastChild.firstChild
+
+ assert.equal(document.body.innerHTML, `
+
+ my-title
+ my-field
+
+ `.replace(/\s*\n+\s+/g, ''))
+ assert.equal(myC3.shadowRoot.innerHTML, `
+
+
+
+
+
+
+ `.replace(/\s*\n+\s+/g, ''))
+ assert.equal(myTitle.dom, slotTitle.assignedElements()[0])
+ assert.equal(myField.dom, slotField.assignedElements()[0])
+
+ document.body.innerHTML = ''
+ }
+
+ /**
+ * Слот по умолчанию работает как и в базовой реализации теневого DOM
+ */
+ ['Слот по умолчанию (первый без имени)']() {
+ /** Контейнер со слотом по умолчанию */
+ class MyContainer4 extends oom.extends(HTMLElement) {
+
+ static tagName = 'my-container4'
+ static attachShadow = true
+
+ template = oom.div(oom.slot())
+
+ }
+
+ oom.define(MyContainer4)
+
+ const myC4 = new MyContainer4()
+ const mySpan = oom.span('my-span')
+
+ document.body.innerHTML = ''
+
+ oom(document.body, oom(myC4, mySpan))
+
+ /** @type {HTMLSlotElement} */
+ // @ts-ignore
+ const slot = myC4.shadowRoot.firstChild.firstChild
+
+ assert.equal(document.body.innerHTML, `
+
+ my-span
+
+ `.replace(/\s*\n+\s+/g, ''))
+ assert.equal(myC4.shadowRoot.innerHTML, `
+
+
+
+ `.replace(/\s*\n+\s+/g, ''))
+ assert.equal(mySpan.dom, slot.assignedElements()[0])
+
+ document.body.innerHTML = ''
+ }
+
+ /**
+ * При помощи теневого дом и шаблона функции можно создать компонент,
+ * который в рамках шаблона будет работать сразу с 2мя деревьями, основным и теневым.
+ * this сам компонент, а root теневой DOM
+ */
+ ['Функция шаблон, основной и теневой DOM']() {
+ /** Шаблон-функция теневого DOM */
+ class MyContainer5 extends oom.extends(HTMLElement) {
+
+ static tagName = 'my-container5'
+ static attachShadow = true
+
+ template = (/** @type {ShadowRoot} */ root) => {
+ oom(root, oom.slot())
+ oom(this, oom.span('test root'))
+ }
+
+ }
+
+ oom.define(MyContainer5)
+ document.body.innerHTML = ''
+
+ const myС5 = new MyContainer5()
+
+ document.body.append(myС5)
+
+ /** @type {HTMLSlotElement} */
+ // @ts-ignore
+ const slot = myС5.shadowRoot.firstChild
+
+ assert.equal(myС5.firstChild, slot.assignedElements()[0])
+
+ assert.equal(myС5.shadowRoot.innerHTML, `
+
+ `.replace(/\s*\n+\s+/g, ''))
+
+ assert.equal(document.body.innerHTML, `
+
+ test root
+
+ `.replace(/\s*\n+\s+/g, ''))
+ document.body.innerHTML = ''
+ }
+
+}
diff --git a/test/emulateDOM.js b/test/emulateDOM.js
new file mode 100644
index 000000000..0e57b3b62
--- /dev/null
+++ b/test/emulateDOM.js
@@ -0,0 +1,10 @@
+import jsdom from 'jsdom'
+
+const { JSDOM } = jsdom
+const { window } = new JSDOM('', {
+ url: 'https://github.com/nodutilus/notml',
+ runScripts: 'dangerously'
+})
+
+
+global.window = window
diff --git a/test/index.js b/test/index.js
new file mode 100644
index 000000000..04f119ada
--- /dev/null
+++ b/test/index.js
@@ -0,0 +1,29 @@
+import './emulateDOM.js'
+// @ts-ignore
+import { Test } from '@nodutilus/test'
+import BasicBehavior from './1.basic-behavior.js'
+import BasicAPI from './2.basic-api.js'
+import OOMStyle from './3.style.js'
+import CustomElements from './4.custom-elements.js'
+import AttachShadow from './5.attach-shadow.js'
+import ContainerElements from './6.container-elements.js'
+
+// import TestOOM from './oom.js'
+
+
+/** Общий тестовый класс */
+class TestNotMLCore extends Test {
+
+ static ['Базовое поведение'] = BasicBehavior
+ static ['Базовое API для OOM элементов'] = BasicAPI
+ static ['Генератор CSS in JS'] = OOMStyle
+ static ['Пользовательские элементы'] = CustomElements
+ static ['Теневой DOM'] = AttachShadow
+ static ['Контейнерные элементы'] = ContainerElements
+
+ // static TestOOM = TestOOM
+
+}
+
+
+Test.runOnCI(new TestNotMLCore())
diff --git a/test/mem.js b/test/mem.js
new file mode 100644
index 000000000..35ccc9889
--- /dev/null
+++ b/test/mem.js
@@ -0,0 +1,30 @@
+import '../test/emulateDOM.js'
+import { memoryUsage } from 'process'
+import { oom } from '../@notml/core/core.js'
+
+let curRSS = 0
+let maxRSS = 0
+
+for (let index = 0; index < 10000; index++) {
+ oom.div()
+}
+
+const initRSS = memoryUsage().rss
+const wait = () => { return new Promise(resolve => { setTimeout(resolve) }) }
+
+(async () => {
+ for (let index = 0; index < 10000; index++) {
+ for (let index = 0; index < 10000; index++) {
+ oom.div()
+ }
+
+ // node --expose_gc test/mem
+ global.gc()
+
+ await wait()
+ curRSS = memoryUsage().rss
+ curRSS = curRSS / initRSS * 100 ^ 0
+ maxRSS = Math.max(maxRSS, curRSS)
+ console.log(curRSS, '%', '/', maxRSS, '%')
+ }
+})()
diff --git a/test/pre-test.js b/test/pre-test.js
new file mode 100644
index 000000000..d6311397c
--- /dev/null
+++ b/test/pre-test.js
@@ -0,0 +1,6 @@
+import { mkdirSync, symlinkSync, existsSync } from 'fs'
+
+mkdirSync('test/node_modules', { recursive: true })
+if (!existsSync('test/node_modules/@notml')) {
+ symlinkSync('../../@notml', 'test/node_modules/@notml', 'dir')
+}
diff --git a/tsconfig.json b/tsconfig.json
new file mode 100644
index 000000000..3d193262f
--- /dev/null
+++ b/tsconfig.json
@@ -0,0 +1,8 @@
+{
+ "extends": "@nodutilus/project-config/tsconfig",
+ "exclude": [
+ "_deprecated/**",
+ "coverage/**",
+ "@notml/notml/**"
+ ]
+}
diff --git a/webtest/compatible-min.html b/webtest/compatible-min.html
new file mode 100644
index 000000000..3b1b75a55
--- /dev/null
+++ b/webtest/compatible-min.html
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+ Compatible NotML Core (min)
+
+
+
+ NotML is compatible (min)
+
+
diff --git a/webtest/compatible.html b/webtest/compatible.html
new file mode 100644
index 000000000..1e75e6f10
--- /dev/null
+++ b/webtest/compatible.html
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+ Compatible NotML Core
+
+
+
+ NotML is compatible
+
+
diff --git a/webtest/core-from-cdn-min.html b/webtest/core-from-cdn-min.html
new file mode 100644
index 000000000..5f23b317b
--- /dev/null
+++ b/webtest/core-from-cdn-min.html
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+ NotML Core - from CDN (All-in-one - minimized)
+
+
+
+
+
+
+
diff --git a/webtest/core-from-cdn-min.js b/webtest/core-from-cdn-min.js
new file mode 100644
index 000000000..10fd9a073
--- /dev/null
+++ b/webtest/core-from-cdn-min.js
@@ -0,0 +1,12 @@
+import { oom } from 'https://cdn.jsdelivr.net/npm/notml@latest/core.min.js'
+
+const style = oom
+ .style({
+ '.test': { background: 'darkorange' },
+ '.label': { color: 'darkgreen' }
+ })
+
+oom(document.head, style)
+oom(document.body, oom
+ .div({ class: 'test' }, oom
+ .span('NotML Core - from CDN (All-in-one - minimized)', { class: 'label' })))
diff --git a/webtest/core-from-cdn-src.html b/webtest/core-from-cdn-src.html
new file mode 100644
index 000000000..9d173ca8c
--- /dev/null
+++ b/webtest/core-from-cdn-src.html
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+ NotML Core - from CDN (Source)
+
+
+
+
+
+
+
diff --git a/webtest/core-from-cdn-src.js b/webtest/core-from-cdn-src.js
new file mode 100644
index 000000000..40b6d8d31
--- /dev/null
+++ b/webtest/core-from-cdn-src.js
@@ -0,0 +1,11 @@
+import { oom } from 'https://cdn.jsdelivr.net/npm/@notml/core@latest/core.js'
+
+
+oom(document.head, oom
+ .style({
+ '.test': { background: 'darkorange' },
+ '.label': { color: 'darkgreen' }
+ }))
+oom(document.body, oom
+ .div({ class: 'test' }, oom
+ .span('NotML Core - from CDN (Source)', { class: 'label' })))
diff --git a/webtest/core-from-cdn.html b/webtest/core-from-cdn.html
new file mode 100644
index 000000000..02115f510
--- /dev/null
+++ b/webtest/core-from-cdn.html
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+ NotML Core - from CDN (All-in-one)
+
+
+
+
+
+
+
diff --git a/webtest/core-from-cdn.js b/webtest/core-from-cdn.js
new file mode 100644
index 000000000..a9e600d0d
--- /dev/null
+++ b/webtest/core-from-cdn.js
@@ -0,0 +1,11 @@
+import { oom } from 'https://cdn.jsdelivr.net/npm/notml@latest/core.js'
+
+
+oom(document.head, oom
+ .style({
+ '.test': { background: 'darkorange' },
+ '.label': { color: 'darkgreen' }
+ }))
+oom(document.body, oom
+ .div({ class: 'test' }, oom
+ .span('NotML Core - from CDN (All-in-one)', { class: 'label' })))
diff --git a/webtest/core-min.html b/webtest/core-min.html
new file mode 100644
index 000000000..0073ecbc8
--- /dev/null
+++ b/webtest/core-min.html
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+ NotML Core - CDN lib (min)
+
+
+
+
+
+
+
diff --git a/webtest/core-min.js b/webtest/core-min.js
new file mode 100644
index 000000000..f993a9698
--- /dev/null
+++ b/webtest/core-min.js
@@ -0,0 +1,10 @@
+import { oom } from '../@notml/notml/core.min.js'
+
+oom(document.head, oom
+ .style({
+ '.test': { background: 'darkorange' },
+ '.label': { color: 'darkgreen' }
+ }))
+oom(document.body, oom
+ .div({ class: 'test' }, oom
+ .span('NotML Core - CDN lib (min)', { class: 'label' })))
diff --git a/webtest/core.html b/webtest/core.html
new file mode 100644
index 000000000..501d87045
--- /dev/null
+++ b/webtest/core.html
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+ NotML Core - CDN lib
+
+
+
+
+
+
+
diff --git a/webtest/core.js b/webtest/core.js
new file mode 100644
index 000000000..aef0d0f03
--- /dev/null
+++ b/webtest/core.js
@@ -0,0 +1,10 @@
+import { oom } from '../@notml/notml/core.js'
+
+oom(document.head, oom
+ .style({
+ '.test': { background: 'darkorange' },
+ '.label': { color: 'darkgreen' }
+ }))
+oom(document.body, oom
+ .div({ class: 'test' }, oom
+ .span('NotML Core - CDN lib', { class: 'label' })))
diff --git a/webtest/css-custom-properties.html b/webtest/css-custom-properties.html
new file mode 100644
index 000000000..23a3d2224
--- /dev/null
+++ b/webtest/css-custom-properties.html
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+ NotML Core - CSS Custom Properties
+
+
+
+
+
+
+
diff --git a/webtest/css-custom-properties.js b/webtest/css-custom-properties.js
new file mode 100644
index 000000000..be2a1cc4c
--- /dev/null
+++ b/webtest/css-custom-properties.js
@@ -0,0 +1,20 @@
+import { oom } from '../@notml/core/core.js'
+
+const style = oom.style({
+ ':root': {
+ '--bg-color': 'red',
+ '--lbl-color': 'darkgreen'
+ },
+ '.test': {
+ background: 'var(--bg-color)'
+ },
+ '.label': { color: 'var(--lbl-color)' }
+})
+
+
+style({ ':root': { '--bg-color': 'darkorange' } })
+
+oom(document.head, style)
+oom(document.body, oom
+ .div({ class: 'test' }, oom
+ .span('NotML Core - CSS Custom Properties', { class: 'label' })))
diff --git a/webtest/custom-elements-min.html b/webtest/custom-elements-min.html
new file mode 100644
index 000000000..69996a041
--- /dev/null
+++ b/webtest/custom-elements-min.html
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+ NotML Core - ContainerElements (min)
+
+
+
+
+
+
+
diff --git a/webtest/custom-elements-min.js b/webtest/custom-elements-min.js
new file mode 100644
index 000000000..b0b7f480e
--- /dev/null
+++ b/webtest/custom-elements-min.js
@@ -0,0 +1,18 @@
+import { oom } from '../@notml/notml/core.min.js'
+
+
+const { HTMLElement, document } = window
+
+
+/** Тестовый класс со стилями */
+class MySpan extends oom.extends(HTMLElement) {
+
+ static tagName = 'my-span'
+ static style = oom.style({ '.my_span': { background: 'darkorange' } })
+
+ template = oom.span('darkorange', { class: 'my_span' })
+
+}
+
+oom.define(MySpan)
+oom(document.body, new MySpan())
diff --git a/webtest/iframe-isolation.html b/webtest/iframe-isolation.html
new file mode 100644
index 000000000..74dab1b66
--- /dev/null
+++ b/webtest/iframe-isolation.html
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+ NotML Core - iframe isolation
+
+
+
+
+
+
+
+
+
diff --git a/webtest/iframe-isolation.js b/webtest/iframe-isolation.js
new file mode 100644
index 000000000..21df7ad14
--- /dev/null
+++ b/webtest/iframe-isolation.js
@@ -0,0 +1,24 @@
+import { oom } from '../@notml/core/core.js'
+
+const { document } = window
+/** @type {HTMLIFrameElement} */
+// @ts-ignore
+const frame = document.getElementById('iframe')
+
+if (frame) {
+ const fDoc = frame.contentDocument
+ const script = fDoc.createElement('script')
+
+ script.type = 'module'
+ script.src = '/webtest/iframe-isolation.js'
+ fDoc.documentElement.appendChild(script)
+}
+
+oom(document.head, oom
+ .style({
+ '.test': { background: frame ? 'darkorange' : 'darkgreen' },
+ '.label': { color: frame ? 'darkgreen' : 'darkorange' }
+ }))
+oom(document.body, oom
+ .div({ class: 'test' }, oom
+ .span('NotML Core - CDN lib', { class: 'label' })))
diff --git a/webtest/index.js b/webtest/index.js
new file mode 100644
index 000000000..513ee6099
--- /dev/null
+++ b/webtest/index.js
@@ -0,0 +1,18 @@
+import Fastify from 'fastify'
+import FastifyStatic from 'fastify-static'
+import { resolve } from 'path'
+
+const fastify = Fastify({ logger: true })
+const dir = resolve('webtest')
+
+
+fastify
+ .register(FastifyStatic, { root: resolve('.'), prefix: '/' }) // @ts-ignore
+ .get('/', (req, reply) => { reply.sendFile('webtest.html', dir) })
+ .listen({ host: '0.0.0.0', port: 3000 }, (err, address) => {
+ if (err) {
+ fastify.log.error(err)
+ process.exit(1)
+ }
+ fastify.log.info(`server listening on ${address}`)
+ })
diff --git a/webtest/shadow-root.html b/webtest/shadow-root.html
new file mode 100644
index 000000000..9516c87f7
--- /dev/null
+++ b/webtest/shadow-root.html
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+ NotML Core - ShadowRoot
+
+
+
+
+
+
+
+
diff --git a/webtest/shadow-root.js b/webtest/shadow-root.js
new file mode 100644
index 000000000..24be914a9
--- /dev/null
+++ b/webtest/shadow-root.js
@@ -0,0 +1,59 @@
+import { oom } from '../@notml/core/core.js'
+
+const { HTMLElement, document } = window
+
+
+/** Тестовый класс со стилями */
+class MySpan extends oom.extends(HTMLElement) {
+
+ static tagName = 'my-span'
+ static style = oom.style({ '.my_shadow': { background: 'darkorange' } })
+
+ template = oom.span('test1', { class: 'my_shadow' })
+
+}
+
+
+/** Тестовый класс со стилями */
+class MySpan2 extends oom.extends(HTMLElement) {
+
+ static tagName = 'my-span2'
+ static style = oom.style({ '.my_shadow2': { background: 'gold' } })
+
+ template = oom.span('test2', { class: 'my_shadow2' })
+
+}
+
+
+/** Тестовый элемент с теневым DOM */
+class MyShadowRoot extends oom.extends(HTMLElement) {
+
+ static tagName = 'my-shadow-root'
+ static attachShadow = true
+
+ template = oom()(new MySpan(), new MySpan(), new MySpan2(), new MySpan2())
+
+}
+
+/** Тестовый элемент с теневым DOM */
+class MyShadowRootClosed extends oom.extends(HTMLElement) {
+
+ static tagName = 'my-shadow-root-closed'
+ static attachShadow = { mode: 'closed' }
+
+ template = oom()(new MySpan(), new MySpan(), new MySpan2(), new MySpan2())
+
+}
+
+
+oom.define(MySpan, MySpan2, MyShadowRoot, MyShadowRootClosed)
+
+
+oom(document.head, oom.style({
+ 'my-span .my_shadow': { color: 'white' },
+ 'my-span2 .my_shadow2': { color: 'white' }
+}))
+
+oom(document.body, oom()(new MySpan(), new MySpan2())
+ .br()(new MyShadowRoot())
+ .br()(new MyShadowRootClosed()))
diff --git a/webtest/types.d.ts b/webtest/types.d.ts
new file mode 100644
index 000000000..789f3b6b8
--- /dev/null
+++ b/webtest/types.d.ts
@@ -0,0 +1,13 @@
+declare module 'https://cdn.jsdelivr.net/npm/@notml/core@latest/core.js' {
+ export { oom } from '@notml/core'
+}
+
+declare module 'https://cdn.jsdelivr.net/npm/notml@latest/core.js' {
+ export { oom } from '@notml/core'
+}
+
+declare module 'https://cdn.jsdelivr.net/npm/notml@latest/core.min.js' {
+ export { oom } from '@notml/core'
+}
+
+
diff --git a/webtest/webtest.html b/webtest/webtest.html
new file mode 100644
index 000000000..fa262dc09
--- /dev/null
+++ b/webtest/webtest.html
@@ -0,0 +1,68 @@
+
+
+
+
+
+
+ NotML Core
+
+
+
+
+
+
+
+
+ Compatible NotML Core
+ Compatible NotML Core (min)
+ NotML Core - CDN lib
+ NotML Core - CDN lib (min)
+ NotML Core - from CDN (All-in-one)
+ NotML Core - from CDN (All-in-one - minimized)
+ NotML Core - from CDN (Source)
+ NotML Core - iframe isolation
+ NotML Core - ShadowRoot
+ NotML Core - ContainerElements (min)
+ NotML Core - CSS Custom Properties
+
+
+
+
Example #1 - базовая верстка
+
+
+
+
+
Example #2 - переиспользование и копирование
+
+
+
+
+
Example #3 - простой кастомный компонент
+
+
+
+
+
Example #4 - генерация style is="oom-style"
+
+
+
+
+
Example #5 - стилизация CustomElement через static style
+
+
+
+
+
+
diff --git a/webtest/webtest.js b/webtest/webtest.js
new file mode 100644
index 000000000..fbe2e7869
--- /dev/null
+++ b/webtest/webtest.js
@@ -0,0 +1,153 @@
+import { oom } from '../@notml/core/core.js'
+
+const { document } = window
+
+
+/**
+ * @param {string} name Имя теста
+ * @param {any} actual Актуальное значение для проверки
+ * @param {any} expected Фактическое значение для проверки
+ */
+function assertEqual(name, actual, expected) {
+ if (actual !== expected) {
+ console.log(name, ' -> failed')
+ console.log('actual =>', actual)
+ console.log('expected =>', expected)
+ console.error(new Error('actual !== expected'))
+ } else {
+ console.log(name, ' -> ok')
+ }
+}
+
+console.time('webtest')
+
+
+// Example #1 - базовая верстка
+const exp1 = document.getElementById('exp1')
+const div1 = oom('div', oom
+ .div({ class: 'header' })
+ .div({ class: 'test' }, oom
+ .span('Name: ', { class: 'test-label' })
+ .span('Test', { class: 'test-name' }))
+ .div({ class: 'footer' }))
+const html1 = div1.html
+
+exp1.append(div1.dom)
+assertEqual('Example #1-1', html1, exp1.innerHTML)
+assertEqual('Example #1-2', exp1.innerHTML,
+ '' +
+ '' +
+ '
' +
+ 'Name: ' +
+ 'Test ' +
+ '
' +
+ '' +
+ '
')
+
+
+// Example #2 - переиспользование и копирование
+const exp2 = document.getElementById('exp2')
+const header2 = oom('div', { class: 'header' }, oom
+ .span('Test Header'))
+const block2 = oom()
+
+block2(oom
+ .div(oom()
+ .append(header2.clone())
+ .div('div 1')))
+block2(oom
+ .div(oom()
+ .append(header2.clone())
+ .div('div 2')))
+
+const html2 = block2.html
+
+exp2.append(block2.dom)
+assertEqual('Example #2-1', html2, exp2.innerHTML)
+assertEqual('Example #2-2', exp2.innerHTML,
+ '' +
+ '' +
+ '
div 1
' +
+ '
' +
+ '' +
+ '' +
+ '
div 2
' +
+ '
')
+
+
+// Example #3 - простой кастомный компонент
+/** Test custom element */
+class MyElementExp3 extends oom.extends(HTMLElement) {
+
+ static tagName = 'my-element-exp3'
+
+ mySpan = oom.span('My element new text')
+
+ template = oom('div', { class: 'MyElement__inner' })
+ .append(this.mySpan.clone())
+ .append(oom('br'))
+ .append(this.mySpan)
+
+}
+
+oom.define(MyElementExp3)
+
+const exp3 = document.getElementById('exp3')
+const block3 = oom.myElementExp3()
+const html3 = block3.html
+
+exp3.append(block3.dom)
+assertEqual('Example #3-1', html3, ' ')
+assertEqual('Example #3-2', exp3.innerHTML,
+ '' +
+ '' +
+ 'My element new text ' +
+ ' ' +
+ 'My element new text ' +
+ '
' +
+ ' ')
+
+
+// Example #4 - генерация style is="oom-style"
+const exp4 = oom(document.getElementById('exp4'), oom
+ .style({ '.exp4__label': { color: 'darkgreen' } })
+ .span('exp4__label', { class: 'exp4__label' }))
+
+assertEqual('Example #4-1', exp4.html, `
+
+
+ exp4__label
+
+`.replace(/\s*\n+\s*/g, ''))
+
+
+// Example #5 - стилизация CustomElement через static style
+/** Тестовая кнопка */
+class MyButton extends oom.extends(HTMLButtonElement, { caption: '' }) {
+
+ static tagName = 'my-butt'
+ static extendsTagName = 'button'
+
+ static style = oom.style({ '.my-butt__caption': { color: 'red' } })
+
+ template = oom.span({ class: 'my-butt__caption' }, this.options.caption)
+
+}
+
+oom.define(MyButton)
+
+const exp5 = oom(document.getElementById('exp5'),
+ oom(new MyButton({ caption: 'Жми тут' })))
+
+assertEqual('Example #5-1', exp5.html, `
+
+
+ Жми тут
+
+
+`.replace(/\s*\n+\s*/g, ''))
+
+
+console.timeEnd('webtest')