├── coverage └── lcov-report │ ├── sort-arrow-sprite.png │ ├── prettify.css │ ├── src │ ├── utils │ │ ├── keycode.js.html │ │ └── index.html │ ├── index.html │ ├── components │ │ ├── index.html │ │ ├── mentionMixin.js.html │ │ └── Panel.jsx.html │ ├── editor-components │ │ ├── index.html │ │ └── baseEditor.js.html │ └── index.js.html │ ├── sorter.js │ ├── base.css │ ├── index.html │ └── prettify.js ├── tests └── index.js ├── src ├── utils │ ├── keycode.js │ ├── util.js │ └── rangy-position.js ├── index.js ├── components │ ├── Panel.jsx │ ├── mentionMixin.js │ ├── Mention.jsx │ └── TinymceMention.jsx ├── Mention.less └── editor-components │ ├── baseEditor.js │ ├── textareaEditor.jsx │ ├── inputEditor.jsx │ └── contentEditableEditor.jsx ├── demo ├── index.jsx ├── MentionDemo.less ├── MentionDemo.jsx └── mockData.json ├── .gitignore ├── .npmignore ├── .eslintrc.json ├── .travis.yml ├── index.html ├── HISTORY.md ├── package.json ├── doc ├── zh-CN.md └── en-US.md └── README.md /coverage/lcov-report/sort-arrow-sprite.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WisestCoder/uxcore-mention/HEAD/coverage/lcov-report/sort-arrow-sprite.png -------------------------------------------------------------------------------- /tests/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * only require other specs here 3 | */ 4 | 5 | const req = require.context('.', false, /\.spec\.js$/); 6 | req.keys().forEach(req); -------------------------------------------------------------------------------- /src/utils/keycode.js: -------------------------------------------------------------------------------- 1 | export const KEYCODE = { 2 | DOWN: 40, 3 | UP: 38, 4 | ESC: 27, 5 | TAB: 9, 6 | ENTER: 13, 7 | CTRL: 17, 8 | BACKSPACE: 8, 9 | DELETE: 46, 10 | }; 11 | -------------------------------------------------------------------------------- /demo/index.jsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Mention Component Demo for uxcore 3 | * @author 4 | * 5 | * Copyright 2014-2015, Uxcore Team, Alinw. 6 | * All rights reserved. 7 | */ 8 | 9 | import React from 'react'; 10 | import { render } from 'react-dom'; 11 | import Demo from './MentionDemo'; 12 | 13 | render(, document.getElementById('UXCoreDemo')); 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | *.log 3 | .idea/ 4 | .ipr 5 | .iws 6 | *~ 7 | ~* 8 | *.diff 9 | *.patch 10 | *.bak 11 | .DS_Store 12 | Thumbs.db 13 | .project 14 | .*proj 15 | .svn/ 16 | *.swp 17 | *.swo 18 | *.pyc 19 | *.pyo 20 | .build 21 | node_modules 22 | _site 23 | sea-modules 24 | spm_modules 25 | .cache 26 | .happypack 27 | dist 28 | build 29 | assets/**/*.css 30 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | bower_components/ 2 | *.cfg 3 | node_modules/ 4 | nohup.out 5 | *.iml 6 | .idea/ 7 | .ipr 8 | .iws 9 | *~ 10 | ~* 11 | *.diff 12 | *.log 13 | *.patch 14 | *.bak 15 | .DS_Store 16 | Thumbs.db 17 | .project 18 | .*proj 19 | .svn/ 20 | *.swp 21 | out/ 22 | .build 23 | .happypack 24 | node_modules 25 | _site 26 | sea-modules 27 | spm_modules 28 | .cache 29 | dist -------------------------------------------------------------------------------- /demo/MentionDemo.less: -------------------------------------------------------------------------------- 1 | /** 2 | * Mention Component Demo Style for Uxcore 3 | * @author 4 | * 5 | * Copyright 2014-2015, Uxcore Team, Alinw. 6 | * All rights reserved. 7 | */ 8 | @import "../node_modules/kuma-base/theme/orange"; 9 | @import "../node_modules/kuma-base/core"; 10 | @import "../src/Mention.less"; 11 | @import "../node_modules/uxcore-dialog/src/Dialog.less"; 12 | 13 | 14 | #UXCoreDemo { 15 | margin-left: 40px; 16 | margin-top: 20vh; 17 | } 18 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Mention Component for uxcore 3 | * @author 4 | * 5 | * Copyright 2014-2015, Uxcore Team, Alinw. 6 | * All rights reserved. 7 | */ 8 | import Mention from './components/Mention'; 9 | import ContenteditableEditor from './editor-components/contentEditableEditor'; 10 | import TextareaEditor from './editor-components/textareaEditor'; 11 | import InputEditor from './editor-components/inputEditor'; 12 | import TinymceMention from './components/TinymceMention'; 13 | 14 | export { ContenteditableEditor, TextareaEditor, InputEditor, TinymceMention }; 15 | export default Mention; 16 | -------------------------------------------------------------------------------- /coverage/lcov-report/prettify.css: -------------------------------------------------------------------------------- 1 | .pln{color:#000}@media screen{.str{color:#080}.kwd{color:#008}.com{color:#800}.typ{color:#606}.lit{color:#066}.pun,.opn,.clo{color:#660}.tag{color:#008}.atn{color:#606}.atv{color:#080}.dec,.var{color:#606}.fun{color:red}}@media print,projection{.str{color:#060}.kwd{color:#006;font-weight:bold}.com{color:#600;font-style:italic}.typ{color:#404;font-weight:bold}.lit{color:#044}.pun,.opn,.clo{color:#440}.tag{color:#006;font-weight:bold}.atn{color:#404}.atv{color:#060}}pre.prettyprint{padding:2px;border:1px solid #888}ol.linenums{margin-top:0;margin-bottom:0}li.L0,li.L1,li.L2,li.L3,li.L5,li.L6,li.L7,li.L8{list-style-type:none}li.L1,li.L3,li.L5,li.L7,li.L9{background:#eee} 2 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "airbnb", 3 | "parser": "babel-eslint", 4 | "plugins": [ 5 | "react" 6 | ], 7 | "env": { 8 | "browser": true, 9 | "mocha": true 10 | }, 11 | "rules": { 12 | "import/no-extraneous-dependencies": "off", 13 | "react/jsx-no-bind": "off", 14 | "no-underscore-dangle": "off", 15 | "jsx-a11y/label-has-for": "off", 16 | "no-plusplus": [ 17 | "error", 18 | { 19 | "allowForLoopAfterthoughts": true 20 | } 21 | ], 22 | "react/no-unused-prop-types": "off", 23 | "react/forbid-prop-types": "off", 24 | "jsx-a11y/no-static-element-interactions": "off" 25 | } 26 | } -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | 3 | sudo: false 4 | 5 | addons: 6 | apt: 7 | packages: 8 | - xvfb 9 | 10 | notification: 11 | email: 12 | - wsj7552715@hotmail.com 13 | 14 | node_js: 15 | - 6.9.0 16 | 17 | before_install: 18 | - | 19 | if ! git diff --name-only $TRAVIS_COMMIT_RANGE | grep -qve '(\.md$)|(\.html$)' 20 | then 21 | echo "Only docs were updated, stopping build process." 22 | exit 23 | fi 24 | phantomjs --version 25 | install: 26 | - export DISPLAY=':99.0' 27 | - Xvfb :99 -screen 0 1024x768x24 > /dev/null 2>&1 & 28 | - npm install 29 | 30 | 31 | script: 32 | - | 33 | if [ "$TEST_TYPE" = test ]; then 34 | npm test 35 | else 36 | npm run $TEST_TYPE 37 | fi 38 | env: 39 | matrix: 40 | - TEST_TYPE=test 41 | - TEST_TYPE=coverage 42 | - TEST_TYPE=saucelabs 43 | 44 | matrix: 45 | allow_failures: 46 | - env: "TEST_TYPE=saucelabs" -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | mention 5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /src/components/Panel.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import classNames from 'classnames'; 4 | 5 | export default class Panel extends Component { 6 | 7 | static displayName = 'uxcore-mention-panel'; 8 | static propTypes = { 9 | prefixCls: PropTypes.string, 10 | list: PropTypes.array, 11 | style: PropTypes.object, 12 | idx: PropTypes.number, 13 | onSelect: PropTypes.func, 14 | formatter: PropTypes.func, 15 | }; 16 | static defaultProps = { 17 | prefixCls: '', 18 | list: [], 19 | style: {}, 20 | idx: 0, 21 | onSelect: null, 22 | formatter: '', 23 | }; 24 | 25 | render() { 26 | const props = this.props; 27 | const { onSelect, list, style, visible, idx, formatter, prefixCls } = props; 28 | return ( 29 | 46 | ); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/Mention.less: -------------------------------------------------------------------------------- 1 | /** 2 | * Mention Component Style for uxcore 3 | * @author 4 | * 5 | * Copyright 2014-2015, Uxcore Team, Alinw. 6 | * All rights reserved. 7 | */ 8 | 9 | @mentionClassPrefix: kuma-mention; 10 | 11 | .@{mentionClassPrefix} { 12 | position: relative; 13 | &-editor { 14 | padding: 7px 10px; 15 | border: 1px solid @border-color; 16 | border-radius: @input-border-radius; 17 | color: @text-primary-color; 18 | overflow: auto; 19 | &:focus { 20 | border-color: @border-focus-color; 21 | outline: 0 none; 22 | } 23 | } 24 | &-node { 25 | padding: 0 2px; 26 | background: none; 27 | border: 0 none; 28 | color: @link-color; 29 | } 30 | &-panel { 31 | position: fixed; 32 | margin: 0; 33 | padding: 0; 34 | background-color: @basic-100; 35 | border: 1px solid @basic-500; 36 | box-shadow: 1px 1px 3px rgba(0, 0, 0, 0.2); 37 | display: none; 38 | z-index: 99; 39 | &-visible { 40 | display: block; 41 | } 42 | &-item { 43 | display: block; 44 | padding: 2px 5px; 45 | height: 30px; 46 | line-height: 26px; 47 | border-top: 1px dotted @basic-500; 48 | cursor: pointer; 49 | &:first-child { 50 | border-top: 0 none; 51 | } 52 | &:hover { 53 | background-color: @basic-400; 54 | } 55 | &-current { 56 | background-color: @basic-400; 57 | } 58 | } 59 | } 60 | &-placeholder { 61 | position: absolute; 62 | top: 8px; 63 | left: 10px; 64 | color: @text-thirdary-color; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /HISTORY.md: -------------------------------------------------------------------------------- 1 | # History 2 | 3 | --- 4 | 5 | ## 0.3.17 6 | `IMPROVE` support react 15.x 7 | 8 | ## 0.3.14 9 | `CHANGED` support multiple mention 10 | 11 | ## 0.3.11 12 | `FIXED` server render bug 13 | 14 | 15 | ## 0.3.10 16 | `FIXED` remove the warning under react@15.x.x. 17 | 18 | ## 0.3.9 19 | `CHANGED` editor's width propType. 20 | 21 | ## 0.3.7 22 | `CHANGED` add value props. 23 | 24 | ## 0.3.6 25 | `FIXED` click on the contentEditable's placeholder, the editor can't focus. 26 | 27 | 28 | ## 0.3.4 29 | `FIXED` inputEditor & textareaEditor trigger change event when mention addded. 30 | 31 | ## 0.3.3 32 | `FIXED` issue when mention's 'matchRange' props start from 0. 33 | 34 | `CHANGED` change panel's position with inputEditor & textareaEditor. 35 | 36 | ## 0.3.2 37 | `FIXED` fixed 'onChange' not trigger in inputEditor & textareaEditor. 38 | 39 | ## 0.3.1 40 | 41 | `FIXED` fixed the select panel's position error when the page is scrolled. 42 | 43 | ## 0.3.0 44 | `CHANGED` Separate editor from mention component with three types 45 | 46 | `ADDED` props `delimiter` `readOnly` `defaultValue` `onAdd` 47 | 48 | `NEW` add `tinymce` support 49 | 50 | 51 | ## 0.2.0 52 | `NEW` add `placeholder` supports 53 | 54 | ## 0.1.10 55 | `FIXED` add scroll when content overflow 56 | 57 | ## 0.1.9 58 | `FIXED` fix require(xxx.jsx) bug when build 59 | 60 | ## 0.1.8 61 | `CHANGED` update scaffold 62 | 63 | ## 0.1.5 64 | `NEW` add onChange method 65 | 66 | ## 0.1.4 67 | `FIXED` sometimes the panel does not show 68 | 69 | ## 0.1.3 70 | 71 | `CHANGED` upgrade to react@0.14 72 | 73 | ## 0.1.2 74 | 75 | `FIXED` panel position bug when page is scrolled 76 | -------------------------------------------------------------------------------- /src/editor-components/baseEditor.js: -------------------------------------------------------------------------------- 1 | import { Component } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { KEYCODE } from '../utils/keycode'; 4 | import '../utils/rangy-position'; 5 | 6 | export default class BaseEditor extends Component { 7 | static displayName = 'BaseEditor'; 8 | static propTypes = { 9 | panelVisible: PropTypes.boolean, 10 | onFocus: PropTypes.func, 11 | mentionFormatter: PropTypes.func, 12 | onAdd: PropTypes.func, 13 | }; 14 | static defaultProps = { 15 | }; 16 | 17 | onFocus() { 18 | this.props.onFocus(this); 19 | } 20 | onKeydown(e) { 21 | const { panelVisible } = this.props; 22 | switch (e.keyCode) { 23 | case KEYCODE.UP: 24 | case KEYCODE.DOWN: 25 | if (panelVisible) { 26 | e.preventDefault(); 27 | } 28 | break; 29 | case KEYCODE.ENTER: 30 | if (panelVisible) { 31 | e.preventDefault(); 32 | } else if (this.handleEnterPress) { 33 | this.handleEnterPress(e); 34 | } 35 | break; 36 | default: 37 | break; 38 | } 39 | } 40 | onKeyup(e) { 41 | const { panelVisible } = this.props; 42 | switch (e.keyCode) { 43 | case KEYCODE.UP: 44 | case KEYCODE.DOWN: 45 | if (panelVisible) { 46 | e.preventDefault(); 47 | } 48 | break; 49 | case KEYCODE.ENTER: 50 | break; 51 | default: 52 | if (this.handleDefaultKeyup) { 53 | this.handleDefaultKeyup(); 54 | } 55 | break; 56 | } 57 | } 58 | insertMentionData(mentionData) { 59 | const { mentionFormatter, onAdd } = this.props; 60 | const insertContent = mentionFormatter(mentionData); 61 | this.insert(insertContent); 62 | onAdd(insertContent, mentionData); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "uxcore-mention", 3 | "version": "0.3.17", 4 | "description": "Mention anywhere with this component", 5 | "main": "build/index.js", 6 | "scripts": { 7 | "start": "uxcore-tools run start", 8 | "server": "uxcore-tools run server", 9 | "lint": "uxcore-tools run lint", 10 | "build": "uxcore-tools run build", 11 | "test": "uxcore-tools run electron", 12 | "coverage": "uxcore-tools run electron-coverage", 13 | "pub": "uxcore-tools run pub", 14 | "dep": "uxcore-tools run dep", 15 | "tnpm-dep": "uxcore-tools run tnpm-dep", 16 | "chrome": "uxcore-tools run chrome", 17 | "browsers": "uxcore-tools run browsers", 18 | "saucelabs": "uxcore-tools run saucelabs", 19 | "update": "uxcore-tools run update", 20 | "tnpm-update": "uxcore-tools run tnpm-update" 21 | }, 22 | "repository": { 23 | "type": "git", 24 | "url": "git@github.com:uxcore/mention.git" 25 | }, 26 | "bugs": { 27 | "url": "http://github.com/uxcore/mention/issues" 28 | }, 29 | "keywords": [ 30 | "react", 31 | "react-component", 32 | "react-mention", 33 | "mention" 34 | ], 35 | "devDependencies": { 36 | "console-polyfill": "^0.2.2", 37 | "enzyme": "^3.0.0", 38 | "enzyme-adapter-react-15": "^1.0.0", 39 | "es5-shim": "^4.5.8", 40 | "expect.js": "~0.3.1", 41 | "kuma-base": "1.x", 42 | "object-assign": "^4.0.0", 43 | "react": "15.x", 44 | "react-addons-test-utils": "^15.6.2", 45 | "react-dom": "15.x", 46 | "react-test-renderer": "15.x", 47 | "uxcore-dialog": "^0.4.0", 48 | "uxcore-kuma": "*", 49 | "uxcore-tinymce": "^0.2.2", 50 | "uxcore-tools": "0.2.x" 51 | }, 52 | "dependencies": { 53 | "classnames": "^2.1.2", 54 | "prop-types": "15.x", 55 | "react-mixin": "^2.0.2" 56 | }, 57 | "author": "vincent.bian", 58 | "contributors": [], 59 | "license": "MIT", 60 | "_from": "uxcore-mention@0.3.10", 61 | "_resolved": "http://registry.npm.alibaba-inc.com/uxcore-mention/download/uxcore-mention-0.3.10.tgz" 62 | } -------------------------------------------------------------------------------- /src/components/mentionMixin.js: -------------------------------------------------------------------------------- 1 | import { KEYCODE } from '../utils/keycode'; 2 | 3 | let __matchTimer; 4 | 5 | export default { 6 | componentDidUpdate(prevProps, prevState) { 7 | if (prevState.mentionList.length !== this.state.mentionList.length) { 8 | this.setState({ 9 | panelVisible: this.state.mentionList.length > 0, 10 | }); 11 | } 12 | if (!prevState.panelVisible && this.state.panelVisible) { 13 | this.setState({ 14 | panelIdx: 0, 15 | }); 16 | } 17 | }, 18 | 19 | onPanelKeyup(e) { 20 | const { panelVisible, panelIdx, mentionList } = this.state; 21 | if (panelVisible) { 22 | const count = mentionList.length; 23 | switch (e.keyCode) { 24 | case KEYCODE.UP: 25 | this.setState({ 26 | panelIdx: panelIdx === 0 ? count - 1 : panelIdx - 1, 27 | }); 28 | break; 29 | case KEYCODE.DOWN: 30 | this.setState({ 31 | panelIdx: panelIdx === count - 1 ? 0 : panelIdx + 1, 32 | }); 33 | break; 34 | case KEYCODE.ENTER: 35 | this.selectItem(mentionList[panelIdx]); 36 | break; 37 | default: 38 | this.setState({ 39 | mentionList: [], 40 | }); 41 | break; 42 | } 43 | } 44 | }, 45 | runMatcher(str) { 46 | if (__matchTimer) { 47 | clearTimeout(__matchTimer); 48 | } 49 | __matchTimer = setTimeout(() => { 50 | this._matcher(str); 51 | }, this.props.delay); 52 | }, 53 | _matcher(str) { 54 | const { source, matchRange } = this.props; 55 | this.setState({ 56 | panelVisible: false, 57 | mentionList: [], 58 | }); 59 | if (str.length >= matchRange[0] && str.length <= matchRange[1]) { 60 | if (Array.isArray(source)) { 61 | this.next(source.filter((item) => item.indexOf(str) !== -1)); 62 | } else { 63 | source(str, this.next.bind(this)); 64 | } 65 | } 66 | }, 67 | 68 | next(matchResult) { 69 | let result = matchResult; 70 | if (this.props.formatter) { 71 | result = this.props.formatter(result); 72 | } 73 | this.setState({ 74 | mentionList: result, 75 | }); 76 | }, 77 | }; 78 | -------------------------------------------------------------------------------- /coverage/lcov-report/src/utils/keycode.js.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Code coverage report for src/utils/keycode.js 5 | 6 | 7 | 8 | 9 | 14 | 15 | 16 |
17 |
18 |

