Repository: edwinm/carbonium Branch: master Commit: e6fa92369015 Files: 22 Total size: 47.2 KB Directory structure: gitextract_aeet774x/ ├── .github/ │ └── workflows/ │ ├── codeql.yml │ ├── coveralls.yml │ └── scorecard.yml ├── .gitignore ├── .husky/ │ └── pre-commit ├── .prettierignore ├── .prettierrc.json ├── LICENSE ├── README.md ├── demo/ │ ├── demo.js │ └── index.html ├── dist/ │ ├── bundle.js │ ├── carbonium.d.ts │ └── src/ │ └── carbonium.d.ts ├── eslint.config.js ├── package.json ├── playwright.config.ts ├── rollup.config.js ├── src/ │ └── carbonium.ts ├── test/ │ └── test.ts ├── tests/ │ └── carbonium.spec.ts └── tsconfig.json ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/workflows/codeql.yml ================================================ name: "CodeQL" on: push: branches: ["master"] pull_request: branches: ["master"] schedule: - cron: "11 3 * * 3" jobs: analyze: name: Analyze runs-on: ubuntu-latest permissions: actions: read contents: read security-events: write strategy: fail-fast: false matrix: language: [javascript] steps: - name: Checkout uses: actions/checkout@v4 - name: Initialize CodeQL uses: github/codeql-action/init@v3 with: languages: ${{ matrix.language }} queries: +security-and-quality - name: Autobuild uses: github/codeql-action/autobuild@v3 - name: Perform CodeQL Analysis uses: github/codeql-action/analyze@v3 with: category: "/language:${{ matrix.language }}" ================================================ FILE: .github/workflows/coveralls.yml ================================================ name: Coveralls on: ["push", "pull_request"] jobs: test: name: Run units tests runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Setup Node.js uses: actions/setup-node@v4 with: node-version: "22" - name: Install run: npm install - name: Install Playwright browsers run: npx playwright install --with-deps chromium firefox - name: Lint run: npm run lint - name: Test run: npm test ================================================ FILE: .github/workflows/scorecard.yml ================================================ # This workflow uses actions that are not certified by GitHub. They are provided # by a third-party and are governed by separate terms of service, privacy # policy, and support documentation. name: Scorecard supply-chain security on: # For Branch-Protection check. Only the default branch is supported. See # https://github.com/ossf/scorecard/blob/main/docs/checks.md#branch-protection branch_protection_rule: # To guarantee Maintained check is occasionally updated. See # https://github.com/ossf/scorecard/blob/main/docs/checks.md#maintained schedule: - cron: "38 20 * * 1" push: branches: ["master"] # Declare default permissions as read only. permissions: contents: read actions: read jobs: analysis: name: Scorecard analysis runs-on: ubuntu-latest permissions: # Needed to upload the results to code-scanning dashboard. security-events: write # Needed to publish results and get a badge (see publish_results below). id-token: write contents: read actions: read steps: - name: "Checkout code" uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd with: persist-credentials: false - name: "Run analysis" uses: ossf/scorecard-action@0864cf19026789058feabb7e87baa5f140aac736 # v2.3.1 with: results_file: results.sarif results_format: sarif repo_token: ${{ secrets.GITHUB_TOKEN }} # Public repositories: # - Publish results to OpenSSF REST API for easy access by consumers # - Allows the repository to include the Scorecard badge. # - See https://github.com/ossf/scorecard-action#publishing-results. # For private repositories: # - `publish_results` will always be set to `false`, regardless # of the value entered here. publish_results: true # Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF # format to the repository Actions tab. - name: "Upload artifact" uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f with: name: sarif-results path: results.sarif retention-days: 5 # Upload the results to GitHub's code scanning dashboard (optional). # Commenting out will disable upload of results to your repo's Code Scanning dashboard - name: "Upload to code-scanning" uses: github/codeql-action/upload-sarif@v3 with: sarif_file: results.sarif ================================================ FILE: .gitignore ================================================ node_modules coverage .idea /playwright-report/ ================================================ FILE: .husky/pre-commit ================================================ npx pretty-quick --staged npm run lint ================================================ FILE: .prettierignore ================================================ dist ================================================ FILE: .prettierrc.json ================================================ { "trailingComma": "es5", "arrowParens": "always" } ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2020 Edwin Martin Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ [![Scorecard supply-chain security](https://github.com/edwinm/carbonium/actions/workflows/scorecard.yml/badge.svg)](https://github.com/edwinm/carbonium/actions/workflows/scorecard.yml) [![CodeQL](https://github.com/edwinm/carbonium/actions/workflows/codeql.yml/badge.svg)](https://github.com/edwinm/carbonium/actions/workflows/codeql.yml) [![Coverage Status](https://coveralls.io/repos/github/edwinm/carbonium/badge.svg?branch=master)](https://coveralls.io/github/edwinm/carbonium?branch=master) [![Socket Badge](https://socket.dev/api/badge/npm/package/carbonium)](https://socket.dev/npm/package/carbonium) [![CodeFactor](https://www.codefactor.io/repository/github/edwinm/carbonium/badge)](https://www.codefactor.io/repository/github/edwinm/carbonium) [![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=edwinm_carbonium&metric=alert_status)](https://sonarcloud.io/summary/new_code?id=edwinm_carbonium) [![Snyk test results](https://snyk.io/test/github/edwinm/carbonium/badge.svg)](https://snyk.io/test/github/edwinm/carbonium) [![Size](https://badgen.net/bundlephobia/minzip/carbonium)](https://bundlephobia.com/package/carbonium) [![npm version](https://badge.fury.io/js/carbonium.svg)](https://www.npmjs.com/package/carbonium) [![GitHub](https://img.shields.io/github/license/edwinm/carbonium.svg)](https://github.com/edwinm/carbonium/blob/master/LICENSE) ![CodeRabbit Pull Request Reviews](https://img.shields.io/coderabbit/prs/github/edwinm/carbonium?utm_source=oss&utm_medium=github&utm_campaign=edwinm%2Fcarbonium&labelColor=171717&color=FF570A&link=https%3A%2F%2Fcoderabbit.ai&label=CodeRabbit+Reviews) [![Carbonium](https://raw.githubusercontent.com/edwinm/carbonium/master/assets/carbonium.svg)](#readme) > One kilobyte library for easy DOM manipulation With carbonium, you can call `$(selector)` and the result can be accessed as both an DOM element and an array of matched elements. DOM element operations are applied to all matched elements. ## Examples To set the `left` CSS property of all elements with the class `indent` to 40 pixels: ```javascript $(".indent").style.left = "40px"; ``` To add the class `important` to all div's with "priority" as content: ```javascript $("div") .filter((el) => el.textContent == "priority") .classList.add("important"); ``` You can use carbonium to create elements: ```javascript const error = $("
An error has occured!
")[0]; ``` ## Installation ```bash npm install --save-dev carbonium ``` Now you can import carbonium: ```javascript import { $ } from "carbonium"; ``` If you don't want to install or use a bundler like webpack or rollup.js, you can import carbonium like this: ```javascript const { $ } = await import( "https://cdn.jsdelivr.net/npm/carbonium@1/dist/bundle.min.js" ); ``` ## API ### Select elements ### `$(selector [, parentNode])` #### Parameters | Name | Type | Description | | ---------- | ------------------------------ | -------------------------------------------------------------------------- | | selector | string | Selector to select elements | | parentNode | Document \| Element (optional) | Document or element in which to apply the selector, defaults to `document` | #### Returns An array of the matched elements, which can also be accessed as a single element. ### Create element ### `$(html [, parentNode])` #### Parameters | Name | Type | Description | | ---------- | ------------------------------ | -------------------------------------------------------------------------- | | html | string | HTML of element to create, starting with "<" | | parentNode | Document \| Element (optional) | Document or element in which to apply the selector, defaults to `document` | #### Returns An array with one created element. ## TypeScript If you use TypeScript, it's good to know Carbonium is written in TypeScript and provides all typings. You can use generics to declare a specific type of element, for example `HTMLInputElement` to make the `disabled` property available: ```typescript $("input, select, button").disabled = true; ``` ## Why? You might find most frameworks are quite bulky and bad for performance ([1](https://css-tricks.com/radeventlistener-a-tale-of-client-side-framework-performance/)). On the other side, you might find using native DOM and writing `document.querySelectorAll(selector)` each time you want to do some DOM operations to become tedious. You can write your own helper function, but that only takes part of the pain away. Carbonium seeks to find the sweet spot between using a framework and using the native DOM. ## jQuery Isn't this just jQuery and isn't that obsolete and bad practice? No. Carbonium doesn't have the disadvantages of jQuery: 1. Carbonium is very small: just around one kilobyte. 2. There's no new API to learn, carbonium provides only standard DOM API's. ## Browser support Carbonium is supported by all modern browsers. It is tested to work on desktop and mobile with Firefox 79, Chrome 84, Safari 13 and Edge 84. It should work with all browsers supporting Proxy, see [Can I use Proxy](https://caniuse.com/#feat=proxy) for support tables. ## Name [Photo of diamond and graphite](https://commons.wikimedia.org/wiki/File:Diamond_and_graphite_without_structures.jpg) Carbonium is the Latin name for carbon. Carbon has two forms (allotropes): graphite and diamond. Just like this library, in which the result presents itself both as one element and as a list of elements. [Photo CC BY-SA 3.0](https://commons.wikimedia.org/wiki/File:Diamond_and_graphite_without_structures.jpg) ## License Copyright 2023 [Edwin Martin](https://bitstorm.org/) and released under the MIT license. ================================================ FILE: demo/demo.js ================================================ const importPromise = import( "https://cdn.jsdelivr.net/npm/carbonium/dist/bundle.min.js" ); const loadPromise = new Promise((resolve) => { document.addEventListener("DOMContentLoaded", resolve); }); // Start code when both carbonium and the page are loaded Promise.all([importPromise, loadPromise]).then(([{ $ }]) => { $("#out").innerText = "Demo."; $("#hello-button").addEventListener("click", () => { $("#out").innerText = "Hello. It is working!"; }); }); ================================================ FILE: demo/index.html ================================================ Demo

