angular-mentions
Version:
Angular mentions for text fields.
851 lines (841 loc) • 38.4 kB
JavaScript
import * as i0 from '@angular/core';
import { EventEmitter, Component, Input, Output, ViewChild, Directive, NgModule } from '@angular/core';
import * as i1 from '@angular/common';
import { CommonModule } from '@angular/common';
// configuration structure, backwards compatible with earlier versions
// DOM element manipulation functions...
//
function setValue(el, value) {
//console.log("setValue", el.nodeName, "["+value+"]");
if (isInputOrTextAreaElement(el)) {
el.value = value;
}
else {
el.textContent = value;
}
}
function getValue(el) {
return isInputOrTextAreaElement(el) ? el.value : el.textContent;
}
function insertValue(el, start, end, text, iframe, noRecursion = false) {
//console.log("insertValue", el.nodeName, start, end, "["+text+"]", el);
if (isTextElement(el)) {
let val = getValue(el);
setValue(el, val.substring(0, start) + text + val.substring(end, val.length));
setCaretPosition(el, start + text.length, iframe);
}
else if (!noRecursion) {
let selObj = getWindowSelection(iframe);
if (selObj && selObj.rangeCount > 0) {
var selRange = selObj.getRangeAt(0);
var position = selRange.startOffset;
var anchorNode = selObj.anchorNode;
// if (text.endsWith(' ')) {
// text = text.substring(0, text.length-1) + '\xA0';
// }
insertValue(anchorNode, position - (end - start), position, text, iframe, true);
}
}
}
function isInputOrTextAreaElement(el) {
return el != null && (el.nodeName == 'INPUT' || el.nodeName == 'TEXTAREA');
}
;
function isTextElement(el) {
return el != null && (el.nodeName == 'INPUT' || el.nodeName == 'TEXTAREA' || el.nodeName == '#text');
}
;
function setCaretPosition(el, pos, iframe = null) {
//console.log("setCaretPosition", pos, el, iframe==null);
if (isInputOrTextAreaElement(el) && el.selectionStart) {
el.focus();
el.setSelectionRange(pos, pos);
}
else {
let range = getDocument(iframe).createRange();
range.setStart(el, pos);
range.collapse(true);
let sel = getWindowSelection(iframe);
sel.removeAllRanges();
sel.addRange(range);
}
}
function getCaretPosition(el, iframe = null) {
//console.log("getCaretPosition", el);
if (isInputOrTextAreaElement(el)) {
var val = el.value;
return val.slice(0, el.selectionStart).length;
}
else {
var selObj = getWindowSelection(iframe); //window.getSelection();
if (selObj.rangeCount > 0) {
var selRange = selObj.getRangeAt(0);
var preCaretRange = selRange.cloneRange();
preCaretRange.selectNodeContents(el);
preCaretRange.setEnd(selRange.endContainer, selRange.endOffset);
var position = preCaretRange.toString().length;
return position;
}
}
}
// Based on ment.io functions...
//
function getDocument(iframe) {
if (!iframe) {
return document;
}
else {
return iframe.contentWindow.document;
}
}
function getWindowSelection(iframe) {
if (!iframe) {
return window.getSelection();
}
else {
return iframe.contentWindow.getSelection();
}
}
function getContentEditableCaretCoords(ctx) {
let markerTextChar = '\ufeff';
let markerId = 'sel_' + new Date().getTime() + '_' + Math.random().toString().substr(2);
let doc = getDocument(ctx ? ctx.iframe : null);
let sel = getWindowSelection(ctx ? ctx.iframe : null);
let prevRange = sel.getRangeAt(0);
// create new range and set postion using prevRange
let range = doc.createRange();
range.setStart(sel.anchorNode, prevRange.startOffset);
range.setEnd(sel.anchorNode, prevRange.startOffset);
range.collapse(false);
// Create the marker element containing a single invisible character
// using DOM methods and insert it at the position in the range
let markerEl = doc.createElement('span');
markerEl.id = markerId;
markerEl.appendChild(doc.createTextNode(markerTextChar));
range.insertNode(markerEl);
sel.removeAllRanges();
sel.addRange(prevRange);
let coordinates = {
left: 0,
top: markerEl.offsetHeight
};
localToRelativeCoordinates(ctx, markerEl, coordinates);
markerEl.parentNode.removeChild(markerEl);
return coordinates;
}
function localToRelativeCoordinates(ctx, element, coordinates) {
let obj = element;
let iframe = ctx ? ctx.iframe : null;
while (obj) {
if (ctx.parent != null && ctx.parent == obj) {
break;
}
coordinates.left += obj.offsetLeft + obj.clientLeft;
coordinates.top += obj.offsetTop + obj.clientTop;
obj = obj.offsetParent;
if (!obj && iframe) {
obj = iframe;
iframe = null;
}
}
obj = element;
iframe = ctx ? ctx.iframe : null;
while (obj !== getDocument(null).body && obj != null) {
if (ctx.parent != null && ctx.parent == obj) {
break;
}
if (obj.scrollTop && obj.scrollTop > 0) {
coordinates.top -= obj.scrollTop;
}
if (obj.scrollLeft && obj.scrollLeft > 0) {
coordinates.left -= obj.scrollLeft;
}
obj = obj.parentNode;
if (!obj && iframe) {
obj = iframe;
iframe = null;
}
}
}
/* From: https://github.com/component/textarea-caret-position */
/* jshint browser: true */
// (function () {
// 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.
var properties = [
'direction',
'boxSizing',
'width',
'height',
'overflowX',
'overflowY',
'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',
'letterSpacing',
'wordSpacing',
'tabSize',
'MozTabSize'
];
var isBrowser = (typeof window !== 'undefined');
var isFirefox = (isBrowser && window['mozInnerScreenX'] != null);
function getCaretCoordinates(element, position, options) {
if (!isBrowser) {
throw new Error('textarea-caret-position#getCaretCoordinates should only be called in a browser');
}
var debug = options && options.debug || false;
if (debug) {
var el = document.querySelector('#input-textarea-caret-position-mirror-div');
if (el)
el.parentNode.removeChild(el);
}
// The mirror div will replicate the textarea's style
var div = document.createElement('div');
div.id = 'input-textarea-caret-position-mirror-div';
document.body.appendChild(div);
var style = div.style;
var computed = window.getComputedStyle ? window.getComputedStyle(element) : element.currentStyle; // currentStyle for IE < 9
var 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(function (prop) {
if (isInput && prop === 'lineHeight') {
// Special case for <input>s because text is rendered centered and line height may be != height
if (computed.boxSizing === "border-box") {
var height = parseInt(computed.height);
var outerHeight = parseInt(computed.paddingTop) +
parseInt(computed.paddingBottom) +
parseInt(computed.borderTopWidth) +
parseInt(computed.borderBottomWidth);
var targetHeight = outerHeight + parseInt(computed.lineHeight);
if (height > targetHeight) {
style.lineHeight = height - outerHeight + "px";
}
else if (height === targetHeight) {
style.lineHeight = computed.lineHeight;
}
else {
style.lineHeight = '0';
}
}
else {
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');
var 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);
var coordinates = {
top: span.offsetTop + parseInt(computed['borderTopWidth']),
left: span.offsetLeft + parseInt(computed['borderLeftWidth']),
height: parseInt(computed['lineHeight'])
};
if (debug) {
span.style.backgroundColor = '#aaa';
}
else {
document.body.removeChild(div);
}
return coordinates;
}
// if (typeof module != 'undefined' && typeof module.exports != 'undefined') {
// module.exports = getCaretCoordinates;
// } else if(isBrowser) {
// window.getCaretCoordinates = getCaretCoordinates;
// }
// }());
/**
* Angular Mentions.
* https://github.com/dmacfarlane/angular-mentions
*
* Copyright (c) 2016 Dan MacFarlane
*/
class MentionListComponent {
constructor(element) {
this.element = element;
this.labelKey = 'label';
this.itemClick = new EventEmitter();
this.items = [];
this.activeIndex = 0;
this.hidden = false;
this.dropUp = false;
this.styleOff = false;
this.coords = { top: 0, left: 0 };
this.offset = 0;
}
ngAfterContentChecked() {
if (!this.itemTemplate) {
this.itemTemplate = this.defaultItemTemplate;
}
}
// lots of confusion here between relative coordinates and containers
position(nativeParentElement, iframe = null) {
if (isInputOrTextAreaElement(nativeParentElement)) {
// parent elements need to have postition:relative for this to work correctly?
this.coords = getCaretCoordinates(nativeParentElement, nativeParentElement.selectionStart, null);
this.coords.top = nativeParentElement.offsetTop + this.coords.top - nativeParentElement.scrollTop;
this.coords.left = nativeParentElement.offsetLeft + this.coords.left - nativeParentElement.scrollLeft;
// getCretCoordinates() for text/input elements needs an additional offset to position the list correctly
this.offset = this.getBlockCursorDimensions(nativeParentElement).height;
}
else if (iframe) {
let context = { iframe: iframe, parent: iframe.offsetParent };
this.coords = getContentEditableCaretCoords(context);
}
else {
let doc = document.documentElement;
let scrollLeft = (window.pageXOffset || doc.scrollLeft) - (doc.clientLeft || 0);
let scrollTop = (window.pageYOffset || doc.scrollTop) - (doc.clientTop || 0);
// bounding rectangles are relative to view, offsets are relative to container?
let caretRelativeToView = getContentEditableCaretCoords({ iframe: iframe });
let parentRelativeToContainer = nativeParentElement.getBoundingClientRect();
this.coords.top = caretRelativeToView.top - parentRelativeToContainer.top + nativeParentElement.offsetTop - scrollTop;
this.coords.left = caretRelativeToView.left - parentRelativeToContainer.left + nativeParentElement.offsetLeft - scrollLeft;
}
// set the default/inital position
this.positionElement();
}
get activeItem() {
return this.items[this.activeIndex];
}
activateNextItem() {
// adjust scrollable-menu offset if the next item is out of view
let listEl = this.list.nativeElement;
let activeEl = listEl.getElementsByClassName('active').item(0);
if (activeEl) {
let nextLiEl = activeEl.nextSibling;
if (nextLiEl && nextLiEl.nodeName == "LI") {
let nextLiRect = nextLiEl.getBoundingClientRect();
if (nextLiRect.bottom > listEl.getBoundingClientRect().bottom) {
listEl.scrollTop = nextLiEl.offsetTop + nextLiRect.height - listEl.clientHeight;
}
}
}
// select the next item
this.activeIndex = Math.max(Math.min(this.activeIndex + 1, this.items.length - 1), 0);
}
activatePreviousItem() {
// adjust the scrollable-menu offset if the previous item is out of view
let listEl = this.list.nativeElement;
let activeEl = listEl.getElementsByClassName('active').item(0);
if (activeEl) {
let prevLiEl = activeEl.previousSibling;
if (prevLiEl && prevLiEl.nodeName == "LI") {
let prevLiRect = prevLiEl.getBoundingClientRect();
if (prevLiRect.top < listEl.getBoundingClientRect().top) {
listEl.scrollTop = prevLiEl.offsetTop;
}
}
}
// select the previous item
this.activeIndex = Math.max(Math.min(this.activeIndex - 1, this.items.length - 1), 0);
}
// reset for a new mention search
reset() {
this.list.nativeElement.scrollTop = 0;
this.checkBounds();
}
// final positioning is done after the list is shown (and the height and width are known)
// ensure it's in the page bounds
checkBounds() {
let left = this.coords.left, top = this.coords.top, dropUp = this.dropUp;
const bounds = this.list.nativeElement.getBoundingClientRect();
// if off right of page, align right
if (bounds.left + bounds.width > window.innerWidth) {
left -= bounds.left + bounds.width - window.innerWidth + 10;
}
// if more than half off the bottom of the page, force dropUp
// if ((bounds.top+bounds.height/2)>window.innerHeight) {
// dropUp = true;
// }
// if top is off page, disable dropUp
if (bounds.top < 0) {
dropUp = false;
}
// set the revised/final position
this.positionElement(left, top, dropUp);
}
positionElement(left = this.coords.left, top = this.coords.top, dropUp = this.dropUp) {
const el = this.element.nativeElement;
top += dropUp ? 0 : this.offset; // top of list is next line
el.className = dropUp ? 'dropup' : null;
el.style.position = "absolute";
el.style.left = left + 'px';
el.style.top = top + 'px';
}
getBlockCursorDimensions(nativeParentElement) {
const parentStyles = window.getComputedStyle(nativeParentElement);
return {
height: parseFloat(parentStyles.lineHeight),
width: parseFloat(parentStyles.fontSize)
};
}
}
MentionListComponent.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "14.0.6", ngImport: i0, type: MentionListComponent, deps: [{ token: i0.ElementRef }], target: i0.ɵɵFactoryTarget.Component });
MentionListComponent.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "14.0.6", type: MentionListComponent, selector: "mention-list", inputs: { labelKey: "labelKey", itemTemplate: "itemTemplate" }, outputs: { itemClick: "itemClick" }, viewQueries: [{ propertyName: "list", first: true, predicate: ["list"], descendants: true, static: true }, { propertyName: "defaultItemTemplate", first: true, predicate: ["defaultItemTemplate"], descendants: true, static: true }], ngImport: i0, template: `
<ng-template #defaultItemTemplate let-item="item">
{{item[labelKey]}}
</ng-template>
<ul #list [hidden]="hidden" class="dropdown-menu scrollable-menu"
[class.mention-menu]="!styleOff" [class.mention-dropdown]="!styleOff && dropUp">
<li *ngFor="let item of items; let i = index"
[class.active]="activeIndex==i" [class.mention-active]="!styleOff && activeIndex==i">
<a class="dropdown-item" [class.mention-item]="!styleOff"
(mousedown)="activeIndex=i;itemClick.emit();$event.preventDefault()">
<ng-template [ngTemplateOutlet]="itemTemplate" [ngTemplateOutletContext]="{'item':item}"></ng-template>
</a>
</li>
</ul>
`, isInline: true, styles: [".mention-menu{position:absolute;top:100%;left:0;z-index:1000;display:none;float:left;min-width:11em;padding:.5em 0;margin:.125em 0 0;font-size:1em;color:#212529;text-align:left;list-style:none;background-color:#fff;background-clip:padding-box;border:1px solid rgba(0,0,0,.15);border-radius:.25em}.mention-item{display:block;padding:.2em 1.5em;line-height:1.5em;clear:both;font-weight:400;color:#212529;text-align:inherit;white-space:nowrap;background-color:transparent;border:0}.mention-active>a{color:#fff;text-decoration:none;background-color:#337ab7;outline:0}.scrollable-menu{display:block;height:auto;max-height:292px;overflow:auto}[hidden]{display:none}.mention-dropdown{bottom:100%;top:auto;margin-bottom:2px}\n"], dependencies: [{ kind: "directive", type: i1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }] });
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "14.0.6", ngImport: i0, type: MentionListComponent, decorators: [{
type: Component,
args: [{ selector: 'mention-list', template: `
<ng-template #defaultItemTemplate let-item="item">
{{item[labelKey]}}
</ng-template>
<ul #list [hidden]="hidden" class="dropdown-menu scrollable-menu"
[class.mention-menu]="!styleOff" [class.mention-dropdown]="!styleOff && dropUp">
<li *ngFor="let item of items; let i = index"
[class.active]="activeIndex==i" [class.mention-active]="!styleOff && activeIndex==i">
<a class="dropdown-item" [class.mention-item]="!styleOff"
(mousedown)="activeIndex=i;itemClick.emit();$event.preventDefault()">
<ng-template [ngTemplateOutlet]="itemTemplate" [ngTemplateOutletContext]="{'item':item}"></ng-template>
</a>
</li>
</ul>
`, styles: [".mention-menu{position:absolute;top:100%;left:0;z-index:1000;display:none;float:left;min-width:11em;padding:.5em 0;margin:.125em 0 0;font-size:1em;color:#212529;text-align:left;list-style:none;background-color:#fff;background-clip:padding-box;border:1px solid rgba(0,0,0,.15);border-radius:.25em}.mention-item{display:block;padding:.2em 1.5em;line-height:1.5em;clear:both;font-weight:400;color:#212529;text-align:inherit;white-space:nowrap;background-color:transparent;border:0}.mention-active>a{color:#fff;text-decoration:none;background-color:#337ab7;outline:0}.scrollable-menu{display:block;height:auto;max-height:292px;overflow:auto}[hidden]{display:none}.mention-dropdown{bottom:100%;top:auto;margin-bottom:2px}\n"] }]
}], ctorParameters: function () { return [{ type: i0.ElementRef }]; }, propDecorators: { labelKey: [{
type: Input
}], itemTemplate: [{
type: Input
}], itemClick: [{
type: Output
}], list: [{
type: ViewChild,
args: ['list', { static: true }]
}], defaultItemTemplate: [{
type: ViewChild,
args: ['defaultItemTemplate', { static: true }]
}] } });
const KEY_BACKSPACE = 8;
const KEY_TAB = 9;
const KEY_ENTER = 13;
const KEY_SHIFT = 16;
const KEY_ESCAPE = 27;
const KEY_SPACE = 32;
const KEY_LEFT = 37;
const KEY_UP = 38;
const KEY_RIGHT = 39;
const KEY_DOWN = 40;
const KEY_BUFFERED = 229;
/**
* Angular Mentions.
* https://github.com/dmacfarlane/angular-mentions
*
* Copyright (c) 2017 Dan MacFarlane
*/
class MentionDirective {
constructor(_element, _componentResolver, _viewContainerRef) {
this._element = _element;
this._componentResolver = _componentResolver;
this._viewContainerRef = _viewContainerRef;
// the provided configuration object
this.mentionConfig = { items: [] };
this.DEFAULT_CONFIG = {
items: [],
triggerChar: '@',
labelKey: 'label',
maxItems: -1,
allowSpace: false,
returnTrigger: false,
mentionSelect: (item, triggerChar) => {
return this.activeConfig.triggerChar + item[this.activeConfig.labelKey];
},
mentionFilter: (searchString, items) => {
const searchStringLowerCase = searchString.toLowerCase();
return items.filter(e => e[this.activeConfig.labelKey].toLowerCase().startsWith(searchStringLowerCase));
}
};
// event emitted whenever the search term changes
this.searchTerm = new EventEmitter();
// event emitted when an item is selected
this.itemSelected = new EventEmitter();
// event emitted whenever the mention list is opened or closed
this.opened = new EventEmitter();
this.closed = new EventEmitter();
this.triggerChars = {};
}
set mention(items) {
this.mentionItems = items;
}
ngOnChanges(changes) {
// console.log('config change', changes);
if (changes['mention'] || changes['mentionConfig']) {
this.updateConfig();
}
}
updateConfig() {
let config = this.mentionConfig;
this.triggerChars = {};
// use items from directive if they have been set
if (this.mentionItems) {
config.items = this.mentionItems;
}
this.addConfig(config);
// nested configs
if (config.mentions) {
config.mentions.forEach(config => this.addConfig(config));
}
}
// add configuration for a trigger char
addConfig(config) {
// defaults
let defaults = Object.assign({}, this.DEFAULT_CONFIG);
config = Object.assign(defaults, config);
// items
let items = config.items;
if (items && items.length > 0) {
// convert strings to objects
if (typeof items[0] == 'string') {
items = items.map((label) => {
let object = {};
object[config.labelKey] = label;
return object;
});
}
if (config.labelKey) {
// remove items without an labelKey (as it's required to filter the list)
items = items.filter(e => e[config.labelKey]);
if (!config.disableSort) {
items.sort((a, b) => a[config.labelKey].localeCompare(b[config.labelKey]));
}
}
}
config.items = items;
// add the config
this.triggerChars[config.triggerChar] = config;
// for async update while menu/search is active
if (this.activeConfig && this.activeConfig.triggerChar == config.triggerChar) {
this.activeConfig = config;
this.updateSearchList();
}
}
setIframe(iframe) {
this.iframe = iframe;
}
stopEvent(event) {
//if (event instanceof KeyboardEvent) { // does not work for iframe
if (!event.wasClick) {
event.preventDefault();
event.stopPropagation();
event.stopImmediatePropagation();
}
}
blurHandler(event) {
this.stopEvent(event);
this.stopSearch();
}
inputHandler(event, nativeElement = this._element.nativeElement) {
if (this.lastKeyCode === KEY_BUFFERED && event.data) {
let keyCode = event.data.charCodeAt(0);
this.keyHandler({ keyCode, inputEvent: true }, nativeElement);
}
}
// @param nativeElement is the alternative text element in an iframe scenario
keyHandler(event, nativeElement = this._element.nativeElement) {
this.lastKeyCode = event.keyCode;
if (event.isComposing || event.keyCode === KEY_BUFFERED) {
return;
}
let val = getValue(nativeElement);
let pos = getCaretPosition(nativeElement, this.iframe);
let charPressed = event.key;
if (!charPressed) {
let charCode = event.which || event.keyCode;
if (!event.shiftKey && (charCode >= 65 && charCode <= 90)) {
charPressed = String.fromCharCode(charCode + 32);
}
// else if (event.shiftKey && charCode === KEY_2) {
// charPressed = this.config.triggerChar;
// }
else {
// TODO (dmacfarlane) fix this for non-alpha keys
// http://stackoverflow.com/questions/2220196/how-to-decode-character-pressed-from-jquerys-keydowns-event-handler?lq=1
charPressed = String.fromCharCode(event.which || event.keyCode);
}
}
if (event.keyCode == KEY_ENTER && event.wasClick && pos < this.startPos) {
// put caret back in position prior to contenteditable menu click
pos = this.startNode.length;
setCaretPosition(this.startNode, pos, this.iframe);
}
//console.log("keyHandler", this.startPos, pos, val, charPressed, event);
let config = this.triggerChars[charPressed];
if (config) {
this.activeConfig = config;
this.startPos = event.inputEvent ? pos - 1 : pos;
this.startNode = (this.iframe ? this.iframe.contentWindow.getSelection() : window.getSelection()).anchorNode;
this.searching = true;
this.searchString = null;
this.showSearchList(nativeElement);
this.updateSearchList();
if (config.returnTrigger) {
this.searchTerm.emit(config.triggerChar);
}
}
else if (this.startPos >= 0 && this.searching) {
if (pos <= this.startPos) {
this.searchList.hidden = true;
}
// ignore shift when pressed alone, but not when used with another key
else if (event.keyCode !== KEY_SHIFT &&
!event.metaKey &&
!event.altKey &&
!event.ctrlKey &&
pos > this.startPos) {
if (!this.activeConfig.allowSpace && event.keyCode === KEY_SPACE) {
this.startPos = -1;
}
else if (event.keyCode === KEY_BACKSPACE && pos > 0) {
pos--;
if (pos == this.startPos) {
this.stopSearch();
}
}
else if (this.searchList.hidden) {
if (event.keyCode === KEY_TAB || event.keyCode === KEY_ENTER) {
this.stopSearch();
return;
}
}
else if (!this.searchList.hidden) {
if (event.keyCode === KEY_TAB || event.keyCode === KEY_ENTER) {
this.stopEvent(event);
// emit the selected list item
this.itemSelected.emit(this.searchList.activeItem);
// optional function to format the selected item before inserting the text
const text = this.activeConfig.mentionSelect(this.searchList.activeItem, this.activeConfig.triggerChar);
// value is inserted without a trailing space for consistency
// between element types (div and iframe do not preserve the space)
insertValue(nativeElement, this.startPos, pos, text, this.iframe);
// fire input event so angular bindings are updated
if ("createEvent" in document) {
let evt = document.createEvent("HTMLEvents");
if (this.iframe) {
// a 'change' event is required to trigger tinymce updates
evt.initEvent("change", true, false);
}
else {
evt.initEvent("input", true, false);
}
// this seems backwards, but fire the event from this elements nativeElement (not the
// one provided that may be in an iframe, as it won't be propogate)
this._element.nativeElement.dispatchEvent(evt);
}
this.startPos = -1;
this.stopSearch();
return false;
}
else if (event.keyCode === KEY_ESCAPE) {
this.stopEvent(event);
this.stopSearch();
return false;
}
else if (event.keyCode === KEY_DOWN) {
this.stopEvent(event);
this.searchList.activateNextItem();
return false;
}
else if (event.keyCode === KEY_UP) {
this.stopEvent(event);
this.searchList.activatePreviousItem();
return false;
}
}
if (charPressed.length != 1 && event.keyCode != KEY_BACKSPACE) {
this.stopEvent(event);
return false;
}
else if (this.searching) {
let mention = val.substring(this.startPos + 1, pos);
if (event.keyCode !== KEY_BACKSPACE && !event.inputEvent) {
mention += charPressed;
}
this.searchString = mention;
if (this.activeConfig.returnTrigger) {
const triggerChar = (this.searchString || event.keyCode === KEY_BACKSPACE) ? val.substring(this.startPos, this.startPos + 1) : '';
this.searchTerm.emit(triggerChar + this.searchString);
}
else {
this.searchTerm.emit(this.searchString);
}
this.updateSearchList();
}
}
}
}
// exposed for external calls to open the mention list, e.g. by clicking a button
startSearch(triggerChar, nativeElement = this._element.nativeElement) {
triggerChar = triggerChar || this.mentionConfig.triggerChar || this.DEFAULT_CONFIG.triggerChar;
const pos = getCaretPosition(nativeElement, this.iframe);
insertValue(nativeElement, pos, pos, triggerChar, this.iframe);
this.keyHandler({ key: triggerChar, inputEvent: true }, nativeElement);
}
stopSearch() {
if (this.searchList && !this.searchList.hidden) {
this.searchList.hidden = true;
this.closed.emit();
}
this.activeConfig = null;
this.searching = false;
}
updateSearchList() {
let matches = [];
if (this.activeConfig && this.activeConfig.items) {
let objects = this.activeConfig.items;
// disabling the search relies on the async operation to do the filtering
if (!this.activeConfig.disableSearch && this.searchString && this.activeConfig.labelKey) {
if (this.activeConfig.mentionFilter) {
objects = this.activeConfig.mentionFilter(this.searchString, objects);
}
}
matches = objects;
if (this.activeConfig.maxItems > 0) {
matches = matches.slice(0, this.activeConfig.maxItems);
}
}
// update the search list
if (this.searchList) {
this.searchList.items = matches;
this.searchList.hidden = matches.length == 0;
}
}
showSearchList(nativeElement) {
this.opened.emit();
if (this.searchList == null) {
let componentFactory = this._componentResolver.resolveComponentFactory(MentionListComponent);
let componentRef = this._viewContainerRef.createComponent(componentFactory);
this.searchList = componentRef.instance;
this.searchList.itemTemplate = this.mentionListTemplate;
componentRef.instance['itemClick'].subscribe(() => {
nativeElement.focus();
let fakeKeydown = { key: 'Enter', keyCode: KEY_ENTER, wasClick: true };
this.keyHandler(fakeKeydown, nativeElement);
});
}
this.searchList.labelKey = this.activeConfig.labelKey;
this.searchList.dropUp = this.activeConfig.dropUp;
this.searchList.styleOff = this.mentionConfig.disableStyle;
this.searchList.activeIndex = 0;
this.searchList.position(nativeElement, this.iframe);
window.requestAnimationFrame(() => this.searchList.reset());
}
}
MentionDirective.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "14.0.6", ngImport: i0, type: MentionDirective, deps: [{ token: i0.ElementRef }, { token: i0.ComponentFactoryResolver }, { token: i0.ViewContainerRef }], target: i0.ɵɵFactoryTarget.Directive });
MentionDirective.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "14.0.6", type: MentionDirective, selector: "[mention], [mentionConfig]", inputs: { mention: "mention", mentionConfig: "mentionConfig", mentionListTemplate: "mentionListTemplate" }, outputs: { searchTerm: "searchTerm", itemSelected: "itemSelected", opened: "opened", closed: "closed" }, host: { attributes: { "autocomplete": "off" }, listeners: { "keydown": "keyHandler($event)", "input": "inputHandler($event)", "blur": "blurHandler($event)" } }, usesOnChanges: true, ngImport: i0 });
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "14.0.6", ngImport: i0, type: MentionDirective, decorators: [{
type: Directive,
args: [{
selector: '[mention], [mentionConfig]',
host: {
'(keydown)': 'keyHandler($event)',
'(input)': 'inputHandler($event)',
'(blur)': 'blurHandler($event)',
'autocomplete': 'off'
}
}]
}], ctorParameters: function () { return [{ type: i0.ElementRef }, { type: i0.ComponentFactoryResolver }, { type: i0.ViewContainerRef }]; }, propDecorators: { mention: [{
type: Input,
args: ['mention']
}], mentionConfig: [{
type: Input
}], mentionListTemplate: [{
type: Input
}], searchTerm: [{
type: Output
}], itemSelected: [{
type: Output
}], opened: [{
type: Output
}], closed: [{
type: Output
}] } });
class MentionModule {
}
MentionModule.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "14.0.6", ngImport: i0, type: MentionModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule });
MentionModule.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "14.0.6", ngImport: i0, type: MentionModule, declarations: [MentionDirective,
MentionListComponent], imports: [CommonModule], exports: [MentionDirective] });
MentionModule.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "14.0.6", ngImport: i0, type: MentionModule, imports: [CommonModule] });
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "14.0.6", ngImport: i0, type: MentionModule, decorators: [{
type: NgModule,
args: [{
declarations: [
MentionDirective,
MentionListComponent
],
imports: [
CommonModule
],
exports: [
MentionDirective
]
}]
}] });
/*
* Public API Surface of angular-mentions
*/
/**
* Generated bundle index. Do not edit.
*/
export { MentionDirective, MentionModule };
//# sourceMappingURL=angular-mentions.mjs.map