Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions .changeset/modernize-utils-dead-polyfills.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
---
"@tko/utils": minor
"@tko/utils.parser": patch
"@tko/observable": patch
"@tko/binding.core": patch
"@tko/binding.foreach": patch
"@tko/computed": patch
"@tko/lifecycle": patch
"@tko/builder": minor
---

Drop dead polyfill probes from `@tko/utils`

Removes runtime feature detection for capabilities that all supported runtimes
(modern browsers, Node, Bun, happy-dom) already expose unconditionally:

- `functionSupportsLengthOverwrite` + `overwriteLengthPropertyIfSupported` —
`Object.defineProperty(fn, 'length', …)` has worked since IE9. Call sites
in `@tko/observable` now invoke `Object.defineProperty` directly.
- `useSymbols` + `createSymbolOrString` — `Symbol` is always defined; call
sites now use `Symbol(identifier)` directly. `createSymbolOrString` is no
longer exposed on `ko.utils` (public API removal — minor bump for
`@tko/utils` and `@tko/builder`).
- `stringTrim` + `stringStartsWith` — removed; call sites use
`String(value ?? '').trim()` / `value.startsWith(prefix)` inline.
- `toggleDomNodeCssClass` SVGAnimatedString fallback — `classList` is
available on every supported `Element` (including SVG since SVG2).
- `parseJson` no longer routes through `stringTrim`; it trims inline when the
input is a string.

`packages/utils.parser/src/preparse.ts` also guards `str.match(bindingToken)`
against the `null` return case using `?? []` — previously relied on the match
never returning `null` for the transformed input.
4 changes: 0 additions & 4 deletions builds/knockout/spec/bindingAttributeBehaviors.js
Original file line number Diff line number Diff line change
Expand Up @@ -260,10 +260,6 @@ describe('Binding attribute syntax', function () {
ko.applyBindings({}, testNode)

var allowedProperties = ['$parents', '$root', 'ko', '$rawData', '$data', '$parentContext', '$parent']
if (ko.utils.createSymbolOrString('') === '') {
allowedProperties.push('_subscribable')
allowedProperties.push('_ancestorBindingInfo')
}
ko.utils.objectForEach(ko.contextFor(testNode.childNodes[0].childNodes[0]), function (prop) {
expect(allowedProperties).to.contain(prop)
})
Expand Down
6 changes: 3 additions & 3 deletions packages/binding.core/src/css.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { createSymbolOrString, toggleDomNodeCssClass, objectForEach, stringTrim } from '@tko/utils'
import { toggleDomNodeCssClass, objectForEach } from '@tko/utils'

import { unwrap } from '@tko/observable'

Expand All @@ -12,11 +12,11 @@ export const css = {
toggleDomNodeCssClass(element, className, shouldHaveClass)
})
} else {
value = stringTrim(String(value || '')) // Make sure we don't try to store or set a non-string value
value = String(value ?? '').trim() // Make sure we don't try to store or set a non-string value
toggleDomNodeCssClass(element, element[css.classesWrittenByBindingKey], false)
element[css.classesWrittenByBindingKey] = value
toggleDomNodeCssClass(element, value, true)
}
},
classesWrittenByBindingKey: createSymbolOrString('__ko__cssValue')
classesWrittenByBindingKey: Symbol('__ko__cssValue')
}
6 changes: 3 additions & 3 deletions packages/binding.core/src/hasfocus.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { createSymbolOrString, triggerEvent, registerEventHandler } from '@tko/utils'
import { triggerEvent, registerEventHandler } from '@tko/utils'

import { unwrap, dependencyDetection, isWriteableObservable } from '@tko/observable'

import type { AllBindings } from '@tko/bind'

const hasfocusUpdatingProperty = createSymbolOrString('__ko_hasfocusUpdating')
const hasfocusLastValue = createSymbolOrString('__ko_hasfocusLastValue')
const hasfocusUpdatingProperty = Symbol('__ko_hasfocusUpdating')
const hasfocusLastValue = Symbol('__ko_hasfocusLastValue')