Demo

================================================ FILE: dist/bundle.js ================================================ /** Carbonium 1.3.0 @copyright 2020 Edwin Martin @license MIT */ function $(selectors, parentNode) { let nodelist; // If the first parameter starts with "<", create a DOM node if (selectors.startsWith("<")) { nodelist = [ new DOMParser().parseFromString(selectors, "text/html").body.firstChild, ]; } else { // Else, do querySelectorAll nodelist = (parentNode || document).querySelectorAll(selectors); } // Wrap it in a Proxy return new Proxy(nodelist, proxyHandler); } // Used by style, classList and relList // When setting one of these, remember the elements to apply to let currentListNodelist; let propList; const proxyHandler = { get(target, prop) { let propValue = null; // Return iterator when asked for iterator, only used in ES2015+ if (prop == Symbol.iterator) { return function* () { for (const element of target) { yield element; } }; } // Special case for style, classList and relList if (prop == "style" || prop == "classList" || prop == "relList") { currentListNodelist = target; propList = prop; // Matched elements can be a list of any element or an empty list // Use getter of, for example, document.body.style const propValue = Reflect.get(document.body, prop); return new Proxy(propValue, proxyHandler); } // style.setProperty, getPropertyValue…, classList.add, contains, remove…, relList… if (target instanceof CSSStyleDeclaration || target instanceof DOMTokenList) { // Matched elements can be a list of any element or an empty list // Use getter of, for example, document.body.style.color propValue = Reflect.get(document.body[propList], prop); // When getter is a function, apply it to all matched elements if (typeof propValue == "function") { return new Proxy(propValue, { apply: function (target, thisArg, argumentsList) { currentListNodelist.forEach((el) => { Reflect.apply(target, el[propList], argumentsList); }); return new Proxy(currentListNodelist, proxyHandler); }, }); } else { return propValue; } } // Are we dealing with an Array function like forEach, map and filter? if (Array.prototype.hasOwnProperty(prop)) { const propValue = Reflect.get(Array.prototype, prop); if (typeof propValue == "function") { return new Proxy(propValue, { apply: function (target, thisArg, argumentsList) { const ret = Reflect.apply(target, thisArg, argumentsList); // When function returns undefined (like forEach), // return all matched elements, so calls can be chained // For example forEach(…).setAttribute(…) const newTarget = typeof ret != "undefined" ? ret : thisArg; return new Proxy(newTarget, proxyHandler); }, }); } } // Get property or call function on DOM elements if (target.length > 0) { // Might be DOM element specific, like input.select(), // so use first array element to get reference if (prop in target[0]) { propValue = Reflect.get(target[0], prop); } } else { // Empty list, targeted DOM element unknown, // use getter of document.body if (prop in document.body) { propValue = Reflect.get(document.body, prop); } } // Propagate DOM prop value if (propValue) { if (typeof propValue == "function") { return new Proxy(propValue, { apply: function (target, thisArg, argumentsList) { let retFirst = null; let first = true; // Apply on individual elements for (const el of thisArg) { const ret = Reflect.apply(target, el, argumentsList); if (first) { retFirst = ret; first = false; } } return retFirst !== null && retFirst !== void 0 ? retFirst : thisArg; }, }); } else { return propValue; } } // Default return Reflect.get(target, prop); }, // DOM property is set set(target, prop, value) { if ("forEach" in target && !(target instanceof CSSStyleDeclaration)) { target.forEach((el) => { Reflect.set(el, prop, value); }); } else { Reflect.set(target, prop, value); } return true; }, deleteProperty(target, prop) { if (prop in target) { return delete target[prop]; } return false; }, }; export { $ }; //# sourceMappingURL=bundle.js.map ================================================ FILE: dist/carbonium.d.ts ================================================ /** Carbonium __buildVersion__ @copyright 2020 Edwin Martin @license MIT */ export declare function $(selectors: string, parentNode?: Document | ShadowRoot | HTMLElement): CarboniumType; export type CarboniumType = CarboniumList & T; interface CarboniumList extends Array { concat(...items: ConcatArray[]): CarboniumType; concat(...items: (T | ConcatArray)[]): CarboniumType; reverse(): CarboniumType; slice(start?: number, end?: number): CarboniumType; splice(start: number, deleteCount?: number): CarboniumType; splice(start: number, deleteCount: number, ...items: T[]): CarboniumType; forEach(callbackfn: (value: T, index: number, array: T[]) => void, thisArg?: any): CarboniumType; filter(callbackfn: (value: T, index: number, array: T[]) => boolean, thisArg?: any): CarboniumType; setAttribute(qualifiedName: string, value: string): CarboniumType; classList: CarboniumClassList; style: CarboniumStyleList; } interface CarboniumClassList extends DOMTokenList { add(...tokens: string[]): CarboniumType; remove(...tokens: string[]): CarboniumType; replace(oldToken: string, newToken: string): boolean; forEach(callbackfn: (value: string, key: number, parent: DOMTokenList) => void, thisArg?: any): CarboniumType; } interface CarboniumStyleList extends CSSStyleDeclaration { removeProperty(property: string): CarboniumList & string; setProperty(property: string, value: string | null, priority?: string): CarboniumType; } export {}; ================================================ FILE: dist/src/carbonium.d.ts ================================================ /** Carbonium __buildVersion__ @copyright 2020 Edwin Martin @license MIT */ export declare function $(selectors: string, parentNode?: Document | ShadowRoot | HTMLElement): CarboniumType; export type CarboniumType = CarboniumList & T; interface CarboniumList extends Array { concat(...items: ConcatArray[]): CarboniumType; concat(...items: (T | ConcatArray)[]): CarboniumType; reverse(): CarboniumType; slice(start?: number, end?: number): CarboniumType; splice(start: number, deleteCount?: number): CarboniumType; splice(start: number, deleteCount: number, ...items: T[]): CarboniumType; forEach(callbackfn: (value: T, index: number, array: T[]) => void, thisArg?: any): CarboniumType; filter(callbackfn: (value: T, index: number, array: T[]) => boolean, thisArg?: any): CarboniumType; setAttribute(qualifiedName: string, value: string): CarboniumType; classList: CarboniumClassList; style: CarboniumStyleList; } interface CarboniumClassList extends DOMTokenList { add(...tokens: string[]): CarboniumType; remove(...tokens: string[]): CarboniumType; replace(oldToken: string, newToken: string): boolean; forEach(callbackfn: (value: string, key: number, parent: DOMTokenList) => void, thisArg?: any): CarboniumType; } interface CarboniumStyleList extends CSSStyleDeclaration { removeProperty(property: string): CarboniumList & string; setProperty(property: string, value: string | null, priority?: string): CarboniumType; } export {}; ================================================ FILE: eslint.config.js ================================================ import tseslint from "typescript-eslint"; import js from "@eslint/js"; import prettier from "eslint-config-prettier"; import globals from "globals"; export default tseslint.config( { ignores: [ "dist/**", "demo/**", "coverage/**", "iteratortest/**", "karma.conf.cjs", ], }, js.configs.recommended, ...tseslint.configs.recommended, { languageOptions: { globals: { ...globals.browser, ...globals.es2021, }, }, rules: { "no-prototype-builtins": "off", "@typescript-eslint/no-explicit-any": "off", "@typescript-eslint/no-unsafe-function-type": "off", }, }, prettier ); ================================================ FILE: package.json ================================================ { "name": "carbonium", "version": "1.3.0", "description": "One kilobyte library for easy DOM manipulation", "type": "module", "browser": "dist/bundle.iife.min.js", "module": "dist/bundle.min.js", "types": "dist/carbonium.d.ts", "sideEffects": false, "scripts": { "prepare": "husky", "start": "http-server -o demo/ --silent", "build": "rollup --config --sourcemap", "dev": "rollup --config --sourcemap --watch", "release": "npm i --package-lock && npm run lint && npm test && npm publish", "pretest": "npm run build", "test": "playwright test", "lint": "npx eslint .", "prettier": "prettier --config .prettierrc.json src/**/*.ts *.json --write" }, "repository": { "type": "git", "url": "git+https://github.com/edwinm/carbonium.git" }, "files": [ "src/carbonium.ts", "dist/carbonium.d.ts", "dist/bundle.min.js", "dist/bundle.min.js.map", "dist/bundle.iife.min.js", "dist/bundle.iife.min.js.map" ], "keywords": [ "front-end", "dom", "qsa", "jquery", "typescript", "front-end", "lightweight", "micro" ], "author": { "name": "Edwin Martin", "email": "edwin@bitstorm.org", "url": "https://bitstorm.org/" }, "license": "MIT", "bugs": { "url": "https://github.com/edwinm/carbonium/issues" }, "homepage": "https://github.com/edwinm/carbonium#readme", "devDependencies": { "@eslint/js": "^9.39.4", "@playwright/test": "^1.59.1", "@rollup/plugin-replace": "^6.0.3", "@rollup/plugin-terser": "^1.0.0", "@rollup/plugin-typescript": "^12.3.0", "@types/node": "^22.19.17", "@typescript-eslint/eslint-plugin": "^8.58.0", "@typescript-eslint/parser": "^8.58.0", "eslint": "^9.39.4", "eslint-config-prettier": "^10.1.8", "globals": "^17.4.0", "http-server": "^14.1.1", "husky": "^9.1.7", "prettier": "^3.8.1", "pretty-quick": "^4.2.2", "rollup": "^4.60.1", "tslib": "^2.8.1", "typescript": "^6.0.2", "typescript-eslint": "^8.58.0" } } ================================================ FILE: playwright.config.ts ================================================ import { defineConfig, devices } from "@playwright/test"; export default defineConfig({ testDir: "./tests", fullyParallel: true, retries: 0, reporter: [["html", { open: "never" }]], projects: [ { name: "chromium", use: { ...devices["Desktop Chrome"] } }, { name: "firefox", use: { ...devices["Desktop Firefox"] } }, ], }); ================================================ FILE: rollup.config.js ================================================ import { createRequire } from "module"; import typescript from "@rollup/plugin-typescript"; import terser from "@rollup/plugin-terser"; import replace from "@rollup/plugin-replace"; const require = createRequire(import.meta.url); const pkg = require("./package.json"); export default { input: "src/carbonium.ts", output: [ { file: "dist/bundle.min.js", format: "es", name: "bundle", sourcemap: true, plugins: [terser()], }, { file: "dist/bundle.js", format: "es", name: "bundle", sourcemap: true, }, { file: "dist/bundle.iife.min.js", format: "iife", name: "carbonium", sourcemap: true, plugins: [terser()], }, ], plugins: [ replace({ preventAssignment: false, __buildVersion__: pkg.version, }), typescript({ declarationDir: "dist" }), ], }; ================================================ FILE: src/carbonium.ts ================================================ /** Carbonium __buildVersion__ @copyright 2020 Edwin Martin @license MIT */ export function $( selectors: string, parentNode?: Document | ShadowRoot | HTMLElement ): CarboniumType { let nodelist: NodeListOf; // If the first parameter starts with "<", create a DOM node if (selectors.startsWith("<")) { nodelist = >( ([ new DOMParser().parseFromString(selectors, "text/html").body.firstChild, ]) ); } else { // Else, do querySelectorAll nodelist = (parentNode || document).querySelectorAll(selectors); } // Wrap it in a Proxy return >( (new Proxy>(nodelist, proxyHandler)) ); } // Used by style, classList and relList // When setting one of these, remember the elements to apply to let currentListNodelist: NodeListOf; let propList: "style" | "classList" | "relList"; const proxyHandler: ProxyHandler> = { get(target, prop) { let propValue = null; // Return iterator when asked for iterator, only used in ES2015+ if (prop == Symbol.iterator) { return function* () { for (const element of target) { yield element; } }; } // Special case for style, classList and relList if (prop == "style" || prop == "classList" || prop == "relList") { currentListNodelist = target; propList = prop; // Matched elements can be a list of any element or an empty list // Use getter of, for example, document.body.style const propValue = Reflect.get(document.body, prop); return new Proxy(propValue, proxyHandler); } // style.setProperty, getPropertyValue…, classList.add, contains, remove…, relList… if ( target instanceof CSSStyleDeclaration || target instanceof DOMTokenList ) { // Matched elements can be a list of any element or an empty list // Use getter of, for example, document.body.style.color propValue = Reflect.get((document.body as any)[propList], prop); // When getter is a function, apply it to all matched elements if (typeof propValue == "function") { return new Proxy(propValue, { apply: function (target, thisArg, argumentsList) { currentListNodelist.forEach((el) => { Reflect.apply(target, (el as any)[propList], argumentsList); }); return new Proxy(currentListNodelist, proxyHandler); }, }); } else { return propValue; } } // Are we dealing with an Array function like forEach, map and filter? if (Array.prototype.hasOwnProperty(prop)) { const propValue = Reflect.get(Array.prototype, prop); if (typeof propValue == "function") { return new Proxy(propValue, { apply: function (target, thisArg, argumentsList) { const ret = Reflect.apply(target, thisArg, argumentsList); // When function returns undefined (like forEach), // return all matched elements, so calls can be chained // For example forEach(…).setAttribute(…) const newTarget = typeof ret != "undefined" ? ret : thisArg; return new Proxy(newTarget, proxyHandler); }, }); } } // Get property or call function on DOM elements if (target.length > 0) { // Might be DOM element specific, like input.select(), // so use first array element to get reference if (prop in target[0]) { propValue = Reflect.get(target[0], prop); } } else { // Empty list, targeted DOM element unknown, // use getter of document.body if (prop in document.body) { propValue = Reflect.get(document.body, prop); } } // Propagate DOM prop value if (propValue) { if (typeof propValue == "function") { return new Proxy(propValue, { apply: function (target, thisArg, argumentsList) { let retFirst = null; let first = true; // Apply on individual elements for (const el of thisArg) { const ret = Reflect.apply(target, el, argumentsList); if (first) { retFirst = ret; first = false; } } return retFirst ?? thisArg; }, }); } else { return propValue; } } // Default return Reflect.get(target, prop); }, // DOM property is set set(target, prop, value) { if ("forEach" in target && !(target instanceof CSSStyleDeclaration)) { target.forEach((el) => { Reflect.set(el, prop, value); }); } else { Reflect.set(target, prop, value); } return true; }, deleteProperty(target, prop) { if (prop in target) { return delete (target as any)[prop]; } return false; }, }; export type CarboniumType = CarboniumList & T; // Interface definitions interface CarboniumList extends Array { concat(...items: ConcatArray[]): CarboniumType; concat(...items: (T | ConcatArray)[]): CarboniumType; reverse(): CarboniumType; slice(start?: number, end?: number): CarboniumType; splice(start: number, deleteCount?: number): CarboniumType; /* tslint:disable:unified-signatures */ splice(start: number, deleteCount: number, ...items: T[]): CarboniumType; forEach( callbackfn: (value: T, index: number, array: T[]) => void, thisArg?: any ): CarboniumType; filter( callbackfn: (value: T, index: number, array: T[]) => boolean, thisArg?: any ): CarboniumType; setAttribute(qualifiedName: string, value: string): CarboniumType; classList: CarboniumClassList; style: CarboniumStyleList; } interface CarboniumClassList extends DOMTokenList { add(...tokens: string[]): CarboniumType; remove(...tokens: string[]): CarboniumType; replace(oldToken: string, newToken: string): boolean; forEach( callbackfn: (value: string, key: number, parent: DOMTokenList) => void, thisArg?: any ): CarboniumType; } interface CarboniumStyleList< T extends HTMLElement, > extends CSSStyleDeclaration { removeProperty(property: string): CarboniumList & string; setProperty( property: string, value: string | null, priority?: string ): CarboniumType; } ================================================ FILE: test/test.ts ================================================ import * as assert from "assert"; import { $, CarboniumType } from "../src/carbonium"; // TODO: this should work - find out why not // import { $, CarboniumType } from "../"; import "@webcomponents/webcomponentsjs/custom-elements-es5-adapter"; /** * Test framework used: * Mocha https://mochajs.org/ * Assert https://nodejs.org/api/assert.html */ describe("$", () => { beforeEach(() => { document.body.textContent = ""; for (let i = 0; i < 6; i++) { const div = document.createElement("div"); div.textContent = `item${i}`; document.body.appendChild(div); } }); it("textContent one element", () => { $("div:first-child").textContent = "hello"; const divs = document.getElementsByTagName("div"); assert.equal(divs[0].textContent, "hello"); }); it("textContent one element", () => { const div: CarboniumType = $("div:first-child"); div.textContent = "hello"; const divs = document.getElementsByTagName("div"); assert.equal(divs[0].textContent, "hello"); }); it("textContent all elements", () => { $("div").textContent = "hello"; assert.equal(document.body.textContent, "hellohellohellohellohellohello"); }); it("length", () => { assert.equal($("div").length, 6); }); it("forEach", () => { const divs = $("div"); divs.forEach((div, i) => { div.textContent = `div ${i}`; }); assert.equal(divs[0].textContent, "div 0"); assert.equal(divs[5].textContent, "div 5"); }); it("for of", () => { const divs = $("div"); let i = 0; for (const div of divs) { div.textContent = `div ${i++}`; } assert.equal(divs[0].textContent, "div 0"); assert.equal(divs[5].textContent, "div 5"); }); it("setAttribute all elements", () => { $("div").setAttribute("aria-label", "List item"); const divs = document.getElementsByTagName("div"); assert.equal(divs[0].getAttribute("aria-label"), "List item"); assert.equal(divs[1].getAttribute("aria-label"), "List item"); assert.equal(divs[5].getAttribute("aria-label"), "List item"); }); it("filter", () => { $("div").filter((el) => el.textContent == "item1").textContent = "hello"; assert.equal(document.body.textContent, "item0helloitem2item3item4item5"); }); it("class add method", () => { $("div").classList.add("some-class"); const divs = document.getElementsByTagName("div"); assert.ok(divs[0].classList.contains("some-class")); assert.ok(divs[5].classList.contains("some-class")); }); it("rel add and contains method", () => { const a = document.createElement("a"); a.relList.add("some-class"); assert.ok(a.relList.contains("some-class")); }); it("class value property", () => { $("div").classList.add("some-class"); const divs = document.getElementsByTagName("div"); assert.equal(divs[0].classList.value, "some-class"); }); it("class add method and textContent property", () => { $("div:first-child").classList.add("some-class").textContent = "hello"; const divs = document.getElementsByTagName("div"); assert.ok(divs[0].classList.contains("some-class")); assert.ok(!divs[5].classList.contains("some-class")); assert.equal(divs[0].textContent, "hello"); assert.equal(divs[5].textContent, "item5"); }); it("filter and class add method and textContent property", () => { $("div") .filter((el) => el.textContent == "item0") .classList.add("some-class").textContent = "hello"; const divs = document.getElementsByTagName("div"); assert.ok(divs[0].classList.contains("some-class")); assert.ok(!divs[5].classList.contains("some-class")); assert.equal(divs[0].textContent, "hello"); assert.equal(divs[5].textContent, "item5"); }); it("filter and style setProperty method and textContent property", () => { $("div") .filter((el) => el.textContent == "item0") .style.setProperty("--leftmargin", "10px").textContent = "hello"; const divs = document.getElementsByTagName("div"); assert.equal(divs[0].style.getPropertyValue("--leftmargin"), "10px"); assert.equal(divs[5].style.getPropertyValue("--leftmargin"), ""); assert.equal(divs[0].textContent, "hello"); assert.equal(divs[5].textContent, "item5"); }); it("combined", () => { $("div") .forEach((el) => (el.title = `A div with content ${el.textContent}`)) .setAttribute("aria-label", "List item") .filter((el) => el.textContent == "item1").textContent = "hello"; const divs = document.getElementsByTagName("div"); assert.equal(divs[0].getAttribute("aria-label"), "List item"); assert.equal(divs[5].getAttribute("aria-label"), "List item"); assert.equal(divs[0].getAttribute("title"), "A div with content item0"); assert.equal(divs[5].getAttribute("title"), "A div with content item5"); assert.equal(document.body.textContent, "item0helloitem2item3item4item5"); }); it("textContent empty list", () => { assert.doesNotThrow(() => { $("div.non-existent").textContent = "hello"; }); }); it("setAttribute empty list", () => { assert.doesNotThrow(() => { $("div.non-existent").setAttribute("aria-label", "List item"); }); }); it("call element specific function", () => { const input = document.createElement("input"); document.querySelector("div:first-child").appendChild(input); assert.doesNotThrow(() => { $("input").select(); }); }); it("addEventListener", (done) => { $("div:first-child").addEventListener("click", () => { done(); }); $("div:first-child").click(); }); it("canvas", () => { const canvas = document.createElement("canvas"); $("div:nth-child(1)").appendChild(canvas); const ctx = $("canvas").getContext("2d", { alpha: false, }); ctx.fillRect(0, 0, 100, 100); }); it("style set/get", () => { $("div:nth-child(1)").style.color = "red"; assert.equal($("div:nth-child(1)").style.color, "red"); }); it("Parse HTML", () => { const div$ = $("
b1
"); assert.ok(div$.classList.contains("a1")); $("div:first-child").appendChild(div$[0]); assert.equal($(".a1").length, 1); assert.equal($(".a1").textContent, "b1"); }); it("Set", () => { const set = new Set(["1a", "2a", "3a"]); let result = ""; set.forEach((item) => { result += `[${item}]`; }); assert.equal(result, "[1a][2a][3a]"); }); it("Custom Element", () => { class GolInfo extends HTMLElement { connectedCallback() { $("nnn").addEventListener("click", () => { console.log("click"); }); } } customElements.define("gol-info", GolInfo); const i = document.createElement("gol-info"); // const i = new GolInfo(); document.body.appendChild(i); }); }); ================================================ FILE: tests/carbonium.spec.ts ================================================ import { test, expect, Page } from "@playwright/test"; import path from "path"; declare global { function $( selector: string, parent?: Document | ShadowRoot | HTMLElement ): any; } const bundlePath = path.resolve("./dist/bundle.iife.min.js"); async function checkFirstClassAndText(page: Page) { const results = await page.evaluate(() => { const divs = document.getElementsByTagName("div"); return [ divs[0].classList.contains("some-class"), divs[5].classList.contains("some-class"), divs[0].textContent, divs[5].textContent, ] as [boolean, boolean, string | null, string | null]; }); expect(results[0]).toBeTruthy(); expect(results[1]).toBeFalsy(); expect(results[2]).toBe("hello"); expect(results[3]).toBe("item5"); } test.beforeEach(async ({ page }) => { await page.goto("about:blank"); await page.addScriptTag({ path: bundlePath }); await page.evaluate(() => { window.$ = (window as any).carbonium.$; document.body.innerHTML = ""; for (let i = 0; i < 6; i++) { const div = document.createElement("div"); div.textContent = `item${i}`; document.body.appendChild(div); } }); }); test.describe("$", () => { test("textContent one element", async ({ page }) => { const text = await page.evaluate(() => { $("div:first-child").textContent = "hello"; return document.getElementsByTagName("div")[0].textContent; }); expect(text).toBe("hello"); }); test("textContent one element with type", async ({ page }) => { const text = await page.evaluate(() => { const div = $("div:first-child"); div.textContent = "hello"; return document.getElementsByTagName("div")[0].textContent; }); expect(text).toBe("hello"); }); test("textContent all elements", async ({ page }) => { const text = await page.evaluate(() => { $("div").textContent = "hello"; return document.body.textContent; }); expect(text).toBe("hellohellohellohellohellohello"); }); test("length", async ({ page }) => { const length = await page.evaluate(() => { return $("div").length; }); expect(length).toBe(6); }); test("forEach", async ({ page }) => { const texts = await page.evaluate(() => { const divs = $("div"); divs.forEach((div: HTMLElement, i: number) => { div.textContent = `div ${i}`; }); return [divs[0].textContent, divs[5].textContent]; }); expect(texts[0]).toBe("div 0"); expect(texts[1]).toBe("div 5"); }); test("for of", async ({ page }) => { const texts = await page.evaluate(() => { const divs = $("div"); let i = 0; for (const div of divs) { (div as HTMLElement).textContent = `div ${i++}`; } return [divs[0].textContent, divs[5].textContent]; }); expect(texts[0]).toBe("div 0"); expect(texts[1]).toBe("div 5"); }); test("setAttribute all elements", async ({ page }) => { const attrs = await page.evaluate(() => { $("div").setAttribute("aria-label", "List item"); const divs = document.getElementsByTagName("div"); return [ divs[0].getAttribute("aria-label"), divs[1].getAttribute("aria-label"), divs[5].getAttribute("aria-label"), ]; }); expect(attrs[0]).toBe("List item"); expect(attrs[1]).toBe("List item"); expect(attrs[2]).toBe("List item"); }); test("filter", async ({ page }) => { const text = await page.evaluate(() => { $("div").filter( (el: HTMLElement) => el.textContent == "item1" ).textContent = "hello"; return document.body.textContent; }); expect(text).toBe("item0helloitem2item3item4item5"); }); test("class add method", async ({ page }) => { const results = await page.evaluate(() => { $("div").classList.add("some-class"); const divs = document.getElementsByTagName("div"); return [ divs[0].classList.contains("some-class"), divs[5].classList.contains("some-class"), ]; }); expect(results[0]).toBeTruthy(); expect(results[1]).toBeTruthy(); }); test("rel add and contains method", async ({ page }) => { const result = await page.evaluate(() => { const a = document.createElement("a"); a.relList.add("some-class"); return a.relList.contains("some-class"); }); expect(result).toBeTruthy(); }); test("class value property", async ({ page }) => { const value = await page.evaluate(() => { $("div").classList.add("some-class"); const divs = document.getElementsByTagName("div"); return divs[0].classList.value; }); expect(value).toBe("some-class"); }); test("class add method and textContent property", async ({ page }) => { await page.evaluate(() => { $("div:first-child").classList.add("some-class").textContent = "hello"; }); await checkFirstClassAndText(page); }); test("filter and class add method and textContent property", async ({ page, }) => { await page.evaluate(() => { $("div") .filter((el: HTMLElement) => el.textContent == "item0") .classList.add("some-class").textContent = "hello"; }); await checkFirstClassAndText(page); }); test("filter and style setProperty method and textContent property", async ({ page, }) => { const results = await page.evaluate(() => { $("div") .filter((el: HTMLElement) => el.textContent == "item0") .style.setProperty("--leftmargin", "10px").textContent = "hello"; const divs = document.getElementsByTagName("div"); return [ divs[0].style.getPropertyValue("--leftmargin"), divs[5].style.getPropertyValue("--leftmargin"), divs[0].textContent, divs[5].textContent, ]; }); expect(results[0]).toBe("10px"); expect(results[1]).toBe(""); expect(results[2]).toBe("hello"); expect(results[3]).toBe("item5"); }); test("combined", async ({ page }) => { const results = await page.evaluate(() => { $("div") .forEach( (el: HTMLElement) => (el.title = `A div with content ${el.textContent}`) ) .setAttribute("aria-label", "List item") .filter((el: HTMLElement) => el.textContent == "item1").textContent = "hello"; const divs = document.getElementsByTagName("div"); return [ divs[0].getAttribute("aria-label"), divs[5].getAttribute("aria-label"), divs[0].getAttribute("title"), divs[5].getAttribute("title"), document.body.textContent, ]; }); expect(results[0]).toBe("List item"); expect(results[1]).toBe("List item"); expect(results[2]).toBe("A div with content item0"); expect(results[3]).toBe("A div with content item5"); expect(results[4]).toBe("item0helloitem2item3item4item5"); }); test("textContent empty list", async ({ page }) => { await page.evaluate(() => { $("div.non-existent").textContent = "hello"; }); }); test("setAttribute empty list", async ({ page }) => { await page.evaluate(() => { $("div.non-existent").setAttribute("aria-label", "List item"); }); }); test("call element specific function", async ({ page }) => { await page.evaluate(() => { const input = document.createElement("input"); document.querySelector("div:first-child")!.appendChild(input); $("input").select(); }); }); test("addEventListener", async ({ page }) => { const fired = await page.evaluate( () => new Promise((resolve) => { $("div:first-child").addEventListener("click", () => resolve(true)); $("div:first-child").click(); }) ); expect(fired).toBe(true); }); test("canvas", async ({ page }) => { await page.evaluate(() => { const canvas = document.createElement("canvas"); $("div:nth-child(1)").appendChild(canvas); const ctx = $("canvas").getContext("2d", { alpha: false }); ctx.fillRect(0, 0, 100, 100); }); }); test("style set/get", async ({ page }) => { const color = await page.evaluate(() => { $("div:nth-child(1)").style.color = "red"; return $("div:nth-child(1)").style.color; }); expect(color).toBe("red"); }); test("Parse HTML", async ({ page }) => { const results = await page.evaluate(() => { const div$ = $("
b1
"); const hasClass = div$.classList.contains("a1"); $("div:first-child").appendChild(div$[0]); return [hasClass, $(".a1").length, $(".a1").textContent]; }); expect(results[0]).toBeTruthy(); expect(results[1]).toBe(1); expect(results[2]).toBe("b1"); }); test("Set", async ({ page }) => { const result = await page.evaluate(() => { const set = new Set(["1a", "2a", "3a"]); let result = ""; set.forEach((item) => { result += `[${item}]`; }); return result; }); expect(result).toBe("[1a][2a][3a]"); }); test("Custom Element", async ({ page }) => { await page.evaluate(() => { class GolInfo extends HTMLElement { connectedCallback() { $("nnn").addEventListener("click", () => { console.log("click"); }); } } customElements.define("gol-info", GolInfo); const i = document.createElement("gol-info"); document.body.appendChild(i); }); }); }); ================================================ FILE: tsconfig.json ================================================ { "compilerOptions": { "target": "es6", "rootDir": "./src", "moduleResolution": "bundler", "sourceMap": true, "declaration": true, "skipLibCheck": true }, "include": ["src"] }