Skip to content

Commit

Permalink
Merge pull request #30 from alienzhou/feat/mobile
Browse files Browse the repository at this point in the history
feat(interaction): support highlighting on mobile device
  • Loading branch information
alienzhou authored Dec 20, 2019
2 parents 7f49133 + 37ada97 commit 111ebaa
Show file tree
Hide file tree
Showing 10 changed files with 152 additions and 46 deletions.
5 changes: 4 additions & 1 deletion example/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,9 @@ function getPosition($node) {
* highlighter event listener
*/
highlighter
.on(Highlighter.event.CLICK, ({id}) => {
log('click -', id);
})
.on(Highlighter.event.HOVER, ({id}) => {
log('hover -', id);
highlighter.addClass('highlight-wrap-hover', id);
Expand Down Expand Up @@ -140,7 +143,7 @@ document.addEventListener('mouseover', e => {
highlighter.removeClass('highlight-wrap-hover');
highlighter.addClass('highlight-wrap-hover', hoveredTipId);
}
else if (!$ele.classList.contains('my-remove-tip')) {
else if (!$ele.classList.contains('my-remove-tip') && !$ele.classList.contains('highlight-mengshou-wrap')) {
highlighter.removeClass('highlight-wrap-hover', hoveredTipId);
hoveredTipId = null;
}
Expand Down
16 changes: 16 additions & 0 deletions example/my.css
Original file line number Diff line number Diff line change
Expand Up @@ -101,3 +101,19 @@ main .highlight-wrap-hover {
.op-panel .op-btn.disabled {
cursor: not-allowed;
}

@media screen and (max-width: 1150px) {
main {
padding: 0 15px;
overflow-x: hidden;
}

.op-panel {
right: 5vw;
bottom: 5vh;
left: auto;
top: auto;
background-color: rgba(0, 0, 0, .9);
color: #fff;
}
}
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "web-highlighter",
"version": "0.3.5",
"version": "0.4.0-beta.0",
"description": "✨A no-runtime dependency lib for text highlighting & persistence on any website ✨🖍️",
"main": "dist/web-highlighter.min.js",
"browser": "dist/web-highlighter.min.js",
Expand Down
57 changes: 37 additions & 20 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,31 @@
import '@src/util/dataset.polyfill';
import EventEmitter from '@src/util/event.emitter';
import {EventType, HighlighterOptions, ERROR, DomNode, DomMeta, HookMap} from './types';
import HighlightRange from '@src/model/range';
import HighlightSource from '@src/model/source';
import {isHighlightWrapNode, getHighlightById, getHighlightsByRoot, getHighlightId} from '@src/util/dom';
import {DEFAULT_OPTIONS} from '@src/util/const';
import uuid from '@src/util/uuid';
import Hook from '@src/util/hook';
import event from '@src/util/interaction';
import Cache from '@src/data/cache';
import Painter from '@src/painter';
import {DEFAULT_OPTIONS} from '@src/util/const';
import {
ERROR,
DomNode,
DomMeta,
HookMap,
EventType,
HighlighterOptions
} from './types';
import {
addClass,
removeClass,
isHighlightWrapNode,
getHighlightById,
getHighlightsByRoot,
getHighlightId,
addEventListener,
removeEventListener
} from '@src/util/dom';

export default class Highlighter extends EventEmitter {
static event = EventType;
Expand All @@ -22,14 +39,12 @@ export default class Highlighter extends EventEmitter {
constructor(options: HighlighterOptions) {
super();
this.options = DEFAULT_OPTIONS;
// initialize hooks
this.hooks = this._getHooks();
this.setOption(options)
// initialize cache
this.cache = new Cache();
// initialize event listener
this.options.$root.addEventListener('mouseover', this._handleHighlightHover);
this.options.$root.addEventListener('click', this._handleHighlightClick);
this.hooks = this._getHooks(); // initialize hooks
this.setOption(options);
this.cache = new Cache(); // initialize cache
const $root = this.options.$root;
addEventListener($root, event.PointerOver, this._handleHighlightHover); // initialize event listener
addEventListener($root, event.PointerTap, this._handleHighlightClick); // initialize event listener
}

private _getHooks = () => ({
Expand Down Expand Up @@ -94,28 +109,30 @@ export default class Highlighter extends EventEmitter {
this.emit(EventType.HOVER, {id: this._hoverId}, this, e);
}

private _handleHighlightClick = e => {
private _handleHighlightClick = (e): void => {
const $target = e.target as HTMLElement;
if (isHighlightWrapNode($target)) {
const id = getHighlightId($target);
this.emit(EventType.CLICK, {id}, this, e);
return;
}
}

run = () => this.options.$root.addEventListener('mouseup', this._handleSelection);
stop = () => this.options.$root.removeEventListener('mouseup', this._handleSelection);
addClass = (className: string, id?: string) => this.getDoms(id).forEach($n => $n.classList.add(className));
removeClass = (className: string, id?: string) => this.getDoms(id).forEach($n => $n.classList.remove(className));
run = () => addEventListener(this.options.$root, event.PointerEnd, this._handleSelection);
stop = () => removeEventListener(this.options.$root, event.PointerEnd, this._handleSelection);

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));

getIdByDom = ($node: HTMLElement): string => getHighlightId($node);
getDoms = (id?: string): Array<HTMLElement> => id
? getHighlightById(this.options.$root, id)
: getHighlightsByRoot(this.options.$root);

dispose = () => {
this.options.$root.removeEventListener('mouseover', this._handleHighlightHover);
this.options.$root.removeEventListener('mouseup', this._handleSelection);
this.options.$root.removeEventListener('click', this._handleHighlightClick);
const $root = this.options.$root;
removeEventListener($root, event.PointerOver, this._handleHighlightHover);
removeEventListener($root, event.PointerEnd, this._handleSelection);
removeEventListener($root, event.PointerTap, this._handleHighlightClick);
this.removeAll();
}

Expand Down
10 changes: 7 additions & 3 deletions src/painter/dom.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import HighlightRange from '../model/range';
import {SplitType, SelectedNode, DomNode, SelectedNodeType} from '../types';
import {isHighlightWrapNode} from '../util/dom';
import {
hasClass,
addClass as addElementClass,
isHighlightWrapNode
} from '../util/dom';
import {
WRAP_TAG,
ID_DIVISION,
Expand All @@ -24,7 +28,7 @@ const isMatchSelector = ($node: HTMLElement, selector: string): boolean => {
}
if (/^\./.test(selector)) {
const className = selector.replace(/^\./, '');
return $node && $node.classList.contains(className);
return $node && hasClass($node, className);
}
else if (/^#/.test(selector)) {
const id = selector.replace(/^#/, '');
Expand Down Expand Up @@ -148,7 +152,7 @@ export const getSelectedNodes = (
function addClass($el: HTMLElement, className?: string | Array<string>): HTMLElement {
let classNames = Array.isArray(className) ? className : [className];
classNames = classNames.length === 0 ? [DEFAULT_OPTIONS.style.className] : classNames;
classNames.forEach(c => $el.classList.add(c));
classNames.forEach(c => addElementClass($el, c));
return $el;
}

Expand Down
52 changes: 34 additions & 18 deletions src/types/index.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,17 @@
import Hook from "@src/util/hook";

export type RootElement = Document | HTMLElement;

export interface HighlighterOptions {
$root: HTMLElement;
$root: RootElement;
exceptSelectors: Array<string>;
style?: {
className?: string | Array<string>;
}
};

export interface PainterOptions {
$root: HTMLElement;
$root: RootElement;
className?: string | Array<string>;
exceptSelectors: Array<string>;
};
Expand All @@ -22,25 +24,25 @@ export enum SplitType {
};

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_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_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 SelectedNodeType {
Expand Down Expand Up @@ -90,3 +92,17 @@ export type HookMap = {
UpdateNodes: Hook;
}
}

export enum UserInputEvent {
touchend = 'touchend',
mouseup = 'mouseup',
touchstart = 'touchstart',
click = 'click',
mouseover = 'mouseover',
}

export interface IInteraction {
PointerEnd: UserInputEvent;
PointerTap: UserInputEvent;
PointerOver: UserInputEvent;
}
2 changes: 1 addition & 1 deletion src/util/const.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ export const CAMEL_DATASET_IDENTIFIER_EXTRA = camel(DATASET_IDENTIFIER_EXTRA);
export const CAMEL_DATASET_SPLIT_TYPE = camel(DATASET_SPLIT_TYPE);

export const DEFAULT_OPTIONS = {
$root: window.document.documentElement,
$root: window.document || window.document.documentElement,
exceptSelectors: null,
style: {
className: 'highlight-mengshou-wrap'
Expand Down
30 changes: 28 additions & 2 deletions src/util/dom.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
* @author [email protected]
*/

import {RootElement} from '../types';
import {
WRAP_TAG,
ID_DIVISION,
Expand Down Expand Up @@ -31,7 +32,7 @@ export const getHighlightId = ($node: HTMLElement): string => {
/**
* get all highlight wrapping nodes nodes from a root node
*/
export const getHighlightsByRoot = ($roots: HTMLElement | Array<HTMLElement>): Array<HTMLElement> => {
export const getHighlightsByRoot = ($roots: RootElement | Array<RootElement>): Array<HTMLElement> => {
if (!Array.isArray($roots)) {
$roots = [$roots];
}
Expand All @@ -47,7 +48,7 @@ export const getHighlightsByRoot = ($roots: HTMLElement | Array<HTMLElement>): A
/**
* get all highlight wrapping nodes by highlight id from a root node
*/
export const getHighlightById = ($root: HTMLElement, id: String): Array<HTMLElement> => {
export const getHighlightById = ($root: RootElement, id: String): Array<HTMLElement> => {
const $highlights = [];
const reg = new RegExp(`(${id}\\${ID_DIVISION}|\\${ID_DIVISION}?${id}$)`);
const $list = $root.querySelectorAll(`${WRAP_TAG}[data-${DATASET_IDENTIFIER}]`);
Expand All @@ -72,3 +73,28 @@ export const forEach = ($nodes: NodeList, cb: Function): void => {
cb($nodes[i], i, $nodes);
}
};

/**
* maybe be need some polyfill methods later
* provide unified dom methods for compatibility
*/
export const addEventListener = ($el: RootElement, evt: string, fn: EventListenerOrEventListenerObject): Function => {
$el.addEventListener(evt, fn);
return () => removeEventListener($el, evt, fn);
};

export const removeEventListener = ($el: RootElement, evt: string, fn: EventListenerOrEventListenerObject): void => {
$el.removeEventListener(evt, fn);
};

export const addClass = ($el: HTMLElement, className: string): void => {
$el.classList.add(className);
};

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);
};
16 changes: 16 additions & 0 deletions src/util/interaction.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
/**
* adapter for mobile and desktop events
*/

import {IInteraction, UserInputEvent} from '../types';
import detectMobile from './is.mobile';

const isMobile = detectMobile(window.navigator.userAgent);
const interaction: IInteraction = {
PointerEnd: isMobile ? UserInputEvent.touchend : UserInputEvent.mouseup,
PointerTap: isMobile ? UserInputEvent.touchstart : UserInputEvent.click,
// hover and click will be the same event in mobile
PointerOver: isMobile ? UserInputEvent.touchstart : UserInputEvent.mouseover,
};

export default interaction;
8 changes: 8 additions & 0 deletions src/util/is.mobile.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
/**
* 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);
}

0 comments on commit 111ebaa

Please sign in to comment.