19 | all files / src/utils/ keycode.js 20 |

21 |
22 |
23 | 100% 24 | Statements 25 | 2/2 26 |
27 |
28 | 100% 29 | Branches 30 | 0/0 31 |
32 |
33 | 100% 34 | Functions 35 | 0/0 36 |
37 |
38 | 100% 39 | Lines 40 | 2/2 41 |
42 |
43 |
44 |
45 |

 46 | 
 92 | 
1 47 | 2 48 | 3 49 | 4 50 | 5 51 | 6 52 | 7 53 | 8 54 | 9 55 | 10 56 | 11 57 | 12 58 | 13 59 | 14 60 | 15 61 | 16  62 |   63 | 64 |   65 |   66 | 67 |   68 |   69 |   70 |   71 |   72 |   73 |   74 |   75 |   76 |  
"use strict";
 77 |  
 78 | Object.defineProperty(exports, "__esModule", {
 79 |   value: true
 80 | });
 81 | var KEYCODE = exports.KEYCODE = {
 82 |   DOWN: 40,
 83 |   UP: 38,
 84 |   ESC: 27,
 85 |   TAB: 9,
 86 |   ENTER: 13,
 87 |   CTRL: 17,
 88 |   BACKSPACE: 8,
 89 |   DELETE: 46
 90 | };
 91 |  
