Code Monkey home page Code Monkey logo

web-highlighter's Introduction

Web Highlighter  🖍️

✨ A no-dependency lib for text highlighting & persistence on any website ✨🖍️

Build status NPM version Coverage Status Gzip size Codebeat MIT Licence


English | 简体中文

Background

It's from an idea: highlight texts on the website and save the highlighted areas just like what you do in PDF.

If you have ever visited medium.com, you must know the feature of highlighting notes: users select a text segment and click the 'highlight' button. Then the text will be highlighted with a shining background color. Besides, the highlighted areas will be saved and recovered when you visit it next time. It's like the simple demo bellow.

This is a useful feature for readers. If you're a developer, you may want your website support it and attract more visits. If you're a user (like me), you may want a browser-plugin to do this.

For this reason, the repo (web-highlighter) aims to help you implement highlighting-note on any website quickly (e.g. blogs, document viewers, online books and so on). It contains the core abilities for note highlighting and persistence. And you can implement your own product by some easy-to-use APIs. It has been used for our sites in production.

Install

npm i web-highlighter

Usage

Only two lines, highlighted when texts are selected.

import Highlighter from 'web-highlighter';
(new Highlighter()).run();

If you need persistence, four lines make it.

import Highlighter from 'web-highlighter';

// 1. initialize
const highlighter = new Highlighter();

// 2. retrieve data from backend, then highlight it on the page
getRemoteData().then(s => highlighter.fromStore(s.startMeta, s.endMeta, s.id, s.text));

// 3. listen for highlight creating, then save to backend
highlighter.on(Highlighter.event.CREATE, ({sources}) => save(sources));

// 4. auto highlight
highlighter.run();

Example

A more complex example

import Highlighter from 'web-highlighter';

// won't highlight pre&code elements
const highlighter = new Highlighter({
    exceptSelectors: ['pre', 'code']
});

// add some listeners to handle interaction, such as hover
highlighter
    .on('selection:hover', ({id}) => {
        // display different bg color when hover
        highlighter.addClass('highlight-wrap-hover', id);
    })
    .on('selection:hover-out', ({id}) => {
        // remove the hover effect when leaving
        highlighter.removeClass('highlight-wrap-hover', id);
    })
    .on('selection:create', ({sources}) => {
        sources = sources.map(hs => ({hs}));
        // save to backend
        store.save(sources);
    });

// retrieve data from store, and display highlights on the website
store.getAll().forEach(
    // hs is the same data saved by 'store.save(sources)'
    ({hs}) => highlighter.fromStore(hs.startMeta, hs.endMeta, hs.text, hs.id)
);

// auto-highlight selections
highlighter.run()

Besides, there is an example in this repo (in example folder). To play with it, you just need ——

Firstly enter the repository and run

npm i

Then start the example

npm start

Finally visit http://127.0.0.1:8085/


Another real product built with web-highlighter (for the highlighting area on the left):

product sample

How it works

It will read the selected range by Selection API. Then the information of the range will be converted to a serializable data structure so that it can be store in backend. When users visit your page next time, these data will be returned and deserialized in your page. The data structure is tech stack independent. So you can use on any 'static' pages made with React / Vue / Angular / jQuery and others.

For more details, please read this article (in Chinese).

APIs

1. Options

const highlighter = new Highlighter([opts])

Create a new highlighter instance.

opts will be merged into the default options (shown bellow).

{
    $root: document.documentElement,
    exceptSelectors: null,
    wrapTag: 'span',
    style: {
        className: 'highlight-mengshou-wrap'
    }
}

All options:

name type description required default
$root `Document HTMLElement` the container to enable highlighting No
exceptSelectors Array<string> if an element matches the selector, it won't be highlighted No null
wrapTag string the html tag used to wrap highlighted texts No span
verbose boolean dose it need to output (print) some warning and error message No false
style Object control highlighted areas style No details below

style field options:

name type description required default
className string the className for wrap element No highlight-mengshou-wrap

exceptSelectors needs null or Array<string>. It supports id selectors, class selectors and tag selectors. For example, to skip h1 and .title elements:

var highlighter = new Highlighter({
    exceptSelectors: ['h1', '.title']
});

2. Static Methods

Highlighter.isHighlightSource(source)

If the source is a highlight source object, it will return true, vice verse.

Highlighter.isHighlightWrapNode($node)

If the $node is a highlight wrapper dom node, it will return true, vice verse.

