diff --git a/packages/tko.utils/src/array.ts b/packages/tko.utils/src/array.ts index 567a0542..c975d1c0 100644 --- a/packages/tko.utils/src/array.ts +++ b/packages/tko.utils/src/array.ts @@ -4,165 +4,193 @@ // Note that the array functions may be called with // Array-like things, such as NodeList. -const {isArray} = Array +const {isArray} = Array; -export function arrayForEach (array, action, thisArg) { - if (arguments.length > 2) { action = action.bind(thisArg) } +// export function arrayForEach(array: T[], action: (this: TThis, item: T, index: number, array: T[]) => void, thisArg: TThis): void; +export function arrayForEach(array: T[], action: (item: T, index: number, array: T[]) => void, thisArg?: any) { + if (thisArg) { action = action.bind(thisArg); } for (let i = 0, j = array.length; i < j; ++i) { - action(array[i], i, array) + action(array[i], i, array); } } -export function arrayIndexOf (array, item) { - return (isArray(array) ? array : [...array]).indexOf(item) +export function arrayIndexOf(array: T[], item: T) { + return (isArray(array) ? array : [...array]).indexOf(item); } -export function arrayFirst (array, predicate, predicateOwner) { - return (isArray(array) ? array : [...array]) - .find(predicate, predicateOwner) || undefined +// export function arrayFirst(array: T[], predicate: (this: TThis, value: T, index: number, obj: T[]) => boolean, predicateOwner: TThis): void; +export function arrayFirst(array: T[], predicate: (value: T, index: number, obj: T[]) => boolean, predicateOwner?: any) { + const arr: T[] = (isArray(array) ? array : [...array]); + return arr.find(predicate, predicateOwner) || undefined; } -export function arrayMap (array = [], mapping, thisArg) { - if (arguments.length > 2) { mapping = mapping.bind(thisArg) } - return Array.from(array, mapping) +export function arrayMap(array: T[], mapping: (this: TTHis, v: T, k: number) => U, thisArg: TTHis): U[]; +export function arrayMap(array: T[] = [], mapping?: (v: T, k: number) => U, thisArg?: any) { + return thisArg && mapping ? Array.from(array, mapping.bind(thisArg)) : Array.from(array); } -export function arrayRemoveItem (array, itemToRemove) { - var index = arrayIndexOf(array, itemToRemove) +export function arrayRemoveItem(array: T[], itemToRemove: T) { + const index = arrayIndexOf(array, itemToRemove); if (index > 0) { - array.splice(index, 1) + array.splice(index, 1); } else if (index === 0) { - array.shift() + array.shift(); } } -export function arrayGetDistinctValues (array = []) { - const seen = new Set() +export function arrayGetDistinctValues(array: T[] = []) { + const seen = new Set(); return (isArray(array) ? array : [...array]) - .filter(item => seen.has(item) ? false : seen.add(item)) + .filter(item => seen.has(item) ? false : seen.add(item)); } -export function arrayFilter (array, predicate, thisArg) { - if (arguments.length > 2) { predicate = predicate.bind(thisArg) } - return (isArray(array) ? array : [...array]).filter(predicate) +export function arrayFilter(array: T[], predicate: (this: TThis, value: T, index: number, array: T[]) => any, thisArg: TThis): T[]; +export function arrayFilter(array: T[], predicate: (value: T, index: number, array: T[]) => any, thisArg?: any) { + if (thisArg) { predicate = predicate.bind(thisArg); } + return (isArray(array) ? array : [...array]).filter(predicate); } -export function arrayPushAll (array, valuesToPush) { +export function arrayPushAll(array: T[], valuesToPush: ArrayLike) { if (isArray(valuesToPush)) { - array.push.apply(array, valuesToPush) + array.push.apply(array, valuesToPush); } else { - for (var i = 0, j = valuesToPush.length; i < j; i++) { array.push(valuesToPush[i]) } + for (let i = 0, j = valuesToPush.length; i < j; i++) { + array.push(valuesToPush[i]); + } } - return array + + return array; } -export function addOrRemoveItem (array, value, included) { - var existingEntryIndex = arrayIndexOf(typeof array.peek === 'function' ? array.peek() : array, value) +export function addOrRemoveItem(array: T[], value: T, included?: boolean) { + const existingEntryIndex = arrayIndexOf('peek' in array && typeof (array as any).peek === 'function' ? (array as any).peek() : array, value); if (existingEntryIndex < 0) { - if (included) { array.push(value) } + if (included) { array.push(value); } } else { - if (!included) { array.splice(existingEntryIndex, 1) } + if (!included) { array.splice(existingEntryIndex, 1); } } } -export function makeArray (arrayLikeObject) { - return Array.from(arrayLikeObject) +export function makeArray(arrayLikeObject: ArrayLike) { + return Array.from(arrayLikeObject); } -export function range (min, max) { - min = typeof min === 'function' ? min() : min - max = typeof max === 'function' ? max() : max - var result = [] - for (var i = min; i <= max; i++) { result.push(i) } - return result +export type RangeFunction = () => number; +export function range(min: number|RangeFunction, max: number|RangeFunction) { + min = typeof min === 'function' ? min() : min; + max = typeof max === 'function' ? max() : max; + const result = []; + + for (let i = min; i <= max; i++) { result.push(i); } + + return result; } // Go through the items that have been added and deleted and try to find matches between them. -export function findMovesInArrayComparison (left, right, limitFailedCompares) { +export function findMovesInArrayComparison(left: any[], right: any[], limitFailedCompares: number|boolean) { if (left.length && right.length) { - var failedCompares, l, r, leftItem, rightItem + let failedCompares, l, r, leftItem, rightItem; + + // tslint:disable-next-line:no-conditional-assignment for (failedCompares = l = 0; (!limitFailedCompares || failedCompares < limitFailedCompares) && (leftItem = left[l]); ++l) { + // tslint:disable-next-line:no-conditional-assignment for (r = 0; rightItem = right[r]; ++r) { - if (leftItem['value'] === rightItem['value']) { - leftItem['moved'] = rightItem['index'] - rightItem['moved'] = leftItem['index'] - right.splice(r, 1) // This item is marked as moved; so remove it from right list - failedCompares = r = 0 // Reset failed compares count because we're checking for consecutive failures - break + if (leftItem.value === rightItem.value) { + leftItem.moved = rightItem.index; + rightItem.moved = leftItem.index; + right.splice(r, 1); // This item is marked as moved; so remove it from right list + failedCompares = r = 0; // Reset failed compares count because we're checking for consecutive failures + break; } } - failedCompares += r + failedCompares += r; } } } -var statusNotInOld = 'added', statusNotInNew = 'deleted' +const statusNotInOld = 'added', statusNotInNew = 'deleted'; + +export interface ICompareArrayOptions { + dontLimitMoves?: boolean; + sparse?: boolean; +} // Simple calculation based on Levenshtein distance. -export function compareArrays (oldArray, newArray, options) { +export function compareArrays(oldArray: T[], newArray: T[], options ?: boolean|ICompareArrayOptions) { // For backward compatibility, if the third arg is actually a bool, interpret // it as the old parameter 'dontLimitMoves'. Newer code should use { dontLimitMoves: true }. - options = (typeof options === 'boolean') ? { 'dontLimitMoves': options } : (options || {}) - oldArray = oldArray || [] - newArray = newArray || [] + options = (typeof options === 'boolean') ? { dontLimitMoves: options } : (options || {}); + oldArray = oldArray || []; + newArray = newArray || []; - if (oldArray.length < newArray.length) { return compareSmallArrayToBigArray(oldArray, newArray, statusNotInOld, statusNotInNew, options) } else { return compareSmallArrayToBigArray(newArray, oldArray, statusNotInNew, statusNotInOld, options) } + if (oldArray.length < newArray.length) { + return compareSmallArrayToBigArray(oldArray, newArray, statusNotInOld, statusNotInNew, options); + } else { + return compareSmallArrayToBigArray(newArray, oldArray, statusNotInNew, statusNotInOld, options); + } } -function compareSmallArrayToBigArray (smlArray, bigArray, statusNotInSml, statusNotInBig, options) { - var myMin = Math.min, +function compareSmallArrayToBigArray(smlArray: T[], bigArray: T[], statusNotInSml: any, statusNotInBig: any, options: ICompareArrayOptions) { + // tslint:disable:prefer-const + let myMin = Math.min, myMax = Math.max, editDistanceMatrix = [], smlIndex, smlIndexMax = smlArray.length, bigIndex, bigIndexMax = bigArray.length, compareRange = (bigIndexMax - smlIndexMax) || 1, maxDistance = smlIndexMax + bigIndexMax + 1, - thisRow, lastRow, - bigIndexMaxForRow, bigIndexMinForRow + thisRow, lastRow: any, + bigIndexMaxForRow, bigIndexMinForRow; for (smlIndex = 0; smlIndex <= smlIndexMax; smlIndex++) { - lastRow = thisRow - editDistanceMatrix.push(thisRow = []) - bigIndexMaxForRow = myMin(bigIndexMax, smlIndex + compareRange) - bigIndexMinForRow = myMax(0, smlIndex - 1) + lastRow = thisRow; + editDistanceMatrix.push(thisRow = []); + bigIndexMaxForRow = myMin(bigIndexMax, smlIndex + compareRange); + bigIndexMinForRow = myMax(0, smlIndex - 1); for (bigIndex = bigIndexMinForRow; bigIndex <= bigIndexMaxForRow; bigIndex++) { - if (!bigIndex) { thisRow[bigIndex] = smlIndex + 1 } else if (!smlIndex) // Top row - transform empty array into new array via additions - { thisRow[bigIndex] = bigIndex + 1 } else if (smlArray[smlIndex - 1] === bigArray[bigIndex - 1]) { thisRow[bigIndex] = lastRow[bigIndex - 1] } // copy value (no edit) - else { - var northDistance = lastRow[bigIndex] || maxDistance // not in big (deletion) - var westDistance = thisRow[bigIndex - 1] || maxDistance // not in small (addition) - thisRow[bigIndex] = myMin(northDistance, westDistance) + 1 - } + if (!bigIndex) { + thisRow[bigIndex] = smlIndex + 1; + } else if (!smlIndex) { + // Top row - transform empty array into new array via additions + thisRow[bigIndex] = bigIndex + 1; + } else if (smlArray[smlIndex - 1] === bigArray[bigIndex - 1]) { + // copy value (no edit) + thisRow[bigIndex] = lastRow && lastRow[bigIndex - 1]; + } else { + const northDistance = lastRow && lastRow[bigIndex] || maxDistance; // not in big (deletion) + const westDistance: any = thisRow[bigIndex - 1] || maxDistance; // not in small (addition) + thisRow[bigIndex] = myMin(northDistance, westDistance) + 1; + } } } - var editScript = [], meMinusOne, notInSml = [], notInBig = [] + let editScript = [], meMinusOne, notInSml = [], notInBig = []; for (smlIndex = smlIndexMax, bigIndex = bigIndexMax; smlIndex || bigIndex;) { - meMinusOne = editDistanceMatrix[smlIndex][bigIndex] - 1 + meMinusOne = editDistanceMatrix[smlIndex][bigIndex] - 1; if (bigIndex && meMinusOne === editDistanceMatrix[smlIndex][bigIndex - 1]) { notInSml.push(editScript[editScript.length] = { // added - 'status': statusNotInSml, - 'value': bigArray[--bigIndex], - 'index': bigIndex }) + status: statusNotInSml, + value: bigArray[--bigIndex], + index: bigIndex }); } else if (smlIndex && meMinusOne === editDistanceMatrix[smlIndex - 1][bigIndex]) { notInBig.push(editScript[editScript.length] = { // deleted - 'status': statusNotInBig, - 'value': smlArray[--smlIndex], - 'index': smlIndex }) + status: statusNotInBig, + value: smlArray[--smlIndex], + index: smlIndex }); } else { - --bigIndex - --smlIndex - if (!options['sparse']) { + --bigIndex; + --smlIndex; + if (!options.sparse) { editScript.push({ - 'status': 'retained', - 'value': bigArray[bigIndex] }) + status: 'retained', + value: bigArray[bigIndex] }); } } } // Set a limit on the number of consecutive non-matching comparisons; having it a multiple of // smlIndexMax keeps the time complexity of this algorithm linear. - findMovesInArrayComparison(notInBig, notInSml, !options['dontLimitMoves'] && smlIndexMax * 10) + findMovesInArrayComparison(notInBig, notInSml, !options.dontLimitMoves && smlIndexMax * 10); - return editScript.reverse() + return editScript.reverse(); } diff --git a/packages/tko.utils/src/async.ts b/packages/tko.utils/src/async.ts index d700aef6..629c2df2 100644 --- a/packages/tko.utils/src/async.ts +++ b/packages/tko.utils/src/async.ts @@ -1,24 +1,26 @@ // // Asynchronous functionality // --- -import { safeSetTimeout } from './error.js' +import { safeSetTimeout } from './error'; -export function throttle (callback, timeout) { - var timeoutInstance - return function (...args) { +// tslint:disable-next-line:ban-types +export function throttle(callback: Function, timeout: number) { + let timeoutInstance: number|undefined; + return (...args: any[]) => { if (!timeoutInstance) { - timeoutInstance = safeSetTimeout(function () { - timeoutInstance = undefined - callback(...args) - }, timeout) + timeoutInstance = safeSetTimeout(() => { + timeoutInstance = undefined; + callback(...args); + }, timeout); } - } + }; } -export function debounce (callback, timeout) { - var timeoutInstance - return function (...args) { - clearTimeout(timeoutInstance) - timeoutInstance = safeSetTimeout(() => callback(...args), timeout) - } +// tslint:disable-next-line:ban-types +export function debounce(callback: Function, timeout: number) { + let timeoutInstance: number|undefined; + return (...args: any[]) => { + clearTimeout(timeoutInstance); + timeoutInstance = safeSetTimeout(() => callback(...args), timeout); + }; } diff --git a/packages/tko.utils/src/css.ts b/packages/tko.utils/src/css.ts index e1e2aefe..4a4fa778 100644 --- a/packages/tko.utils/src/css.ts +++ b/packages/tko.utils/src/css.ts @@ -2,36 +2,37 @@ // DOM - CSS // -import { arrayForEach, addOrRemoveItem } from './array.js' +import { arrayForEach, addOrRemoveItem } from './array'; // For details on the pattern for changing node classes // see: https://github.com/knockout/knockout/issues/1597 -var cssClassNameRegex = /\S+/g +const cssClassNameRegex = /\S+/g; -function toggleDomNodeCssClass (node, classNames, shouldHaveClass) { - var addOrRemoveFn - if (!classNames) { return } +function toggleDomNodeCssClass(node: HTMLElement, classNames: string, shouldHaveClass?: boolean) { + let addOrRemoveFn: (className: string) => void; + 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') { + addOrRemoveFn = node.classList[shouldHaveClass ? 'add' : 'remove']; + arrayForEach(classNames.match(cssClassNameRegex)!, (className: string) => { + addOrRemoveFn.call(node.classList, className); + }); + } else if (typeof (node.className as any).baseVal === 'string') { // SVG tag .classNames is an SVGAnimatedString instance - toggleObjectClassPropertyString(node.className, 'baseVal', classNames, shouldHaveClass) + toggleObjectClassPropertyString(node.className, 'baseVal', classNames, shouldHaveClass); } else { // node.className ought to be a string. - toggleObjectClassPropertyString(node, 'className', classNames, shouldHaveClass) + toggleObjectClassPropertyString(node, 'className', classNames, shouldHaveClass); } } -function toggleObjectClassPropertyString (obj, prop, classNames, shouldHaveClass) { +function toggleObjectClassPropertyString(obj: any, prop: string, classNames: string, shouldHaveClass?: boolean) { // obj/prop is either a node/'className' or a SVGAnimatedString/'baseVal'. - var currentClassNames = obj[prop].match(cssClassNameRegex) || [] - arrayForEach(classNames.match(cssClassNameRegex), function (className) { - addOrRemoveItem(currentClassNames, className, shouldHaveClass) - }) - obj[prop] = currentClassNames.join(' ') + const currentClassNames = obj[prop].match(cssClassNameRegex) || []; + arrayForEach(classNames.match(cssClassNameRegex)!, (className: string) => { + addOrRemoveItem(currentClassNames, className, shouldHaveClass); + }); + + obj[prop] = currentClassNames.join(' '); } -export { toggleDomNodeCssClass } +export { toggleDomNodeCssClass }; diff --git a/packages/tko.utils/src/dom/data.ts b/packages/tko.utils/src/dom/data.ts index d7f1215a..7eb022e9 100644 --- a/packages/tko.utils/src/dom/data.ts +++ b/packages/tko.utils/src/dom/data.ts @@ -1,13 +1,13 @@ // // DOM node data // -import { ieVersion } from '../ie' +import { ieVersion } from '../ie'; -const datastoreTime = new Date().getTime() -const dataStoreKeyExpandoPropertyName = `__ko__${datastoreTime}` -const dataStoreSymbol = Symbol('Knockout data') -var dataStore -let uniqueId = 0 +const datastoreTime = new Date().getTime(); +const dataStoreKeyExpandoPropertyName = `__ko__${datastoreTime}`; +const dataStoreSymbol = Symbol('Knockout data'); +const dataStore: any = {}; +let uniqueId = 0; /* * We considered using WeakMap, but it has a problem in IE 11 and Edge that @@ -15,70 +15,74 @@ let uniqueId = 0 * on the node. See https://github.com/knockout/knockout/issues/2141 */ const modern = { - getDataForNode (node, createIfNotFound) { - let dataForNode = node[dataStoreSymbol] + getDataForNode(node: any, createIfNotFound?: boolean): any { + let dataForNode = node[dataStoreSymbol]; if (!dataForNode && createIfNotFound) { - dataForNode = node[dataStoreSymbol] = {} + dataForNode = node[dataStoreSymbol] = {}; } - return dataForNode + return dataForNode; }, - clear (node) { - if (node[dataStoreSymbol]) { - delete node[dataStoreSymbol] - return true + clear(node: Node) { + const internalNode = node as any; + if (internalNode[dataStoreSymbol]) { + delete internalNode[dataStoreSymbol]; + return true; } - return false + return false; } -} +}; /** * Old IE versions have memory issues if you store objects on the node, so we * use a separate data storage and link to it from the node using a string key. */ const IE = { - getDataforNode (node, createIfNotFound) { - let dataStoreKey = node[dataStoreKeyExpandoPropertyName] - const hasExistingDataStore = dataStoreKey && (dataStoreKey !== 'null') && dataStore[dataStoreKey] + getDataForNode(node: any, createIfNotFound?: boolean): any { + let dataStoreKey = node[dataStoreKeyExpandoPropertyName]; + const hasExistingDataStore = dataStoreKey && (dataStoreKey !== 'null') && dataStore[dataStoreKey]; if (!hasExistingDataStore) { if (!createIfNotFound) { - return undefined + return undefined; } - dataStoreKey = node[dataStoreKeyExpandoPropertyName] = 'ko' + uniqueId++ - dataStore[dataStoreKey] = {} + dataStoreKey = node[dataStoreKeyExpandoPropertyName] = 'ko' + uniqueId++; + dataStore[dataStoreKey] = {}; } - return dataStore[dataStoreKey] + return dataStore[dataStoreKey]; }, - clear (node) { - const dataStoreKey = node[dataStoreKeyExpandoPropertyName] + clear(node: Node) { + const internalNode = node as any; + const dataStoreKey = internalNode[dataStoreKeyExpandoPropertyName]; if (dataStoreKey) { - delete dataStore[dataStoreKey] - node[dataStoreKeyExpandoPropertyName] = null - return true // Exposing 'did clean' flag purely so specs can infer whether things have been cleaned up as intended + delete dataStore[dataStoreKey]; + internalNode[dataStoreKeyExpandoPropertyName] = null; + return true; // Exposing 'did clean' flag purely so specs can infer whether things have been cleaned up as intended } - return false + return false; } -} +}; -const {getDataForNode, clear} = ieVersion ? IE : modern +const {getDataForNode, clear} = ieVersion ? IE : modern; /** * Create a unique key-string identifier. */ -export function nextKey () { - return (uniqueId++) + dataStoreKeyExpandoPropertyName +export function nextKey() { + return (uniqueId++) + dataStoreKeyExpandoPropertyName; } -function get (node, key) { - const dataForNode = getDataForNode(node, false) - return dataForNode && dataForNode[key] +function get(node: Node, key: string) { + const dataForNode = getDataForNode(node, false); + return dataForNode && dataForNode[key]; } -function set (node, key, value) { +function set(node: Node, key: string, value?: T) { // Make sure we don't actually create a new domData key if we are actually deleting a value - var dataForNode = getDataForNode(node, value !== undefined /* createIfNotFound */) - dataForNode && (dataForNode[key] = value) + const dataForNode = getDataForNode(node, value !== undefined /* createIfNotFound */); + if (dataForNode) { + dataForNode[key] = value; + } } -export { get, set, clear } +export { get, set, clear }; diff --git a/packages/tko.utils/src/dom/disposal.ts b/packages/tko.utils/src/dom/disposal.ts index 96b80ed7..a65a4b00 100644 --- a/packages/tko.utils/src/dom/disposal.ts +++ b/packages/tko.utils/src/dom/disposal.ts @@ -2,116 +2,132 @@ // DOM node disposal // /* eslint no-cond-assign: 0 */ -import * as domData from './data.js' -import {arrayRemoveItem} from '../array.js' -import {jQueryInstance} from '../jquery.js' +import * as domData from './data'; +import {arrayRemoveItem} from '../array'; +import {jQueryInstance} from '../jquery'; + +const domDataKey = domData.nextKey(); -var domDataKey = domData.nextKey() // Node types: // 1: Element // 8: Comment // 9: Document -var cleanableNodeTypes = { 1: true, 8: true, 9: true } -var cleanableNodeTypesWithDescendants = { 1: true, 9: true } +const cleanableNodeTypes: {[nodeType: number]: boolean|undefined} = { 1: true, 8: true, 9: true }; +const cleanableNodeTypesWithDescendants: {[nodeType: number]: boolean|undefined} = { 1: true, 9: true }; -function getDisposeCallbacksCollection (node, createIfNotFound) { - var allDisposeCallbacks = domData.get(node, domDataKey) +function getDisposeCallbacksCollection(node: Node, createIfNotFound?: boolean) { + let allDisposeCallbacks = domData.get(node, domDataKey); if ((allDisposeCallbacks === undefined) && createIfNotFound) { - allDisposeCallbacks = [] - domData.set(node, domDataKey, allDisposeCallbacks) + allDisposeCallbacks = []; + domData.set(node, domDataKey, allDisposeCallbacks); } - return allDisposeCallbacks + return allDisposeCallbacks; } -function destroyCallbacksCollection (node) { - domData.set(node, domDataKey, undefined) + +function destroyCallbacksCollection(node: Node) { + domData.set(node, domDataKey, undefined); } -function cleanSingleNode (node) { - // Run all the dispose callbacks - var callbacks = getDisposeCallbacksCollection(node, false) +// Expose supplemental node cleaning functions. +export type NodeCleanerCallback = (node: Node) => void; +export const otherNodeCleanerFunctions: NodeCleanerCallback[] = []; + +function cleanSingleNode(node: Node) { + // Run all the dispose callbacks + let callbacks = getDisposeCallbacksCollection(node, false); if (callbacks) { - callbacks = callbacks.slice(0) // Clone, as the array may be modified during iteration (typically, callbacks will remove themselves) - for (let i = 0; i < callbacks.length; i++) { callbacks[i](node) } + callbacks = callbacks.slice(0); // Clone, as the array may be modified during iteration (typically, callbacks will remove themselves) + for (const cb of callbacks) { + callbacks(node); + } } // Erase the DOM data - domData.clear(node) + domData.clear(node); // Perform cleanup needed by external libraries (currently only jQuery, but can be extended) for (let i = 0, j = otherNodeCleanerFunctions.length; i < j; ++i) { - otherNodeCleanerFunctions[i](node) + otherNodeCleanerFunctions[i](node); } // Clear any immediate-child comment nodes, as these wouldn't have been found by // node.getElementsByTagName('*') in cleanNode() (comment nodes aren't elements) - if (cleanableNodeTypesWithDescendants[node.nodeType]) { cleanImmediateCommentTypeChildren(node) } + if (cleanableNodeTypesWithDescendants[node.nodeType]) { cleanImmediateCommentTypeChildren(node); } } -function cleanImmediateCommentTypeChildren (nodeWithChildren) { - const children = nodeWithChildren.childNodes - let cleanedNode +function cleanImmediateCommentTypeChildren(nodeWithChildren: Node) { + const children = nodeWithChildren.childNodes; + let cleanedNode: Node; + + // tslint:disable-next-line:prefer-for-of for (let i = 0; i < children.length; ++i) { if (children[i].nodeType === 8) { - cleanSingleNode(cleanedNode = children[i]) + cleanSingleNode(cleanedNode = children[i]); if (children[i] !== cleanedNode) { - throw Error('ko.cleanNode: An already cleaned node was removed from the document') + throw Error('ko.cleanNode: An already cleaned node was removed from the document'); } } } } // Exports -export function addDisposeCallback (node, callback) { - if (typeof callback !== 'function') { throw new Error('Callback must be a function') } - getDisposeCallbacksCollection(node, true).push(callback) +export type DisposeCallback = () => void; + +export function addDisposeCallback(node: Node, callback: DisposeCallback) { + if (typeof callback !== 'function') { throw new Error('Callback must be a function'); } + getDisposeCallbacksCollection(node, true).push(callback); } -export function removeDisposeCallback (node, callback) { - var callbacksCollection = getDisposeCallbacksCollection(node, false) +export function removeDisposeCallback(node: Node, callback: DisposeCallback) { + const callbacksCollection = getDisposeCallbacksCollection(node, false); if (callbacksCollection) { - arrayRemoveItem(callbacksCollection, callback) - if (callbacksCollection.length === 0) { destroyCallbacksCollection(node) } + arrayRemoveItem(callbacksCollection, callback); + if (callbacksCollection.length === 0) { destroyCallbacksCollection(node); } } } -export function cleanNode (node) { +function isCleanableNodeWithDescendants(node: Node): node is Element { + return cleanableNodeTypesWithDescendants[node.nodeType] === true; +} + +export function cleanNode(node: Node) { // First clean this node, where applicable if (cleanableNodeTypes[node.nodeType]) { - cleanSingleNode(node) + cleanSingleNode(node); // ... then its descendants, where applicable - if (cleanableNodeTypesWithDescendants[node.nodeType]) { - const descendants = node.getElementsByTagName('*') + if (isCleanableNodeWithDescendants(node)) { + const descendants = node.getElementsByTagName('*'); + + // tslint:disable-next-line:prefer-for-of for (let i = 0; i < descendants.length; ++i) { - let cleanedNode = descendants[i] - cleanSingleNode(cleanedNode) + const cleanedNode = descendants[i]; + cleanSingleNode(cleanedNode); if (descendants[i] !== cleanedNode) { - throw Error('ko.cleanNode: An already cleaned node was removed from the document') + throw Error('ko.cleanNode: An already cleaned node was removed from the document'); } } } } - return node + return node; } -export function removeNode (node) { - cleanNode(node) - if (node.parentNode) { node.parentNode.removeChild(node) } +export function removeNode(node: Node) { + cleanNode(node); + if (node.parentNode) { node.parentNode.removeChild(node); } } -// Expose supplemental node cleaning functions. -export var otherNodeCleanerFunctions = [] - // Special support for jQuery here because it's so commonly used. // Many jQuery plugins (including jquery.tmpl) store data using jQuery's equivalent of domData // so notify it to tear down any resources associated with the node & descendants here. -export function cleanjQueryData (node) { - var jQueryCleanNodeFn = jQueryInstance - ? jQueryInstance.cleanData : null +export function cleanjQueryData(node: Node) { + // Note: cleanData is an internal jQuery function + const jQueryCleanNodeFn = jQueryInstance + ? (jQueryInstance as any).cleanData : null; if (jQueryCleanNodeFn) { - jQueryCleanNodeFn([node]) + jQueryCleanNodeFn([node]); } } -otherNodeCleanerFunctions.push(cleanjQueryData) +otherNodeCleanerFunctions.push(cleanjQueryData); diff --git a/packages/tko.utils/src/dom/event.ts b/packages/tko.utils/src/dom/event.ts index ccc08444..0d660951 100644 --- a/packages/tko.utils/src/dom/event.ts +++ b/packages/tko.utils/src/dom/event.ts @@ -2,94 +2,100 @@ // DOM Events // -import { objectForEach } from '../object.js' -import { jQueryInstance } from '../jquery.js' -import { ieVersion } from '../ie.js' -import { catchFunctionErrors } from '../error.js' +import { objectForEach } from '../object'; +import { jQueryInstance } from '../jquery'; +import { ieVersion } from '../ie'; +import { catchFunctionErrors } from '../error'; -import { tagNameLower } from './info.js' -import { addDisposeCallback } from './disposal.js' -import options from '../options.js' +import { tagNameLower } from './info'; +import { addDisposeCallback } from './disposal'; +import options from '../options'; // Represent the known event types in a compact way, then at runtime transform it into a hash with event name as key (for fast lookup) -var knownEvents = {}, - knownEventTypesByEventName = {} +const knownEvents: {[evType: string]: string[]} = {}, + knownEventTypesByEventName: {[evType: string]: string} = {}; -var keyEventTypeName = (options.global.navigator && /Firefox\/2/i.test(options.global.navigator.userAgent)) ? 'KeyboardEvent' : 'UIEvents' +const keyEventTypeName = (options.global.navigator && /Firefox\/2/i.test(options.global.navigator.userAgent)) ? 'KeyboardEvent' : 'UIEvents'; -knownEvents[keyEventTypeName] = ['keyup', 'keydown', 'keypress'] +knownEvents[keyEventTypeName] = ['keyup', 'keydown', 'keypress']; -knownEvents['MouseEvents'] = [ +knownEvents.MouseEvents = [ 'click', 'dblclick', 'mousedown', 'mouseup', 'mousemove', 'mouseover', - 'mouseout', 'mouseenter', 'mouseleave'] + 'mouseout', 'mouseenter', 'mouseleave']; -objectForEach(knownEvents, function (eventType, knownEventsForType) { +objectForEach(knownEvents, (eventType, knownEventsForType) => { if (knownEventsForType.length) { - for (var i = 0, j = knownEventsForType.length; i < j; i++) { knownEventTypesByEventName[knownEventsForType[i]] = eventType } + for (let i = 0, j = knownEventsForType.length; i < j; i++) { knownEventTypesByEventName[knownEventsForType[i]] = eventType; } } -}) +}); -function isClickOnCheckableElement (element, eventType) { - if ((tagNameLower(element) !== 'input') || !element.type) return false - if (eventType.toLowerCase() != 'click') return false - var inputType = element.type - return (inputType == 'checkbox') || (inputType == 'radio') +function isClickOnCheckableElement(element: HTMLElement, eventType: string) { + if ((tagNameLower(element) !== 'input') || !(element as HTMLInputElement).type) { + return false; + } + + if (eventType.toLowerCase() !== 'click') { + return false; + } + + const inputType = (element as HTMLInputElement).type; + return (inputType === 'checkbox') || (inputType === 'radio'); } // Workaround for an IE9 issue - https://github.com/SteveSanderson/knockout/issues/406 -var eventsThatMustBeRegisteredUsingAttachEvent = { 'propertychange': true } -let jQueryEventAttachName +const eventsThatMustBeRegisteredUsingAttachEvent: {[type: string]: boolean} = { propertychange: true }; +let jQueryEventAttachName: any; -export function registerEventHandler (element, eventType, handler, eventOptions = false) { - const wrappedHandler = catchFunctionErrors(handler) - const mustUseAttachEvent = ieVersion && eventsThatMustBeRegisteredUsingAttachEvent[eventType] - const mustUseNative = Boolean(eventOptions) +export function registerEventHandler(element: HTMLElement, eventType: string, handler: EventListener, eventOptions = false) { + const wrappedHandler = catchFunctionErrors(handler); + const mustUseAttachEvent = ieVersion && eventsThatMustBeRegisteredUsingAttachEvent[eventType]; + const mustUseNative = Boolean(eventOptions); if (!options.useOnlyNativeEvents && !mustUseAttachEvent && !mustUseNative && jQueryInstance) { if (!jQueryEventAttachName) { - jQueryEventAttachName = (typeof jQueryInstance(element).on === 'function') ? 'on' : 'bind' + jQueryEventAttachName = (typeof jQueryInstance(element).on === 'function') ? 'on' : 'bind'; } - jQueryInstance(element)[jQueryEventAttachName](eventType, wrappedHandler) + (jQueryInstance(element) as any)[jQueryEventAttachName](eventType, wrappedHandler); } else if (!mustUseAttachEvent && typeof element.addEventListener === 'function') { - element.addEventListener(eventType, wrappedHandler, eventOptions) - } else if (typeof element.attachEvent !== 'undefined') { - const attachEventHandler = function (event) { wrappedHandler.call(element, event) } - const attachEventName = 'on' + eventType - element.attachEvent(attachEventName, attachEventHandler) + element.addEventListener(eventType, wrappedHandler, eventOptions); + } else if (typeof (element as any).attachEvent !== 'undefined') { + const attachEventHandler = (event: any) => wrappedHandler.call(element, event); + const attachEventName = 'on' + eventType; + (element as any).attachEvent(attachEventName, attachEventHandler); // IE does not dispose attachEvent handlers automatically (unlike with addEventListener) // so to avoid leaks, we have to remove them manually. See bug #856 - addDisposeCallback(element, function () { - element.detachEvent(attachEventName, attachEventHandler) - }) + addDisposeCallback(element, () => { + (element as any).detachEvent(attachEventName, attachEventHandler); + }); } else { - throw new Error("Browser doesn't support addEventListener or attachEvent") + throw new Error("Browser doesn't support addEventListener or attachEvent"); } } -export function triggerEvent (element, eventType) { - if (!(element && element.nodeType)) { throw new Error('element must be a DOM node when calling triggerEvent') } +export function triggerEvent(element: HTMLElement, eventType: string) { + if (!(element && element.nodeType)) { throw new Error('element must be a DOM node when calling triggerEvent'); } // For click events on checkboxes and radio buttons, jQuery toggles the element checked state *after* the // event handler runs instead of *before*. (This was fixed in 1.9 for checkboxes but not for radio buttons.) // IE doesn't change the checked state when you trigger the click event using "fireEvent". // In both cases, we'll use the click method instead. - var useClickWorkaround = isClickOnCheckableElement(element, eventType) + const useClickWorkaround = isClickOnCheckableElement(element, eventType); if (!options.useOnlyNativeEvents && jQueryInstance && !useClickWorkaround) { - jQueryInstance(element).trigger(eventType) + jQueryInstance(element).trigger(eventType); } else if (typeof document.createEvent === 'function') { if (typeof element.dispatchEvent === 'function') { - var eventCategory = knownEventTypesByEventName[eventType] || 'HTMLEvents' - var event = document.createEvent(eventCategory) - event.initEvent(eventType, true, true, options.global, 0, 0, 0, 0, 0, false, false, false, false, 0, element) - element.dispatchEvent(event) - } else { throw new Error("The supplied element doesn't support dispatchEvent") } + const eventCategory = knownEventTypesByEventName[eventType] || 'HTMLEvents'; + const event = document.createEvent(eventCategory); + (event as any).initEvent(eventType, true, true, options.global, 0, 0, 0, 0, 0, false, false, false, false, 0, element); + element.dispatchEvent(event); + } else { throw new Error("The supplied element doesn't support dispatchEvent"); } } else if (useClickWorkaround && element.click) { - element.click() - } else if (typeof element.fireEvent !== 'undefined') { - element.fireEvent('on' + eventType) + element.click(); + } else if (typeof (element as any).fireEvent !== 'undefined') { + (element as any).fireEvent('on' + eventType); } else { - throw new Error("Browser doesn't support triggering events") + throw new Error("Browser doesn't support triggering events"); } } diff --git a/packages/tko.utils/src/dom/fixes.ts b/packages/tko.utils/src/dom/fixes.ts index e99b7903..0db13c7e 100644 --- a/packages/tko.utils/src/dom/fixes.ts +++ b/packages/tko.utils/src/dom/fixes.ts @@ -1,9 +1,9 @@ // // DOM node manipulation // -import { ieVersion } from '../ie.js' +import { ieVersion } from '../ie'; -export function fixUpContinuousNodeArray (continuousNodeArray, parentNode) { +export function fixUpContinuousNodeArray(continuousNodeArray: Node[], parentNode: Node) { // Before acting on a set of nodes that were previously outputted by a template function, we have to reconcile // them against what is in the DOM right now. It may be that some of the nodes have already been removed, or that // new nodes might have been inserted in the middle, for example by a binding. Also, there may previously have been @@ -22,50 +22,51 @@ export function fixUpContinuousNodeArray (continuousNodeArray, parentNode) { if (continuousNodeArray.length) { // The parent node can be a virtual element; so get the real parent node - parentNode = (parentNode.nodeType === 8 && parentNode.parentNode) || parentNode + parentNode = (parentNode.nodeType === 8 && parentNode.parentNode) || parentNode; // Rule [A] - while (continuousNodeArray.length && continuousNodeArray[0].parentNode !== parentNode) { continuousNodeArray.splice(0, 1) } + while (continuousNodeArray.length && continuousNodeArray[0].parentNode !== parentNode) { continuousNodeArray.splice(0, 1); } // Rule [B] - while (continuousNodeArray.length > 1 && continuousNodeArray[continuousNodeArray.length - 1].parentNode !== parentNode) { continuousNodeArray.length-- } + while (continuousNodeArray.length > 1 && continuousNodeArray[continuousNodeArray.length - 1].parentNode !== parentNode) { continuousNodeArray.length--; } // Rule [C] if (continuousNodeArray.length > 1) { - var current = continuousNodeArray[0], last = continuousNodeArray[continuousNodeArray.length - 1] + let current: Node|null = continuousNodeArray[0]; + const last = continuousNodeArray[continuousNodeArray.length - 1]; // Replace with the actual new continuous node set - continuousNodeArray.length = 0 - while (current !== last) { - continuousNodeArray.push(current) - current = current.nextSibling + continuousNodeArray.length = 0; + while (current !== last && current) { + continuousNodeArray.push(current); + current = current.nextSibling; } - continuousNodeArray.push(last) + continuousNodeArray.push(last); } } - return continuousNodeArray + return continuousNodeArray; } -export function setOptionNodeSelectionState (optionNode, isSelected) { +export function setOptionNodeSelectionState(optionNode: HTMLOptionElement, isSelected: boolean) { // IE6 sometimes throws "unknown error" if you try to write to .selected directly, whereas Firefox struggles with setAttribute. Pick one based on browser. - if (ieVersion < 7) { optionNode.setAttribute('selected', isSelected) } else { optionNode.selected = isSelected } + if (ieVersion && ieVersion < 7) { optionNode.setAttribute('selected', isSelected.toString()); } else { optionNode.selected = isSelected; } } -export function forceRefresh (node) { +export function forceRefresh(node: Node) { // Workaround for an IE9 rendering bug - https://github.com/SteveSanderson/knockout/issues/209 - if (ieVersion >= 9) { + if (ieVersion && ieVersion >= 9) { // For text nodes and comment nodes (most likely virtual elements), we will have to refresh the container - var elem = node.nodeType == 1 ? node : node.parentNode - if (elem.style) { elem.style.zoom = elem.style.zoom } + const elem = (node.nodeType === 1 ? node : node.parentNode) as HTMLElement; + if (elem && elem.style) { elem.style.zoom = elem.style.zoom; } } } -export function ensureSelectElementIsRenderedCorrectly (selectElement) { +export function ensureSelectElementIsRenderedCorrectly(selectElement: HTMLSelectElement) { // Workaround for IE9 rendering bug - it doesn't reliably display all the text in dynamically-added select boxes unless you force it to re-render by updating the width. // (See https://github.com/SteveSanderson/knockout/issues/312, http://stackoverflow.com/questions/5908494/select-only-shows-first-char-of-selected-option) // Also fixes IE7 and IE8 bug that causes selects to be zero width if enclosed by 'if' or 'with'. (See issue #839) if (ieVersion) { - var originalWidth = selectElement.style.width - selectElement.style.width = 0 - selectElement.style.width = originalWidth + const originalWidth = selectElement.style.width; + selectElement.style.width = '0'; + selectElement.style.width = originalWidth; } } diff --git a/packages/tko.utils/src/dom/html.ts b/packages/tko.utils/src/dom/html.ts index 09bade62..2bef2aa9 100644 --- a/packages/tko.utils/src/dom/html.ts +++ b/packages/tko.utils/src/dom/html.ts @@ -1,50 +1,54 @@ // // HTML-based manipulation // -import { stringTrim } from '../string.js' -import { makeArray } from '../array.js' -import { emptyDomNode, moveCleanedNodesToContainerElement } from './manipulation.js' -import { jQueryInstance } from '../jquery.js' -import * as virtualElements from './virtualElements' -import options from '../options' - -var none = [0, '', ''], - table = [1, '', '
'], - tbody = [2, '', '
'], - colgroup = [ 2, '', '
'], - tr = [3, '', '
'], - select = [1, "'], - fieldset = [1, '
', '
'], - map = [1, '', ''], - object = [1, '', ''], - lookup = { - 'area': map, - 'col': colgroup, - 'colgroup': table, - 'caption': table, - 'legend': fieldset, - 'thead': table, - 'tbody': table, - 'tfoot': table, - 'tr': tbody, - 'td': tr, - 'th': tr, - 'option': select, - 'optgroup': select, - 'param': object +import { stringTrim } from '../string'; +import { makeArray } from '../array'; +import { emptyDomNode, moveCleanedNodesToContainerElement } from './manipulation'; +import { jQueryInstance } from '../jquery'; +import * as virtualElements from './virtualElements'; +import options from '../options'; + +type TagMap = [number, string, string]; +const none: TagMap = [0, '', ''], + table: TagMap = [1, '', '
'], + tbody: TagMap = [2, '', '
'], + colgroup: TagMap = [ 2, '', '
'], + tr: TagMap = [3, '', '
'], + select: TagMap = [1, "'], + fieldset: TagMap = [1, '
', '
'], + map: TagMap = [1, '', ''], + object: TagMap = [1, '', ''], + lookup: {[tag: string]: TagMap} = { + area: map, + col: colgroup, + colgroup: table, + caption: table, + legend: fieldset, + thead: table, + tbody: table, + tfoot: table, + tr: tbody, + td: tr, + th: tr, + option: select, + optgroup: select, + param: object }, // The canonical way to test that the HTML5