93 |
94 |
95 | 99 | 100 | 101 | 108 | 109 | 110 | 111 | -------------------------------------------------------------------------------- /doc/zh-CN.md: -------------------------------------------------------------------------------- 1 | ## Mention 2 | 3 | Mention Component 4 | 5 | ### API 6 | 7 | - onFocus(editorInstance): 测试api文档 8 | - setPanelPos(pos): description of setPanelPos 9 | 10 | ### PROPS 11 | Name | Type | Required | DefaultValue | Description 12 | --- | --- | --- | --- | --- 13 | prefixCls | string | false | 'kuma-mention' | class前缀 14 | source | union (array, func) | false | [] | 定义数据源 15 | delay | number | false | 100 | 数据源查询延时 16 | matchRange | arrayOf number | false | [2, 8] | 匹配字符区间 17 | formatter | func | false | (data) => data | 数据源格式化匹配 18 | panelFormatter | func | false | (data) => `${data.text}` | 自定义选择列表 19 | onChange | func | false | (e, value) => {} | 发生变化后的触发 20 | ## uxcore-mention-panel 21 | 22 | 23 | 24 | ### PROPS 25 | Name | Type | Required | DefaultValue | Description 26 | --- | --- | --- | --- | --- 27 | prefixCls | string | false | '' | 28 | list | array | false | [] | 29 | style | object | false | {} | 30 | idx | number | false | 0 | 31 | onSelect | func | false | null | 32 | formatter | func | false | '' | 33 | ## TinymceMention 34 | 35 | 用于tinymce的mention 36 | 37 | ### PROPS 38 | Name | Type | Required | DefaultValue | Description 39 | --- | --- | --- | --- | --- 40 | prefixCls | string | false | 'kuma-mention' | class前缀 41 | source | union (array, func) | false | [] | 定义数据源 42 | delay | number | false | 100 | 数据源查询延时 43 | matchRange | arrayOf number | false | [2, 8] | 匹配字符区间 44 | formatter | func | false | (data) => data | 数据源格式化匹配 45 | mentionFormatter | func | false | (data) => `@${data.text}` | 自定义插入的mention内容 46 | panelFormatter | func | false | (data) => `${data.text}` | 自定义选择列表 47 | onChange | func | false | (e, value) => {} | 发生变化后的触发 48 | onAdd | func | false | () => {} | 添加mention后触发 49 | insertMode | enum ('ELEMENT_NODE', 'TEXT_NODE') | false | 'ELEMENT_NODE' | `ELEMENT_NODE` 插入button, `TEXT_NODE` 插入纯字符串 50 | ## ContentEditableEditor 51 | 52 | 53 | 54 | ### PROPS 55 | Name | Type | Required | DefaultValue | Description 56 | --- | --- | --- | --- | --- 57 | prefixCls | string | false | '' | class前缀 58 | width | number | false | 200 | 编辑区域宽度 59 | height | number | false | 100 | 编辑区域高度 60 | placeholder | string | false | '' | placeholder 61 | mentionFormatter | func | false | (data) => `@${data.text}` | 自定义插入的mention内容 62 | onChange | func | false | () => {} | 发生变化后的触发 63 | onAdd | func | false | () => {} | 添加mention后触发 64 | defaultValue | string | false | '' | 默认内容 65 | readOnly | bool | false | false | 只读 66 | delimiter | string | false | '@' | 触发字符 67 | ## InputEditor 68 | 69 | input中使用mention 70 | 71 | ### PROPS 72 | Name | Type | Required | DefaultValue | Description 73 | --- | --- | --- | --- | --- 74 | prefixCls | string | false | '' | class前缀 75 | width | number | false | 200 | 编辑区域宽度 76 | height | number | false | 30 | 编辑区域高度 77 | placeholder | string | false | '' | placeholder 78 | mentionFormatter | func | false | (data) => ` @${data.text} ` | 自定义插入的mention内容 79 | onChange | func | false | () => {} | 发生变化后的触发 80 | onAdd | func | false | () => {} | 添加mention后触发 81 | defaultValue | string | false | '' | 默认内容 82 | readOnly | bool | false | false | 只读 83 | delimiter | string | false | '@' | 触发字符 84 | ## TextareaEditor 85 | 86 | textarea中使用mention 87 | 88 | ### PROPS 89 | Name | Type | Required | DefaultValue | Description 90 | --- | --- | --- | --- | --- 91 | prefixCls | string | false | '' | class前缀 92 | width | number | false | 200 | 编辑区域宽度 93 | height | number | false | 100 | 编辑区域高度 94 | placeholder | string | false | '' | placeholder 95 | mentionFormatter | func | false | (data) => ` @${data.text} ` | 自定义插入的mention内容 96 | onChange | func | false | () => {} | 发生变化后的触发 97 | onAdd | func | false | () => {} | 添加mention后触发 98 | defaultValue | string | false | '' | 默认内容 99 | readOnly | bool | false | false | 只读 100 | delimiter | string | false | '@' | 触发字符 -------------------------------------------------------------------------------- /coverage/lcov-report/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Code coverage report for src/ 5 | 6 | 7 | 8 | 9 | 14 | 15 | 16 |
17 |
18 |