3. Instance Methods

highlighter.run()

Start auto-highlighting. When the user select a text segment, a highlighting will be added to the text automatically.

highlighter.stop()

It will stop the auto-highlighting.

highlighter.dispose()

When you don't want the highlighter anymore, remember to call it first. It will remove some listeners and do some cleanup.

highlighter.fromRange(range)

You can pass a Range object to it and then it will be highlighted. You can use window.getSelection().getRangeAt(0) to get a range object or use document.createRange() to create a new range.

Use it as bellow:

const selection = window.getSelection();
if (!selection.isCollapsed) {
    highlighter.fromRange(selection.getRangeAt(0));
}

highlighter.fromStore(start, end, text, id)

Mostly, you use this api to highlight text by the persisted information stored from backend.

These four values are from the HighlightSource object. HighlightSource object is a special object created by web-highlighter when highlighted area created. For persistence in backend (database), it's necessary to find a data structure to represent a dom node. This structure is called HighlightSource in web-highlighter.

Four attributes' meanings:

  • start Object: meta info about the beginning element
  • end Object: meta info about then end element
  • text string: text content
  • id string: unique id

highlighter.remove(id)

Remove (clean) a highlighted area by it's unique id. The id will be generated by web-highlighter by default. You can also add a hook for your own rule. Hooks doc here.

highlighter.removeAll()

Remove all highlighted areas belonging to the root.

highlighter.addClass(className, id)

Add a className for highlighted areas (wrap elements) by unique id. You can change a highlighted area's style by using this api.

highlighter.removeClass(className, id)

Remove the className by unique id. It's highlighter.addClass's inverse operation.

highlighter.getDoms([id])

Get all the wrap nodes in a highlighted area. A highlighted area may contain many segments. It will return all the dom nodes wrapping these segments.

If the id is not passed, it will return all the areas' wrap nodes.

highlighter.getIdByDom(node)

If you have a DOM node, it can return the unique highlight id for you. When passing a non-wrapper element, it will find the nearest ancestor wrapper node.

highlighter.getExtraIdByDom(node)

If you have a DOM node, it can return the extra unique highlight id for you. When passing a non-wrapper element, it will find the nearest ancestor wrapper node.

highlighter.setOption(opt)

You can use this API to change the highlighter's options. The parameters' structure is the same as the constructor's. You can pass partial options.

4. Event Listener

web-highlighter use listeners to handle the events.

e.g.

var highlighter = new Highlighter();
highlighter.on(Highlighter.event.CREATE, function (data, inst, e) {
    // ...
});

The callback function will receive three parameters:

  • data any: event data
  • inst Highlighter: current Highlighter instance
  • e Event: some event is triggered by the browser (such as click), web-highlighter will expose it

Highlighter.event is EventType type. It contains:

  • EventType.CLICK: click the highlighted area
  • EventType.HOVER: mouse enter the highlighted area
  • EventType.HOVER_OUT: mouse leave the highlighted area
  • EventType.CREATE: a highlighted area is created
  • EventType.REMOVE: a highlighted area is removed

Different event has different data. Attributes below:

EventType.CLICK

name description type
id the highlight id string

EventType.HOVER

name description type
id the highlight id string

EventType.HOVER_OUT

name description type
id the highlight id string

EventType.CREATE

no parameter e

name description type
source HighlightSource object Array
type the reason for creating string

source is a HighlightSource object. It is an object created by web-highlighter when highlighted area created. For persistence in backend (database), it's necessary to use a data structure which can be serialized (JSON.stringify()) to represent a dom node in browsers. HighlightSource is the data structure designed for this.

type explains why a highlighted area is be created. Now type has two possible values: from-input and from-store. from-input shows that a highlighted area is created because of user's selection. from-store means it from a storage.

EventType.REMOVE

no parameter e

name description type
ids a list of the highlight id Array

5. Hooks

Hooks let you control the highlighting flow powerfully. You can almost customize any logic by hooks. See more in 'Advance' part.

Compatibility

It depends on Selection API.

  • IE 11
  • Edge
  • Firefox 52+
  • Chrome 15+
  • Safari 5.1+
  • Opera 15+

Mobile supports: automatically detect whether mobile devices and use touch events when on mobile devices.

Advance

It provides some hooks for you so that the highlighting behaviour can be controlled better by your own.

To learn more about the hooks, read this doc.

License

MIT