export const hasfocus = {
init: function (element, valueAccessor, _allBindings: AllBindings) {
Expand Down
4 changes: 2 additions & 2 deletions packages/binding.core/src/value.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { stringStartsWith, safeSetTimeout, tagNameLower, arrayForEach, selectExtensions } from '@tko/utils'
import { safeSetTimeout, tagNameLower, arrayForEach, selectExtensions } from '@tko/utils'

import { unwrap, dependencyDetection } from '@tko/observable'

Expand Down Expand Up @@ -82,7 +82,7 @@ export class value extends BindingHandler {
// This is useful, for example, to catch "keydown" events after the browser has updated the control
// (otherwise, selectExtensions.readValue(this) will receive the control's value *before* the key event)
let handler = this.valueUpdateHandler.bind(this)
if (stringStartsWith(eventName, 'after')) {
if (eventName.startsWith('after')) {
handler = () => {
// The elementValueBeforeEvent variable is non-null *only* during the brief gap between
// a keyX event firing and the valueUpdateHandler running, which is scheduled to happen
Expand Down
12 changes: 2 additions & 10 deletions packages/binding.foreach/src/foreach.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,7 @@
// Employing sound techniques to make a faster Knockout foreach binding.
// --------

import {
arrayForEach,
cleanNode,
options,
virtualElements,
createSymbolOrString,
domData,
domNodeIsContainedBy
} from '@tko/utils'
import { arrayForEach, cleanNode, options, virtualElements, domData, domNodeIsContainedBy } from '@tko/utils'

import { isObservable, unwrap, observable } from '@tko/observable'

Expand Down Expand Up @@ -87,7 +79,7 @@ function valueToChangeAddItem(value, index): ChangeAddItem {
}

// store a symbol for caching the pending delete info index in the data item objects
const PENDING_DELETE_INDEX_SYM = createSymbolOrString('_ko_ffe_pending_delete_index')
const PENDING_DELETE_INDEX_SYM = Symbol('_ko_ffe_pending_delete_index')

export class ForEachBinding extends AsyncBindingHandler {
// NOTE: valid valueAccessors include:
Expand Down
3 changes: 0 additions & 3 deletions packages/builder/src/Builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ import {
cleanNode,
cloneNodes,
compareArrays,
createSymbolOrString,
domData,
extend,
memoization,
Expand Down Expand Up @@ -116,7 +115,6 @@ export type Utils = {
arrayRemoveItem: typeof arrayRemoveItem
cloneNodes: typeof cloneNodes
compareArrays: typeof compareArrays
createSymbolOrString: typeof createSymbolOrString
domData: typeof domData
domNodeDisposal: typeof domNodeDisposal
extend: typeof extend
Expand Down Expand Up @@ -149,7 +147,6 @@ const utils: Utils = {
arrayRemoveItem,
cloneNodes,
compareArrays,
createSymbolOrString,
domData,
domNodeDisposal,
extend,
Expand Down
3 changes: 1 addition & 2 deletions packages/computed/src/computed.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
import {
addDisposeCallback,
arrayForEach,
createSymbolOrString,
domNodeIsAttachedToDocument,
extend,
options,
Expand All @@ -28,7 +27,7 @@ import {

import type { Observable, Subscribable } from '@tko/observable'

const computedState: symbol = createSymbolOrString('_state')
const computedState: symbol = Symbol('_state')
const DISPOSED_STATE = {
dependencyTracking: null,
dependenciesCount: 0,
Expand Down
6 changes: 3 additions & 3 deletions packages/lifecycle/src/LifeCycle.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { addDisposeCallback, createSymbolOrString } from '@tko/utils'
import { addDisposeCallback } from '@tko/utils'

import { computed } from '@tko/computed'
import type { Observable } from '@tko/observable'

const SUBSCRIPTIONS = createSymbolOrString('LifeCycle Subscriptions List')
const ANCHOR_NODE = createSymbolOrString('LifeCycle Anchor Node')
const SUBSCRIPTIONS = Symbol('LifeCycle Subscriptions List')
const ANCHOR_NODE = Symbol('LifeCycle Anchor Node')

export default class LifeCycle {
// NOTE: For more advanced integration as an ES6 mixin, see e.g.:
Expand Down
4 changes: 2 additions & 2 deletions packages/observable/src/observable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
// Observable values
// ---
//
import { options, overwriteLengthPropertyIfSupported } from '@tko/utils'
import { options } from '@tko/utils'

import * as dependencyDetection from './dependencyDetection'
import { deferUpdates } from './defer'
Expand Down Expand Up @@ -103,7 +103,7 @@ export function observable<T = any>(initialValue?: T): Observable {
}
}

overwriteLengthPropertyIfSupported(Observable as any, { value: undefined })
Object.defineProperty(Observable, 'length', { value: undefined })

Observable[LATEST_VALUE] = initialValue

Expand Down
4 changes: 2 additions & 2 deletions packages/observable/src/observableArray.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
// Observable Arrays
// ===
//
import { arrayIndexOf, arrayForEach, overwriteLengthPropertyIfSupported } from '@tko/utils'
import { arrayIndexOf, arrayForEach } from '@tko/utils'

import type { CompareArraysOptions } from '@tko/utils'

Expand Down Expand Up @@ -152,7 +152,7 @@ export function observableArray<T = any>(initialValues?: T[]): ObservableArray<T
const result = Object.setPrototypeOf(observable(initialValues), observableArray.fn) as ObservableArray<T>
trackArrayChanges(result)
// ^== result.extend({ trackArrayChanges: true })
overwriteLengthPropertyIfSupported(result, { get: () => result()?.length })
Object.defineProperty(result, 'length', { get: () => result()?.length })
return result
}

Expand Down
8 changes: 3 additions & 5 deletions packages/utils.parser/src/preparse.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import { stringTrim } from '@tko/utils'

/* eslint no-cond-assign: 0 */

// The following regular expressions will be used to split an object-literal string into tokens
Expand Down Expand Up @@ -42,7 +40,7 @@ const keywordRegexLookBehind = { in: 1, return: 1, typeof: 1 }
*/
export default function parseObjectLiteral(objectLiteralString) {
// Trim leading and trailing spaces from the string
let str = stringTrim(objectLiteralString)
let str = String(objectLiteralString ?? '').trim()

// Trim braces '{' surrounding the whole object literal
if (str.charCodeAt(0) === 123) str = str.slice(1, -1)
Expand All @@ -53,7 +51,7 @@ export default function parseObjectLiteral(objectLiteralString) {

// Split into tokens
const result = new Array()
let toks = str.match(bindingToken)
let toks = str.match(bindingToken) ?? []
let key
let values = new Array()
let depth = 0
Expand Down Expand Up @@ -92,7 +90,7 @@ export default function parseObjectLiteral(objectLiteralString) {
if (match && !keywordRegexLookBehind[match[0]]) {
// The slash is actually a division punctuator; re-parse the remainder of the string (not including the slash)
str = str.substring(str.indexOf(tok) + 1)
toks = str.match(bindingToken)
toks = str.match(bindingToken) ?? []
i = -1
// Continue with just the slash
tok = '/'
Expand Down
33 changes: 8 additions & 25 deletions packages/utils/src/css.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,38 +2,21 @@
// DOM - CSS
//

import { arrayForEach, addOrRemoveItem } from './array'

// For details on the pattern for changing node classes
// see: https://github.com/knockout/knockout/issues/1597
// See: https://github.com/knockout/knockout/issues/1597
const cssClassNameRegex = /\S+/g

function toggleDomNodeCssClass(node: Element, classNames: string, shouldHaveClass?: boolean): void {
let addOrRemoveFn
if (!classNames) {
return
}
if (typeof node.classList === 'object') {
addOrRemoveFn = node.classList[shouldHaveClass ? 'add' : 'remove']
arrayForEach(classNames.match(cssClassNameRegex)!, function (className) {
addOrRemoveFn.call(node.classList, className)
})
} else if (typeof node.className['baseVal'] === 'string') {
// SVG tag .classNames is an SVGAnimatedString instance
toggleObjectClassPropertyString(node.className, 'baseVal', classNames, shouldHaveClass)
} else {
// node.className ought to be a string.
toggleObjectClassPropertyString(node, 'className', classNames, shouldHaveClass)
const tokens = classNames.match(cssClassNameRegex)
if (!tokens) {
return
}
const method = shouldHaveClass ? 'add' : 'remove'
for (const token of tokens) {
node.classList[method](token)
}
}

function toggleObjectClassPropertyString(obj, prop, classNames, shouldHaveClass) {
// obj/prop is either a node/'className' or a SVGAnimatedString/'baseVal'.
const currentClassNames = obj[prop].match(cssClassNameRegex) || []
arrayForEach(classNames.match(cssClassNameRegex), function (className) {
addOrRemoveItem(currentClassNames, className, shouldHaveClass)
})
obj[prop] = currentClassNames.join(' ')
}

export { toggleDomNodeCssClass }
16 changes: 0 additions & 16 deletions packages/utils/src/function.ts

This file was deleted.

2 changes: 0 additions & 2 deletions packages/utils/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,7 @@ export * from './array'
export * from './async'
export * from './error'
export * from './object'
export * from './function'
export * from './string'
export * from './symbol'
export * from './css'
export { default as options, defineOption, Options } from './options'

Expand Down
22 changes: 3 additions & 19 deletions packages/utils/src/string.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,27 +2,11 @@
// String (and JSON)
//

export function stringTrim(string) {
return string === null || string === undefined
? ''
: string.trim
? string.trim()
: string.toString().replace(/^[\s\xa0]+|[\s\xa0]+$/g, '')
}

export function stringStartsWith(string, startsWith) {
string = string || ''
if (startsWith.length > string.length) {
return false
}
return string.substring(0, startsWith.length) === startsWith
}

export function parseJson<T = any>(jsonString: string): T | null {
if (typeof jsonString === 'string') {
jsonString = stringTrim(jsonString)
if (jsonString) {
return JSON.parse(jsonString) as T
const trimmed = jsonString.trim()
if (trimmed) {
return JSON.parse(trimmed) as T
}
}
return null
Expand Down
9 changes: 0 additions & 9 deletions packages/utils/src/symbol.ts

This file was deleted.

7 changes: 1 addition & 6 deletions packages/utils/src/tasks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,7 @@ const schedulerGlobal = options.global

if (schedulerGlobal && typeof schedulerGlobal.queueMicrotask === 'function') {
options.taskScheduler = callback => schedulerGlobal.queueMicrotask(callback)
} else if (
schedulerGlobal &&
schedulerGlobal.MutationObserver &&
schedulerGlobal.document &&
!(schedulerGlobal.navigator && schedulerGlobal.navigator.standalone)
) {
} else if (schedulerGlobal?.MutationObserver && schedulerGlobal.document && !schedulerGlobal.navigator?.standalone) {
options.taskScheduler = (function () {
let scheduledCallback: null | (() => void) = null
let toggle = false
Expand Down
Loading