19 | all files src/ 20 |

21 |
22 |
23 | 100% 24 | Statements 25 | 19/19 26 |
27 |
28 | 75% 29 | Branches 30 | 3/4 31 |
32 |
33 | 100% 34 | Functions 35 | 1/1 36 |
37 |
38 | 100% 39 | Lines 40 | 18/18 41 |
42 |
43 |
44 |
45 |
46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 |
FileStatementsBranchesFunctionsLines
index.js
100%19/1975%3/4100%1/1100%18/18
76 |
77 |
78 | 82 | 83 | 84 | 91 | 92 | 93 | 94 | -------------------------------------------------------------------------------- /src/components/Mention.jsx: -------------------------------------------------------------------------------- 1 | /** 2 | * @author: vincent.bian 3 | */ 4 | import React, { Component } from 'react'; 5 | import PropTypes from 'prop-types'; 6 | import reactMixin from 'react-mixin'; 7 | import Panel from './Panel'; 8 | import mentionMixin from './mentionMixin'; 9 | 10 | /** 11 | * Mention Component 12 | */ 13 | class Mention extends Component { 14 | 15 | static displayName = 'Mention'; 16 | static propTypes = { 17 | /** 18 | * @i18n {zh-CN} class前缀 19 | * @i18n {en-US} class prefix 20 | */ 21 | prefixCls: PropTypes.string, 22 | /** 23 | * @i18n {zh-CN} 定义数据源 24 | * @i18n {en-US} data source for mention content 25 | */ 26 | source: PropTypes.oneOfType([ 27 | PropTypes.array, 28 | PropTypes.func, 29 | ]), 30 | /** 31 | * @i18n {zh-CN} 数据源查询延时 32 | * @i18n {en-US} debounce of the request to data source 33 | */ 34 | delay: PropTypes.number, 35 | /** 36 | * @i18n {zh-CN} 匹配字符区间 37 | * @i18n {en-US} only match the string after delimiter which the length in this range 38 | */ 39 | matchRange: PropTypes.arrayOf(PropTypes.number), 40 | /** 41 | * @i18n {zh-CN} 数据源格式化匹配 42 | * @i18n {en-US} format the data form source 43 | */ 44 | formatter: PropTypes.func, 45 | /** 46 | * @i18n {zh-CN} 自定义选择列表 47 | * @i18n {en-US} customize the panel display 48 | */ 49 | panelFormatter: PropTypes.func, 50 | /** 51 | * @i18n {zh-CN} 发生变化后的触发 52 | * @i18n {en-US} trigger when editor content change 53 | */ 54 | onChange: PropTypes.func, 55 | children: PropTypes.any, 56 | }; 57 | static defaultProps = { 58 | prefixCls: 'kuma-mention', 59 | source: [], 60 | delay: 100, 61 | matchRange: [2, 8], 62 | formatter: (data) => data, 63 | panelFormatter: (data) => `${data.text}`, 64 | onChange: () => {}, 65 | }; 66 | 67 | constructor(props) { 68 | super(props); 69 | this.state = { 70 | mentionList: [], 71 | cursorPosition: { 72 | x: 0, 73 | y: 0, 74 | }, 75 | panelVisible: false, 76 | panelIdx: 0, 77 | }; 78 | } 79 | componentDidMount() { 80 | this.activeEditor = null; 81 | } 82 | /** 83 | * description of onfocus 84 | * @i18n {zh-CN} 测试api文档 85 | * @i18n {en-US} test api doc 86 | */ 87 | onFocus(editorInstance) { 88 | this.activeEditor = editorInstance; 89 | } 90 | /** 91 | * description of setPanelPos 92 | */ 93 | setPanelPos(pos) { 94 | const position = { 95 | x: pos.x, 96 | y: pos.y, 97 | }; 98 | this.setState({ 99 | cursorPosition: position, 100 | }); 101 | } 102 | selectItem(data) { 103 | this.setState({ 104 | mentionList: [], 105 | }); 106 | this.activeEditor.insertMentionData(data); 107 | } 108 | 109 | render() { 110 | const panelPosition = { 111 | left: this.state.cursorPosition.x, 112 | top: this.state.cursorPosition.y, 113 | }; 114 | const { prefixCls, onChange, children, panelFormatter, matchRange } = this.props; 115 | return ( 116 |
117 | { 118 | React.Children.map(children, (Comp) => 119 | React.cloneElement(Comp, { 120 | prefixCls, 121 | panelVisible: this.state.panelVisible, 122 | matcher: this.runMatcher.bind(this), 123 | setCursorPos: this.setPanelPos.bind(this), 124 | onChange, 125 | onFocus: this.onFocus.bind(this), 126 | matchRange, 127 | }) 128 | ) 129 | } 130 | 139 |
140 | ); 141 | } 142 | } 143 | 144 | reactMixin(Mention.prototype, mentionMixin); 145 | 146 | export default Mention; 147 | -------------------------------------------------------------------------------- /doc/en-US.md: -------------------------------------------------------------------------------- 1 | ## Mention 2 | 3 | Mention Component 4 | 5 | ### API 6 | 7 | - onFocus(editorInstance): test api doc 8 | - setPanelPos(pos): description of setPanelPos 9 | 10 | ### PROPS 11 | Name | Type | Required | DefaultValue | Description 12 | --- | --- | --- | --- | --- 13 | prefixCls | string | false | 'kuma-mention' | class prefix 14 | source | union (array, func) | false | [] | data source for mention content 15 | delay | number | false | 100 | debounce of the request to data source 16 | matchRange | arrayOf number | false | [2, 8] | only match the string after delimiter which the length in this range 17 | formatter | func | false | (data) => data | format the data form source 18 | panelFormatter | func | false | (data) => `${data.text}` | customize the panel display 19 | onChange | func | false | (e, value) => {} | trigger when editor content change 20 | ## uxcore-mention-panel 21 | 22 | 23 | 24 | ### PROPS 25 | Name | Type | Required | DefaultValue | Description 26 | --- | --- | --- | --- | --- 27 | prefixCls | string | false | '' | 28 | list | array | false | [] | 29 | style | object | false | {} | 30 | idx | number | false | 0 | 31 | onSelect | func | false | null | 32 | formatter | func | false | '' | 33 | ## TinymceMention 34 | 35 | Mention for Tinymce 36 | 37 | ### PROPS 38 | Name | Type | Required | DefaultValue | Description 39 | --- | --- | --- | --- | --- 40 | prefixCls | string | false | 'kuma-mention' | class prefix 41 | source | union (array, func) | false | [] | data source for mention content 42 | delay | number | false | 100 | debounce of the request to data source 43 | matchRange | arrayOf number | false | [2, 8] | only match the string after delimiter which the length in this range 44 | formatter | func | false | (data) => data | format the data form source 45 | mentionFormatter | func | false | (data) => `@${data.text}` | customize the insert content with this function | function 46 | panelFormatter | func | false | (data) => `${data.text}` | customize the panel display 47 | onChange | func | false | (e, value) => {} | trigger when editor content change 48 | onAdd | func | false | () => {} | Callback invoked when a mention has been added 49 | insertMode | enum ('ELEMENT_NODE', 'TEXT_NODE') | false | 'ELEMENT_NODE' | `ELEMENT_NODE` will insert mention content with a button, `TEXT_NODE` will insert with a text node 50 | ## ContentEditableEditor 51 | 52 | 53 | 54 | ### PROPS 55 | Name | Type | Required | DefaultValue | Description 56 | --- | --- | --- | --- | --- 57 | prefixCls | string | false | '' | class prefix 58 | width | number | false | 200 | editor's width 59 | height | number | false | 100 | editor's height 60 | placeholder | string | false | '' | placeholder 61 | mentionFormatter | func | false | (data) => `@${data.text}` | customize the insert content with this function | function 62 | onChange | func | false | () => {} | trigger when editor content change 63 | onAdd | func | false | () => {} | Callback invoked when a mention has been added 64 | defaultValue | string | false | '' | default value 65 | readOnly | bool | false | false | read only 66 | delimiter | string | false | '@' | Defines the char sequence upon which to trigger querying the data source 67 | ## InputEditor 68 | 69 | mention in input 70 | 71 | ### PROPS 72 | Name | Type | Required | DefaultValue | Description 73 | --- | --- | --- | --- | --- 74 | prefixCls | string | false | '' | class prefix 75 | width | number | false | 200 | editor's width 76 | height | number | false | 30 | editor's height 77 | placeholder | string | false | '' | placeholder 78 | mentionFormatter | func | false | (data) => ` @${data.text} ` | customize the insert content with this function | function 79 | onChange | func | false | () => {} | trigger when editor content change 80 | onAdd | func | false | () => {} | Callback invoked when a mention has been added 81 | defaultValue | string | false | '' | default value 82 | readOnly | bool | false | false | read only 83 | delimiter | string | false | '@' | Defines the char sequence upon which to trigger querying the data source 84 | ## TextareaEditor 85 | 86 | mention in textarea 87 | 88 | ### PROPS 89 | Name | Type | Required | DefaultValue | Description 90 | --- | --- | --- | --- | --- 91 | prefixCls | string | false | '' | class prefix 92 | width | number | false | 200 | editor's width 93 | height | number | false | 100 | editor's height 94 | placeholder | string | false | '' | placeholder 95 | mentionFormatter | func | false | (data) => ` @${data.text} ` | customize the insert content with this function | function 96 | onChange | func | false | () => {} | trigger when editor content change 97 | onAdd | func | false | () => {} | Callback invoked when a mention has been added 98 | defaultValue | string | false | '' | default value 99 | readOnly | bool | false | false | read only 100 | delimiter | string | false | '@' | Defines the char sequence upon which to trigger querying the data source -------------------------------------------------------------------------------- /demo/MentionDemo.jsx: -------------------------------------------------------------------------------- 1 | /** 2 | * example index 3 | */ 4 | import React, { Component } from 'react'; 5 | import Tinymce from 'uxcore-tinymce'; 6 | import Dialog from 'uxcore-dialog'; 7 | import Mention, { 8 | ContenteditableEditor, 9 | TextareaEditor, 10 | InputEditor, 11 | TinymceMention, 12 | } from '../src'; 13 | import mockData from './mockData.json'; 14 | 15 | function formatter(data) { 16 | return data.map((text) => ({ text })); 17 | } 18 | 19 | const source = ['aaaaa', 'aabbb', 'aaccc', 'bbbcc', 'dddee', 'fffqq', 'pppaa', 'ppccc']; 20 | 21 | function getPersonData(keyword, next) { 22 | setTimeout(() => next(mockData), 100); 23 | } 24 | 25 | function personDataFormatter(data) { 26 | return data.map((item) => Object.assign(item, { 27 | displayName: item.name + (item.nickNameCn ? `(${item.nickNameCn})` : ''), 28 | })); 29 | } 30 | 31 | function personPanelFormatter(data) { 32 | return ` 33 | ${data.displayName} - ${data.emplId}`; 34 | } 35 | 36 | function personMentionFormatter(data) { 37 | return `@${data.name}(${data.emplId})`; 38 | } 39 | 40 | export default class Demo extends Component { 41 | constructor(props) { 42 | super(props); 43 | this.state = { 44 | content: 'xxx', 45 | basicContent: 'basic content', 46 | personContent: '', 47 | readOnly: false, 48 | showDialog: false, 49 | }; 50 | } 51 | onToggleReadOnly() { 52 | this.setState({ 53 | readOnly: !this.state.readOnly, 54 | }); 55 | } 56 | toggleDialog() { 57 | this.setState({ 58 | showDialog: !this.state.showDialog, 59 | }); 60 | } 61 | render() { 62 | return ( 63 |
64 | 65 | 72 | 78 | 84 | 85 | 86 |
87 | 95 |
96 |