web-highlighter's People

Contributors

alienzhou avatar arronf2e avatar kiinlam avatar ruanyl avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

web-highlighter's Issues

是否支持直接设置 style

场景是这样的:
image

可以给文本添加对应的标签类型,标签类型可能会有多个,无法通过 class 的方式覆盖,所以需要支持自定义 style 能力

删除标记后,该标记后面的标记位置错乱

请教下作者:比如我标记了1,2,3,4个标记,每次标记把序列化后的数据发送到后端,刷新页面从接口里获取标记列表循环后这4个标记会正常的显示。但是当我调用api删除标记2后,再次刷新页面,发现标记3、4位置就不对了。多次测试,发现只要删除前面的标记,刷新后,后面的标记的位置就错乱了。 后端返回的标记列表的数据和删除前是一致的,对此百思不得其解
删除前:
image
删除标记2后刷新页面,标记3的位置往后了:
image

请问下调用api删除标记后,还需要做其他操作吗?

This highlight source isn't rendered. May be the exception skips it or the dom structure has changed.

Hello. First of all, that's a pretty nice library, but I'm getting this error. Actually two errors:

image

My setup is that I save the highlights in my database and then I send them to the template and use fromStore(). But if I delete one of my highlights somewhere at the start of the page in can sometimes break rendering for all of the highlights below it.

I've just read (using google translate) in the Chinese article you've linked in the README that user hast to delete from back to front in the order of addition. Is it still the case? Are there any workarounds?

Error occured when npm start

~/Documents/projects/web-highlighter(master) » npm start

> [email protected] start
> node script/dev.js

[convert] /Users/kyu/Documents/projects/web-highlighter/README.md - converting...
[convert] /Users/kyu/Documents/projects/web-highlighter/README.md - convert md to html success!
/Users/kyu/Documents/projects/web-highlighter/script/dev.js:11
WebpackDevServer.addDevServerEntrypoints(config, serverConfig);
                 ^

TypeError: WebpackDevServer.addDevServerEntrypoints is not a function
    at Object.<anonymous> (/Users/kyu/Documents/projects/web-highlighter/script/dev.js:11:18)
    at Module._compile (node:internal/modules/cjs/loader:1101:14)
    at Object.Module._extensions..js (node:internal/modules/cjs/loader:1153:10)
    at Module.load (node:internal/modules/cjs/loader:981:32)
    at Function.Module._load (node:internal/modules/cjs/loader:822:12)
    at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:79:12)
    at node:internal/main/run_main_module:17:47

It happens in every versions you released, and I don't know how to solve this problem.

浏览器插件支持相关

Hello,请问有支持浏览器插件的计划么?

我试过商店里很多款”标记“、”Annotation“、”Mark“等标签的插件,都不好用。期待 web-highlighter 更新相应的浏览器插件功能。

className如何能动态改变

请教作者,我的问题是如何动态的改变className,我的需求是切换不同的颜色来完成标记,万分感谢

Highlighting in iframe

Currently I have a remote html document and I want to implement the auto highlighting feature on the remote html using web-highlighter. Below is my code

<iframe id="iframe-id" src="https://content.my-server.com/file.html">

// after the iframe have been loaded
let iframeDocument = document.getElementById("iframe-id").contentWindow.document
let highlighter = new Highlighter({
      $root: iframeDocument.documentElement,
      wrap: "span",
      style: {
        className: "highlight-class"
      }
    })

highlighter.run()

I have tested this code on the local document and it works great, however, the I can't do auto highlighting on remote resource.

标记数学公式后,会导致数学公式结构错乱,无法正常显示

如题,此现象在firefox上重现,firefox对数学公式有完美的支持。
代码如下:
<math> <mrow> <msub> <mi>α</mi> <mi>EFF</mi> </msub> <mo>=</mo> <mfrac> <mrow> <munderover> <mi>Σ</mi> <mrow> <mi>i</mi> <mo>=</mo> <mn>1</mn> </mrow> <mi>n</mi> </munderover> <msub> <mi>A</mi> <mi>i</mi> </msub> <msub> <mi>E</mi> <mi>i</mi> </msub> <msub> <mi>α</mi> <mi>i</mi> </msub> </mrow> <mrow> <munderover> <mi>Σ</mi> <mrow> <mi>i</mi> <mo>=</mo> <mn>1</mn> </mrow> <mi>n</mi> </munderover> <msub> <mi>A</mi> <mi>i</mi> </msub> <msub> <mi>E</mi> <mi>i</mi> </msub> </mrow> </mfrac> <mo>-</mo> <mo>-</mo> <mo>-</mo> <mrow> <mo>(</mo> <mn>1</mn> <mo>)</mo> </mrow> </mrow> </math>
截图:

