Skip to content

Commit

Permalink
feat(module:mention): add mention component (#1182)
Browse files Browse the repository at this point in the history
style(module:mention): formatting the code

docs(module:mention): add the docs & demo

refactor(module:mention): refactor

test(module:mention): add test

fix(module:mention): fix getMentions method

update PROGRESS.md
  • Loading branch information
hsuanxyz authored and vthinkxie committed Apr 4, 2018
1 parent 880f0e8 commit e28c1b5
Show file tree
Hide file tree
Showing 35 changed files with 1,910 additions and 1 deletion.
2 changes: 1 addition & 1 deletion PROGRESS.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@
| tree | x | x | x | simplejason | - |
| cascader | x | x | x | fbchen | - |
| autocomplete || 100% | 100% | HsuanXyz | - |
| mention | x | x | x | HsuanXyz | - |
| mention | | 100% | 100% | HsuanXyz | - |



Expand Down
1 change: 1 addition & 0 deletions components/components.less
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
@import "./layout/style/index.less";
@import "./list/style/index.less";
@import "./menu/style/index.less";
@import "./mention/style/index.less";
@import "./message/style/index.less";
@import "./modal/style/index.less";
@import "./notification/style/index.less";
Expand Down
10 changes: 10 additions & 0 deletions components/core/overlay/overlay-position-map.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,16 @@ export const DEFAULT_DATEPICKER_POSITIONS = [
}
] as ConnectionPositionPair[];

export const DEFAULT_MENTION_POSITIONS = [
POSITION_MAP.bottomLeft,
{
originX : 'start',
originY : 'bottom',
overlayX: 'start',
overlayY: 'bottom'
}
] as ConnectionPositionPair[];

