From 5344957abb55dbbd561812b746fe8406d61b5252 Mon Sep 17 00:00:00 2001 From: Sebazzz Date: Wed, 25 Apr 2018 22:38:10 +0200 Subject: [PATCH] Initial Typescriptification of tko.utils - pending compilation issues in rollup --- package.json | 2 + packages/node_modules | 1 - packages/tko.utils/package.json | 3 +- packages/tko.utils/src/array.js | 168 ------------ packages/tko.utils/src/array.ts | 196 ++++++++++++++ packages/tko.utils/src/async.js | 24 -- packages/tko.utils/src/async.ts | 26 ++ .../src/{bind-shim.js => bind-shim.ts} | 0 packages/tko.utils/src/css.js | 37 --- packages/tko.utils/src/css.ts | 38 +++ packages/tko.utils/src/dom/data.js | 84 ------ packages/tko.utils/src/dom/data.ts | 88 +++++++ packages/tko.utils/src/dom/disposal.js | 117 --------- packages/tko.utils/src/dom/disposal.ts | 133 ++++++++++ packages/tko.utils/src/dom/event.js | 95 ------- packages/tko.utils/src/dom/event.ts | 101 +++++++ .../tko.utils/src/dom/{fixes.js => fixes.ts} | 45 ++-- .../tko.utils/src/dom/{html.js => html.ts} | 155 +++++------ packages/tko.utils/src/dom/info.js | 46 ---- packages/tko.utils/src/dom/info.ts | 55 ++++ packages/tko.utils/src/dom/manipulation.js | 85 ------ packages/tko.utils/src/dom/manipulation.ts | 91 +++++++ ...electExtensions.js => selectExtensions.ts} | 62 ++--- packages/tko.utils/src/dom/virtualElements.js | 216 --------------- packages/tko.utils/src/dom/virtualElements.ts | 248 ++++++++++++++++++ packages/tko.utils/src/error.js | 24 -- packages/tko.utils/src/error.ts | 26 ++ packages/tko.utils/src/function.js | 17 -- packages/tko.utils/src/function.ts | 18 ++ packages/tko.utils/src/ie.js | 26 -- packages/tko.utils/src/ie.ts | 27 ++ packages/tko.utils/src/index.js | 35 --- packages/tko.utils/src/index.ts | 35 +++ packages/tko.utils/src/jquery.js | 12 - packages/tko.utils/src/jquery.ts | 12 + packages/tko.utils/src/memoization.js | 58 ---- packages/tko.utils/src/memoization.ts | 63 +++++ packages/tko.utils/src/object.js | 63 ----- packages/tko.utils/src/object.ts | 64 +++++ packages/tko.utils/src/options.js | 59 ----- packages/tko.utils/src/options.ts | 87 ++++++ packages/tko.utils/src/proto.js | 16 -- packages/tko.utils/src/proto.ts | 16 ++ packages/tko.utils/src/string.js | 33 --- packages/tko.utils/src/string.ts | 35 +++ packages/tko.utils/src/symbol.js | 9 - packages/tko.utils/src/symbol.ts | 9 + packages/tko.utils/src/tasks.js | 103 -------- packages/tko.utils/src/tasks.ts | 108 ++++++++ tsconfig.json | 4 +- tslint.json | 46 ++++ yarn.lock | 78 +++++- 52 files changed, 1740 insertions(+), 1459 deletions(-) delete mode 120000 packages/node_modules delete mode 100644 packages/tko.utils/src/array.js create mode 100644 packages/tko.utils/src/array.ts delete mode 100644 packages/tko.utils/src/async.js create mode 100644 packages/tko.utils/src/async.ts rename packages/tko.utils/src/{bind-shim.js => bind-shim.ts} (100%) delete mode 100644 packages/tko.utils/src/css.js create mode 100644 packages/tko.utils/src/css.ts delete mode 100644 packages/tko.utils/src/dom/data.js create mode 100644 packages/tko.utils/src/dom/data.ts delete mode 100644 packages/tko.utils/src/dom/disposal.js create mode 100644 packages/tko.utils/src/dom/disposal.ts delete mode 100644 packages/tko.utils/src/dom/event.js create mode 100644 packages/tko.utils/src/dom/event.ts rename packages/tko.utils/src/dom/{fixes.js => fixes.ts} (68%) rename packages/tko.utils/src/dom/{html.js => html.ts} (56%) delete mode 100644 packages/tko.utils/src/dom/info.js create mode 100644 packages/tko.utils/src/dom/info.ts delete mode 100644 packages/tko.utils/src/dom/manipulation.js create mode 100644 packages/tko.utils/src/dom/manipulation.ts rename packages/tko.utils/src/dom/{selectExtensions.js => selectExtensions.ts} (62%) delete mode 100644 packages/tko.utils/src/dom/virtualElements.js create mode 100644 packages/tko.utils/src/dom/virtualElements.ts delete mode 100644 packages/tko.utils/src/error.js create mode 100644 packages/tko.utils/src/error.ts delete mode 100644 packages/tko.utils/src/function.js create mode 100644 packages/tko.utils/src/function.ts delete mode 100644 packages/tko.utils/src/ie.js create mode 100644 packages/tko.utils/src/ie.ts delete mode 100644 packages/tko.utils/src/index.js create mode 100644 packages/tko.utils/src/index.ts delete mode 100644 packages/tko.utils/src/jquery.js create mode 100644 packages/tko.utils/src/jquery.ts delete mode 100644 packages/tko.utils/src/memoization.js create mode 100644 packages/tko.utils/src/memoization.ts delete mode 100644 packages/tko.utils/src/object.js create mode 100644 packages/tko.utils/src/object.ts delete mode 100644 packages/tko.utils/src/options.js create mode 100644 packages/tko.utils/src/options.ts delete mode 100644 packages/tko.utils/src/proto.js create mode 100644 packages/tko.utils/src/proto.ts delete mode 100644 packages/tko.utils/src/string.js create mode 100644 packages/tko.utils/src/string.ts delete mode 100644 packages/tko.utils/src/symbol.js create mode 100644 packages/tko.utils/src/symbol.ts delete mode 100644 packages/tko.utils/src/tasks.js create mode 100644 packages/tko.utils/src/tasks.ts create mode 100644 tslint.json diff --git a/package.json b/package.json index 09d5e711..40998c0c 100644 --- a/package.json +++ b/package.json @@ -62,6 +62,8 @@ "rollup-plugin-visualizer": "^0.3.1", "sinon": "^4.1", "standard": "^10.0.3", + "tslint": "5.9.1", + "tslib": "1.9.0", "typescript": "^2.6.2" }, "workspaces": [ diff --git a/packages/node_modules b/packages/node_modules deleted file mode 120000 index 945c9b46..00000000 --- a/packages/node_modules +++ /dev/null @@ -1 +0,0 @@ -. \ No newline at end of file diff --git a/packages/tko.utils/package.json b/packages/tko.utils/package.json index 3055fa0b..5f98e772 100644 --- a/packages/tko.utils/package.json +++ b/packages/tko.utils/package.json @@ -27,7 +27,8 @@ ] }, "dependencies": { - "tslib": "^1.8.0" + "@types/jquery": "3.3.1", + "tslib": "1.9.0" }, "__about__shared.package.json": "These properties are copied into all packages/*/package.json", "standard": { diff --git a/packages/tko.utils/src/array.js b/packages/tko.utils/src/array.js deleted file mode 100644 index 567a0542..00000000 --- a/packages/tko.utils/src/array.js +++ /dev/null @@ -1,168 +0,0 @@ -// -// Array utilities -// -// Note that the array functions may be called with -// Array-like things, such as NodeList. - -const {isArray} = Array - -export function arrayForEach (array, action, thisArg) { - if (arguments.length > 2) { action = action.bind(thisArg) } - for (let i = 0, j = array.length; i < j; ++i) { - action(array[i], i, array) - } -} - -export function arrayIndexOf (array, item) { - return (isArray(array) ? array : [...array]).indexOf(item) -} - -export function arrayFirst (array, predicate, predicateOwner) { - return (isArray(array) ? array : [...array]) - .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 arrayRemoveItem (array, itemToRemove) { - var index = arrayIndexOf(array, itemToRemove) - if (index > 0) { - array.splice(index, 1) - } else if (index === 0) { - array.shift() - } -} - -export function arrayGetDistinctValues (array = []) { - const seen = new Set() - return (isArray(array) ? array : [...array]) - .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 arrayPushAll (array, valuesToPush) { - if (isArray(valuesToPush)) { - array.push.apply(array, valuesToPush) - } else { - for (var i = 0, j = valuesToPush.length; i < j; i++) { array.push(valuesToPush[i]) } - } - return array -} - -export function addOrRemoveItem (array, value, included) { - var existingEntryIndex = arrayIndexOf(typeof array.peek === 'function' ? array.peek() : array, value) - if (existingEntryIndex < 0) { - if (included) { array.push(value) } - } else { - if (!included) { array.splice(existingEntryIndex, 1) } - } -} - -export function makeArray (arrayLikeObject) { - 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 -} - -// Go through the items that have been added and deleted and try to find matches between them. -export function findMovesInArrayComparison (left, right, limitFailedCompares) { - if (left.length && right.length) { - var failedCompares, l, r, leftItem, rightItem - for (failedCompares = l = 0; (!limitFailedCompares || failedCompares < limitFailedCompares) && (leftItem = left[l]); ++l) { - 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 - } - } - failedCompares += r - } - } -} - -var statusNotInOld = 'added', statusNotInNew = 'deleted' - - // Simple calculation based on Levenshtein distance. -export function compareArrays (oldArray, newArray, options) { - // 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 || [] - - 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, - myMax = Math.max, - editDistanceMatrix = [], - smlIndex, smlIndexMax = smlArray.length, - bigIndex, bigIndexMax = bigArray.length, - compareRange = (bigIndexMax - smlIndexMax) || 1, - maxDistance = smlIndexMax + bigIndexMax + 1, - thisRow, lastRow, - bigIndexMaxForRow, bigIndexMinForRow - - for (smlIndex = 0; smlIndex <= smlIndexMax; smlIndex++) { - 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 - } - } - } - - var editScript = [], meMinusOne, notInSml = [], notInBig = [] - for (smlIndex = smlIndexMax, bigIndex = bigIndexMax; smlIndex || bigIndex;) { - 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 }) - } else if (smlIndex && meMinusOne === editDistanceMatrix[smlIndex - 1][bigIndex]) { - notInBig.push(editScript[editScript.length] = { // deleted - 'status': statusNotInBig, - 'value': smlArray[--smlIndex], - 'index': smlIndex }) - } else { - --bigIndex - --smlIndex - if (!options['sparse']) { - editScript.push({ - '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) - - return editScript.reverse() -} diff --git a/packages/tko.utils/src/array.ts b/packages/tko.utils/src/array.ts new file mode 100644 index 00000000..c975d1c0 --- /dev/null +++ b/packages/tko.utils/src/array.ts @@ -0,0 +1,196 @@ +// +// Array utilities +// +// Note that the array functions may be called with +// Array-like things, such as NodeList. + +const {isArray} = Array; + +// 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); + } +} + +export function arrayIndexOf(array: T[], item: T) { + return (isArray(array) ? array : [...array]).indexOf(item); +} + +// 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: 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: T[], itemToRemove: T) { + const index = arrayIndexOf(array, itemToRemove); + if (index > 0) { + array.splice(index, 1); + } else if (index === 0) { + array.shift(); + } +} + +export function arrayGetDistinctValues(array: T[] = []) { + const seen = new Set(); + return (isArray(array) ? array : [...array]) + .filter(item => seen.has(item) ? false : seen.add(item)); +} + +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: T[], valuesToPush: ArrayLike) { + if (isArray(valuesToPush)) { + array.push.apply(array, valuesToPush); + } else { + for (let i = 0, j = valuesToPush.length; i < j; i++) { + array.push(valuesToPush[i]); + } + } + + return array; +} + +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); } + } else { + if (!included) { array.splice(existingEntryIndex, 1); } + } +} + +export function makeArray(arrayLikeObject: ArrayLike) { + return Array.from(arrayLikeObject); +} + +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: any[], right: any[], limitFailedCompares: number|boolean) { + if (left.length && right.length) { + 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; + } + } + failedCompares += r; + } + } +} + +const statusNotInOld = 'added', statusNotInNew = 'deleted'; + +export interface ICompareArrayOptions { + dontLimitMoves?: boolean; + sparse?: boolean; +} + + // Simple calculation based on Levenshtein distance. +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 || []; + + if (oldArray.length < newArray.length) { + return compareSmallArrayToBigArray(oldArray, newArray, statusNotInOld, statusNotInNew, options); + } else { + return compareSmallArrayToBigArray(newArray, oldArray, statusNotInNew, statusNotInOld, options); + } +} + +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: any, + bigIndexMaxForRow, bigIndexMinForRow; + + for (smlIndex = 0; smlIndex <= smlIndexMax; smlIndex++) { + 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]) { + // 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; + } + } + } + + let editScript = [], meMinusOne, notInSml = [], notInBig = []; + for (smlIndex = smlIndexMax, bigIndex = bigIndexMax; smlIndex || bigIndex;) { + 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 }); + } else if (smlIndex && meMinusOne === editDistanceMatrix[smlIndex - 1][bigIndex]) { + notInBig.push(editScript[editScript.length] = { // deleted + status: statusNotInBig, + value: smlArray[--smlIndex], + index: smlIndex }); + } else { + --bigIndex; + --smlIndex; + if (!options.sparse) { + editScript.push({ + 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); + + return editScript.reverse(); +} diff --git a/packages/tko.utils/src/async.js b/packages/tko.utils/src/async.js deleted file mode 100644 index d700aef6..00000000 --- a/packages/tko.utils/src/async.js +++ /dev/null @@ -1,24 +0,0 @@ -// -// Asynchronous functionality -// --- -import { safeSetTimeout } from './error.js' - -export function throttle (callback, timeout) { - var timeoutInstance - return function (...args) { - if (!timeoutInstance) { - timeoutInstance = safeSetTimeout(function () { - timeoutInstance = undefined - callback(...args) - }, timeout) - } - } -} - -export function debounce (callback, timeout) { - var timeoutInstance - return function (...args) { - clearTimeout(timeoutInstance) - timeoutInstance = safeSetTimeout(() => callback(...args), timeout) - } -} diff --git a/packages/tko.utils/src/async.ts b/packages/tko.utils/src/async.ts new file mode 100644 index 00000000..629c2df2 --- /dev/null +++ b/packages/tko.utils/src/async.ts @@ -0,0 +1,26 @@ +// +// Asynchronous functionality +// --- +import { safeSetTimeout } from './error'; + +// tslint:disable-next-line:ban-types +export function throttle(callback: Function, timeout: number) { + let timeoutInstance: number|undefined; + return (...args: any[]) => { + if (!timeoutInstance) { + timeoutInstance = safeSetTimeout(() => { + timeoutInstance = undefined; + 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/bind-shim.js b/packages/tko.utils/src/bind-shim.ts similarity index 100% rename from packages/tko.utils/src/bind-shim.js rename to packages/tko.utils/src/bind-shim.ts diff --git a/packages/tko.utils/src/css.js b/packages/tko.utils/src/css.js deleted file mode 100644 index e1e2aefe..00000000 --- a/packages/tko.utils/src/css.js +++ /dev/null @@ -1,37 +0,0 @@ -// -// DOM - CSS -// - -import { arrayForEach, addOrRemoveItem } from './array.js' - -// For details on the pattern for changing node classes -// see: https://github.com/knockout/knockout/issues/1597 -var cssClassNameRegex = /\S+/g - -function toggleDomNodeCssClass (node, classNames, shouldHaveClass) { - var 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) - } -} - -function toggleObjectClassPropertyString (obj, prop, classNames, shouldHaveClass) { - // 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(' ') -} - -export { toggleDomNodeCssClass } diff --git a/packages/tko.utils/src/css.ts b/packages/tko.utils/src/css.ts new file mode 100644 index 00000000..4a4fa778 --- /dev/null +++ b/packages/tko.utils/src/css.ts @@ -0,0 +1,38 @@ +// +// DOM - CSS +// + +import { arrayForEach, addOrRemoveItem } from './array'; + +// For details on the pattern for changing node classes +// see: https://github.com/knockout/knockout/issues/1597 +const cssClassNameRegex = /\S+/g; + +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)!, (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); + } else { + // node.className ought to be a string. + toggleObjectClassPropertyString(node, 'className', classNames, shouldHaveClass); + } +} + +function toggleObjectClassPropertyString(obj: any, prop: string, classNames: string, shouldHaveClass?: boolean) { + // obj/prop is either a node/'className' or a SVGAnimatedString/'baseVal'. + const currentClassNames = obj[prop].match(cssClassNameRegex) || []; + arrayForEach(classNames.match(cssClassNameRegex)!, (className: string) => { + addOrRemoveItem(currentClassNames, className, shouldHaveClass); + }); + + obj[prop] = currentClassNames.join(' '); +} + +export { toggleDomNodeCssClass }; diff --git a/packages/tko.utils/src/dom/data.js b/packages/tko.utils/src/dom/data.js deleted file mode 100644 index d7f1215a..00000000 --- a/packages/tko.utils/src/dom/data.js +++ /dev/null @@ -1,84 +0,0 @@ -// -// DOM node data -// -import { ieVersion } from '../ie' - -const datastoreTime = new Date().getTime() -const dataStoreKeyExpandoPropertyName = `__ko__${datastoreTime}` -const dataStoreSymbol = Symbol('Knockout data') -var dataStore -let uniqueId = 0 - -/* - * We considered using WeakMap, but it has a problem in IE 11 and Edge that - * prevents using it cross-window, so instead we just store the data directly - * on the node. See https://github.com/knockout/knockout/issues/2141 - */ -const modern = { - getDataForNode (node, createIfNotFound) { - let dataForNode = node[dataStoreSymbol] - if (!dataForNode && createIfNotFound) { - dataForNode = node[dataStoreSymbol] = {} - } - return dataForNode - }, - - clear (node) { - if (node[dataStoreSymbol]) { - delete node[dataStoreSymbol] - return true - } - 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] - if (!hasExistingDataStore) { - if (!createIfNotFound) { - return undefined - } - dataStoreKey = node[dataStoreKeyExpandoPropertyName] = 'ko' + uniqueId++ - dataStore[dataStoreKey] = {} - } - return dataStore[dataStoreKey] - }, - - clear (node) { - const dataStoreKey = node[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 - } - return false - } -} - -const {getDataForNode, clear} = ieVersion ? IE : modern - -/** - * Create a unique key-string identifier. - */ -export function nextKey () { - return (uniqueId++) + dataStoreKeyExpandoPropertyName -} - -function get (node, key) { - const dataForNode = getDataForNode(node, false) - return dataForNode && dataForNode[key] -} - -function set (node, key, value) { - // 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) -} - -export { get, set, clear } diff --git a/packages/tko.utils/src/dom/data.ts b/packages/tko.utils/src/dom/data.ts new file mode 100644 index 00000000..7eb022e9 --- /dev/null +++ b/packages/tko.utils/src/dom/data.ts @@ -0,0 +1,88 @@ +// +// DOM node data +// +import { ieVersion } from '../ie'; + +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 + * prevents using it cross-window, so instead we just store the data directly + * on the node. See https://github.com/knockout/knockout/issues/2141 + */ +const modern = { + getDataForNode(node: any, createIfNotFound?: boolean): any { + let dataForNode = node[dataStoreSymbol]; + if (!dataForNode && createIfNotFound) { + dataForNode = node[dataStoreSymbol] = {}; + } + return dataForNode; + }, + + clear(node: Node) { + const internalNode = node as any; + if (internalNode[dataStoreSymbol]) { + delete internalNode[dataStoreSymbol]; + return true; + } + 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: any, createIfNotFound?: boolean): any { + let dataStoreKey = node[dataStoreKeyExpandoPropertyName]; + const hasExistingDataStore = dataStoreKey && (dataStoreKey !== 'null') && dataStore[dataStoreKey]; + if (!hasExistingDataStore) { + if (!createIfNotFound) { + return undefined; + } + dataStoreKey = node[dataStoreKeyExpandoPropertyName] = 'ko' + uniqueId++; + dataStore[dataStoreKey] = {}; + } + return dataStore[dataStoreKey]; + }, + + clear(node: Node) { + const internalNode = node as any; + const dataStoreKey = internalNode[dataStoreKeyExpandoPropertyName]; + if (dataStoreKey) { + 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; + } +}; + +const {getDataForNode, clear} = ieVersion ? IE : modern; + +/** + * Create a unique key-string identifier. + */ +export function nextKey() { + return (uniqueId++) + dataStoreKeyExpandoPropertyName; +} + +function get(node: Node, key: string) { + const dataForNode = getDataForNode(node, false); + return dataForNode && dataForNode[key]; +} + +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 + const dataForNode = getDataForNode(node, value !== undefined /* createIfNotFound */); + if (dataForNode) { + dataForNode[key] = value; + } +} + +export { get, set, clear }; diff --git a/packages/tko.utils/src/dom/disposal.js b/packages/tko.utils/src/dom/disposal.js deleted file mode 100644 index 96b80ed7..00000000 --- a/packages/tko.utils/src/dom/disposal.js +++ /dev/null @@ -1,117 +0,0 @@ -// -// DOM node disposal -// -/* eslint no-cond-assign: 0 */ -import * as domData from './data.js' -import {arrayRemoveItem} from '../array.js' -import {jQueryInstance} from '../jquery.js' - -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 } - -function getDisposeCallbacksCollection (node, createIfNotFound) { - var allDisposeCallbacks = domData.get(node, domDataKey) - if ((allDisposeCallbacks === undefined) && createIfNotFound) { - allDisposeCallbacks = [] - domData.set(node, domDataKey, allDisposeCallbacks) - } - return allDisposeCallbacks -} -function destroyCallbacksCollection (node) { - domData.set(node, domDataKey, undefined) -} - -function cleanSingleNode (node) { - // Run all the dispose callbacks - var 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) } - } - - // Erase the DOM data - 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) - } - - // 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) } -} - -function cleanImmediateCommentTypeChildren (nodeWithChildren) { - const children = nodeWithChildren.childNodes - let cleanedNode - for (let i = 0; i < children.length; ++i) { - if (children[i].nodeType === 8) { - cleanSingleNode(cleanedNode = children[i]) - if (children[i] !== cleanedNode) { - 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 function removeDisposeCallback (node, callback) { - var callbacksCollection = getDisposeCallbacksCollection(node, false) - if (callbacksCollection) { - arrayRemoveItem(callbacksCollection, callback) - if (callbacksCollection.length === 0) { destroyCallbacksCollection(node) } - } -} - -export function cleanNode (node) { - // First clean this node, where applicable - if (cleanableNodeTypes[node.nodeType]) { - cleanSingleNode(node) - - // ... then its descendants, where applicable - if (cleanableNodeTypesWithDescendants[node.nodeType]) { - const descendants = node.getElementsByTagName('*') - for (let i = 0; i < descendants.length; ++i) { - let cleanedNode = descendants[i] - cleanSingleNode(cleanedNode) - if (descendants[i] !== cleanedNode) { - throw Error('ko.cleanNode: An already cleaned node was removed from the document') - } - } - } - } - return node -} - -export function removeNode (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 - - if (jQueryCleanNodeFn) { - jQueryCleanNodeFn([node]) - } -} - -otherNodeCleanerFunctions.push(cleanjQueryData) diff --git a/packages/tko.utils/src/dom/disposal.ts b/packages/tko.utils/src/dom/disposal.ts new file mode 100644 index 00000000..a65a4b00 --- /dev/null +++ b/packages/tko.utils/src/dom/disposal.ts @@ -0,0 +1,133 @@ +// +// DOM node disposal +// +/* eslint no-cond-assign: 0 */ +import * as domData from './data'; +import {arrayRemoveItem} from '../array'; +import {jQueryInstance} from '../jquery'; + +const domDataKey = domData.nextKey(); + +// Node types: +// 1: Element +// 8: Comment +// 9: Document +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: Node, createIfNotFound?: boolean) { + let allDisposeCallbacks = domData.get(node, domDataKey); + if ((allDisposeCallbacks === undefined) && createIfNotFound) { + allDisposeCallbacks = []; + domData.set(node, domDataKey, allDisposeCallbacks); + } + return allDisposeCallbacks; +} + +function destroyCallbacksCollection(node: Node) { + domData.set(node, domDataKey, undefined); +} + +// 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 (const cb of callbacks) { + callbacks(node); + } + } + + // Erase the DOM data + 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); + } + + // 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); } +} + +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]); + if (children[i] !== cleanedNode) { + throw Error('ko.cleanNode: An already cleaned node was removed from the document'); + } + } + } +} + +// Exports +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: Node, callback: DisposeCallback) { + const callbacksCollection = getDisposeCallbacksCollection(node, false); + if (callbacksCollection) { + arrayRemoveItem(callbacksCollection, callback); + if (callbacksCollection.length === 0) { destroyCallbacksCollection(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); + + // ... then its descendants, where applicable + if (isCleanableNodeWithDescendants(node)) { + const descendants = node.getElementsByTagName('*'); + + // tslint:disable-next-line:prefer-for-of + for (let i = 0; i < descendants.length; ++i) { + const cleanedNode = descendants[i]; + cleanSingleNode(cleanedNode); + if (descendants[i] !== cleanedNode) { + throw Error('ko.cleanNode: An already cleaned node was removed from the document'); + } + } + } + } + return node; +} + +export function removeNode(node: Node) { + cleanNode(node); + if (node.parentNode) { node.parentNode.removeChild(node); } +} + +// 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: Node) { + // Note: cleanData is an internal jQuery function + const jQueryCleanNodeFn = jQueryInstance + ? (jQueryInstance as any).cleanData : null; + + if (jQueryCleanNodeFn) { + jQueryCleanNodeFn([node]); + } +} + +otherNodeCleanerFunctions.push(cleanjQueryData); diff --git a/packages/tko.utils/src/dom/event.js b/packages/tko.utils/src/dom/event.js deleted file mode 100644 index ccc08444..00000000 --- a/packages/tko.utils/src/dom/event.js +++ /dev/null @@ -1,95 +0,0 @@ -// -// DOM Events -// - -import { objectForEach } from '../object.js' -import { jQueryInstance } from '../jquery.js' -import { ieVersion } from '../ie.js' -import { catchFunctionErrors } from '../error.js' - -import { tagNameLower } from './info.js' -import { addDisposeCallback } from './disposal.js' -import options from '../options.js' - -// 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 = {} - -var keyEventTypeName = (options.global.navigator && /Firefox\/2/i.test(options.global.navigator.userAgent)) ? 'KeyboardEvent' : 'UIEvents' - -knownEvents[keyEventTypeName] = ['keyup', 'keydown', 'keypress'] - -knownEvents['MouseEvents'] = [ - 'click', 'dblclick', 'mousedown', 'mouseup', 'mousemove', 'mouseover', - 'mouseout', 'mouseenter', 'mouseleave'] - -objectForEach(knownEvents, function (eventType, knownEventsForType) { - if (knownEventsForType.length) { - for (var 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') -} - -// Workaround for an IE9 issue - https://github.com/SteveSanderson/knockout/issues/406 -var eventsThatMustBeRegisteredUsingAttachEvent = { 'propertychange': true } -let jQueryEventAttachName - -export function registerEventHandler (element, eventType, handler, 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' - } - jQueryInstance(element)[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) - - // 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) - }) - } else { - 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') } - - // 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) - - if (!options.useOnlyNativeEvents && jQueryInstance && !useClickWorkaround) { - 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") } - } else if (useClickWorkaround && element.click) { - element.click() - } else if (typeof element.fireEvent !== 'undefined') { - element.fireEvent('on' + eventType) - } else { - throw new Error("Browser doesn't support triggering events") - } -} diff --git a/packages/tko.utils/src/dom/event.ts b/packages/tko.utils/src/dom/event.ts new file mode 100644 index 00000000..0d660951 --- /dev/null +++ b/packages/tko.utils/src/dom/event.ts @@ -0,0 +1,101 @@ +// +// DOM Events +// + +import { objectForEach } from '../object'; +import { jQueryInstance } from '../jquery'; +import { ieVersion } from '../ie'; +import { catchFunctionErrors } from '../error'; + +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) +const knownEvents: {[evType: string]: string[]} = {}, + knownEventTypesByEventName: {[evType: string]: string} = {}; + +const keyEventTypeName = (options.global.navigator && /Firefox\/2/i.test(options.global.navigator.userAgent)) ? 'KeyboardEvent' : 'UIEvents'; + +knownEvents[keyEventTypeName] = ['keyup', 'keydown', 'keypress']; + +knownEvents.MouseEvents = [ + 'click', 'dblclick', 'mousedown', 'mouseup', 'mousemove', 'mouseover', + 'mouseout', 'mouseenter', 'mouseleave']; + +objectForEach(knownEvents, (eventType, knownEventsForType) => { + if (knownEventsForType.length) { + for (let i = 0, j = knownEventsForType.length; i < j; i++) { knownEventTypesByEventName[knownEventsForType[i]] = eventType; } + } +}); + +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 +const eventsThatMustBeRegisteredUsingAttachEvent: {[type: string]: boolean} = { propertychange: true }; +let jQueryEventAttachName: any; + +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'; + } + (jQueryInstance(element) as any)[jQueryEventAttachName](eventType, wrappedHandler); + } else if (!mustUseAttachEvent && typeof element.addEventListener === 'function') { + 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, () => { + (element as any).detachEvent(attachEventName, attachEventHandler); + }); + } else { + throw new Error("Browser doesn't support addEventListener or attachEvent"); + } +} + +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. + const useClickWorkaround = isClickOnCheckableElement(element, eventType); + + if (!options.useOnlyNativeEvents && jQueryInstance && !useClickWorkaround) { + jQueryInstance(element).trigger(eventType); + } else if (typeof document.createEvent === 'function') { + if (typeof element.dispatchEvent === 'function') { + 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 as any).fireEvent !== 'undefined') { + (element as any).fireEvent('on' + eventType); + } else { + throw new Error("Browser doesn't support triggering events"); + } +} diff --git a/packages/tko.utils/src/dom/fixes.js b/packages/tko.utils/src/dom/fixes.ts similarity index 68% rename from packages/tko.utils/src/dom/fixes.js rename to packages/tko.utils/src/dom/fixes.ts index e99b7903..0db13c7e 100644 --- a/packages/tko.utils/src/dom/fixes.js +++ 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.js b/packages/tko.utils/src/dom/html.ts similarity index 56% rename from packages/tko.utils/src/dom/html.js rename to packages/tko.utils/src/dom/html.ts index 09bade62..2bef2aa9 100644 --- a/packages/tko.utils/src/dom/html.js +++ 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