数学公式

标记之后:

数学公式err

highlighter.fromStore执行两遍以上会报错?

从localstorage获取高亮信息还原至网页的这块代码:
const storeInfos = store.getAll();
storeInfos.forEach(
({hs}) => highlighter.fromStore(hs.startMeta, hs.endMeta, hs.text, hs.id)
);

我发现,如果执行第二遍的话,会报这样的错:

TypeError: Cannot read property 'startMeta' of undefined
at Proxy.render (eval at ./node_modules/_cache-loader@4.1.0@cache-loader/dist/cjs.js?{"cacheDirectory":"node_modules/.cache/vue-loader","cacheIdentifier":"52e6bb2a-vue-loader-template"}!./node_modules/_vue-loader@15.9.2@vue-loader/lib/loaders/templateLoader.js?!./node_modules/_cache-loader@4.1.0@cache-loader/dist/cjs.js?!./node_modules/_vue-loader@15.9.2@vue-loader/lib/index.js?!./src/views/examples/Detail.vue?vue&type=template&id=0598524d&scoped=true& (app.js:1634), :314:35)
at VueComponent.Vue._render (vue.runtime.esm.js?0261:3548)
at VueComponent.updateComponent (vue.runtime.esm.js?0261:4066)
at Watcher.get (vue.runtime.esm.js?0261:4479)
at Watcher.run (vue.runtime.esm.js?0261:4554)
at flushSchedulerQueue (vue.runtime.esm.js?0261:4310)
at Array.eval (vue.runtime.esm.js?0261:1980)
at flushCallbacks (vue.runtime.esm.js?0261:1906)
at

highlighter.removeAll() doesn't work

Version

0.3.3

Problem

highlighter.removeAll()API不起作用。追踪了代码,发现util/dom.ts 42 行getHighlightsByRoot()函数中没有返回concat后的新值。

export const getHighlightsByRoot = ($roots: HTMLElement | Array<HTMLElement>): Array<HTMLElement> => {
    if (!Array.isArray($roots)) {
        $roots = [$roots];
    }

    const $wraps = [];
    for (let i = 0; i < $roots.length; i++) {
        const $list = $roots[i].querySelectorAll(`${WRAP_TAG}[data-${DATASET_IDENTIFIER}]`);
        // $wraps.concat($list);
        $wraps.push.apply($wraps, $list);
    }
    return $wraps;
}

有一个可以改进的地方

考虑如下HTML:

<p>春天到了</p>
<p>花儿开了</p>

如果是在第一个p标签结尾开始创建选区,界面上实际选中的是在第二个p标签里的文字,此时,删除按钮会出现在第一个p标签的结尾,检查元素时,可以看到此处生成了一个空子节点的span标签。

这种情况可以在第一段结尾空白处按下鼠标并往下拖拽后出现。造成删除按钮跟高亮区域分离较远。

讨论一下 可否在fromStore这个方法中 做一些优化呢

现状:当前如果存储HighlightSource的父级节点结构发生变化的话 会直接报错(目前应该是直接去匹配落点标签的)
预期:是否可以做一个缓冲的逻辑 在一定匹配次数内 优先匹配附近的几个节点再对比内容 如果一致则生成高亮 否则就不生成

Create highlight wrapper inside another wrapper will take all the classes from parent

Related commit 35560fa

The issue I'm facing is when I create multi-color highlights, the highlight wrapper created inside another wrapper will take all the class of the parent wrapper. And this will override the color of the child wrapper if the child has a different color.

image

As you can see in the above screenshot, the highlight wrapper has both class names highlighter-color-yellow from its parent and highlighter-color-green which is the expected color of itself.

This retain the wrapper's classname is a bit odd, what's the intention of this change?

add typescript types

Try npm install @types/web-highlighter if it exists or add a new declaration (.d.ts) file containing declare module 'web-highlighter';

IE下报错“正则表达式语法错误”

IE下提示正则表达式错误

错误描述

系统信息:

  • browser: IE 11 (version: 1909 操作系统内部版本 18363.1440)
  • os: windows 10
  • web-highlighter@^0.7.1

