diff --git a/.eslintrc.js b/.eslintrc.js new file mode 100644 index 0000000..e8f70fd --- /dev/null +++ b/.eslintrc.js @@ -0,0 +1,248 @@ +module.exports = { + parser: '@typescript-eslint/parser', + parserOptions: { + project: 'tsconfig.json', + sourceType: 'module', + }, + plugins: [ + '@typescript-eslint/eslint-plugin', + 'prettier', + 'import', + ], + extends: [ + "eslint:all", + 'plugin:@typescript-eslint/all', + 'prettier', + 'prettier/@typescript-eslint', + ], + root: true, + env: { + es6: true, + node: true, + jest: true, + }, + rules: { + "array-bracket-newline": [ + "error", + "consistent" + ], + "array-element-newline": [ + "error", + "consistent" + ], + "arrow-parens": [ + "error", + "as-needed" + ], + "class-methods-use-this": "off", + "capitalized-comments": "off", + "comma-dangle": [ + "error", + "always-multiline" + ], + "dot-location": [ + "error", + "property" + ], + "func-names": [ + "error", + "as-needed" + ], + "id-length": "off", + "implicit-arrow-linebreak": "off", + "init-declarations": "off", + "max-len": [ + "error", + 120 + ], + "max-lines": "off", + "max-lines-per-function": "off", + "max-params": "off", + "max-statements": "off", + "multiline-comment-style": "off", + "multiline-ternary": [ + "error", + "always-multiline" + ], + "newline-per-chained-call": [ + "error", + { + "ignoreChainWithDepth": 2 + } + ], + "no-await-in-loop": "off", + "no-bitwise": "off", + "no-confusing-arrow": "off", + "no-constant-condition": [ + "error", + { + "checkLoops": false + } + ], + "no-continue": "off", + "no-duplicate-imports": "off", + "no-param-reassign": "off", + "no-underscore-dangle": "off", + "import/no-duplicates": [ + "error", + { + "considerQueryString": true + } + ], + "no-empty": "off", + "no-implicit-coercion": "off", + "no-invalid-this": "off", + "no-mixed-operators": "off", + "no-multiple-empty-lines": [ + "error", + { + "max": 2, + "maxEOF": 1 + } + ], + "no-plusplus": [ + "error", + { + "allowForLoopAfterthoughts": true + } + ], + "no-process-env": "off", + "no-shadow": "off", + "no-ternary": "off", + "no-unused-expressions": "off", + "no-warning-comments": "off", + "new-cap": "off", + "one-var": [ + "error", + "never" + ], + "padded-blocks": [ + "error", + "never" + ], + "padding-line-between-statements": [ + "error", + { + "blankLine": "always", + "prev": [ + "const", + "let", + "var" + ], + "next": "*" + }, + { + "blankLine": "always", + "prev": "*", + "next": [ + "const", + "let", + "var" + ] + }, + { + "blankLine": "any", + "prev": [ + "const", + "let", + "var" + ], + "next": [ + "const", + "let", + "var" + ] + }, + { + "blankLine": "always", + "prev": "*", + "next": "return" + }, + { + "blankLine": "always", + "prev": "block-like", + "next": "*" + }, + { + "blankLine": "always", + "prev": "*", + "next": "block-like" + } + ], + "prefer-destructuring": "off", + "quote-props": [ + "error", + "as-needed" + ], + "quotes": [ + "error", + "single", + { + "avoidEscape": true + } + ], + "object-curly-spacing": [ + "error", + "always" + ], + "require-atomic-updates": "off", + "require-unicode-regexp": "off", + "semi": "off", + "sort-imports": "off", + "sort-keys": "off", + "wrap-regex": "off", + "import/default": "off", + + "@typescript-eslint/explicit-function-return-type": "off", + "@typescript-eslint/explicit-member-accessibility": [ + "error", + { + "accessibility": "no-public" + } + ], + "@typescript-eslint/explicit-module-boundary-types": "off", + "@typescript-eslint/generic-type-naming": "off", + "@typescript-eslint/init-declarations": "off", + "@typescript-eslint/naming-convention": "off", + "@typescript-eslint/no-dynamic-delete": "off", + "@typescript-eslint/no-empty-function": "off", + "@typescript-eslint/no-explicit-any": "off", + "@typescript-eslint/no-extraneous-class": [ + "error", + { + "allowWithDecorator": true + } + ], + "@typescript-eslint/no-invalid-this": "off", + "@typescript-eslint/no-invalid-void-type": "off", + "@typescript-eslint/no-magic-numbers": "off", + "@typescript-eslint/no-misused-promises": "off", + "@typescript-eslint/no-parameter-properties": "off", + "@typescript-eslint/no-throw-literal": "off", + "@typescript-eslint/no-type-alias": "off", + "@typescript-eslint/no-unnecessary-condition": "off", + "@typescript-eslint/no-unsafe-assignment": "off", + "@typescript-eslint/no-unused-expressions": "off", + "@typescript-eslint/no-unused-vars": [ + "error", + { + "argsIgnorePattern": "^_$", + "varsIgnorePattern": "^_$", + "ignoreRestSiblings": true + } + ], + "@typescript-eslint/prefer-nullish-coalescing": "off", + "@typescript-eslint/prefer-readonly-parameter-types": "off", + "@typescript-eslint/promise-function-async": "off", + "@typescript-eslint/restrict-template-expressions": [ + "warn", + { + "allowNumber": true + } + ], + "@typescript-eslint/semi": "error", + "@typescript-eslint/strict-boolean-expressions": "off", + "@typescript-eslint/typedef": "off", + "prettier/prettier": "error" + }, +}; diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..2b78c00 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,9 @@ +{ + "singleQuote": true, + "trailingComma": "all", + "printWidth": 120, + "tabWidth": 4, + "arrowParens": "avoid", + "bracketSpacing": true, + "parser": "typescript" +} diff --git a/package.json b/package.json index 46da4a9..75041ab 100644 --- a/package.json +++ b/package.json @@ -5,6 +5,7 @@ "main": "dist/web-highlighter.min.js", "browser": "dist/web-highlighter.min.js", "scripts": { + "lint": "eslint \"src/**/*.ts\" --fix", "test": "mocha -r ts-node/register -r tsconfig-paths/register test/**.spec.ts", "coverage": "nyc -r lcov -e .ts -x \"test/**/*.ts\" npm run test", "serve-example": "http-server example/static", @@ -20,9 +21,15 @@ }, "husky": { "hooks": { - "pre-commit": "npm run test" + "pre-commit": "lint-staged && npm run test" } }, + "lint-staged": { + "src/**/*.ts": [ + "prettier --write", + "eslint --fix" + ] + }, "homepage": "https://alienzhou.github.io/web-highlighter", "repository": { "type": "git", @@ -46,21 +53,29 @@ "@types/jsdom-global": "^3.0.2", "@types/mocha": "^7.0.2", "@types/sinon": "^9.0.1", + "@typescript-eslint/eslint-plugin": "^4.13.0", + "@typescript-eslint/parser": "^4.13.0", "better-opn": "^1.0.0", "chai": "^4.2.0", "chalk": "^2.4.2", "clean-webpack-plugin": "^1.0.0", "coveralls": "^3.1.0", "css-loader": "^1.0.1", + "eslint": "^7.18.0", + "eslint-config-prettier": "^7.1.0", + "eslint-plugin-import": "^2.22.1", + "eslint-plugin-prettier": "^3.3.1", "fs-extra": "^7.0.1", "html-webpack-plugin": "^3.2.0", "http-server": "^0.11.1", - "husky": "^4.2.5", + "husky": "^4.3.8", "jsdom": "^16.2.2", "jsdom-global": "^3.0.2", + "lint-staged": "^10.5.3", "mocha": "^7.1.2", "npm-run-all": "^4.1.5", "nyc": "^15.0.1", + "prettier": "^2.2.1", "showdown": "^1.9.0", "sinon": "^9.0.2", "style-loader": "^0.23.1", @@ -69,7 +84,7 @@ "ts-node": "^8.10.1", "tsconfig-paths": "^3.9.0", "tscpaths": "0.0.9", - "typescript": "^3.1.6", + "typescript": "^4.1.3", "webpack": "^4.25.1", "webpack-cli": "^3.1.2", "webpack-dev-server": ">=3.1.11", diff --git a/src/data/cache.ts b/src/data/cache.ts index e96b68f..16d5251 100644 --- a/src/data/cache.ts +++ b/src/data/cache.ts @@ -1,14 +1,10 @@ import EventEmitter from '@src/util/event.emitter'; -import HighlightSource from '../model/source'; -import {ERROR} from '../types' +import type HighlightSource from '../model/source'; +import { ERROR } from '../types'; class Cache extends EventEmitter { private _data: Map = new Map(); - constructor() { - super(); - } - get data() { return this.getAll(); } @@ -20,8 +16,10 @@ class Cache extends EventEmitter { save(source: HighlightSource | HighlightSource[]): void { if (!Array.isArray(source)) { this._data.set(source.id, source); + return; } + source.forEach(s => this._data.set(s.id, s)); } @@ -35,20 +33,25 @@ class Cache extends EventEmitter { getAll(): HighlightSource[] { const list: HighlightSource[] = []; - for (let pair of this._data) { + + for (const pair of this._data) { list.push(pair[1]); } + return list; } removeAll(): string[] { const ids: string[] = []; - for (let pair of this._data) { + + for (const pair of this._data) { ids.push(pair[0]); } + this._data = new Map(); + return ids; } } -export default Cache; \ No newline at end of file +export default Cache; diff --git a/src/index.ts b/src/index.ts index f9a3d60..835f1c6 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,4 +1,3 @@ -// import '@src/util/dataset.polyfill'; import EventEmitter from '@src/util/event.emitter'; import HighlightRange from '@src/model/range'; import HighlightSource from '@src/model/source'; @@ -7,20 +6,9 @@ import Hook from '@src/util/hook'; import getInteraction from '@src/util/interaction'; import Cache from '@src/data/cache'; import Painter from '@src/painter'; -import { - eventEmitter, - getDefaultOptions, - INTERNAL_ERROR_EVENT -} from '@src/util/const'; -import { - ERROR, - DomNode, - DomMeta, - HookMap, - EventType, - CreateFrom, - HighlighterOptions -} from './types'; +import { eventEmitter, getDefaultOptions, INTERNAL_ERROR_EVENT } from '@src/util/const'; +import type { DomNode, DomMeta, HookMap, HighlighterOptions } from './types'; +import { ERROR, EventType, CreateFrom } from './types'; import { addClass, removeClass, @@ -30,200 +18,256 @@ import { getHighlightsByRoot, getHighlightId, addEventListener, - removeEventListener + removeEventListener, } from '@src/util/dom'; export default class Highlighter extends EventEmitter { static event = EventType; - static isHighlightSource = (d: any) => { - return !!d.__isHighlightSource; - } + static isHighlightWrapNode = isHighlightWrapNode; - private _hoverId: string; - private event = getInteraction(); options: HighlighterOptions; + hooks: HookMap; + painter: Painter; + cache: Cache; + private _hoverId: string; + + private readonly event = getInteraction(); + constructor(options?: HighlighterOptions) { super(); this.options = getDefaultOptions(); - this.hooks = this._getHooks(); // initialize hooks + // initialize hooks + this.hooks = this._getHooks(); this.setOption(options); - this.cache = new Cache(); // initialize cache - const $root = this.options.$root; - addEventListener($root, this.event.PointerOver, this._handleHighlightHover); // initialize event listener - addEventListener($root, this.event.PointerTap, this._handleHighlightClick); // initialize event listener - eventEmitter.on(INTERNAL_ERROR_EVENT, this._handleError); - } + // initialize cache + this.cache = new Cache(); - private _getHooks = (): HookMap => ({ - Render: { - UUID: new Hook('Render.UUID'), - SelectedNodes: new Hook('Render.SelectedNodes'), - WrapNode: new Hook('Render.WrapNode') - }, - Serialize: { - Restore: new Hook('Serialize.Restore'), - RecordInfo: new Hook('Serialize.RecordInfo') - }, - Remove: { - UpdateNodes: new Hook('Remove.UpdateNodes') - } - }); - - private _highlightFromHRange = (range: HighlightRange): HighlightSource => { - const source: HighlightSource = range.serialize(this.options.$root, this.hooks); - const $wraps = this.painter.highlightRange(range); - if ($wraps.length === 0) { - eventEmitter.emit(INTERNAL_ERROR_EVENT, { - type: ERROR.DOM_SELECTION_EMPTY - }); - return null; - } - this.cache.save(source); - this.emit(EventType.CREATE, {sources: [source], type: CreateFrom.INPUT}, this); - return source; - } - - private _highlightFromHSource(sources: HighlightSource[] | HighlightSource = []) { - const renderedSources: Array = this.painter.highlightSource(sources);; - this.emit(EventType.CREATE, {sources: renderedSources, type: CreateFrom.STORE}, this); - this.cache.save(sources); - } + const $root = this.options.$root; - private _handleSelection = (e?: Event) => { - const range = HighlightRange.fromSelection(this.hooks.Render.UUID); - if (range) { - this._highlightFromHRange(range); - HighlightRange.removeDomRange(); - } + // initialize event listener + addEventListener($root, this.event.PointerOver, this._handleHighlightHover); + // initialize event listener + addEventListener($root, this.event.PointerTap, this._handleHighlightClick); + eventEmitter.on(INTERNAL_ERROR_EVENT, this._handleError); } - private _handleHighlightHover = e => { - const $target = e.target as HTMLElement; - if (!isHighlightWrapNode($target)) { - this._hoverId && this.emit(EventType.HOVER_OUT, {id: this._hoverId}, this, e); - this._hoverId = null; - return; - } - - const id = getHighlightId($target, this.options.$root); - // prevent trigger in the same highlight range - if (this._hoverId === id) { - return; - } - - // hover another highlight range, need to trigger previous highlight hover out event - if (this._hoverId) { - this.emit(EventType.HOVER_OUT, {id: this._hoverId}, this, e); - } - this._hoverId = id; - this.emit(EventType.HOVER, {id: this._hoverId}, this, e); - } + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + static isHighlightSource = (d: any) => !!d.__isHighlightSource; - private _handleError = (type: string, detail?) => { - if(this.options.verbose) { - console.warn(type); - } - } + run = () => addEventListener(this.options.$root, this.event.PointerEnd, this._handleSelection); - private _handleHighlightClick = (e): void => { - const $target = e.target as HTMLElement; - if (isHighlightWrapNode($target)) { - const id = getHighlightId($target, this.options.$root); - this.emit(EventType.CLICK, {id}, this, e); - } - } + stop = () => { + removeEventListener(this.options.$root, this.event.PointerEnd, this._handleSelection); + }; - run = () => addEventListener(this.options.$root, this.event.PointerEnd, this._handleSelection); - stop = () => removeEventListener(this.options.$root, this.event.PointerEnd, this._handleSelection); + addClass = (className: string, id?: string) => { + this.getDoms(id).forEach($n => { + addClass($n, className); + }); + }; - addClass = (className: string, id?: string) => this.getDoms(id).forEach($n => addClass($n, className)); - removeClass = (className: string, id?: string) => this.getDoms(id).forEach($n => removeClass($n, className)); + removeClass = (className: string, id?: string) => { + this.getDoms(id).forEach($n => { + removeClass($n, className); + }); + }; getIdByDom = ($node: HTMLElement): string => getHighlightId($node, this.options.$root); + getExtraIdByDom = ($node: HTMLElement): string[] => getExtraHighlightId($node, this.options.$root); - getDoms = (id?: string): Array => id - ? getHighlightById(this.options.$root, id, this.options.wrapTag) - : getHighlightsByRoot(this.options.$root, this.options.wrapTag); + + getDoms = (id?: string): HTMLElement[] => + id + ? getHighlightById(this.options.$root, id, this.options.wrapTag) + : getHighlightsByRoot(this.options.$root, this.options.wrapTag); dispose = () => { const $root = this.options.$root; + removeEventListener($root, this.event.PointerOver, this._handleHighlightHover); removeEventListener($root, this.event.PointerEnd, this._handleSelection); removeEventListener($root, this.event.PointerTap, this._handleHighlightClick); this.removeAll(); - } + }; setOption = (options?: HighlighterOptions) => { this.options = { ...this.options, - ...options + ...options, }; - this.painter = new Painter({ - $root: this.options.$root, - wrapTag: this.options.wrapTag, - className: this.options.style.className, - exceptSelectors: this.options.exceptSelectors - }, this.hooks); - } + this.painter = new Painter( + { + $root: this.options.$root, + wrapTag: this.options.wrapTag, + className: this.options.style.className, + exceptSelectors: this.options.exceptSelectors, + }, + this.hooks, + ); + }; fromRange = (range: Range): HighlightSource => { const start: DomNode = { $node: range.startContainer, - offset: range.startOffset + offset: range.startOffset, }; const end: DomNode = { $node: range.endContainer, - offset: range.endOffset - } + offset: range.endOffset, + }; const text = range.toString(); let id = this.hooks.Render.UUID.call(start, end, text); - id = id !== undefined && id !== null ? id : uuid(); + + id = typeof id !== 'undefined' && id !== null ? id : uuid(); + const hRange = new HighlightRange(start, end, text, id); + if (!hRange) { eventEmitter.emit(INTERNAL_ERROR_EVENT, { - type: ERROR.RANGE_INVALID + type: ERROR.RANGE_INVALID, }); + return null; } + return this._highlightFromHRange(hRange); - } + }; fromStore = (start: DomMeta, end: DomMeta, text: string, id: string, extra?: unknown): HighlightSource => { try { const hs = new HighlightSource(start, end, text, id, extra); + this._highlightFromHSource(hs); + return hs; - } - catch (err) { + } catch (err: unknown) { eventEmitter.emit(INTERNAL_ERROR_EVENT, { type: ERROR.HIGHLIGHT_SOURCE_RECREATE, - detail: { err, id, text, start, end } + detail: { err, id, text, start, end }, }); + return null; } - } + }; remove(id: string) { if (!id) { return; } + const doseExist = this.painter.removeHighlight(id); + this.cache.remove(id); + // only emit REMOVE event when highlight exist if (doseExist) { - this.emit(EventType.REMOVE, {ids: [id]}, this); + this.emit(EventType.REMOVE, { ids: [id] }, this); } } removeAll() { this.painter.removeAllHighlight(); + const ids = this.cache.removeAll(); - this.emit(EventType.REMOVE, {ids: ids}, this); + + this.emit(EventType.REMOVE, { ids }, this); } + + private readonly _getHooks = (): HookMap => ({ + Render: { + UUID: new Hook('Render.UUID'), + SelectedNodes: new Hook('Render.SelectedNodes'), + WrapNode: new Hook('Render.WrapNode'), + }, + Serialize: { + Restore: new Hook('Serialize.Restore'), + RecordInfo: new Hook('Serialize.RecordInfo'), + }, + Remove: { + UpdateNodes: new Hook('Remove.UpdateNodes'), + }, + }); + + private readonly _highlightFromHRange = (range: HighlightRange): HighlightSource => { + const source: HighlightSource = range.serialize(this.options.$root, this.hooks); + const $wraps = this.painter.highlightRange(range); + + if ($wraps.length === 0) { + eventEmitter.emit(INTERNAL_ERROR_EVENT, { + type: ERROR.DOM_SELECTION_EMPTY, + }); + + return null; + } + + this.cache.save(source); + this.emit(EventType.CREATE, { sources: [source], type: CreateFrom.INPUT }, this); + + return source; + }; + + private _highlightFromHSource(sources: HighlightSource | HighlightSource[] = []) { + const renderedSources: HighlightSource[] = this.painter.highlightSource(sources); + + this.emit(EventType.CREATE, { sources: renderedSources, type: CreateFrom.STORE }, this); + this.cache.save(sources); + } + + private readonly _handleSelection = () => { + const range = HighlightRange.fromSelection(this.hooks.Render.UUID); + + if (range) { + this._highlightFromHRange(range); + HighlightRange.removeDomRange(); + } + }; + + private readonly _handleHighlightHover = (e: Event) => { + const $target = e.target as HTMLElement; + + if (!isHighlightWrapNode($target)) { + this._hoverId && this.emit(EventType.HOVER_OUT, { id: this._hoverId }, this, e); + this._hoverId = null; + + return; + } + + const id = getHighlightId($target, this.options.$root); + + // prevent trigger in the same highlight range + if (this._hoverId === id) { + return; + } + + // hover another highlight range, need to trigger previous highlight hover out event + if (this._hoverId) { + this.emit(EventType.HOVER_OUT, { id: this._hoverId }, this, e); + } + + this._hoverId = id; + this.emit(EventType.HOVER, { id: this._hoverId }, this, e); + }; + + private readonly _handleError = (type: string) => { + if (this.options.verbose) { + // eslint-disable-next-line no-console + console.warn(type); + } + }; + + private readonly _handleHighlightClick = (e: Event) => { + const $target = e.target as HTMLElement; + + if (isHighlightWrapNode($target)) { + const id = getHighlightId($target, this.options.$root); + + this.emit(EventType.CLICK, { id }, this, e); + } + }; } diff --git a/src/model/range/dom.ts b/src/model/range/dom.ts index d8fe5c1..abd6f95 100644 --- a/src/model/range/dom.ts +++ b/src/model/range/dom.ts @@ -2,17 +2,19 @@ * some dom operations about HighlightRange */ -import {CAMEL_DATASET_IDENTIFIER, ROOT_IDX, UNKNOWN_IDX} from '@src/util/const'; -import {DomMeta, DomNode} from '@src/types'; +import { CAMEL_DATASET_IDENTIFIER, ROOT_IDX, UNKNOWN_IDX } from '@src/util/const'; +import type { DomMeta, DomNode } from '@src/types'; const countGlobalNodeIndex = ($node: Node, $root: Document | HTMLElement): number => { const tagName = ($node as HTMLElement).tagName; const $list = $root.getElementsByTagName(tagName); + for (let i = 0; i < $list.length; i++) { if ($node === $list[i]) { return i; } } + return UNKNOWN_IDX; }; @@ -21,71 +23,66 @@ const countGlobalNodeIndex = ($node: Node, $root: Document | HTMLElement): numbe * (without offset in current node) */ const getTextPreOffset = ($root: Node, $text: Node): number => { - const nodeStack: Array = [$root]; + const nodeStack: Node[] = [$root]; let $curNode: Node = null; let offset = 0; - while ($curNode = nodeStack.pop()) { + + while (($curNode = nodeStack.pop())) { const children = $curNode.childNodes; + for (let i = children.length - 1; i >= 0; i--) { nodeStack.push(children[i]); } if ($curNode.nodeType === 3 && $curNode !== $text) { offset += $curNode.textContent.length; - } - else if ($curNode.nodeType === 3) { + } else if ($curNode.nodeType === 3) { break; } } return offset; -} +}; /** * find the original dom parent node (none highlight dom) */ -const getOriginParent = ($node: Text | HTMLElement): HTMLElement => { - if ( - $node instanceof HTMLElement - && (!$node.dataset || !$node.dataset[CAMEL_DATASET_IDENTIFIER]) - ) { +const getOriginParent = ($node: HTMLElement | Text): HTMLElement => { + if ($node instanceof HTMLElement && (!$node.dataset || !$node.dataset[CAMEL_DATASET_IDENTIFIER])) { return $node; } let $originParent = $node.parentNode as HTMLElement; - while ( - $originParent.dataset - && $originParent.dataset[CAMEL_DATASET_IDENTIFIER] - ) { + + while ($originParent?.dataset[CAMEL_DATASET_IDENTIFIER]) { $originParent = $originParent.parentNode as HTMLElement; } + return $originParent; }; -export const getDomMeta = ($node: Text | HTMLElement, offset: number, $root: Document | HTMLElement): DomMeta => { +export const getDomMeta = ($node: HTMLElement | Text, offset: number, $root: Document | HTMLElement): DomMeta => { const $originParent = getOriginParent($node); - const index = $originParent === $root - ? ROOT_IDX - : countGlobalNodeIndex($originParent, $root); + const index = $originParent === $root ? ROOT_IDX : countGlobalNodeIndex($originParent, $root); const preNodeOffset = getTextPreOffset($originParent, $node); const tagName = $originParent.tagName; return { parentTagName: tagName, parentIndex: index, - textOffset: preNodeOffset + offset + textOffset: preNodeOffset + offset, }; }; export const formatDomNode = (n: DomNode): DomNode => { if ( // Text - n.$node.nodeType === 3 + n.$node.nodeType === 3 || // CDATASection - || n.$node.nodeType === 4 + n.$node.nodeType === 4 || // Comment - || n.$node.nodeType === 8 + n.$node.nodeType === 8 ) { return n; } @@ -94,4 +91,4 @@ export const formatDomNode = (n: DomNode): DomNode => { $node: n.$node.childNodes[n.offset], offset: 0, }; -} \ No newline at end of file +}; diff --git a/src/model/range/index.ts b/src/model/range/index.ts index d39ddfb..63d3bcb 100644 --- a/src/model/range/index.ts +++ b/src/model/range/index.ts @@ -5,78 +5,81 @@ */ import HighlightSource from '../source/index'; -import {DomNode, ERROR, HookMap} from '@src/types'; -import {getDomRange, removeSelection} from './selection'; -import Hook from '@src/util/hook'; +import type { DomNode, HookMap } from '@src/types'; +import { ERROR } from '@src/types'; +import { getDomRange, removeSelection } from './selection'; +import type Hook from '@src/util/hook'; import uuid from '@src/util/uuid'; -import {getDomMeta, formatDomNode} from './dom'; +import { getDomMeta, formatDomNode } from './dom'; import { eventEmitter, INTERNAL_ERROR_EVENT } from '@src/util/const'; class HighlightRange { + static removeDomRange = removeSelection; + start: DomNode; + end: DomNode; + text: string; + id: string; + frozen: boolean; - static removeDomRange = removeSelection; + constructor(start: DomNode, end: DomNode, text: string, id: string, frozen = false) { + if (start.$node.nodeType !== 3 || end.$node.nodeType !== 3) { + eventEmitter.emit(INTERNAL_ERROR_EVENT, { + type: ERROR.RANGE_NODE_INVALID, + }); + } + + this.start = formatDomNode(start); + this.end = formatDomNode(end); + this.text = text; + this.frozen = frozen; + this.id = id; + } - static fromSelection(idHook: Hook) { + static fromSelection(idHook: Hook) { const range = getDomRange(); + if (!range) { return null; } const start: DomNode = { $node: range.startContainer, - offset: range.startOffset + offset: range.startOffset, }; const end: DomNode = { $node: range.endContainer, - offset: range.endOffset + offset: range.endOffset, }; const text = range.toString(); let id = idHook.call(start, end, text); - id = id !== undefined && id !== null ? id : uuid(); - - return new HighlightRange(start, end, text, id); - } - constructor( - start: DomNode, - end: DomNode, - text: string, - id: string, - frozen: boolean = false - ) { - if (start.$node.nodeType !== 3 || end.$node.nodeType !== 3) { - eventEmitter.emit(INTERNAL_ERROR_EVENT, { - type: ERROR.RANGE_NODE_INVALID - }); - } + id = typeof id !== 'undefined' && id !== null ? id : uuid(); - this.start = formatDomNode(start); - this.end = formatDomNode(end); - this.text = text; - this.frozen = frozen; - this.id = id; + return new HighlightRange(start, end, text, id); } // serialize the HRange instance // so that you can save the returned object (e.g. use JSON.stringify on it and send to backend) - serialize($root: HTMLElement | Document, hooks: HookMap): HighlightSource { + serialize($root: Document | HTMLElement, hooks: HookMap): HighlightSource { const startMeta = getDomMeta(this.start.$node as Text, this.start.offset, $root); const endMeta = getDomMeta(this.end.$node as Text, this.end.offset, $root); let extra; + if (!hooks.Serialize.RecordInfo.isEmpty()) { extra = hooks.Serialize.RecordInfo.call(this.start, this.end, $root); } this.frozen = true; + return new HighlightSource(startMeta, endMeta, this.text, this.id, extra); } } -export default HighlightRange; \ No newline at end of file +export default HighlightRange; diff --git a/src/model/range/selection.ts b/src/model/range/selection.ts index bff8f3b..58689fb 100644 --- a/src/model/range/selection.ts +++ b/src/model/range/selection.ts @@ -6,13 +6,17 @@ export const getDomRange = (): Range => { const selection = window.getSelection(); + if (selection.isCollapsed) { + // eslint-disable-next-line no-console console.debug('no text selected'); + return null; } + return selection.getRangeAt(0); }; export const removeSelection = (): void => { window.getSelection().removeAllRanges(); -}; \ No newline at end of file +}; diff --git a/src/model/source/dom.ts b/src/model/source/dom.ts index fbf8556..c7a0e9d 100644 --- a/src/model/source/dom.ts +++ b/src/model/source/dom.ts @@ -1,24 +1,26 @@ -import {DomNode} from '@src//types'; -import HighlightSource from './index'; -import {ROOT_IDX} from '@src/util/const'; +import type { DomNode } from '@src//types'; +import type HighlightSource from './index'; +import { ROOT_IDX } from '@src/util/const'; /** - * Because of supporting highlighting a same area (range overlapping), + * Because of supporting highlighting a same area (range overlapping), * Highlighter will calculate which text-node and how much offset it actually be, * based on the origin website dom node and the text offset. - * + * * @param {Node} $parent element node in the origin website dom tree * @param {number} offset text offset in the origin website dom tree * @return {DomNode} DOM a dom info object */ export const getTextChildByOffset = ($parent: Node, offset: number): DomNode => { - const nodeStack: Array = [$parent]; + const nodeStack: Node[] = [$parent]; let $curNode: Node = null; let curOffset = 0; let startOffset = 0; - while ($curNode = nodeStack.pop()) { + + while (($curNode = nodeStack.pop())) { const children = $curNode.childNodes; + for (let i = children.length - 1; i >= 0; i--) { nodeStack.push(children[i]); } @@ -26,6 +28,7 @@ export const getTextChildByOffset = ($parent: Node, offset: number): DomNode => if ($curNode.nodeType === 3) { startOffset = offset - curOffset; curOffset += $curNode.textContent.length; + if (curOffset >= offset) { break; } @@ -38,26 +41,26 @@ export const getTextChildByOffset = ($parent: Node, offset: number): DomNode => return { $node: $curNode, - offset: startOffset + offset: startOffset, }; -} +}; /** * get start and end parent element from meta info - * - * @param {HighlightSource} hs + * + * @param {HighlightSource} hs * @param {HTMLElement | Document} $root root element, default document * @return {Object} */ -export const queryElementNode = ( - hs: HighlightSource, - $root: HTMLElement | Document -): {start: Node, end: Node} => { - const start = hs.startMeta.parentIndex === ROOT_IDX - ? $root - : $root.getElementsByTagName(hs.startMeta.parentTagName)[hs.startMeta.parentIndex]; - const end = hs.endMeta.parentIndex === ROOT_IDX - ? $root - : $root.getElementsByTagName(hs.endMeta.parentTagName)[hs.endMeta.parentIndex]; +export const queryElementNode = (hs: HighlightSource, $root: Document | HTMLElement): { start: Node; end: Node } => { + const start = + hs.startMeta.parentIndex === ROOT_IDX + ? $root + : $root.getElementsByTagName(hs.startMeta.parentTagName)[hs.startMeta.parentIndex]; + const end = + hs.endMeta.parentIndex === ROOT_IDX + ? $root + : $root.getElementsByTagName(hs.endMeta.parentTagName)[hs.endMeta.parentIndex]; + return { start, end }; }; diff --git a/src/model/source/index.ts b/src/model/source/index.ts index d43719d..9e284d4 100644 --- a/src/model/source/index.ts +++ b/src/model/source/index.ts @@ -4,53 +4,49 @@ * Also it has the ability for persistence. */ -import {DomMeta, HookMap, DomNode} from '../../types'; +import type { DomMeta, HookMap, DomNode } from '../../types'; import HighlightRange from '../range/index'; -import {queryElementNode, getTextChildByOffset} from './dom'; +import { queryElementNode, getTextChildByOffset } from './dom'; class HighlightSource { startMeta: DomMeta; + endMeta: DomMeta; + text: string; + id: string; + extra?: unknown; + __isHighlightSource: unknown; - constructor( - startMeta: DomMeta, - endMeta: DomMeta, - text: string, - id: string, - extra?: unknown - ) { + constructor(startMeta: DomMeta, endMeta: DomMeta, text: string, id: string, extra?: unknown) { this.startMeta = startMeta; this.endMeta = endMeta; this.text = text; this.id = id; this.__isHighlightSource = {}; + if (extra) { this.extra = extra; } } - deSerialize($root: HTMLElement | Document, hooks: HookMap): HighlightRange { - const {start, end} = queryElementNode(this, $root); + deSerialize($root: Document | HTMLElement, hooks: HookMap): HighlightRange { + const { start, end } = queryElementNode(this, $root); let startInfo = getTextChildByOffset(start, this.startMeta.textOffset); let endInfo = getTextChildByOffset(end, this.endMeta.textOffset); if (!hooks.Serialize.Restore.isEmpty()) { const res: DomNode[] = hooks.Serialize.Restore.call(this, startInfo, endInfo) || []; + startInfo = res[0] || startInfo; endInfo = res[1] || endInfo; } - const range = new HighlightRange( - startInfo, - endInfo, - this.text, - this.id, - true - ); + const range = new HighlightRange(startInfo, endInfo, this.text, this.id, true); + return range; } } diff --git a/src/painter/dom.ts b/src/painter/dom.ts index bd1a30d..f55e239 100644 --- a/src/painter/dom.ts +++ b/src/painter/dom.ts @@ -1,10 +1,7 @@ -import HighlightRange from '../model/range'; -import {SplitType, SelectedNode, DomNode, SelectedNodeType} from '../types'; -import { - hasClass, - addClass as addElementClass, - isHighlightWrapNode -} from '../util/dom'; +import type HighlightRange from '../model/range'; +import type { SelectedNode, DomNode } from '../types'; +import { SplitType, SelectedNodeType } from '../types'; +import { hasClass, addClass as addElementClass, isHighlightWrapNode } from '../util/dom'; import { ID_DIVISION, getDefaultOptions, @@ -12,7 +9,7 @@ import { CAMEL_DATASET_IDENTIFIER_EXTRA, DATASET_IDENTIFIER, DATASET_SPLIT_TYPE, - DATASET_IDENTIFIER_EXTRA + DATASET_IDENTIFIER_EXTRA, } from '../util/const'; import { unique } from '../util/tool'; @@ -26,58 +23,66 @@ const isMatchSelector = ($node: HTMLElement, selector: string): boolean => { if (!$node) { return false; } - if (/^\./.test(selector)) { - const className = selector.replace(/^\./, ''); + + if (selector.startsWith('.')) { + const className = selector.replace(/^\./u, ''); + return $node && hasClass($node, className); - } - else if (/^#/.test(selector)) { - const id = selector.replace(/^#/, ''); + } else if (selector.startsWith('#')) { + const id = selector.replace(/^#/u, ''); + return $node && $node.id === id; } - else { - const tagName = selector.toUpperCase() - return $node && $node.tagName === tagName; - } -} + + const tagName = selector.toUpperCase(); + + return $node && $node.tagName === tagName; +}; /** * If start node and end node is the same, don't need to tranvers the dom tree. */ -function getNodesIfSameStartEnd( +const getNodesIfSameStartEnd = ( $startNode: Text, startOffset: number, endOffset: number, - exceptSelectors: Array -) { + exceptSelectors?: string[], +) => { let $element = $startNode as Node; + + const isExcepted = ($e: HTMLElement) => exceptSelectors?.some(s => isMatchSelector($e, s)); + while ($element) { - if ($element.nodeType === 1 - && exceptSelectors - && exceptSelectors.some(s => isMatchSelector($element as HTMLElement, s)) - ) { + if ($element.nodeType === 1 && isExcepted($element as HTMLElement)) { return []; } + $element = $element.parentNode; } $startNode.splitText(startOffset); - let passedNode = $startNode.nextSibling as Text; + + const passedNode = $startNode.nextSibling as Text; + passedNode.splitText(endOffset - startOffset); - return [{ - $node: passedNode, - type: SelectedNodeType.text, - splitType: SplitType.both - }]; -} + + return [ + { + $node: passedNode, + type: SelectedNodeType.text, + splitType: SplitType.both, + }, + ]; +}; /** * get all the dom nodes between the start and end node */ export const getSelectedNodes = ( - $root: HTMLElement | Document, + $root: Document | HTMLElement, start: DomNode, end: DomNode, - exceptSelectors: Array + exceptSelectors: string[], ): SelectedNode[] => { const $startNode = start.$node; const $endNode = end.$node; @@ -89,22 +94,22 @@ export const getSelectedNodes = ( return getNodesIfSameStartEnd($startNode, startOffset, endOffset, exceptSelectors); } - const nodeStack: Array = [$root]; + const nodeStack: (ChildNode | Document | HTMLElement | Text)[] = [$root]; const selectedNodes: SelectedNode[] = []; + const isExcepted = ($e: HTMLElement) => exceptSelectors?.some(s => isMatchSelector($e, s)); + let withinSelectedRange = false; let curNode: Node = null; - while (curNode = nodeStack.pop()) { + + while ((curNode = nodeStack.pop())) { // do not traverse the excepted node - if ( - curNode.nodeType === 1 - && exceptSelectors - && exceptSelectors.some(s => isMatchSelector(curNode as HTMLElement, s)) - ) { + if (curNode.nodeType === 1 && isExcepted(curNode as HTMLElement)) { continue; } const children = curNode.childNodes; + for (let i = children.length - 1; i >= 0; i--) { nodeStack.push(children[i]); } @@ -113,27 +118,30 @@ export const getSelectedNodes = ( if (curNode === $startNode) { if (curNode.nodeType === 3) { (curNode as Text).splitText(startOffset); + const node = curNode.nextSibling as Text; + selectedNodes.push({ $node: node, type: SelectedNodeType.text, - splitType: SplitType.head + splitType: SplitType.head, }); - } + // meet the start-node (begin to traverse) withinSelectedRange = true; - } - else if (curNode === $endNode) { + } else if (curNode === $endNode) { if (curNode.nodeType === 3) { - const node = (curNode as Text); + const node = curNode as Text; + node.splitText(endOffset); selectedNodes.push({ $node: node, type: SelectedNodeType.text, - splitType: SplitType.tail + splitType: SplitType.tail, }); } + // meet the end-node break; } @@ -142,35 +150,38 @@ export const getSelectedNodes = ( selectedNodes.push({ $node: curNode as Text, type: SelectedNodeType.text, - splitType: SplitType.none + splitType: SplitType.none, }); } } + return selectedNodes; }; -function addClass($el: HTMLElement, className?: string | Array): HTMLElement { +const addClass = ($el: HTMLElement, className?: string[] | string): HTMLElement => { let classNames = Array.isArray(className) ? className : [className]; + classNames = classNames.length === 0 ? [getDefaultOptions().style.className] : classNames; - classNames.forEach(c => addElementClass($el, c)); + classNames.forEach(c => { + addElementClass($el, c); + }); + return $el; -} +}; -function isNodeEmpty($n: Node): boolean { - return !$n || !$n.textContent; -} +const isNodeEmpty = ($n: Node): boolean => !$n || !$n.textContent; /** * Wrap a common wrapper. */ -function wrapNewNode( +const wrapNewNode = ( selected: SelectedNode, range: HighlightRange, - className: string | Array, - wrapTag: string -): HTMLElement { - let $wrap: HTMLElement; - $wrap = document.createElement(wrapTag); + className: string[] | string, + wrapTag: string, +): HTMLElement => { + const $wrap = document.createElement(wrapTag); + addClass($wrap, className); $wrap.appendChild(selected.$node.cloneNode(false)); @@ -181,18 +192,18 @@ function wrapNewNode( $wrap.setAttribute(`data-${DATASET_IDENTIFIER_EXTRA}`, ''); return $wrap; -} +}; /** * Split and wrapper each one. */ -function wrapPartialNode( +const wrapPartialNode = ( selected: SelectedNode, range: HighlightRange, - className: string | Array, - wrapTag: string -): HTMLElement { - let $wrap: HTMLElement = document.createElement(wrapTag); + className: string[] | string, + wrapTag: string, +): HTMLElement => { + const $wrap: HTMLElement = document.createElement(wrapTag); const $parent = selected.$node.parentNode as HTMLElement; const $prev = selected.$node.previousSibling; @@ -212,26 +223,30 @@ function wrapPartialNode( if ($prev) { const $span = $parent.cloneNode(false); + $span.textContent = $prev.textContent; $fr.appendChild($span); headSplit = true; } const classNameList: string[] = []; + if (isHighlightWrapNode($parent)) { $parent.classList.forEach(c => classNameList.push(c)); } + if (Array.isArray(className)) { classNameList.push(...className); - } - else { + } else { classNameList.push(className); } + addClass($wrap, unique(classNameList)); $fr.appendChild($wrap); if ($next) { const $span = $parent.cloneNode(false); + $span.textContent = $next.textContent; $fr.appendChild($span); tailSplit = true; @@ -239,14 +254,11 @@ function wrapPartialNode( if (headSplit && tailSplit) { splitType = SplitType.both; - } - else if (headSplit) { + } else if (headSplit) { splitType = SplitType.head; - } - else if (tailSplit) { + } else if (tailSplit) { splitType = SplitType.tail; - } - else { + } else { splitType = SplitType.none; } @@ -254,34 +266,31 @@ function wrapPartialNode( $parent.parentNode.replaceChild($fr, $parent); return $wrap; -} +}; /** * Just update id info (no wrapper updated). */ -function wrapOverlapNode( - selected: SelectedNode, - range: HighlightRange, - className: string | Array -): HTMLElement { +const wrapOverlapNode = (selected: SelectedNode, range: HighlightRange, className: string[] | string): HTMLElement => { const $parent = selected.$node.parentNode as HTMLElement; - let $wrap: HTMLElement = $parent; + const $wrap: HTMLElement = $parent; addClass($wrap, className); const dataset = $parent.dataset; const formerId = dataset[CAMEL_DATASET_IDENTIFIER]; + dataset[CAMEL_DATASET_IDENTIFIER] = range.id; dataset[CAMEL_DATASET_IDENTIFIER_EXTRA] = dataset[CAMEL_DATASET_IDENTIFIER_EXTRA] ? formerId + ID_DIVISION + dataset[CAMEL_DATASET_IDENTIFIER_EXTRA] : formerId; return $wrap; -} +}; /** * wrap a dom node with highlight wrapper - * + * * Because of supporting the highlight-overlapping, * Highlighter can't just wrap all nodes in a simple way. * There are three types: @@ -292,14 +301,15 @@ function wrapOverlapNode( export const wrapHighlight = ( selected: SelectedNode, range: HighlightRange, - className: string | Array, - wrapTag: string + className: string[] | string, + wrapTag: string, ): HTMLElement => { const $parent = selected.$node.parentNode as HTMLElement; const $prev = selected.$node.previousSibling; const $next = selected.$node.nextSibling; let $wrap: HTMLElement; + // text node, not in a highlight wrapper -> should be wrapped in a highlight wrapper if (!isHighlightWrapNode($parent)) { $wrap = wrapNewNode(selected, range, className, wrapTag); @@ -312,6 +322,7 @@ export const wrapHighlight = ( else { $wrap = wrapOverlapNode(selected, range, className); } + return $wrap; }; @@ -319,15 +330,19 @@ export const wrapHighlight = ( * merge the adjacent text nodes * .normalize() API has some bugs in IE11 */ -export const normalizeSiblingText = ($s: Node, isNext: boolean = true) => { +export const normalizeSiblingText = ($s: Node, isNext = true) => { if (!$s || $s.nodeType !== 3) { return; } + const $sibling = isNext ? $s.nextSibling : $s.previousSibling; + if ($sibling.nodeType !== 3) { return; } + const text = $sibling.nodeValue; - $s.nodeValue = isNext ? ($s.nodeValue + text) : (text + $s.nodeValue); + + $s.nodeValue = isNext ? $s.nodeValue + text : text + $s.nodeValue; $sibling.parentNode.removeChild($sibling); -} \ No newline at end of file +}; diff --git a/src/painter/index.ts b/src/painter/index.ts index da9cb1c..dc73b87 100644 --- a/src/painter/index.ts +++ b/src/painter/index.ts @@ -5,24 +5,28 @@ */ import HighlightSource from '@src/model/source'; -import HighlightRange from '@src/model/range'; -import {wrapHighlight, getSelectedNodes, normalizeSiblingText} from './dom'; -import {getHighlightsByRoot, forEach} from '@src/util/dom'; -import {ERROR, PainterOptions, HookMap} from '@src/types'; -import {initDefaultStylesheet} from './style'; +import type HighlightRange from '@src/model/range'; +import { wrapHighlight, getSelectedNodes, normalizeSiblingText } from './dom'; +import { getHighlightsByRoot, forEach } from '@src/util/dom'; +import type { PainterOptions, HookMap } from '@src/types'; +import { ERROR } from '@src/types'; +import { initDefaultStylesheet } from './style'; import { ID_DIVISION, eventEmitter, DATASET_IDENTIFIER, INTERNAL_ERROR_EVENT, CAMEL_DATASET_IDENTIFIER, - CAMEL_DATASET_IDENTIFIER_EXTRA + CAMEL_DATASET_IDENTIFIER_EXTRA, } from '../util/const'; export default class Painter { options: PainterOptions; + $style: HTMLStyleElement; + styleId: string; + hooks: HookMap; constructor(options: PainterOptions, hooks: HookMap) { @@ -30,7 +34,7 @@ export default class Painter { $root: options.$root, wrapTag: options.wrapTag, exceptSelectors: options.exceptSelectors, - className: options.className + className: options.className, }; this.hooks = hooks; @@ -38,50 +42,54 @@ export default class Painter { } /* =========================== render =========================== */ - highlightRange(range: HighlightRange): Array { + highlightRange(range: HighlightRange): HTMLElement[] { if (!range.frozen) { throw ERROR.HIGHLIGHT_RANGE_FROZEN; } - const {$root, className, exceptSelectors} = this.options; + const { $root, className, exceptSelectors } = this.options; const hooks = this.hooks; let $selectedNodes = getSelectedNodes($root, range.start, range.end, exceptSelectors); + if (!hooks.Render.SelectedNodes.isEmpty()) { $selectedNodes = hooks.Render.SelectedNodes.call(range.id, $selectedNodes) || []; } return $selectedNodes.map(n => { let $node = wrapHighlight(n, range, className, this.options.wrapTag); + if (!hooks.Render.WrapNode.isEmpty()) { $node = hooks.Render.WrapNode.call(range.id, $node); } + return $node; }); } - highlightSource(sources: HighlightSource | HighlightSource[]): Array { - const list = Array.isArray(sources) - ? sources as HighlightSource[] - : [sources as HighlightSource]; + highlightSource(sources: HighlightSource | HighlightSource[]): HighlightSource[] { + const list = Array.isArray(sources) ? sources : [sources]; + + const renderedSources: HighlightSource[] = []; - const renderedSources: Array = []; list.forEach(s => { if (!(s instanceof HighlightSource)) { eventEmitter.emit(INTERNAL_ERROR_EVENT, { - type: ERROR.SOURCE_TYPE_ERROR + type: ERROR.SOURCE_TYPE_ERROR, }); + return; } + const range = s.deSerialize(this.options.$root, this.hooks); const $nodes = this.highlightRange(range); + if ($nodes.length > 0) { renderedSources.push(s); - } - else { + } else { eventEmitter.emit(INTERNAL_ERROR_EVENT, { type: ERROR.HIGHLIGHT_SOURCE_NONE_RENDER, - detail: s + detail: s, }); } }); @@ -99,7 +107,7 @@ export default class Painter { const hooks = this.hooks; const wrapTag = this.options.wrapTag; - const $spans = document.querySelectorAll(`${wrapTag}[data-${DATASET_IDENTIFIER}]`) as NodeListOf; + const $spans = document.querySelectorAll(`${wrapTag}[data-${DATASET_IDENTIFIER}]`); // nodes to remove const $toRemove: HTMLElement[] = []; @@ -108,29 +116,33 @@ export default class Painter { // nodes to update extra id const $extraToUpdate: HTMLElement[] = []; - for (let i = 0; i < $spans.length; i++) { - const spanId = $spans[i].dataset[CAMEL_DATASET_IDENTIFIER]; - const spanExtraIds = $spans[i].dataset[CAMEL_DATASET_IDENTIFIER_EXTRA]; + for (const $s of $spans) { + const spanId = $s.dataset[CAMEL_DATASET_IDENTIFIER]; + const spanExtraIds = $s.dataset[CAMEL_DATASET_IDENTIFIER_EXTRA]; + // main id is the target id and no extra ids --> to remove if (spanId === id && !spanExtraIds) { - $toRemove.push($spans[i]); + $toRemove.push($s); } // main id is the target id but there is some extra ids -> update main id & extra id else if (spanId === id) { - $idToUpdate.push($spans[i]); + $idToUpdate.push($s); } // main id isn't the target id but extra ids contains it -> just remove it from extra id else if (spanId !== id && reg.test(spanExtraIds)) { - $extraToUpdate.push($spans[i]); + $extraToUpdate.push($s); } } $toRemove.forEach($s => { const $parent = $s.parentNode; const $fr = document.createDocumentFragment(); - forEach($s.childNodes, $c => $fr.appendChild($c.cloneNode(false))); + + forEach($s.childNodes, ($c: Node) => $fr.appendChild($c.cloneNode(false))); + const $prev = $s.previousSibling; const $next = $s.nextSibling; + $parent.replaceChild($fr, $s); // there are bugs in IE11, so use a more reliable function normalizeSiblingText($prev, true); @@ -141,6 +153,7 @@ export default class Painter { $idToUpdate.forEach($s => { const dataset = $s.dataset; const ids = dataset[CAMEL_DATASET_IDENTIFIER_EXTRA].split(ID_DIVISION); + dataset[CAMEL_DATASET_IDENTIFIER] = ids.shift(); dataset[CAMEL_DATASET_IDENTIFIER_EXTRA] = ids.join(ID_DIVISION); hooks.Remove.UpdateNodes.call(id, $s, 'id-update'); @@ -148,22 +161,25 @@ export default class Painter { $extraToUpdate.forEach($s => { const extraIds = $s.dataset[CAMEL_DATASET_IDENTIFIER_EXTRA]; + $s.dataset[CAMEL_DATASET_IDENTIFIER_EXTRA] = extraIds.replace(reg, ''); hooks.Remove.UpdateNodes.call(id, $s, 'extra-update'); }); - return $toRemove.length + $idToUpdate.length + $extraToUpdate.length !== 0 + return $toRemove.length + $idToUpdate.length + $extraToUpdate.length !== 0; } removeAllHighlight() { - const {wrapTag, $root} = this.options; + const { wrapTag, $root } = this.options; const $spans = getHighlightsByRoot($root, wrapTag); + $spans.forEach($s => { const $parent = $s.parentNode; const $fr = document.createDocumentFragment(); - forEach($s.childNodes, $c => $fr.appendChild($c.cloneNode(false))); + + forEach($s.childNodes, ($c: Node) => $fr.appendChild($c.cloneNode(false))); $parent.replaceChild($fr, $s); }); } /* ============================================================== */ -}; +} diff --git a/src/painter/style.ts b/src/painter/style.ts index 1a48ae4..12b6959 100644 --- a/src/painter/style.ts +++ b/src/painter/style.ts @@ -1,14 +1,16 @@ /** * inject styles */ -import {STYLESHEET_ID, getStylesheet} from '@src/util/const'; +import { STYLESHEET_ID, getStylesheet } from '@src/util/const'; -export function initDefaultStylesheet () { +export const initDefaultStylesheet = () => { const styleId = STYLESHEET_ID; let $style: HTMLStyleElement = document.getElementById(styleId) as HTMLStyleElement; + if (!$style) { const $cssNode = document.createTextNode(getStylesheet()); + $style = document.createElement('style'); $style.id = styleId; $style.appendChild($cssNode); @@ -16,4 +18,4 @@ export function initDefaultStylesheet () { } return $style; -} +}; diff --git a/src/types/index.ts b/src/types/index.ts index d02d2c2..4c3d514 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -1,69 +1,69 @@ -import Hook from "@src/util/hook"; +import type Hook from '@src/util/hook'; export type RootElement = Document | HTMLElement; export interface HighlighterOptions { $root?: RootElement; - exceptSelectors?: Array; + exceptSelectors?: string[]; wrapTag?: string; verbose?: boolean; style?: { - className?: string | Array; - } -}; + className?: string[] | string; + }; +} export interface PainterOptions { $root: RootElement; wrapTag: string; - className: string | Array; - exceptSelectors: Array; -}; + className: string[] | string; + exceptSelectors: string[]; +} export enum SplitType { none = 'none', head = 'head', tail = 'tail', - both = 'both' -}; + both = 'both', +} export enum ERROR { - DOM_TYPE_ERROR = '[DOM] Receive wrong node type.', - DOM_SELECTION_EMPTY = '[DOM] The selection contains no dom node, may be you except them.', - RANGE_INVALID = '[RANGE] Got invalid dom range, can\'t convert to a valid highlight range.', - RANGE_NODE_INVALID = '[RANGE] Start or end node isn\'t a text node, it may occur an error.', - DB_ID_DUPLICATE_ERROR = '[STORE] Unique id conflict.', - CACHE_SET_ERROR = '[CACHE] Cache.data can\'t be set manually, please use .save().', - SOURCE_TYPE_ERROR = '[SOURCE] Object isn\'t a highlight source instance.', - HIGHLIGHT_RANGE_FROZEN = '[HIGHLIGHT_RANGE] A highlight range must be frozen before render.', - HIGHLIGHT_SOURCE_RECREATE = '[HIGHLIGHT_SOURCE] Recreate highlights from sources error.', - HIGHLIGHT_SOURCE_NONE_RENDER = '[HIGHLIGHT_SOURCE] This highlight source isn\'t rendered.' - + ' May be the exception skips it or the dom structure has changed.' -}; + DOM_TYPE_ERROR = '[DOM] Receive wrong node type.', + DOM_SELECTION_EMPTY = '[DOM] The selection contains no dom node, may be you except them.', + RANGE_INVALID = "[RANGE] Got invalid dom range, can't convert to a valid highlight range.", + RANGE_NODE_INVALID = "[RANGE] Start or end node isn't a text node, it may occur an error.", + DB_ID_DUPLICATE_ERROR = '[STORE] Unique id conflict.', + CACHE_SET_ERROR = "[CACHE] Cache.data can't be set manually, please use .save().", + SOURCE_TYPE_ERROR = "[SOURCE] Object isn't a highlight source instance.", + HIGHLIGHT_RANGE_FROZEN = '[HIGHLIGHT_RANGE] A highlight range must be frozen before render.', + HIGHLIGHT_SOURCE_RECREATE = '[HIGHLIGHT_SOURCE] Recreate highlights from sources error.', + // eslint-disable-next-line max-len + HIGHLIGHT_SOURCE_NONE_RENDER = "[HIGHLIGHT_SOURCE] This highlight source isn't rendered. May be the exception skips it or the dom structure has changed.", +} export enum EventType { - CREATE = 'selection:create', - REMOVE = 'selection:remove', - MODIFY = 'selection:modify', - HOVER = 'selection:hover', - HOVER_OUT = 'selection:hover-out', - CLICK = 'selection:click', -}; + CREATE = 'selection:create', + REMOVE = 'selection:remove', + MODIFY = 'selection:modify', + HOVER = 'selection:hover', + HOVER_OUT = 'selection:hover-out', + CLICK = 'selection:click', +} export enum CreateFrom { STORE = 'from-store', INPUT = 'from-input', -}; +} export enum SelectedNodeType { text = 'text', - span = 'span' -}; + span = 'span', +} export interface SelectedNode { - $node: Text | Node, - type: SelectedNodeType, - splitType: SplitType -}; + $node: Node | Text; + type: SelectedNodeType; + splitType: SplitType; +} export interface DomMeta { parentTagName: string; @@ -85,34 +85,34 @@ export interface HighlightPosition { end: { top: number; left: number; - } + }; } -export type HookMap = { +export interface HookMap { Render: { - UUID: Hook; - SelectedNodes: Hook; - WrapNode: Hook; + UUID: Hook; + SelectedNodes: Hook; + WrapNode: Hook; }; Serialize: { - Restore: Hook; - RecordInfo: Hook; + Restore: Hook; + RecordInfo: Hook; }; Remove: { UpdateNodes: Hook; - } + }; } export enum UserInputEvent { - touchend = 'touchend', - mouseup = 'mouseup', - touchstart = 'touchstart', - click = 'click', - mouseover = 'mouseover', + touchend = 'touchend', + mouseup = 'mouseup', + touchstart = 'touchstart', + click = 'click', + mouseover = 'mouseover', } export interface IInteraction { PointerEnd: UserInputEvent; PointerTap: UserInputEvent; PointerOver: UserInputEvent; -} \ No newline at end of file +} diff --git a/src/util/camel.ts b/src/util/camel.ts index 6489b2d..25f9519 100644 --- a/src/util/camel.ts +++ b/src/util/camel.ts @@ -2,6 +2,5 @@ * convert dash-joined string to camel case */ -export default (str: string): string => ( - str.split('-').reduce((str, s, idx) => str + (idx === 0 ? s: s[0].toUpperCase() + s.slice(1)), '') -); +export default (a: string): string => + a.split('-').reduce((str, s, idx) => str + (idx === 0 ? s : s[0].toUpperCase() + s.slice(1)), ''); diff --git a/src/util/const.ts b/src/util/const.ts index c49f3be..2eb229f 100644 --- a/src/util/const.ts +++ b/src/util/const.ts @@ -17,14 +17,15 @@ export const CAMEL_DATASET_IDENTIFIER_EXTRA = camel(DATASET_IDENTIFIER_EXTRA); export const CAMEL_DATASET_SPLIT_TYPE = camel(DATASET_SPLIT_TYPE); const DEFAULT_WRAP_TAG = 'span'; + export const getDefaultOptions = () => ({ $root: document || document.documentElement, exceptSelectors: null, wrapTag: DEFAULT_WRAP_TAG, verbose: false, style: { - className: 'highlight-mengshou-wrap' - } + className: 'highlight-mengshou-wrap', + }, }); export const getStylesheet = () => ` @@ -40,4 +41,4 @@ export const getStylesheet = () => ` export const ROOT_IDX = -2; export const UNKNOWN_IDX = -1; export const INTERNAL_ERROR_EVENT = 'error'; -export const eventEmitter = new EventEmitter(); \ No newline at end of file +export const eventEmitter = new EventEmitter(); diff --git a/src/util/dataset.polyfill.ts b/src/util/dataset.polyfill.ts deleted file mode 100644 index 36b8728..0000000 --- a/src/util/dataset.polyfill.ts +++ /dev/null @@ -1,54 +0,0 @@ -/** - * Dataset Polyfill - * cSpell: ignore Polyfill - */ - -(function elementDatasetPolyfill(): void { - if (!document.documentElement.dataset - && ( - !Object.getOwnPropertyDescriptor(HTMLElement.prototype, 'dataset') - ||!Object.getOwnPropertyDescriptor(HTMLElement.prototype, 'dataset').get - ) - ) { - const descriptor: PropertyDescriptor = {}; - descriptor.enumerable = true; - descriptor.get = function get(): Object { - const element: HTMLElement = this; - const map = {}; - const attributes: NamedNodeMap = this.attributes; - - function toUpperCase(n: string): string { - return n.charAt(1).toUpperCase(); - } - - function getter():string { - return this.value; - } - - function setter(name, value): void { - if (typeof value !== 'undefined') { - this.setAttribute(name, value); - } else { - this.removeAttribute(name); - } - } - - for (let i = 0; i < attributes.length; i++) { - const attribute = attributes[i]; - if (attribute && attribute.name && (/^data-\w[\w-]*$/).test(attribute.name)) { - const name = attribute.name; - const value = attribute.value; - const propName = name.substr(5).replace(/-./g, toUpperCase); - - Object.defineProperty(map, propName, { - enumerable: descriptor.enumerable, - get: getter.bind({ value: value || '' }), - set: setter.bind(element, name) - }); - } - } - return map; - } - Object.defineProperty(HTMLElement.prototype, 'dataset', descriptor); - } -})(); \ No newline at end of file diff --git a/src/util/deferred.ts b/src/util/deferred.ts index 07a2a63..77e2c84 100644 --- a/src/util/deferred.ts +++ b/src/util/deferred.ts @@ -1,32 +1,37 @@ interface Deferred { - promise: Promise, - resolve: Function, - reject: Function -}; + promise: Promise; + resolve: (args: T) => unknown; + reject: (e?: unknown) => unknown; +} export default function getDeferred(): Deferred { - let promise: Promise; - let resolve: Function; - let reject: Function; - promise = new Promise((r, j) => { + let resolve: (args: T) => unknown; + let reject: (e?: unknown) => unknown; + + const promise = new Promise((r, j) => { resolve = r; reject = j; }); + return { promise, resolve, - reject + reject, }; -}; +} export const resolve = (data) => { const defer = getDeferred(); + defer.resolve(data); + return defer.promise; }; export const reject = (data) => { const defer = getDeferred(); - defer.reject(data) + + defer.reject(data); + return defer.promise; }; diff --git a/src/util/dom.ts b/src/util/dom.ts index dc609b6..f200f3f 100644 --- a/src/util/dom.ts +++ b/src/util/dom.ts @@ -1,17 +1,11 @@ -import {RootElement} from '../types'; -import { - ID_DIVISION, - DATASET_IDENTIFIER, - CAMEL_DATASET_IDENTIFIER, - CAMEL_DATASET_IDENTIFIER_EXTRA -} from './const'; +import type { RootElement } from '../types'; +import { ID_DIVISION, DATASET_IDENTIFIER, CAMEL_DATASET_IDENTIFIER, CAMEL_DATASET_IDENTIFIER_EXTRA } from './const'; /** * whether a wrapper node */ -export const isHighlightWrapNode = ($node: HTMLElement): boolean => ( - !!$node.dataset && !!$node.dataset[CAMEL_DATASET_IDENTIFIER] -); +export const isHighlightWrapNode = ($node: HTMLElement): boolean => + !!$node.dataset && !!$node.dataset[CAMEL_DATASET_IDENTIFIER]; /** * =================================================================================== @@ -24,27 +18,32 @@ export const isHighlightWrapNode = ($node: HTMLElement): boolean => ( const findAncestorWrapperInRoot = ($node: HTMLElement, $root: RootElement): HTMLElement => { let isInsideRoot = false; let $wrapper: HTMLElement = null; + while ($node) { if (isHighlightWrapNode($node)) { $wrapper = $node; } + if ($node === $root) { isInsideRoot = true; break; } + $node = $node.parentNode as HTMLElement; } + return isInsideRoot ? $wrapper : null; -} +}; /** * get highlight id by a node */ export const getHighlightId = ($node: HTMLElement, $root: RootElement): string => { $node = findAncestorWrapperInRoot($node, $root); + if (!$node) { return ''; - } + } return $node.dataset[CAMEL_DATASET_IDENTIFIER]; }; @@ -54,81 +53,85 @@ export const getHighlightId = ($node: HTMLElement, $root: RootElement): string = */ export const getExtraHighlightId = ($node: HTMLElement, $root: RootElement): string[] => { $node = findAncestorWrapperInRoot($node, $root); + if (!$node) { - return []; + return []; } - return $node.dataset[CAMEL_DATASET_IDENTIFIER_EXTRA] - .split(ID_DIVISION) - .filter(i => i); + return $node.dataset[CAMEL_DATASET_IDENTIFIER_EXTRA].split(ID_DIVISION).filter(i => i); }; /** * get all highlight wrapping nodes nodes from a root node */ -export const getHighlightsByRoot = ( - $roots: RootElement | Array, - wrapTag: string -): Array => { +export const getHighlightsByRoot = ($roots: RootElement | RootElement[], wrapTag: string): HTMLElement[] => { if (!Array.isArray($roots)) { $roots = [$roots]; } - const $wraps = []; - for (let i = 0; i < $roots.length; i++) { - const $list = $roots[i].querySelectorAll(`${wrapTag}[data-${DATASET_IDENTIFIER}]`); + const $wraps: HTMLElement[] = []; + + for (const $r of $roots) { + const $list = $r.querySelectorAll(`${wrapTag}[data-${DATASET_IDENTIFIER}]`); + + // eslint-disable-next-line prefer-spread $wraps.push.apply($wraps, $list); } + return $wraps; -} +}; /** * get all highlight wrapping nodes by highlight id from a root node */ -export const getHighlightById = ( - $root: RootElement, - id: String, - wrapTag: string -): Array => { - const $highlights = []; +export const getHighlightById = ($root: RootElement, id: string, wrapTag: string): HTMLElement[] => { + const $highlights: HTMLElement[] = []; const reg = new RegExp(`(${id}\\${ID_DIVISION}|\\${ID_DIVISION}?${id}$)`); - const $list = $root.querySelectorAll(`${wrapTag}[data-${DATASET_IDENTIFIER}]`); - for (let k = 0; k < $list.length; k++) { - const $n = $list[k] as HTMLElement; + const $list = $root.querySelectorAll(`${wrapTag}[data-${DATASET_IDENTIFIER}]`); + + for (const $l of $list) { + const $n = $l; const nid = $n.dataset[CAMEL_DATASET_IDENTIFIER]; + if (nid === id) { $highlights.push($n); continue; } + const extraId = $n.dataset[CAMEL_DATASET_IDENTIFIER_EXTRA]; + if (reg.test(extraId)) { $highlights.push($n); continue; } } + return $highlights; }; -export const forEach = ($nodes: NodeList, cb: Function): void => { +export const forEach = ($nodes: NodeList, cb: (n: Node, idx: number, s: NodeList) => void): void => { for (let i = 0; i < $nodes.length; i++) { cb($nodes[i], i, $nodes); } }; +export const removeEventListener = ($el: RootElement, evt: string, fn: EventListenerOrEventListenerObject) => { + $el.removeEventListener(evt, fn); +}; + /** * maybe be need some polyfill methods later * provide unified dom methods for compatibility */ -export const addEventListener = ($el: RootElement, evt: string, fn: EventListenerOrEventListenerObject): Function => { +export const addEventListener = ($el: RootElement, evt: string, fn: EventListenerOrEventListenerObject) => { $el.addEventListener(evt, fn); - return () => removeEventListener($el, evt, fn); -}; -export const removeEventListener = ($el: RootElement, evt: string, fn: EventListenerOrEventListenerObject): void => { - $el.removeEventListener(evt, fn); + return () => { + removeEventListener($el, evt, fn); + }; }; -export const addClass = ($el: HTMLElement, className: string): void => { +export const addClass = ($el: HTMLElement, className: string) => { $el.classList.add(className); }; @@ -136,6 +139,4 @@ export const removeClass = ($el: HTMLElement, className: string): void => { $el.classList.remove(className); }; -export const hasClass = ($el: HTMLElement, className: string): boolean => { - return $el.classList.contains(className); -}; \ No newline at end of file +export const hasClass = ($el: HTMLElement, className: string): boolean => $el.classList.contains(className); diff --git a/src/util/event.emitter.ts b/src/util/event.emitter.ts index 495465b..eeca4ff 100644 --- a/src/util/event.emitter.ts +++ b/src/util/event.emitter.ts @@ -1,11 +1,11 @@ /** * tiny event emitter * modify from mitt -*/ + */ type EventHandler = (event?: unknown) => void; -type EventHandlerList = Array; -type HandlersMap = {[propName: string]: EventHandlerList}; +type EventHandlerList = EventHandler[]; +type HandlersMap = Record; class EventEmitter { private handlersMap: HandlersMap = Object.create(null); @@ -14,7 +14,9 @@ class EventEmitter { if (!this.handlersMap[type]) { this.handlersMap[type] = []; } + this.handlersMap[type].push(handler); + return this; } @@ -22,15 +24,19 @@ class EventEmitter { if (this.handlersMap[type]) { this.handlersMap[type].splice(this.handlersMap[type].indexOf(handler) >>> 0, 1); } + return this; } emit(type: string, ...data) { if (this.handlersMap[type]) { - this.handlersMap[type].slice().forEach(handler => handler(...data)); + this.handlersMap[type].slice().forEach(handler => { + handler(...data); + }); } + return this; } } -export default EventEmitter; \ No newline at end of file +export default EventEmitter; diff --git a/src/util/hook.ts b/src/util/hook.ts index 949000b..8cb3961 100644 --- a/src/util/hook.ts +++ b/src/util/hook.ts @@ -3,36 +3,50 @@ * webpack-plugin-liked api */ -class Hook { +type HookCallback = (...args: unknown[]) => T; + +class Hook { name = ''; - private ops: Array = []; + + private readonly ops: HookCallback[] = []; constructor(name?) { this.name = name; } - tap(cb: Function): Function { - if (this.ops.indexOf(cb) < 0) { + tap(cb: HookCallback) { + if (!this.ops.includes(cb)) { this.ops.push(cb); } - return () => this.remove(cb); + + return () => { + this.remove(cb); + }; } - remove(cb: Function): void { + remove(cb: HookCallback) { const idx = this.ops.indexOf(cb); + if (idx < 0) { return; } + this.ops.splice(idx, 1); } - isEmpty(): boolean { + isEmpty() { return this.ops.length === 0; } - call(...args) { - return this.ops.reduce((result, op) => op(...args), null); + call(...args: unknown[]) { + let ret: T; + + this.ops.forEach(op => { + ret = op(...args); + }); + + return ret; } } -export default Hook; \ No newline at end of file +export default Hook; diff --git a/src/util/interaction.ts b/src/util/interaction.ts index e057c1d..b11c1b5 100644 --- a/src/util/interaction.ts +++ b/src/util/interaction.ts @@ -2,7 +2,8 @@ * adapter for mobile and desktop events */ -import {IInteraction, UserInputEvent} from '../types'; +import type { IInteraction } from '../types'; +import { UserInputEvent } from '../types'; import detectMobile from './is.mobile'; export default (): IInteraction => { @@ -16,4 +17,4 @@ export default (): IInteraction => { }; return interaction; -} +}; diff --git a/src/util/is.mobile.ts b/src/util/is.mobile.ts index f569af1..8629e90 100644 --- a/src/util/is.mobile.ts +++ b/src/util/is.mobile.ts @@ -2,7 +2,6 @@ * is mobile device? */ -const regMobile: RegExp = /Android|iPhone|BlackBerry|BB10|Opera Mini|Phone|Mobile|Silk|Windows Phone|Mobile(?:.+)Firefox\b/i; -export default function (userAgent: string): boolean { - return regMobile.test(userAgent); -} +const regMobile = /Android|iPhone|BlackBerry|BB10|Opera Mini|Phone|Mobile|Silk|Windows Phone|Mobile(?:.+)Firefox\b/iu; + +export default (userAgent: string) => regMobile.test(userAgent); diff --git a/src/util/tool.ts b/src/util/tool.ts index 94f5e9f..303ab5a 100644 --- a/src/util/tool.ts +++ b/src/util/tool.ts @@ -1,15 +1,14 @@ /** - * in order to support IE 10, so can't use Set + * support IE 10 */ -export function unique(arr: Array): Array { - const record: any = {}; - const res: Array = []; - for (let i = 0; i< arr.length; i++) { - const el = arr[i]; - if (!record[el]) { +export const unique = (arr: T[]): T[] => { + const res: T[] = []; + + for (const el of arr) { + if (!res.includes(el)) { res.push(el); - record[el] = true; } } + return res; -} \ No newline at end of file +}; diff --git a/src/util/uuid.ts b/src/util/uuid.ts index c2030a0..888f4e9 100644 --- a/src/util/uuid.ts +++ b/src/util/uuid.ts @@ -2,7 +2,9 @@ * generate UUID */ +/* eslint-disable @typescript-eslint/restrict-plus-operands */ export default function createUUID(a?): string { - return a ? (a ^ Math.random() * 16 >> a / 4).toString(16) - : (([1e7] as string) + -1e3 + -4e3 + -8e3 + -1e11).replace(/[018]/g, createUUID); -} \ No newline at end of file + return a + ? (a ^ ((Math.random() * 16) >> (a / 4))).toString(16) + : ((([1e7] as unknown) as string) + -1e3 + -4e3 + -8e3 + -1e11).replace(/[018]/gu, createUUID); +} diff --git a/test/hook.spec.ts b/test/hook.spec.ts index 96d4e84..5c27b1d 100644 --- a/test/hook.spec.ts +++ b/test/hook.spec.ts @@ -43,7 +43,7 @@ describe('Highlighter Hooks', function () { }); it('should use the internal uuid id when return undefined in the hook', () => { - const spy: sinon.SinonSpy = sinon.spy(); + const spy: sinon.SinonSpy = sinon.spy(); highlighter.hooks.Render.UUID.tap(spy); const range = document.createRange(); @@ -57,7 +57,7 @@ describe('Highlighter Hooks', function () { }); it('should get correct arguments in the hook', () => { - const spy: sinon.SinonSpy = sinon.spy(); + const spy: sinon.SinonSpy = sinon.spy(); highlighter.hooks.Render.UUID.tap(spy); const range = document.createRange(); @@ -91,7 +91,7 @@ describe('Highlighter Hooks', function () { }); it('should not affect the document when return undefined in the hook', () => { - highlighter.hooks.Render.SelectedNodes.tap(() => {}); + highlighter.hooks.Render.SelectedNodes.tap(() => []); const html = document.body.innerHTML; const range = document.createRange(); @@ -122,7 +122,7 @@ describe('Highlighter Hooks', function () { describe('#Render.WrapNode', () => { it('should get correct arguments in the hook', () => { - const spy: sinon.SinonSpy = sinon.spy(); + const spy: sinon.SinonSpy = sinon.spy(); highlighter.hooks.Render.WrapNode.tap(spy); const range = document.createRange(); @@ -137,7 +137,7 @@ describe('Highlighter Hooks', function () { }); it('should be called multiple times when creating multiple wrappers', () => { - const spy: sinon.SinonSpy = sinon.spy(); + const spy: sinon.SinonSpy = sinon.spy(); highlighter.hooks.Render.WrapNode.tap(spy); const range = document.createRange(); @@ -193,7 +193,7 @@ describe('Highlighter Hooks', function () { describe('#Serialize.RecordInfo', () => { it('should get correct arguments in the hook', () => { - const spy: sinon.SinonSpy = sinon.spy(); + const spy: sinon.SinonSpy = sinon.spy(); highlighter.hooks.Serialize.RecordInfo.tap(spy); const range = document.createRange();