diff --git a/.babelrc b/.babelrc deleted file mode 100644 index 6eadcda..0000000 --- a/.babelrc +++ /dev/null @@ -1,3 +0,0 @@ -{ - presets: ['es2015'] -} diff --git a/.npmignore b/.npmignore index 0cb3d6e..5b6c28d 100644 --- a/.npmignore +++ b/.npmignore @@ -2,4 +2,4 @@ src test .travis.yml .gitignore -.babelrc +tsconfig.json diff --git a/index.js b/index.js new file mode 100644 index 0000000..c519b43 --- /dev/null +++ b/index.js @@ -0,0 +1 @@ +module.exports = require('./lib').default diff --git a/nodes.js b/nodes.js index 10b3840..d6c6b58 100644 --- a/nodes.js +++ b/nodes.js @@ -1 +1 @@ -module.exports = require('./lib/nodes'); +module.exports = require('./lib/nodes').default; diff --git a/package.json b/package.json index ff2b1a0..6893771 100644 --- a/package.json +++ b/package.json @@ -14,15 +14,12 @@ }, "scripts": { "prepublish": "npm run build", - "build": "babel src -d lib", + "build": "tsc", "watch": "npm run build -- --watch", "test": "npm run build && karma start test/karma.conf.js && mocha test/nodejs_tests.js" }, - "main": "lib/index.js", + "main": "index.js", "devDependencies": { - "babel-cli": "^6.3.17", - "babel-loader": "^6.2.2", - "babel-preset-es2015": "^6.3.13", "chai": "^3.5.0", "jsdom": "^9.4.2", "karma": "^0.13.19", @@ -38,12 +35,13 @@ "mocha": "^2.4.5", "sinon": "^1.17.3", "sinon-chai": "^2.8.0", - "snabbdom": "~0", + "snabbdom": "~0.6.6", "socket.io": "1.4.6", + "typescript": "^2.2.1", "webpack": "^1.12.12" }, "peerDependencies": { - "snabbdom": "~0" + "snabbdom": "~0.6.6" }, "dependencies": { "html-parse-stringify2": "^1.2.1" diff --git a/src/event-listeners.js b/src/event-listeners.ts similarity index 93% rename from src/event-listeners.js rename to src/event-listeners.ts index 71dbd1f..8a2f39f 100644 --- a/src/event-listeners.js +++ b/src/event-listeners.ts @@ -1,5 +1,6 @@ // List from https://html.spec.whatwg.org/multipage/webappapis.html#globaleventhandlers. -export default [ + +const eventNames: Array = [ 'onabort', 'onautocomplete', 'onautocompleteerror', @@ -63,3 +64,5 @@ export default [ 'onvolumechange', 'onwaiting' ]; + +export default eventNames; diff --git a/src/html-element.d.ts b/src/html-element.d.ts new file mode 100644 index 0000000..bcff269 --- /dev/null +++ b/src/html-element.d.ts @@ -0,0 +1,11 @@ +// because TypeScript's HTMLElement seems lacking: https://github.com/Microsoft/TSJS-lib-generator/issues/101 +interface HTMLElement { + onautocomplete: (this: HTMLElement, ev: Event) => any + onautocompleteerror: (this: HTMLElement, ev: Event) => any + oncancel: (this: HTMLElement, ev: Event) => any + onclose: (this: HTMLElement, ev: Event) => any + ondragexit: (this: HTMLElement, ev: Event) => any + onresize: (this: HTMLElement, ev: Event) => any + onshow: (this: HTMLElement, ev: Event) => any + ontoggle: (this: HTMLElement, ev: Event) => any +} diff --git a/src/html-parse-stringify2.d.ts b/src/html-parse-stringify2.d.ts new file mode 100644 index 0000000..0e7d5d4 --- /dev/null +++ b/src/html-parse-stringify2.d.ts @@ -0,0 +1,14 @@ +// just enough to get us through +declare module 'html-parse-stringify2' { + export interface ASTNode { + type: string + content: string + name?: string + children?: ASTNode[] + attrs?: { + [key: string]: string + } + } + + export const parse: (html: string) => ASTNode[] +} diff --git a/src/index.js b/src/index.ts similarity index 72% rename from src/index.js rename to src/index.ts index 3cca129..c1bec82 100644 --- a/src/index.js +++ b/src/index.ts @@ -1,7 +1,8 @@ import virtualizeNode from './nodes'; import virtualizeString from './strings'; +import { Options } from './interfaces' -export default function (el, options) { +export default function (el: string | Node, options: Options) { if (typeof el === 'string') { return virtualizeString(el, options); } diff --git a/src/interfaces.ts b/src/interfaces.ts new file mode 100644 index 0000000..414e109 --- /dev/null +++ b/src/interfaces.ts @@ -0,0 +1,11 @@ +import { VNode } from 'snabbdom/vnode' + +interface Hooks { + create?: (vnode: VNode) => void +} + +export interface Options { + context?: HTMLDocument + hooks?: Hooks +} + diff --git a/src/nodes.js b/src/nodes.ts similarity index 74% rename from src/nodes.js rename to src/nodes.ts index a2644a6..1bae7c9 100644 --- a/src/nodes.js +++ b/src/nodes.ts @@ -1,38 +1,40 @@ import h from 'snabbdom/h'; +import { VNode, VNodeData } from 'snabbdom/vnode' import { createTextVNode, transformName } from './utils'; import listeners from './event-listeners'; +import { Options } from './interfaces' -export default function virtualizeNodes(element, options = {}) { +export default function virtualizeNodes(node: Node, options: Options = {}) { const context = options.context || document; - if (!element) { + if (!node) { return null; } - const createdVNodes = []; - const vnode = convertNode(element, createdVNodes, context); + const createdVNodes: VNode[] = []; + const vnode = convertNode(node, createdVNodes, context); options.hooks && options.hooks.create && createdVNodes.forEach((node) => { options.hooks.create(node); }); return vnode; } - -function convertNode(element, createdVNodes, context) { +function convertNode(node: Node, createdVNodes: VNode[], context: HTMLDocument): VNode { // If our node is a text node, then we only want to set the `text` part of // the VNode. - if (element.nodeType === context.defaultView.Node.TEXT_NODE) { - const newNode = createTextVNode(element.textContent, context); - newNode.elm = element; + if (node.nodeType === context.defaultView.Node.TEXT_NODE) { + const newNode = createTextVNode(node.textContent, context); + newNode.elm = node; createdVNodes.push(newNode); return newNode } + let element = node as HTMLElement // If not a text node, then build up a VNode based on the element's tag // name, class and style attributes, and remaining attributes. // Special values: style, class. We don't include these in the attrs hash // of the VNode. - const data = {}; + const data: VNodeData = {}; const classes = getClasses(element); if (Object.keys(classes).length !== 0) { data.class = classes; @@ -56,10 +58,10 @@ function convertNode(element, createdVNodes, context) { } // Check for event listeners. - const on = {}; + const on: { [key: string]: EventListener } = {}; listeners.forEach((key) => { if (element[key]) { - on[key.substring(2)] = element[key]; + on[key.substring(2)] = element[key] as EventListener; } }); if (Object.keys(on).length > 0) { @@ -82,9 +84,9 @@ function convertNode(element, createdVNodes, context) { } // Builds the class object for the VNode. -function getClasses(element) { +function getClasses(element: HTMLElement) { const className = element.className; - const classes = {}; + const classes: { [name: string]: boolean } = {}; if (className !== null && className.length > 0) { className.split(' ').forEach((className) => { classes[className] = true; @@ -94,9 +96,9 @@ function getClasses(element) { } // Builds the style object for the VNode. -function getStyle(element) { +function getStyle(element: HTMLElement) { const style = element.style; - const styles = {}; + const styles: { [name: string]: string } = {}; for (let i = 0; i < style.length; i++) { const name = style.item(i); const transformedName = transformName(name); diff --git a/src/strings.js b/src/strings.ts similarity index 80% rename from src/strings.js rename to src/strings.ts index d047741..7fc7b6c 100644 --- a/src/strings.js +++ b/src/strings.ts @@ -1,8 +1,10 @@ -import parse from 'html-parse-stringify2/lib/parse'; +import { parse, ASTNode } from 'html-parse-stringify2'; import h from 'snabbdom/h'; +import { VNode, VNodeData } from 'snabbdom/vnode' import { createTextVNode, transformName, unescapeEntities } from './utils'; +import { Options } from './interfaces' -export default function(html, options = {}) { +export default function(html: string, options: Options = {}) { const context = options.context || document; @@ -12,7 +14,7 @@ export default function(html, options = {}) { } // Maintain a list of created vnodes so we can call the create hook. - const createdVNodes = []; + const createdVNodes: VNode[] = []; // Parse the string into the AST and convert to VNodes. const vnodes = convertNodes(parse(html), createdVNodes, context); @@ -38,7 +40,7 @@ export default function(html, options = {}) { return res; } -function convertNodes(nodes, createdVNodes, context) { +function convertNodes(nodes: ASTNode[], createdVNodes: VNode[], context: HTMLDocument): VNode[] | undefined { if (nodes instanceof Array && nodes.length > 0) { return nodes.map((node) => { return toVNode(node, createdVNodes, context); }); } @@ -47,7 +49,7 @@ function convertNodes(nodes, createdVNodes, context) { } } -function toVNode(node, createdVNodes, context) { +function toVNode(node: ASTNode, createdVNodes: VNode[], context: HTMLDocument) { let newNode; if (node.type === 'text') { newNode = createTextVNode(node.content, context); @@ -59,8 +61,8 @@ function toVNode(node, createdVNodes, context) { return newNode; } -function buildVNodeData(node, context) { - const data = {}; +function buildVNodeData(node: ASTNode, context: HTMLDocument) { + const data: VNodeData = {}; if (!node.attrs) { return data; } @@ -71,7 +73,7 @@ function buildVNodeData(node, context) { memo ? memo[name] = val : memo = { [name]: val }; } return memo; - }, null); + }, null as null | { [key: string]: string }); if (attrs) { data.attrs = attrs; } @@ -89,7 +91,7 @@ function buildVNodeData(node, context) { return data; } -function parseStyle(node) { +function parseStyle(node: ASTNode) { try { return node.attrs.style.split(';').reduce((memo, styleProp) => { const res = styleProp.split(':'); @@ -99,14 +101,14 @@ function parseStyle(node) { memo ? memo[name] = val : memo = { [name]: val }; } return memo; - }, null); + }, null as null | { [key: string]: string }); } catch (e) { return null; } } -function parseClass(node) { +function parseClass(node: ASTNode) { try { return node.attrs.class.split(' ').reduce((memo, className) => { className = className.trim(); @@ -114,7 +116,7 @@ function parseClass(node) { memo ? memo[className] = true : memo = { [className]: true }; } return memo; - }, null); + }, null as null | { [key: string]: boolean }); } catch (e) { return null; diff --git a/src/utils.js b/src/utils.ts similarity index 66% rename from src/utils.js rename to src/utils.ts index 93170ba..e0a988f 100644 --- a/src/utils.js +++ b/src/utils.ts @@ -1,10 +1,10 @@ -import VNode from 'snabbdom/vnode'; +import vnode from 'snabbdom/vnode'; -export function createTextVNode(text, context) { - return VNode(undefined, undefined, undefined, unescapeEntities(text, context)); +export function createTextVNode(text: string, context: HTMLDocument) { + return vnode(undefined, undefined, undefined, unescapeEntities(text, context), undefined); } -export function transformName(name) { +export function transformName(name: string) { // Replace -a with A to help camel case style property names. name = name.replace( /-(\w)/g, function _replace( $1, $2 ) { return $2.toUpperCase(); @@ -17,9 +17,9 @@ export function transformName(name) { // Regex for matching HTML entities. const entityRegex = new RegExp('&[a-z0-9]+;', 'gi') // Element for setting innerHTML for transforming entities. -let el = null; +let el: HTMLDivElement | null = null; -export function unescapeEntities(text, context) { +export function unescapeEntities(text: string, context: HTMLDocument) { // Create the element using the context if it doesn't exist. if (!el) { el = context.createElement('div'); diff --git a/src/window.d.ts b/src/window.d.ts new file mode 100644 index 0000000..b213140 --- /dev/null +++ b/src/window.d.ts @@ -0,0 +1,3 @@ +interface Window { + Node: Node +} diff --git a/strings.js b/strings.js index 8bfbcde..d034544 100644 --- a/strings.js +++ b/strings.js @@ -1 +1 @@ -module.exports = require('./lib/strings'); +module.exports = require('./lib/strings').default; diff --git a/test/karma.conf.js b/test/karma.conf.js index 4a6d48f..4d85cc1 100644 --- a/test/karma.conf.js +++ b/test/karma.conf.js @@ -16,13 +16,6 @@ module.exports = function (config) { webpack: { devtool: 'inline-source-map', - module: { - loaders: [{ - test: /\.js$/, - exclude: /node_modules/, - loader: 'babel' - }] - } }, webpackMiddleware: { diff --git a/test/nodejs_tests.js b/test/nodejs_tests.js index ea5e9e7..8507663 100644 --- a/test/nodejs_tests.js +++ b/test/nodejs_tests.js @@ -1,11 +1,11 @@ "use strict"; const expect = require('chai').expect; -const virtualizeString = require('../strings').default; -const virtualizeNodes = require('../nodes').default; -const h = require('snabbdom/h'); +const virtualizeString = require('../strings') +const virtualizeNodes = require('../nodes') +const { h } = require('snabbdom/h'); const jsdom = require('jsdom').jsdom; const extendVnode = require('./lib/helpers').extendVnode; -const VNode = require('snabbdom/vnode'); +const { vnode } = require('snabbdom/vnode'); const opts = { context: (typeof document != 'undefined') ? document : jsdom('') }; @@ -43,10 +43,10 @@ describe("In a nodejs environment", () => { ul.innerHTML = "
  • One
  • Fish
  • Two
  • Fish
  • "; expect(virtualizeNodes(ul, opts)).to.deep.equal( extendVnode(h('ul', [ - extendVnode(h('li', [ extendVnode(VNode(undefined, undefined, undefined, 'One'), ul.childNodes[0].firstChild) ]), ul.childNodes[0]), - extendVnode(h('li', [ extendVnode(VNode(undefined, undefined, undefined, 'Fish'), ul.childNodes[1].firstChild) ]), ul.childNodes[1]), - extendVnode(h('li', [ extendVnode(VNode(undefined, undefined, undefined, 'Two'), ul.childNodes[2].firstChild) ]), ul.childNodes[2]), - extendVnode(h('li', [ extendVnode(VNode(undefined, undefined, undefined, 'Fish'), ul.childNodes[3].firstChild) ]), ul.childNodes[3]) + extendVnode(h('li', [ extendVnode(vnode(undefined, undefined, undefined, 'One'), ul.childNodes[0].firstChild) ]), ul.childNodes[0]), + extendVnode(h('li', [ extendVnode(vnode(undefined, undefined, undefined, 'Fish'), ul.childNodes[1].firstChild) ]), ul.childNodes[1]), + extendVnode(h('li', [ extendVnode(vnode(undefined, undefined, undefined, 'Two'), ul.childNodes[2].firstChild) ]), ul.childNodes[2]), + extendVnode(h('li', [ extendVnode(vnode(undefined, undefined, undefined, 'Fish'), ul.childNodes[3].firstChild) ]), ul.childNodes[3]) ]), ul) ); }); @@ -55,7 +55,7 @@ describe("In a nodejs environment", () => { const div = doc.createElement('div'); div.innerHTML = "& is an ampersand! and ½ is 1/2!"; expect(virtualizeNodes(div, opts)).to.deep.equal( - extendVnode(h('div', [ extendVnode(VNode(undefined, undefined, undefined,'& is an ampersand! and ½ is 1/2!'), div.firstChild) ]), div) + extendVnode(h('div', [ extendVnode(vnode(undefined, undefined, undefined,'& is an ampersand! and ½ is 1/2!'), div.firstChild) ]), div) ); }); }); diff --git a/test/tests/index_test.js b/test/tests/index_test.js index 3a250e2..88d5cdb 100644 --- a/test/tests/index_test.js +++ b/test/tests/index_test.js @@ -1,7 +1,7 @@ -import virtualize from '../../src/index'; -import h from 'snabbdom/h'; -import VNode from 'snabbdom/vnode'; -import { extendVnode } from '../lib/helpers'; +const virtualize = require('../..'); +const { h } = require('snabbdom/h'); +const { vnode } = require('snabbdom/vnode'); +const { extendVnode } = require('../lib/helpers'); describe("virtualize", () => { @@ -57,10 +57,10 @@ describe("virtualize", () => { expect(virtualize(top)).to.deep.equal(extendVnode(h('div', { class: { container: true }, style: { position: 'absolute' } }, [ extendVnode(h('ul', [ extendVnode(h('li', { class: { first: true } }, [ - extendVnode(VNode(undefined, undefined, undefined, 'First'), child2a.firstChild) + extendVnode(vnode(undefined, undefined, undefined, 'First'), child2a.firstChild) ]), child2a), extendVnode(h('li', { class: { second: true }, style: { fontWeight: '300' } }, [ - extendVnode(VNode(undefined, undefined, undefined, 'Second'), child2b.firstChild) + extendVnode(vnode(undefined, undefined, undefined, 'Second'), child2b.firstChild) ]), child2b) ]), child1) ]), top)); @@ -83,15 +83,15 @@ describe("virtualize", () => { top.appendChild(text3); expect(virtualize(top)).to.deep.equal( extendVnode(h('p', { class: { container: true } }, [ - extendVnode(VNode(undefined, undefined, undefined, 'Hey there, '), text1), + extendVnode(vnode(undefined, undefined, undefined, 'Hey there, '), text1), extendVnode(h('a', { attrs: { href: 'http://example.com' }}, [ - extendVnode(VNode(undefined, undefined, undefined, 'check out this link'), link.firstChild) + extendVnode(vnode(undefined, undefined, undefined, 'check out this link'), link.firstChild) ]), link), - extendVnode(VNode(undefined, undefined, undefined, '. And this '), text2), + extendVnode(vnode(undefined, undefined, undefined, '. And this '), text2), extendVnode(h('code', { class: { javascript: true } }, [ - extendVnode(VNode(undefined, undefined, undefined, 'niceLookingCode();'), code.firstChild) + extendVnode(vnode(undefined, undefined, undefined, 'niceLookingCode();'), code.firstChild) ]), code), - extendVnode(VNode(undefined, undefined, undefined, '.'), text3), + extendVnode(vnode(undefined, undefined, undefined, '.'), text3), ]), top) ); }); @@ -115,14 +115,14 @@ describe("virtualize", () => { expect(virtualize('This is something.')) .to.deep.equal( h('span', [ - VNode(undefined, undefined, undefined, 'This is something.') + vnode(undefined, undefined, undefined, 'This is something.') ]) ); }); it("should handle a single text node", () => { expect(virtualize('Text content!')).to.deep.equal( - VNode(undefined, undefined, undefined, 'Text content!') + vnode(undefined, undefined, undefined, 'Text content!') ); }); diff --git a/test/tests/nodes_test.js b/test/tests/nodes_test.js index 0df8a0a..bfdf53c 100644 --- a/test/tests/nodes_test.js +++ b/test/tests/nodes_test.js @@ -1,5 +1,5 @@ -import virtualizeNodes from '../../src/nodes'; -import h from 'snabbdom/h'; +const virtualizeNodes = require('../../nodes'); +const { h } = require('snabbdom/h'); describe("#virtualizeNodes", () => { diff --git a/test/tests/strings_test.js b/test/tests/strings_test.js index 0477beb..2fa350c 100644 --- a/test/tests/strings_test.js +++ b/test/tests/strings_test.js @@ -1,5 +1,5 @@ -import virtualizeString from '../../src/strings'; -import h from 'snabbdom/h'; +const { default: virtualizeString } = require('../../lib/strings'); +const { h } = require('snabbdom/h'); describe("#virtualizeString", () => { diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..734618a --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "module": "commonjs", + "target": "es5", + "noImplicitAny": true, + "strictNullChecks": false, + "sourceMap": true, + "outDir": "lib" + }, + "include": [ + "src/**/*.ts" + ] +}