function arrayMap<T, S>(array: T[], iteratee: (item: T, index: number, arr: T[]) => S): S[] {
let index = -1;
const length = array == null ? 0 : array.length;
Expand Down
20 changes: 20 additions & 0 deletions components/core/util/getMentions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@

export function getRegExp(prefix: string | string[]): RegExp {
const prefixArray = Array.isArray(prefix) ? prefix : [prefix];
let prefixToken = prefixArray.join('').replace(/(\$|\^)/g, '\\$1');

if (prefixArray.length > 1) {
prefixToken = `[${prefixToken}]`;
}

return new RegExp(`(\\s|^)(${prefixToken})[^\\s]*`, 'g');
}

export function getMentions(value: string, prefix: string | string[] = '@'): string[] {
if (typeof value !== 'string') {
return [];
}
const regex = getRegExp(prefix);
const mentions = value.match(regex);
return mentions !== null ? mentions.map(e => e.trim().substring(1)) : [];
}
161 changes: 161 additions & 0 deletions components/core/util/textarea-caret-position.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
// from https://github.com/component/textarea-caret-position

// We'll copy the properties below into the mirror div.
// Note that some browsers, such as Firefox, do not concatenate properties
// into their shorthand (e.g. padding-top, padding-bottom etc. -> padding),
// so we have to list every single property explicitly.
export const properties = [
'direction', // RTL support
'boxSizing',
'width', // on Chrome and IE, exclude the scrollbar, so the mirror div wraps exactly as the textarea does
'height',
'overflowX',
'overflowY', // copy the scrollbar for IE

'borderTopWidth',
'borderRightWidth',
'borderBottomWidth',
'borderLeftWidth',
'borderStyle',

'paddingTop',
'paddingRight',
'paddingBottom',
'paddingLeft',

// https://developer.mozilla.org/en-US/docs/Web/CSS/font
'fontStyle',
'fontVariant',
'fontWeight',
'fontStretch',
'fontSize',
'fontSizeAdjust',
'lineHeight',
'fontFamily',

'textAlign',
'textTransform',
'textIndent',
'textDecoration', // might not make a difference, but better be safe

'letterSpacing',
'wordSpacing',

'tabSize',
'MozTabSize'

];

const isBrowser = (typeof window !== 'undefined');

// tslint:disable-next-line:no-any
const isFirefox = (isBrowser && (window as any).mozInnerScreenX != null);

const _parseInt = (str: string) => parseInt(str, 10);

export interface Coordinates {
top: number;
left: number;
height: number;
}

export function getCaretCoordinates(element: HTMLInputElement | HTMLTextAreaElement, position: number, options?: { debug?: boolean }): Coordinates {
if (!isBrowser) {
throw new Error('textarea-caret-position#getCaretCoordinates should only be called in a browser');
}

const debug = options && options.debug || false;
if (debug) {
const el = document.querySelector('#input-textarea-caret-position-mirror-div');
if (el) { el.parentNode.removeChild(el); }
}

// The mirror div will replicate the textarea's style
const div = document.createElement('div');
div.id = 'input-textarea-caret-position-mirror-div';
document.body.appendChild(div);

const style = div.style;

// tslint:disable-next-line:no-any
const computed = window.getComputedStyle ? window.getComputedStyle(element) : (element as any).currentStyle; // currentStyle for IE < 9
const isInput = element.nodeName === 'INPUT';

// Default textarea styles
style.whiteSpace = 'pre-wrap';
if (!isInput) {
style.wordWrap = 'break-word'; // only for textarea-s
}

// Position off-screen
style.position = 'absolute'; // required to return coordinates properly
if (!debug) {
style.visibility = 'hidden';
} // not 'display: none' because we want rendering

// Transfer the element's properties to the div
properties.forEach((prop: string) => {
if (isInput && prop === 'lineHeight') {
// Special case for <input>s because text is rendered centered and line height may be != height
style.lineHeight = computed.height;
} else {
style[prop] = computed[prop];
}
});

if (isFirefox) {
// Firefox lies about the overflow property for textareas: https://bugzilla.mozilla.org/show_bug.cgi?id=984275
if (element.scrollHeight > _parseInt(computed.height)) {
style.overflowY = 'scroll';
}
} else {
style.overflow = 'hidden'; // for Chrome to not render a scrollbar; IE keeps overflowY = 'scroll'
}

div.textContent = element.value.substring(0, position);
// The second special handling for input type="text" vs textarea:
// spaces need to be replaced with non-breaking spaces - http://stackoverflow.com/a/13402035/1269037
if (isInput) {
div.textContent = div.textContent.replace(/\s/g, '\u00a0');
}

const span = document.createElement('span');
// Wrapping must be replicated *exactly*, including when a long word gets
// onto the next line, with whitespace at the end of the line before (#7).
// The *only* reliable way to do that is to copy the *entire* rest of the
// textarea's content into the <span> created at the caret position.
// For inputs, just '.' would be enough, but no need to bother.
span.textContent = element.value.substring(position) || '.'; // || because a completely empty faux span doesn't render at all
div.appendChild(span);

const coordinates = {
top: span.offsetTop + _parseInt(computed.borderTopWidth),
left: span.offsetLeft + _parseInt(computed.borderLeftWidth),
height: _parseInt(computed.lineHeight)
};

if (debug) {
span.style.backgroundColor = '#eee';
createDebugEle(element, coordinates);
} else {
document.body.removeChild(div);
}

return coordinates;
}

export function createDebugEle(element: HTMLInputElement | HTMLTextAreaElement, coordinates: Coordinates): void {
const fontSize = getComputedStyle(element).getPropertyValue('font-size');
const rect: HTMLSpanElement = (document.querySelector('#DEBUG') as HTMLSpanElement)
|| document.createElement('div');
document.body.appendChild(rect);
rect.id = 'DEBUG';
rect.style.position = 'absolute';
rect.style.backgroundColor = 'red';
rect.style.height = fontSize;
rect.style.width = '1px';
rect.style.top = `${element.getBoundingClientRect().top - element.scrollTop + window.pageYOffset + coordinates.top}px`;
rect.style.left = `${element.getBoundingClientRect().left - element.scrollLeft + window.pageXOffset + coordinates.left}px`;
console.log(rect.style.top);
console.log(rect.style.left);
}
15 changes: 15 additions & 0 deletions components/mention/demo/async.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
---
order: 1
title:
zh-CN: 异步加载
en-US: Asynchronous loading
---

## zh-CN

匹配内容列表为异步返回时。

## en-US

async

39 changes: 39 additions & 0 deletions components/mention/demo/async.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { Component, ViewEncapsulation } from '@angular/core';

@Component({
selector : 'nz-demo-mention-async',
encapsulation: ViewEncapsulation.None,
template : `
<nz-mention
[nzSuggestions]="suggestions"
[nzLoading]="loading"
(nzOnSearchChange)="onSearchChange($event)">
<input
nzMentionTrigger
nz-input
[(ngModel)]="inputValue">
</nz-mention>
`
})
export class NzDemoMentionAsyncComponent {
inputValue: string;
loading = false;
suggestions = [];

onSearchChange(value: string): void {
console.log(`search: ${value}`);
this.loading = true;
this.fetchSuggestions(value, (suggestions) => {
console.log(suggestions);
this.suggestions = suggestions;
this.loading = false;
});
}

fetchSuggestions(value: string, callback: (suggestions: string[]) => void): void {
const users = ['afc163', 'benjycui', 'yiminghe', 'jljsj33', 'dqaria', 'RaoHai'];
setTimeout(() => {
return callback(users.filter(item => item.indexOf(value) !== -1));
}, 500);
}
}
16 changes: 16 additions & 0 deletions components/mention/demo/avatar.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
---
order: 3
title:
zh-CN: 头像
en-US: Icon Image
---

## zh-CN

自定义建议(含头像)

注意,`nzSuggestions` 不为 `string[]` 时,需要提供 `valueWith`

## en-US

Customize suggestions.
44 changes: 44 additions & 0 deletions components/mention/demo/avatar.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { Component, ViewEncapsulation } from '@angular/core';

@Component({
selector : 'nz-demo-mention-avatar',
encapsulation: ViewEncapsulation.None,
template : `
<nz-mention
[nzSuggestions]="webFrameworks"
[nzValueWith]="valueWith"
(nzOnSelect)="onSelect($event)">
<input
nz-input
nzMentionTrigger
[(ngModel)]="inputValue">
<ng-container *nzMentionSuggestion="let framework">
<nz-avatar nzSize="small" [nzText]="framework.name" [nzSrc]="framework.icon"></nz-avatar>
<span>{{ framework.name }} - {{ framework.type }}</span>
</ng-container>
</nz-mention>
`,
styles: [`
.ant-avatar.ant-avatar-sm {
width: 14px;
height: 14px;
margin-right: 8px;
position: relative
}
`]
})
export class NzDemoMentionAvatarComponent {
inputValue: string;
webFrameworks = [
{ name: 'React', type: 'JavaScript', icon: 'https://zos.alipayobjects.com/rmsportal/LFIeMPzdLcLnEUe.svg' },
{ name: 'Angular', type: 'JavaScript', icon: 'https://zos.alipayobjects.com/rmsportal/PJTbxSvzYWjDZnJ.png' },
{ name: 'Dva', type: 'Javascript', icon: 'https://zos.alipayobjects.com/rmsportal/EYPwSeEJKxDtVxI.png' },
{ name: 'Flask', type: 'Python', icon: 'https://zos.alipayobjects.com/rmsportal/xaypBUijfnpAlXE.png' },
];

valueWith = data => data.name;

onSelect(value: string): void {
console.log(value);
}
}
14 changes: 14 additions & 0 deletions components/mention/demo/basic.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
---
order: 0
title:
zh-CN: 基本使用
en-US: Basic
---

## zh-CN

基本使用

## en-US

Basic usage.
31 changes: 31 additions & 0 deletions components/mention/demo/basic.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { Component, ViewEncapsulation } from '@angular/core';

@Component({
selector : 'nz-demo-mention-basic',
encapsulation: ViewEncapsulation.None,
template : `
<nz-mention
[nzSuggestions]="suggestions"
(nzOnSelect)="onSelect($event)">
<input
placeholder="input here"
nzMentionTrigger
nz-input
[(ngModel)]="inputValue"
(ngModelChange)="onChange($event)"
>
</nz-mention>
`
})
export class NzDemoMentionBasicComponent {
inputValue: string = '@afc163';
suggestions = ['afc163', 'benjycui', 'yiminghe', 'RaoHai', '中文', 'にほんご'];

onChange(value: string): void {
console.log(value);
}

onSelect(suggestion: string): void {
console.log(`onSelect ${suggestion}`);
}
}
14 changes: 14 additions & 0 deletions components/mention/demo/controlled.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
---
order: 4
title:
zh-CN: 配合 Form 使用
en-US: With Form
---

## zh-CN

受控模式,例如配合 Form 使用。

## en-US

Controlled mode, for example, to work with `Form`.
Loading

0 comments on commit e28c1b5

Please sign in to comment.