image

问题原因

经检查,发现 IE未实现正则表达式语法flag的uRegExp.prototype.unicode

image

./src/util/is.mobile.ts文件里面,修改正则 为 const regMobile = /Android|iPhone|BlackBerry|BB10|Opera Mini|Phone|Mobile|Silk|Windows Phone|Mobile(?:.+)Firefox\b/i;

关于高亮问题交流

您好 我自己曾写过这个功能 但我实现的很不理想 想跟您探讨下您的实现思路 不知是否方便

位置计算不准

如果一个p标签中包含有多个span标签,类似于

123456

,如果选择了34两个字符,那这个位置就计算不准了

npm start失败

$ npm start

[email protected] start E:\node_project\web-highlighter
node script/dev.js

[convert] E:\node_project\web-highlighter\README.md - converting...
[convert] E:\node_project\web-highlighter\README.md - convert md to html success!
i 「wds」: Project is running at http://0.0.0.0:8085/
i 「wds」: webpack output is served from undefined
i 「wds」: Content not from webpack is served from E:\node_project\web-highlighter\example\static
Starting the development server...

E:\node_project\web-highlighter\node_modules\open\index.js:29
...options
^^^

SyntaxError: Unexpected token ...
at createScript (vm.js:74:10)
at Object.runInThisContext (vm.js:116:10)
at Module._compile (module.js:533:28)
at Object.Module._extensions..js (module.js:580:10)
at Module.load (module.js:503:32)
at tryModuleLoad (module.js:466:12)
at Function.Module._load (module.js:458:3)
at Module.require (module.js:513:17)
at require (internal/module.js:11:18)
at startBrowserProcess (E:\node_project\web-highlighter\node_modules\better-opn\dist\index.js:81:10)
npm ERR! code ELIFECYCLE
npm ERR! errno 1
npm ERR! [email protected] start: node script/dev.js
npm ERR! Exit status 1
npm ERR!
npm ERR! Failed at the [email protected] start script.
npm ERR! This is probably not a problem with npm. There is likely additional logging output above.

npm ERR! A complete log of this run can be found in:
npm ERR! C:\Users\d\AppData\Roaming\npm-cache_logs\2020-04-17T07_10_28_121Z-debug.log

SelectedNodes Hook 的处理

场景

需要实现反向exceptSelectors的功能,即,将高亮功能限定在某个容器内

思路

使用SelectedNodesHook 先对高亮区创建前的nodeList进行过淲

// 相关代码
highlighter.hooks.Render.SelectedNodes.tap((id, doms) => {
        // 仅高亮容器内部
        const container = self.$refs.pre;
        const validNodes = doms.filter(({$node}) => container.compareDocumentPosition($node) === 20)
        console.log('SelectedNodes', doms, validNodes);
        return validNodes;
      })

问题

过滤后,在event.CREATE事件回调中,取得的source.startMetasource.endMetasource.text,没有根据过滤结果重新设置。

重叠划词,位置计算不正确

第一次划词,正确
第二次划词,与第一次有重叠,第二次的tart位于第一次选中字符串内
第二次的位置计算不正确,sartMeta和endMeta都不正确,偏移量甚至超过字符串总长度很多。end-start也比选中的字符串长度大很多

位置计算不准确

如果一个p标签中包含有多个span标签,类似于

123456

,如果选择了34两个字符,那这个位置就计算不准了

> 您好,请问划词的时候可以进行批注操作吗?

您好,请问划词的时候可以进行批注操作吗?

目前这个库里没有包含批注功能,不过可以通过暴露的事件监听或者钩子来扩展与集成。

我们的产品在右侧是有笔记栏的(类似批注),两者可以通过事件或者全局状态来通信。由于笔记(批注)更注重业务交互逻辑,不同产品的交互差别大,不好满足定制化需求,所以没有把这部分放进来。

Y18mdF8UiT

Originally posted by @alienzhou in #20 (comment)

怎么阻止生成后双击的事件?


image
如图一所示,我使用fromstore生成了高亮,但是我还是可以再次划选,导致高亮失效(图二),我应该怎样阻止这个行为?我希望的是可以不重复选区
另外,还有一个问题想请教,如何阻止click事件的响应,因为我给event.CLICK绑了事件,但是希望能自己控制在编辑状态下,click事件才响应。尝试过return,但无法阻止事件

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.