Skip to content

Commit

Permalink
Typescriptify part III utils 🦸‍♀️
Browse files Browse the repository at this point in the history
  • Loading branch information
ZeeJab committed Jun 3, 2020
1 parent 70da8d5 commit 2dccf98
Show file tree
Hide file tree
Showing 7 changed files with 135 additions and 87 deletions.
3 changes: 1 addition & 2 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -247,8 +247,7 @@ module.exports = {
"no-underscore-dangle": "off",
"no-unmodified-loop-condition": "off",
"no-unneeded-ternary": "off",
"no-unused-expressions": "error",
"no-use-before-define": "error",
"no-unused-expressions": "off",
"no-useless-backreference": "error",
"no-useless-call": "off",
"no-useless-computed-key": "error",
Expand Down
6 changes: 6 additions & 0 deletions src/js/utils/assert.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,9 @@ export default function (message: string, conditional: boolean) {
throw new MobiledocError(message)
}
}

export function assertNotNull<T>(message: string, value: T | null): asserts value is T {
if (value === null) {
throw new MobiledocError(message)
}
}
21 changes: 18 additions & 3 deletions src/js/utils/compiler.js → src/js/utils/compiler.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,28 @@
import { forEach } from './array-utils'
import assert from './assert'

export function visit(visitor, node, opcodes) {
type Opcode = [string] | [string, unknown] | [string, unknown, unknown]
type Opcodes = Opcode[]

interface Visitor {
[key: string]: (node: CompileNode, opcodes: Opcodes) => void
}

interface CompileNode {
type: string
}

export function visit(visitor: Visitor, node: CompileNode, opcodes: Opcodes) {
const method = node.type
assert(`Cannot visit unknown type ${method}`, !!visitor[method])
visitor[method](node, opcodes)
}

export function compile(compiler, opcodes) {
interface Compiler {
[key: string]: (...args: unknown[]) => void
}

export function compile(compiler: Compiler, opcodes: Opcodes) {
for (var i = 0, l = opcodes.length; i < l; i++) {
let [method, ...params] = opcodes[i]
let length = params.length
Expand All @@ -23,7 +38,7 @@ export function compile(compiler, opcodes) {
}
}

export function visitArray(visitor, nodes, opcodes) {
export function visitArray(visitor: Visitor, nodes: CompileNode[], opcodes: Opcodes) {
if (!nodes || nodes.length === 0) {
return
}
Expand Down
52 changes: 20 additions & 32 deletions src/js/utils/dom-utils.js → src/js/utils/dom-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,22 +6,26 @@ export const NODE_TYPES = {
COMMENT: 8,
}

function isTextNode(node) {
export function isTextNode(node: Node): node is Text {
return node.nodeType === NODE_TYPES.TEXT
}

function isCommentNode(node) {
export function isCommentNode(node: Node) {
return node.nodeType === NODE_TYPES.COMMENT
}

function isElementNode(node) {
export function isElementNode(node: Node): node is Element {
return node.nodeType === NODE_TYPES.ELEMENT
}

// perform a pre-order tree traversal of the dom, calling `callbackFn(node)`
// for every node for which `conditionFn(node)` is true
function walkDOM(topNode, callbackFn = () => {}, conditionFn = () => true) {
let currentNode = topNode
export function walkDOM(
topNode: Node,
callbackFn: (node: Node) => void = () => {},
conditionFn: (node: Node) => boolean = () => true
) {
let currentNode: Node | null = topNode

if (conditionFn(currentNode)) {
callbackFn(currentNode)
Expand All @@ -35,12 +39,12 @@ function walkDOM(topNode, callbackFn = () => {}, conditionFn = () => true) {
}
}

function walkTextNodes(topNode, callbackFn = () => {}) {
const conditionFn = node => isTextNode(node)
export function walkTextNodes(topNode: Node, callbackFn = () => {}) {
const conditionFn = (node: Node) => isTextNode(node)
walkDOM(topNode, callbackFn, conditionFn)
}

function clearChildNodes(element) {
export function clearChildNodes(element: Element) {
while (element.childNodes.length) {
element.removeChild(element.childNodes[0])
}
Expand All @@ -53,7 +57,7 @@ function clearChildNodes(element) {
* Mimics the behavior of `Node.contains`, which is broken in IE 10
* @private
*/
function containsNode(parentNode, childNode) {
export function containsNode(parentNode: Node, childNode: Node) {
if (parentNode === childNode) {
return true
}
Expand All @@ -68,8 +72,8 @@ function containsNode(parentNode, childNode) {
* @return {Object} key-value pairs
* @private
*/
function getAttributes(element) {
const result = {}
export function getAttributes(element: Element) {
const result: { [key: string]: unknown } = {}
if (element.hasAttributes()) {
forEach(element.attributes, ({ name, value }) => {
result[name] = value
Expand All @@ -78,42 +82,26 @@ function getAttributes(element) {
return result
}

function addClassName(element, className) {
export function addClassName(element: Element, className: string) {
element.classList.add(className)
}

function removeClassName(element, className) {
export function removeClassName(element: Element, className: string) {
element.classList.remove(className)
}

function normalizeTagName(tagName) {
export function normalizeTagName(tagName: string) {
return tagName.toLowerCase()
}

function parseHTML(html) {
export function parseHTML(html: string) {
const div = document.createElement('div')
div.innerHTML = html
return div
}

function serializeHTML(node) {
export function serializeHTML(node: Node) {
const div = document.createElement('div')
div.appendChild(node)
return div.innerHTML
}

export {
containsNode,
clearChildNodes,
getAttributes,
walkDOM,
walkTextNodes,
addClassName,
removeClassName,
normalizeTagName,
isTextNode,
isCommentNode,
isElementNode,
parseHTML,
serializeHTML,
}
29 changes: 0 additions & 29 deletions src/js/utils/element-map.js

This file was deleted.

38 changes: 38 additions & 0 deletions src/js/utils/element-map.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import assert from './assert'

// start at one to make the falsy semantics easier
let uuidGenerator = 1

interface ElementKey {
_uuid?: string
}

export default class ElementMap {
_map: {
[key: string]: unknown
} = {}

set(key: ElementKey, value: unknown) {
let uuid = key._uuid
if (!uuid) {
key._uuid = uuid = '' + uuidGenerator++
}
this._map[uuid] = value
}

get(key: ElementKey) {
if (key._uuid) {
return this._map[key._uuid]
}
return null
}

remove(key: ElementKey) {
assertHasUuid(key)
delete this._map[key._uuid]
}
}

function assertHasUuid(key: ElementKey): asserts key is { _uuid: string } {
assert('tried to fetch a value for an element not seen before', !!key._uuid)
}
73 changes: 52 additions & 21 deletions src/js/utils/selection-utils.js → src/js/utils/selection-utils.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,31 @@
import { DIRECTION } from '../utils/key'
import { isTextNode, isElementNode } from 'mobiledoc-kit/utils/dom-utils'
import { DIRECTION } from './key'
import { isTextNode, isElementNode } from './dom-utils'
import { assertNotNull } from './assert'

function clearSelection() {
window.getSelection().removeAllRanges()
export function clearSelection() {
const selection = window.getSelection()
selection && selection.removeAllRanges()
}

function textNodeRects(node) {
function textNodeRects(node: Text) {
let range = document.createRange()
range.setEnd(node, node.nodeValue.length)
range.setEnd(node, node.nodeValue!.length)
range.setStart(node, 0)
return range.getClientRects()
}

function findOffsetInTextNode(node, coords) {
let len = node.nodeValue.length
interface PartialCoords {
top: number
left: number
}

interface Coords extends PartialCoords {
bottom: number
right: number
}

function findOffsetInTextNode(node: Text, coords: PartialCoords) {
let len = node.nodeValue!.length
let range = document.createRange()
for (let i = 0; i < len; i++) {
range.setEnd(node, i + 1)
Expand All @@ -35,10 +47,10 @@ function findOffsetInTextNode(node, coords) {
* @return {Object} {node, offset}
*/
/* eslint-disable complexity */
function findOffsetInNode(node, coords) {
export function findOffsetInNode(node: Node, coords: PartialCoords): { node: Node; offset: number } {
let closest,
dyClosest = 1e8,
coordsClosest,
coordsClosest: PartialCoords,
offset = 0
for (let child = node.firstChild; child; child = child.nextSibling) {
let rects
Expand Down Expand Up @@ -73,16 +85,16 @@ function findOffsetInNode(node, coords) {
return { node, offset }
}
if (isTextNode(closest)) {
return findOffsetInTextNode(closest, coordsClosest)
return findOffsetInTextNode(closest, coordsClosest!)
}
if (closest.firstChild) {
return findOffsetInNode(closest, coordsClosest)
return findOffsetInNode(closest, coordsClosest!)
}
return { node, offset }
}
/* eslint-enable complexity */

function constrainNodeTo(node, parentNode, existingOffset) {
function constrainNodeTo(node: Node, parentNode: Node, existingOffset: number) {
let compare = parentNode.compareDocumentPosition(node)
if (compare & Node.DOCUMENT_POSITION_CONTAINED_BY) {
// the node is inside parentNode, do nothing
Expand All @@ -93,18 +105,18 @@ function constrainNodeTo(node, parentNode, existingOffset) {
} else if (compare & Node.DOCUMENT_POSITION_PRECEDING) {
// node is before parentNode. return start of deepest first child
let child = parentNode.firstChild
while (child.firstChild) {
while (child && child.firstChild) {
child = child.firstChild
}
return { node: child, offset: 0 }
} else if (compare & Node.DOCUMENT_POSITION_FOLLOWING) {
// node is after parentNode. return end of deepest last child
let child = parentNode.lastChild
let child = parentNode.lastChild!
while (child.lastChild) {
child = child.lastChild
}

let offset = isTextNode(child) ? child.textContent.length : 1
let offset = isTextNode(child) ? child.textContent!.length : 1
return { node: child, offset }
} else {
return { node, offset: existingOffset }
Expand All @@ -116,7 +128,10 @@ function constrainNodeTo(node, parentNode, existingOffset) {
* If the anchorNode or focusNode are outside the parentNode, they are replaced with the beginning
* or end of the parentNode's children
*/
function constrainSelectionTo(selection, parentNode) {
export function constrainSelectionTo(selection: Selection, parentNode: Node) {
assertNotNull('selection anchorNode should not be null', selection.anchorNode)
assertNotNull('selection focusNode should not be null', selection.focusNode)

let { node: anchorNode, offset: anchorOffset } = constrainNodeTo(
selection.anchorNode,
parentNode,
Expand All @@ -127,7 +142,25 @@ function constrainSelectionTo(selection, parentNode) {
return { anchorNode, anchorOffset, focusNode, focusOffset }
}

function comparePosition(selection) {
interface ComparePositionResult {
headNode: Node
headOffset: number
tailNode: Node
tailOffset: number
direction: number | null
}

interface PartialSelection {
focusNode: Node
focusOffset: number
anchorNode: Node
anchorOffset: number
}

export function comparePosition(selection: PartialSelection): ComparePositionResult {
assertNotNull('selection anchorNode should not be null', selection.anchorNode)
assertNotNull('selection focusNode should not be null', selection.focusNode)

let { anchorNode, focusNode, anchorOffset, focusOffset } = selection
let headNode, tailNode, headOffset, tailOffset, direction

Expand Down Expand Up @@ -155,7 +188,7 @@ function comparePosition(selection) {
while (focusNode.lastChild) {
focusNode = focusNode.lastChild
}
focusOffset = focusNode.textContent.length
focusOffset = focusNode.textContent!.length
}

return comparePosition({
Expand Down Expand Up @@ -207,5 +240,3 @@ function comparePosition(selection) {

return { headNode, headOffset, tailNode, tailOffset, direction }
}

export { clearSelection, comparePosition, findOffsetInNode, constrainSelectionTo }

0 comments on commit 2dccf98

Please sign in to comment.