BASIC:

97 | global.console.log(e, content)} 101 | formatter={formatter} 102 | > 103 | 112 | 120 | 129 | 130 | 131 |

SELECT PERSON:

132 | {}} 138 | > 139 | 140 | 141 |

Tinymce

142 | 149 | 150 | 151 |
152 | ); 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /coverage/lcov-report/src/utils/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Code coverage report for src/utils/ 5 | 6 | 7 | 8 | 9 | 14 | 15 | 16 |
17 |
18 |

19 | all files src/utils/ 20 |

21 |
22 |
23 | 39.28% 24 | Statements 25 | 163/415 26 |
27 |
28 | 25.37% 29 | Branches 30 | 51/201 31 |
32 |
33 | 29.79% 34 | Functions 35 | 14/47 36 |
37 |
38 | 39.56% 39 | Lines 40 | 163/412 41 |
42 |
43 |
44 |
45 |
46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 |
FileStatementsBranchesFunctionsLines
keycode.js
100%2/2100%0/0100%0/0100%2/2
rangy-position.js
21.84%64/2939.93%15/15117.5%7/4022.07%64/290
util.js
80.83%97/12072%36/50100%7/780.83%97/120
102 |
103 |
104 | 108 | 109 | 110 | 117 | 118 | 119 | 120 | -------------------------------------------------------------------------------- /demo/mockData.json: -------------------------------------------------------------------------------- 1 | [{ 2 | "emplId": "38900", 3 | "nickNameCn": "沧竹", 4 | "deptDesc": "安全部-安全技术与产品-开放安全", 5 | "hrStatus": "", 6 | "jobDesc": "总监", 7 | "orderNum": "1", 8 | "jobSubCount": 12, 9 | "havanaId": 917386119, 10 | "mobilePhone": "", 11 | "deptId": "", 12 | "email": "xiong.wang@taobao.com", 13 | "leader": true, 14 | "hasTeam": false, 15 | "jobTeamCount": 12, 16 | "name": "WANG XIONG", 17 | "id": "1050", 18 | "avatar": "//work.alibaba-inc.com/photo/38900.jpg", 19 | "pinyin": "", 20 | "avatarBig": "" 21 | }, { 22 | "emplId": "45358", 23 | "nickNameCn": "秦予", 24 | "deptDesc": "数据技术及产品部-数据科学", 25 | "hrStatus": "", 26 | "jobDesc": "资深总监", 27 | "orderNum": "0", 28 | "jobSubCount": 0, 29 | "havanaId": 650455055, 30 | "mobilePhone": "", 31 | "deptId": "", 32 | "email": "amy.wangqy@alipay.com", 33 | "leader": false, 34 | "hasTeam": false, 35 | "jobTeamCount": 0, 36 | "name": "WANG QINYU", 37 | "id": "75222", 38 | "avatar": "//work.alibaba-inc.com/photo/45358.jpg?1361881993", 39 | "pinyin": "", 40 | "avatarBig": "" 41 | }, { 42 | "emplId": "45924", 43 | "nickNameCn": "", 44 | "deptDesc": "综合支持部-置业部-平台中心", 45 | "hrStatus": "", 46 | "jobDesc": "资深专家", 47 | "orderNum": "0", 48 | "jobSubCount": 3, 49 | "havanaId": 1095274110879, 50 | "mobilePhone": "", 51 | "deptId": "", 52 | "email": "ally.wangl@alibaba-inc.com", 53 | "leader": true, 54 | "hasTeam": false, 55 | "jobTeamCount": 3, 56 | "name": "WANG LEI", 57 | "id": "76014", 58 | "avatar": "//work.alibaba-inc.com/photo/45924.jpg?1363620243", 59 | "pinyin": "", 60 | "avatarBig": "" 61 | }, { 62 | "emplId": "67471", 63 | "nickNameCn": "", 64 | "deptDesc": "关联公司-11Main-Product&Research", 65 | "hrStatus": "", 66 | "jobDesc": "Development Intern", 67 | "orderNum": "1", 68 | "jobSubCount": 0, 69 | "havanaId": 1728150404, 70 | "mobilePhone": "", 71 | "deptId": "", 72 | "email": "hwang@alibaba-inc.com", 73 | "leader": false, 74 | "hasTeam": false, 75 | "jobTeamCount": 0, 76 | "name": "WANG, Haiyang", 77 | "id": "5986153", 78 | "avatar": "//work.alibaba-inc.com/photo/67471.jpg", 79 | "pinyin": "", 80 | "avatarBig": "" 81 | }, { 82 | "emplId": "68699", 83 | "nickNameCn": "", 84 | "deptDesc": "关联公司-11Main-Sales&Marketing", 85 | "hrStatus": "", 86 | "jobDesc": "Category Manager", 87 | "orderNum": "0", 88 | "jobSubCount": 0, 89 | "havanaId": 1820630230, 90 | "mobilePhone": "", 91 | "deptId": "", 92 | "email": "ywang@alibaba-inc.com", 93 | "leader": false, 94 | "hasTeam": false, 95 | "jobTeamCount": 0, 96 | "name": "WANG, Yan", 97 | "id": "5996961", 98 | "avatar": "//work.alibaba-inc.com/photo/68699.jpg", 99 | "pinyin": "", 100 | "avatarBig": "" 101 | }, { 102 | "emplId": "71821", 103 | "nickNameCn": "", 104 | "deptDesc": "国际事业部-IBDM&ICVS-海外市场-美国", 105 | "hrStatus": "", 106 | "jobDesc": "Office Manager", 107 | "orderNum": "0", 108 | "jobSubCount": 0, 109 | "havanaId": 2156383741, 110 | "mobilePhone": "", 111 | "deptId": "", 112 | "email": "stella.wang@alibaba-inc.com", 113 | "leader": false, 114 | "hasTeam": false, 115 | "jobTeamCount": 0, 116 | "name": "WANG, Yinghua", 117 | "id": "6125609", 118 | "avatar": "//work.alibaba-inc.com/photo/71821.jpg?1404355781", 119 | "pinyin": "", 120 | "avatarBig": "" 121 | }, { 122 | "emplId": "58", 123 | "nickNameCn": "", 124 | "deptDesc": "OS事业群-OS TV事业部-商务合作", 125 | "hrStatus": "", 126 | "jobDesc": "资深总监", 127 | "orderNum": "0", 128 | "jobSubCount": 8, 129 | "havanaId": 1095274160586, 130 | "mobilePhone": "", 131 | "deptId": "", 132 | "email": "bean@alibaba-inc.com", 133 | "leader": true, 134 | "hasTeam": true, 135 | "jobTeamCount": 9, 136 | "name": "王志雷", 137 | "id": "3132", 138 | "avatar": "//work.alibaba-inc.com/photo/58.jpg?1378781324", 139 | "pinyin": "", 140 | "avatarBig": "" 141 | }, { 142 | "emplId": "83", 143 | "nickNameCn": "纯臻", 144 | "deptDesc": "人力资源部-业务HR", 145 | "hrStatus": "", 146 | "jobDesc": "总监", 147 | "orderNum": "1", 148 | "jobSubCount": 1, 149 | "havanaId": 1095274200588, 150 | "mobilePhone": "", 151 | "deptId": "", 152 | "email": "chris_w@alibaba-inc.com", 153 | "leader": true, 154 | "hasTeam": false, 155 | "jobTeamCount": 1, 156 | "name": "王颖", 157 | "id": "3158", 158 | "avatar": "//work.alibaba-inc.com/photo/83.jpg", 159 | "pinyin": "", 160 | "avatarBig": "" 161 | }, { 162 | "emplId": "144", 163 | "nickNameCn": "王建勋", 164 | "deptDesc": "综合支持部-置业部", 165 | "hrStatus": "", 166 | "jobDesc": "副总裁", 167 | "orderNum": "0", 168 | "jobSubCount": 4, 169 | "havanaId": 22045531, 170 | "mobilePhone": "", 171 | "deptId": "", 172 | "email": "wjxbill@taobao.com", 173 | "leader": true, 174 | "hasTeam": true, 175 | "jobTeamCount": 125, 176 | "name": "王建勋", 177 | "id": "3216", 178 | "avatar": "//work.alibaba-inc.com/photo/144.jpg", 179 | "pinyin": "", 180 | "avatarBig": "" 181 | }, { 182 | "emplId": "158", 183 | "nickNameCn": "悦航", 184 | "deptDesc": "小微金服-人力资源部-业务HR", 185 | "hrStatus": "", 186 | "jobDesc": "高级人事专家", 187 | "orderNum": "0", 188 | "jobSubCount": 7, 189 | "havanaId": 418559989, 190 | "mobilePhone": "", 191 | "deptId": "", 192 | "email": "mashaw@alipay.com", 193 | "leader": true, 194 | "hasTeam": false, 195 | "jobTeamCount": 7, 196 | "name": "王珏", 197 | "id": "3228", 198 | "avatar": "//work.alibaba-inc.com/photo/158.jpg", 199 | "pinyin": "", 200 | "avatarBig": "" 201 | }] 202 | -------------------------------------------------------------------------------- /coverage/lcov-report/sorter.js: -------------------------------------------------------------------------------- 1 | var addSorting = (function () { 2 | "use strict"; 3 | var cols, 4 | currentSort = { 5 | index: 0, 6 | desc: false 7 | }; 8 | 9 | // returns the summary table element 10 | function getTable() { return document.querySelector('.coverage-summary'); } 11 | // returns the thead element of the summary table 12 | function getTableHeader() { return getTable().querySelector('thead tr'); } 13 | // returns the tbody element of the summary table 14 | function getTableBody() { return getTable().querySelector('tbody'); } 15 | // returns the th element for nth column 16 | function getNthColumn(n) { return getTableHeader().querySelectorAll('th')[n]; } 17 | 18 | // loads all columns 19 | function loadColumns() { 20 | var colNodes = getTableHeader().querySelectorAll('th'), 21 | colNode, 22 | cols = [], 23 | col, 24 | i; 25 | 26 | for (i = 0; i < colNodes.length; i += 1) { 27 | colNode = colNodes[i]; 28 | col = { 29 | key: colNode.getAttribute('data-col'), 30 | sortable: !colNode.getAttribute('data-nosort'), 31 | type: colNode.getAttribute('data-type') || 'string' 32 | }; 33 | cols.push(col); 34 | if (col.sortable) { 35 | col.defaultDescSort = col.type === 'number'; 36 | colNode.innerHTML = colNode.innerHTML + ''; 37 | } 38 | } 39 | return cols; 40 | } 41 | // attaches a data attribute to every tr element with an object 42 | // of data values keyed by column name 43 | function loadRowData(tableRow) { 44 | var tableCols = tableRow.querySelectorAll('td'), 45 | colNode, 46 | col, 47 | data = {}, 48 | i, 49 | val; 50 | for (i = 0; i < tableCols.length; i += 1) { 51 | colNode = tableCols[i]; 52 | col = cols[i]; 53 | val = colNode.getAttribute('data-value'); 54 | if (col.type === 'number') { 55 | val = Number(val); 56 | } 57 | data[col.key] = val; 58 | } 59 | return data; 60 | } 61 | // loads all row data 62 | function loadData() { 63 | var rows = getTableBody().querySelectorAll('tr'), 64 | i; 65 | 66 | for (i = 0; i < rows.length; i += 1) { 67 | rows[i].data = loadRowData(rows[i]); 68 | } 69 | } 70 | // sorts the table using the data for the ith column 71 | function sortByIndex(index, desc) { 72 | var key = cols[index].key, 73 | sorter = function (a, b) { 74 | a = a.data[key]; 75 | b = b.data[key]; 76 | return a < b ? -1 : a > b ? 1 : 0; 77 | }, 78 | finalSorter = sorter, 79 | tableBody = document.querySelector('.coverage-summary tbody'), 80 | rowNodes = tableBody.querySelectorAll('tr'), 81 | rows = [], 82 | i; 83 | 84 | if (desc) { 85 | finalSorter = function (a, b) { 86 | return -1 * sorter(a, b); 87 | }; 88 | } 89 | 90 | for (i = 0; i < rowNodes.length; i += 1) { 91 | rows.push(rowNodes[i]); 92 | tableBody.removeChild(rowNodes[i]); 93 | } 94 | 95 | rows.sort(finalSorter); 96 | 97 | for (i = 0; i < rows.length; i += 1) { 98 | tableBody.appendChild(rows[i]); 99 | } 100 | } 101 | // removes sort indicators for current column being sorted 102 | function removeSortIndicators() { 103 | var col = getNthColumn(currentSort.index), 104 | cls = col.className; 105 | 106 | cls = cls.replace(/ sorted$/, '').replace(/ sorted-desc$/, ''); 107 | col.className = cls; 108 | } 109 | // adds sort indicators for current column being sorted 110 | function addSortIndicators() { 111 | getNthColumn(currentSort.index).className += currentSort.desc ? ' sorted-desc' : ' sorted'; 112 | } 113 | // adds event listeners for all sorter widgets 114 | function enableUI() { 115 | var i, 116 | el, 117 | ithSorter = function ithSorter(i) { 118 | var col = cols[i]; 119 | 120 | return function () { 121 | var desc = col.defaultDescSort; 122 | 123 | if (currentSort.index === i) { 124 | desc = !currentSort.desc; 125 | } 126 | sortByIndex(i, desc); 127 | removeSortIndicators(); 128 | currentSort.index = i; 129 | currentSort.desc = desc; 130 | addSortIndicators(); 131 | }; 132 | }; 133 | for (i =0 ; i < cols.length; i += 1) { 134 | if (cols[i].sortable) { 135 | // add the click event handler on the th so users 136 | // dont have to click on those tiny arrows 137 | el = getNthColumn(i).querySelector('.sorter').parentElement; 138 | if (el.addEventListener) { 139 | el.addEventListener('click', ithSorter(i)); 140 | } else { 141 | el.attachEvent('onclick', ithSorter(i)); 142 | } 143 | } 144 | } 145 | } 146 | // adds sorting functionality to the UI 147 | return function () { 148 | if (!getTable()) { 149 | return; 150 | } 151 | cols = loadColumns(); 152 | loadData(cols); 153 | addSortIndicators(); 154 | enableUI(); 155 | }; 156 | })(); 157 | 158 | window.addEventListener('load', addSorting); 159 | -------------------------------------------------------------------------------- /coverage/lcov-report/base.css: -------------------------------------------------------------------------------- 1 | body, html { 2 | margin:0; padding: 0; 3 | height: 100%; 4 | } 5 | body { 6 | font-family: Helvetica Neue, Helvetica, Arial; 7 | font-size: 14px; 8 | color:#333; 9 | } 10 | .small { font-size: 12px; } 11 | *, *:after, *:before { 12 | -webkit-box-sizing:border-box; 13 | -moz-box-sizing:border-box; 14 | box-sizing:border-box; 15 | } 16 | h1 { font-size: 20px; margin: 0;} 17 | h2 { font-size: 14px; } 18 | pre { 19 | font: 12px/1.4 Consolas, "Liberation Mono", Menlo, Courier, monospace; 20 | margin: 0; 21 | padding: 0; 22 | -moz-tab-size: 2; 23 | -o-tab-size: 2; 24 | tab-size: 2; 25 | } 26 | a { color:#0074D9; text-decoration:none; } 27 | a:hover { text-decoration:underline; } 28 | .strong { font-weight: bold; } 29 | .space-top1 { padding: 10px 0 0 0; } 30 | .pad2y { padding: 20px 0; } 31 | .pad1y { padding: 10px 0; } 32 | .pad2x { padding: 0 20px; } 33 | .pad2 { padding: 20px; } 34 | .pad1 { padding: 10px; } 35 | .space-left2 { padding-left:55px; } 36 | .space-right2 { padding-right:20px; } 37 | .center { text-align:center; } 38 | .clearfix { display:block; } 39 | .clearfix:after { 40 | content:''; 41 | display:block; 42 | height:0; 43 | clear:both; 44 | visibility:hidden; 45 | } 46 | .fl { float: left; } 47 | @media only screen and (max-width:640px) { 48 | .col3 { width:100%; max-width:100%; } 49 | .hide-mobile { display:none!important; } 50 | } 51 | 52 | .quiet { 53 | color: #7f7f7f; 54 | color: rgba(0,0,0,0.5); 55 | } 56 | .quiet a { opacity: 0.7; } 57 | 58 | .fraction { 59 | font-family: Consolas, 'Liberation Mono', Menlo, Courier, monospace; 60 | font-size: 10px; 61 | color: #555; 62 | background: #E8E8E8; 63 | padding: 4px 5px; 64 | border-radius: 3px; 65 | vertical-align: middle; 66 | } 67 | 68 | div.path a:link, div.path a:visited { color: #333; } 69 | table.coverage { 70 | border-collapse: collapse; 71 | margin: 10px 0 0 0; 72 | padding: 0; 73 | } 74 | 75 | table.coverage td { 76 | margin: 0; 77 | padding: 0; 78 | vertical-align: top; 79 | } 80 | table.coverage td.line-count { 81 | text-align: right; 82 | padding: 0 5px 0 20px; 83 | } 84 | table.coverage td.line-coverage { 85 | text-align: right; 86 | padding-right: 10px; 87 | min-width:20px; 88 | } 89 | 90 | table.coverage td span.cline-any { 91 | display: inline-block; 92 | padding: 0 5px; 93 | width: 100%; 94 | } 95 | .missing-if-branch { 96 | display: inline-block; 97 | margin-right: 5px; 98 | border-radius: 3px; 99 | position: relative; 100 | padding: 0 4px; 101 | background: #333; 102 | color: yellow; 103 | } 104 | 105 | .skip-if-branch { 106 | display: none; 107 | margin-right: 10px; 108 | position: relative; 109 | padding: 0 4px; 110 | background: #ccc; 111 | color: white; 112 | } 113 | .missing-if-branch .typ, .skip-if-branch .typ { 114 | color: inherit !important; 115 | } 116 | .coverage-summary { 117 | border-collapse: collapse; 118 | width: 100%; 119 | } 120 | .coverage-summary tr { border-bottom: 1px solid #bbb; } 121 | .keyline-all { border: 1px solid #ddd; } 122 | .coverage-summary td, .coverage-summary th { padding: 10px; } 123 | .coverage-summary tbody { border: 1px solid #bbb; } 124 | .coverage-summary td { border-right: 1px solid #bbb; } 125 | .coverage-summary td:last-child { border-right: none; } 126 | .coverage-summary th { 127 | text-align: left; 128 | font-weight: normal; 129 | white-space: nowrap; 130 | } 131 | .coverage-summary th.file { border-right: none !important; } 132 | .coverage-summary th.pct { } 133 | .coverage-summary th.pic, 134 | .coverage-summary th.abs, 135 | .coverage-summary td.pct, 136 | .coverage-summary td.abs { text-align: right; } 137 | .coverage-summary td.file { white-space: nowrap; } 138 | .coverage-summary td.pic { min-width: 120px !important; } 139 | .coverage-summary tfoot td { } 140 | 141 | .coverage-summary .sorter { 142 | height: 10px; 143 | width: 7px; 144 | display: inline-block; 145 | margin-left: 0.5em; 146 | background: url(sort-arrow-sprite.png) no-repeat scroll 0 0 transparent; 147 | } 148 | .coverage-summary .sorted .sorter { 149 | background-position: 0 -20px; 150 | } 151 | .coverage-summary .sorted-desc .sorter { 152 | background-position: 0 -10px; 153 | } 154 | .status-line { height: 10px; } 155 | /* dark red */ 156 | .red.solid, .status-line.low, .low .cover-fill { background:#C21F39 } 157 | .low .chart { border:1px solid #C21F39 } 158 | /* medium red */ 159 | .cstat-no, .fstat-no, .cbranch-no, .cbranch-no { background:#F6C6CE } 160 | /* light red */ 161 | .low, .cline-no { background:#FCE1E5 } 162 | /* light green */ 163 | .high, .cline-yes { background:rgb(230,245,208) } 164 | /* medium green */ 165 | .cstat-yes { background:rgb(161,215,106) } 166 | /* dark green */ 167 | .status-line.high, .high .cover-fill { background:rgb(77,146,33) } 168 | .high .chart { border:1px solid rgb(77,146,33) } 169 | /* dark yellow (gold) */ 170 | .medium .chart { border:1px solid #f9cd0b; } 171 | .status-line.medium, .medium .cover-fill { background: #f9cd0b; } 172 | /* light yellow */ 173 | .medium { background: #fff4c2; } 174 | /* light gray */ 175 | span.cline-neutral { background: #eaeaea; } 176 | 177 | .cbranch-no { background: yellow !important; color: #111; } 178 | 179 | .cstat-skip { background: #ddd; color: #111; } 180 | .fstat-skip { background: #ddd; color: #111 !important; } 181 | .cbranch-skip { background: #ddd !important; color: #111; } 182 | 183 | 184 | .cover-fill, .cover-empty { 185 | display:inline-block; 186 | height: 12px; 187 | } 188 | .chart { 189 | line-height: 0; 190 | } 191 | .cover-empty { 192 | background: white; 193 | } 194 | .cover-full { 195 | border-right: none !important; 196 | } 197 | pre.prettyprint { 198 | border: none !important; 199 | padding: 0 !important; 200 | margin: 0 !important; 201 | } 202 | .com { color: #999 !important; } 203 | .ignore-none { color: #999; font-weight: normal; } 204 | 205 | .wrapper { 206 | min-height: 100%; 207 | height: auto !important; 208 | height: 100%; 209 | margin: 0 auto -48px; 210 | } 211 | .footer, .push { 212 | height: 48px; 213 | } 214 | -------------------------------------------------------------------------------- /coverage/lcov-report/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Code coverage report for All files 5 | 6 | 7 | 8 | 9 | 14 | 15 | 16 |
17 |
18 |

19 | / 20 |

21 |
22 |
23 | 69.1% 24 | Statements 25 | 834/1207 26 |
27 |
28 | 46.3% 29 | Branches 30 | 269/581 31 |
32 |
33 | 75.13% 34 | Functions 35 | 142/189 36 |
37 |
38 | 72.95% 39 | Lines 40 | 774/1061 41 |
42 |
43 |
44 |
45 |
46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 |
FileStatementsBranchesFunctionsLines
src/
100%19/1975%3/4100%1/1100%18/18
src/components/
87.2%286/32857.62%87/15188.89%56/6395.93%259/270
src/editor-components/
82.25%366/44556.89%128/22591.03%71/7892.52%334/361
src/utils/
39.28%163/41525.37%51/20129.79%14/4739.56%163/412
115 |
116 |
117 | 121 | 122 | 123 | 130 | 131 | 132 | 133 | -------------------------------------------------------------------------------- /coverage/lcov-report/src/components/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Code coverage report for src/components/ 5 | 6 | 7 | 8 | 9 | 14 | 15 | 16 |
17 |
18 |

19 | all files src/components/ 20 |

21 |
22 |
23 | 87.2% 24 | Statements 25 | 286/328 26 |
27 |
28 | 57.62% 29 | Branches 30 | 87/151 31 |
32 |
33 | 88.89% 34 | Functions 35 | 56/63 36 |
37 |
38 | 95.93% 39 | Lines 40 | 259/270 41 |
42 |
43 |
44 |
45 |
46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 |
FileStatementsBranchesFunctionsLines
Mention.jsx
83.33%55/6646.43%13/2887.5%14/1697.92%47/48
Panel.jsx
78.85%41/5246.67%14/3090%9/10100%30/30
TinymceMention.jsx
89.02%154/17358.21%39/6786.67%26/3094.19%146/155
mentionMixin.js
97.3%36/3780.77%21/26100%7/797.3%36/37
115 |
116 |
117 | 121 | 122 | 123 | 130 | 131 | 132 | 133 | -------------------------------------------------------------------------------- /src/editor-components/textareaEditor.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import BaseEditor from './baseEditor'; 4 | import { parseStrByDelimiter, getCaretOffset, getCaretPosition, createEvent } from '../utils/util'; 5 | 6 | /** 7 | * @i18n {zh-CN} textarea中使用mention 8 | * @i18n {en-US} mention in textarea 9 | */ 10 | export default class TextareaEditor extends BaseEditor { 11 | 12 | static displayName = 'TextareaEditor'; 13 | static propTypes = { 14 | /** 15 | * @i18n {zh-CN} class前缀 16 | * @i18n {en-US} class prefix 17 | */ 18 | prefixCls: PropTypes.string, 19 | /** 20 | * @i18n {zh-CN} 编辑区域宽度 21 | * @i18n {en-US} editor's width 22 | */ 23 | width: PropTypes.oneOfType([ 24 | PropTypes.number, 25 | PropTypes.string, 26 | ]), 27 | /** 28 | * @i18n {zh-CN} 编辑区域高度 29 | * @i18n {en-US} editor's height 30 | */ 31 | height: PropTypes.number, 32 | /** 33 | * @i18n {zh-CN} placeholder 34 | * @i18n {en-US} placeholder 35 | */ 36 | placeholder: PropTypes.string, 37 | /** 38 | * @i18n {zh-CN} 自定义插入的mention内容 39 | * @i18n {en-US} customize the insert content with this function | function 40 | */ 41 | mentionFormatter: PropTypes.func, 42 | /** 43 | * @i18n {zh-CN} 发生变化后的触发 44 | * @i18n {en-US} trigger when editor content change 45 | */ 46 | // onChange: PropTypes.func, 47 | /** 48 | * @i18n {zh-CN} 添加mention后触发 49 | * @i18n {en-US} Callback invoked when a mention has been added 50 | */ 51 | onAdd: PropTypes.func, 52 | /** 53 | * @i18n {zh-CN} 默认内容 54 | * @i18n {en-US} default value 55 | */ 56 | defaultValue: PropTypes.string, 57 | /** 58 | * @i18n {zh-CN} 内容 59 | * @i18n {en-US} value 60 | */ 61 | value: PropTypes.string, 62 | /** 63 | * @i18n {zh-CN} 只读 64 | * @i18n {en-US} read only 65 | */ 66 | readOnly: PropTypes.bool, 67 | /** 68 | * @i18n {zh-CN} 触发字符 69 | * @i18n {en-US} Defines the char sequence upon which to trigger querying the data source 70 | */ 71 | delimiter: PropTypes.string, 72 | /** 73 | * @i18n {zh-CN} 最大长度 74 | * @i18n {en-US} max length of content 75 | */ 76 | maxLength: PropTypes.number, 77 | }; 78 | static defaultProps = { 79 | prefixCls: '', 80 | width: 200, 81 | height: 100, 82 | placeholder: '', 83 | mentionFormatter: (data) => ` @${data.text} `, 84 | // onChange: () => {}, 85 | onAdd: () => {}, 86 | defaultValue: '', 87 | readOnly: false, 88 | delimiter: '@', 89 | value: '', 90 | maxLength: -1, 91 | }; 92 | 93 | constructor(props) { 94 | super(props); 95 | this.state = { 96 | value: props.value || props.defaultValue, 97 | }; 98 | this.handleChange = this.handleChange.bind(this); 99 | } 100 | componentWillReceiveProps(nextProps) { 101 | if (nextProps.value !== this.props.value) { 102 | this.setState({ 103 | value: nextProps.value, 104 | }); 105 | } 106 | } 107 | componentDidMount() { 108 | this.selectionPosition = { 109 | start: 0, 110 | end: 0, 111 | }; 112 | } 113 | handleDefaultKeyup() { 114 | const editor = this.editor; 115 | const { delimiter } = this.props; 116 | const offset = getCaretOffset(editor); 117 | let { value } = this.state; 118 | value = value.replace(/(\r\n)|\n|\r/g, '\n'); 119 | const originStr = value.slice(0, offset.end); 120 | const str = parseStrByDelimiter(originStr, delimiter); 121 | this.props.matcher(str); 122 | this.selectionPosition = { 123 | start: offset.start - str.length - 1, 124 | end: offset.end, 125 | }; 126 | if (str !== false) { 127 | const position = getCaretPosition(editor); 128 | this.props.setCursorPos({ 129 | x: position.left, 130 | y: position.top, 131 | }); 132 | } 133 | } 134 | insert(mentionContent) { 135 | this.insertContentAtCaret(mentionContent); 136 | } 137 | insertContentAtCaret(text) { 138 | const editor = this.editor; 139 | if (document.selection) { 140 | editor.focus(); 141 | if (editor.createTextRange) { 142 | const range = editor.createTextRange(); 143 | range.collapse(true); 144 | range.moveStart('character', this.selectionPosition.start); 145 | range.moveEnd('character', this.selectionPosition.end - this.selectionPosition.start); 146 | range.text = text; 147 | } else if (editor.setSelectionRange) { 148 | editor.setSelectionRange(this.selectionPosition.start, this.selectionPosition.end); 149 | } 150 | } else { 151 | const scrollTop = editor.scrollTop; 152 | let { value } = this.state; 153 | value = value.substring(0, this.selectionPosition.start) + 154 | text + 155 | value.substring(this.selectionPosition.end, value.length); 156 | this.setState({ 157 | value, 158 | }, () => { 159 | editor.focus(); 160 | editor.scrollTop = scrollTop; 161 | }); 162 | } 163 | const changeEvt = createEvent(editor, 'change'); 164 | this.props.onChange(changeEvt, this.state.value); 165 | } 166 | handleChange(e) { 167 | this.setState({ 168 | value: e.target.value, 169 | }); 170 | this.props.onChange(e, this.state.value); 171 | } 172 | render() { 173 | const { value } = this.state; 174 | const { readOnly, placeholder, maxLength } = this.props; 175 | let style = { 176 | width: this.props.width, 177 | height: this.props.height, 178 | }; 179 | return ( 180 |
181 |