Loading README.md +3 −4 Original line number Diff line number Diff line Loading @@ -32,10 +32,9 @@ Where `items` is a string array of the items to suggest. For example: #### TODO: - Remove debug (always) - Fix line-feed issue in content editable - Iframe based (TinyMCE) - Styled menu items - Load items via http (config for number of chars before search) - Menu clicks not working in content editable - Configurable prefix - Configurable limit on number of items shown via config - Load items via http (config for number of chars before search) - Styled menu items - Tests... app/app.component.ts +6 −2 Original line number Diff line number Diff line import {Component} from 'angular2/core'; import {Mention} from './mention/mention'; import {COMMON_NAMES} from './common-names'; import {TinyMCE} from './tinymce.component'; @Component({ selector: 'my-app', Loading @@ -17,12 +18,15 @@ import {COMMON_NAMES} from './common-names'; <textarea [mention]="items" class="form-control" cols="60" rows="4"></textarea> <h3>Content Editable</h3> <div [mention]="items" contenteditable="true" style="border:1px lightgrey solid;min-height:88px"></div> <div [mention]="items" class="form-control" contenteditable="true" style="border:1px lightgrey solid;min-height:88px"></div> <h3>Embedded Editor</h3> <tinymce></tinymce> <br><p style="color:grey">ng2-mentions on <a href="">Github</a></p> <a href="https://github.com/dmacfarlane/ng2-mentions"><img style="position: absolute; top: 0; right: 0; border: 0;" src="https://camo.githubusercontent.com/652c5b9acfaddf3a9c326fa6bde407b87f7be0f4/68747470733a2f2f73332e616d617a6f6e6177732e636f6d2f6769746875622f726962626f6e732f666f726b6d655f72696768745f6f72616e67655f6666373630302e706e67" alt="Fork me on GitHub" data-canonical-src="https://s3.amazonaws.com/github/ribbons/forkme_right_orange_ff7600.png"></a> `, directives: [Mention] directives: [Mention, TinyMCE] }) export class AppComponent { items:string [] = COMMON_NAMES; Loading app/mention/mention-list.ts +12 −10 Original line number Diff line number Diff line import {Component, ElementRef, Output, EventEmitter} from 'angular2/core'; import {isInputOrTextArea, getSelectionCoords} from './mention-utils'; import {isInputOrTextAreaElement, getContentEditableCaretCoords} from './mention-utils'; declare var getCaretCoordinates:any; /** * Angular 2 Mentions. * https://github.com/dmacfarlane/ng2-mentions * * Copyright (c) 2016 Dan MacFarlane */ @Component({ selector: 'mention-list', styles: [` Loading Loading @@ -31,23 +37,19 @@ export class MentionList { hidden = false; @Output() itemClick = new EventEmitter(); constructor(private _element: ElementRef) {} position(nativeParentElement) { position(nativeParentElement, iframe=null) { var coords = {top:0,left:0}; if (isInputOrTextArea(nativeParentElement)) { if (isInputOrTextAreaElement(nativeParentElement)) { coords = getCaretCoordinates(nativeParentElement, nativeParentElement.selectionStart); coords.top = nativeParentElement.offsetTop + coords.top; coords.top = nativeParentElement.offsetTop + coords.top + 16; coords.left = nativeParentElement.offsetLeft + coords.left; } else { var doc = document.documentElement; var scrollLeft = (window.pageXOffset || doc.scrollLeft) - (doc.clientLeft || 0); var scrollTop = (window.pageYOffset || doc.scrollTop) - (doc.clientTop || 0); var position = getSelectionCoords(window); coords = {top:position.y+scrollTop, left:position.x+scrollLeft}; coords = getContentEditableCaretCoords(nativeParentElement, iframe); } this._element.nativeElement.style.position = "absolute"; this._element.nativeElement.style.left = coords.left + 'px'; this._element.nativeElement.style.top = coords.top+16 + 'px'; this._element.nativeElement.style.top = coords.top + 'px'; } get activeItem() { return this.items[this.activeIndex]; Loading app/mention/mention-utils.ts +125 −127 Original line number Diff line number Diff line // DOM element manipulation functions // DOM element manipulation functions... // function setValue(el, value) { if (isInputOrTextArea(el)) { //console.log("setValue", el.nodeName, value); if (isInputOrTextAreaElement(el)) { el.value = value; } else { //el.textContent = value; //el.appendChild(document.createTextNode(value)); el.innerHTML = value + (value.endsWith(" ") ? " " : ""); el.textContent = value; } } export function getValue(el) { return isInputOrTextArea(el) ? el.value : el.textContent; return isInputOrTextAreaElement(el) ? el.value : el.textContent; } export function insertValue(el, start, end, text) { export function insertValue(el, start, end, text, iframe, noRecursion=false) { //console.log("insertValue", el.nodeName, start, end, text, el); if (isTextElement(el)) { var val = getValue(el); setValue(el, val.substring(0, start) + text + val.substring(end, val.length)); setCaretPosition(el, start+text.length); setCaretPosition(el, start+text.length, iframe); } else if (!noRecursion) { var selObj = getWindowSelection(iframe); var selRange = selObj.getRangeAt(0); var position = selRange.startOffset; var anchorNode = selObj.anchorNode; insertValue(anchorNode, position-(end-start), position, text, iframe, true); } } export function isInputOrTextAreaElement(el) { return el!=null && (el.nodeName == 'INPUT' || el.nodeName == 'TEXTAREA'); }; export function isInputOrTextArea(el) { return el.nodeName == 'INPUT' || el.nodeName == 'TEXTAREA'; export function isTextElement(el) { return el!=null && (el.nodeName == 'INPUT' || el.nodeName == 'TEXTAREA' || el.nodeName=='#text'); }; export function getCaretPosition(el) { if (isInputOrTextArea(el)) { export 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); } } export function getCaretPosition(el, iframe=null) { //console.log("getCaretPosition", el); if (isInputOrTextAreaElement(el)) { var val = el.value; return val.slice(0, el.selectionStart).length; } else { return getCaretCharacterOffsetWithin(el); var selObj = getWindowSelection(iframe); //window.getSelection(); var selRange = selObj.getRangeAt(0); var position = selRange.startOffset; return position; } } export function setCaretPosition(el, pos) { if (isInputOrTextArea(el)) { // http://stackoverflow.com/questions/512528/set-cursor-position-in-html-textbox if(el.createTextRange) { let range = el.createTextRange(); range.move('character', pos); range.select(); } else if(el.selectionStart) { el.focus(); el.setSelectionRange(pos, pos); export function getContentEditableCaretCoords(nativeParentElement, iframe) { let ctx = iframe ? { iframe: iframe } : null; return getContentEditableCaretPositionMentIo(ctx); } else { el.focus(); // Based on ment.io functions... // function getDocument(iframe) { if (!iframe) { return document; } else { return iframe.contentWindow.document; } } else { // http://stackoverflow.com/questions/6249095/how-to-set-caretcursor-position-in-contenteditable-element-div let range = document.createRange(); let sel = window.getSelection(); range.setStart(el.firstChild, pos); range.collapse(true); sel.removeAllRanges(); sel.addRange(range); function getWindowSelection(iframe) { if (!iframe) { return window.getSelection(); } else { return iframe.contentWindow.getSelection(); } } // http://stackoverflow.com/questions/4811822/get-a-ranges-start-and-end-offsets-relative-to-its-parent-container/4812022#4812022 export function getCaretCharacterOffsetWithin(element) { var caretOffset = 0; var doc = element.ownerDocument || element.document; var win = doc.defaultView || doc.parentWindow; var sel; if (typeof win.getSelection != "undefined") { sel = win.getSelection(); if (sel.rangeCount > 0) { var range = win.getSelection().getRangeAt(0); var preCaretRange = range.cloneRange(); preCaretRange.selectNodeContents(element); preCaretRange.setEnd(range.endContainer, range.endOffset); caretOffset = preCaretRange.toString().length; } } else if ( (sel = doc.selection) && sel.type != "Control") { var textRange = sel.createRange(); var preCaretTextRange = doc.body.createTextRange(); preCaretTextRange.moveToElementText(element); preCaretTextRange.setEndPoint("EndToEnd", textRange); caretOffset = preCaretTextRange.text.length; } return caretOffset; } // http://stackoverflow.com/questions/6846230/coordinates-of-selected-text-in-browser-page export function getSelectionCoords(win) { win = win || window; var doc = win.document; var sel = doc.selection, range, rects, rect; var x = 0, y = 0; if (sel) { if (sel.type != "Control") { range = sel.createRange(); range.collapse(true); x = range.boundingLeft; y = range.boundingTop; } } else if (win.getSelection) { sel = win.getSelection(); if (sel.rangeCount) { range = sel.getRangeAt(0).cloneRange(); if (range.getClientRects) { range.collapse(true); rects = range.getClientRects(); if (rects.length > 0) { rect = rects[0]; } x = rect.left; y = rect.top; } // Fall back to inserting a temporary element if (x == 0 && y == 0) { var span = doc.createElement("span"); if (span.getClientRects) { // Ensure span has dimensions and position by // adding a zero-width space character span.appendChild( doc.createTextNode("\u200b") ); range.insertNode(span); rect = span.getClientRects()[0]; x = rect.left; y = rect.top; var spanParent = span.parentNode; spanParent.removeChild(span); // Glue any broken text nodes back together spanParent.normalize(); } } } } return { x: x, y: y }; } /* insertNodeAtCaret(el, start, end, text) { // var sel=window.getSelection(); // if (sel.rangeCount) { // var range = sel.getRangeAt(0); // range.collapse(false); // range.insertNode(node); // range.collapseAfter(node); // sel.setSingleRange(range); // } //var el = document.getElementById("editable"); var range = document.createRange(); var sel = window.getSelection(); //var range = sel.getRangeAt(0); range.setStart(el.firstChild, start); range.setEnd(el.firstChild, end); range.deleteContents(); //range.collapse(true); range.insertNode(document.createTextNode(text)); function getContentEditableCaretPositionMentIo(ctx/*, selectedNodePosition*/) { var markerTextChar = '\ufeff'; var markerId = 'sel_' + new Date().getTime() + '_' + Math.random().toString().substr(2); var doc = getDocument(ctx?ctx.iframe:null); var sel = getWindowSelection(ctx?ctx.iframe:null); var prevRange = sel.getRangeAt(0); // create new range and set postion using prevRange var 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 var markerEl = doc.createElement('span'); markerEl.id = markerId; markerEl.appendChild(doc.createTextNode(markerTextChar)); range.insertNode(markerEl); sel.removeAllRanges(); //sel.addRange(range); }*/ sel.addRange(prevRange); var coordinates = { left: 0, top: markerEl.offsetHeight }; localToGlobalCoordinates(ctx, markerEl, coordinates); markerEl.parentNode.removeChild(markerEl); return coordinates; } function localToGlobalCoordinates(ctx, element, coordinates) { var obj = element; var iframe = ctx ? ctx.iframe : null; while(obj) { 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 (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; } } } app/mention/mention.ts +59 −47 Original line number Diff line number Diff line Loading @@ -16,6 +16,12 @@ const KEY_RIGHT = 39; const KEY_DOWN = 40; const KEY_2 = 50; /** * Angular 2 Mentions. * https://github.com/dmacfarlane/ng2-mentions * * Copyright (c) 2016 Dan MacFarlane */ @Component({ selector: '[mention]', template: '', Loading @@ -28,21 +34,26 @@ export class Mention { mentionStart:number; searchList: MentionList; escapePressed:boolean; iframe:any; // optional constructor(private _element: ElementRef, private _dcl: DynamicComponentLoader) {} @Input() set mention(items:string []){ this.items = items.sort(); } setIframe(iframe) { this.iframe = iframe; } stopEvent(event) { event.preventDefault(); event.stopPropagation(); event.stopImmediatePropagation(); } keyHandler(event) { let val = getValue(this._element.nativeElement); let pos = getCaretPosition(this._element.nativeElement); keyHandler(event, nativeElement=this._element.nativeElement) { let val = getValue(nativeElement); let pos = getCaretPosition(nativeElement, this.iframe); let charPressed = event.key; if (!charPressed) { let charCode = event.which || event.keyCode; Loading @@ -58,22 +69,27 @@ export class Mention { charPressed = String.fromCharCode(event.which || event.keyCode); } } //console.log(pos, val, event, charPressed); console.log("keyHandler", this.mentionStart, pos, val, charPressed, event); if (charPressed=="@") { this.mentionStart = pos; this.escapePressed = false; this.showSeachList(); this.showSearchList(nativeElement); } else if (this.mentionStart>=0 && !this.escapePressed) { if (event.keyCode!=KEY_SHIFT && pos>this.mentionStart) { if (event.keyCode === KEY_SPACE) { this.mentionStart = -1; } else if (event.keyCode === KEY_TAB || event.keyCode === KEY_ENTER) { else if (event.keyCode === KEY_BACKSPACE && pos>0) { this.searchList.hidden = this.escapePressed; pos--; } else if (!this.searchList.hidden) { if (event.keyCode === KEY_TAB || event.keyCode === KEY_ENTER) { this.stopEvent(event); this.searchList.hidden = true; insertValue(this._element.nativeElement, this.mentionStart, pos, "@"+this.searchList.activeItem+" "); insertValue(nativeElement, this.mentionStart, pos, "@"+this.searchList.activeItem+" ", this.iframe); this.mentionStart = -1; return false; } Loading @@ -93,12 +109,8 @@ export class Mention { this.searchList.activatePreviousItem(); return false; } else if (event.keyCode === KEY_BACKSPACE && pos>0) { this.searchList.hidden = this.escapePressed; pos--; } if (!this.searchList.hidden) { if (event.keyCode === KEY_LEFT || event.keyCode === KEY_RIGHT) { this.stopEvent(event); return false; Loading @@ -117,26 +129,26 @@ export class Mention { } } } } showSeachList() { showSearchList(nativeElement) { if (this.searchList==null) { this._dcl.loadNextToLocation(MentionList, this._element) .then((containerRef: ComponentRef) => { this.searchList = containerRef.instance; this.searchList.items = this.items; //matches; this.searchList.items = this.items; this.searchList.hidden = false; this.searchList.position(this._element.nativeElement); this.searchList.position(nativeElement, this.iframe); containerRef.instance['itemClick'].subscribe(ev => { let fakeKeydown = new KeyboardEvent('keydown', <KeyboardEventInit>{"keyCode":KEY_ENTER}); this.keyHandler(fakeKeydown); this.keyHandler(fakeKeydown, nativeElement); }); }); } else { this.searchList.activeIndex = 0; this.searchList.items = this.items; this.searchList.hidden = false; this.searchList.position(this._element.nativeElement); this.searchList.position(nativeElement, this.iframe); } } } Loading
README.md +3 −4 Original line number Diff line number Diff line Loading @@ -32,10 +32,9 @@ Where `items` is a string array of the items to suggest. For example: #### TODO: - Remove debug (always) - Fix line-feed issue in content editable - Iframe based (TinyMCE) - Styled menu items - Load items via http (config for number of chars before search) - Menu clicks not working in content editable - Configurable prefix - Configurable limit on number of items shown via config - Load items via http (config for number of chars before search) - Styled menu items - Tests...
app/app.component.ts +6 −2 Original line number Diff line number Diff line import {Component} from 'angular2/core'; import {Mention} from './mention/mention'; import {COMMON_NAMES} from './common-names'; import {TinyMCE} from './tinymce.component'; @Component({ selector: 'my-app', Loading @@ -17,12 +18,15 @@ import {COMMON_NAMES} from './common-names'; <textarea [mention]="items" class="form-control" cols="60" rows="4"></textarea> <h3>Content Editable</h3> <div [mention]="items" contenteditable="true" style="border:1px lightgrey solid;min-height:88px"></div> <div [mention]="items" class="form-control" contenteditable="true" style="border:1px lightgrey solid;min-height:88px"></div> <h3>Embedded Editor</h3> <tinymce></tinymce> <br><p style="color:grey">ng2-mentions on <a href="">Github</a></p> <a href="https://github.com/dmacfarlane/ng2-mentions"><img style="position: absolute; top: 0; right: 0; border: 0;" src="https://camo.githubusercontent.com/652c5b9acfaddf3a9c326fa6bde407b87f7be0f4/68747470733a2f2f73332e616d617a6f6e6177732e636f6d2f6769746875622f726962626f6e732f666f726b6d655f72696768745f6f72616e67655f6666373630302e706e67" alt="Fork me on GitHub" data-canonical-src="https://s3.amazonaws.com/github/ribbons/forkme_right_orange_ff7600.png"></a> `, directives: [Mention] directives: [Mention, TinyMCE] }) export class AppComponent { items:string [] = COMMON_NAMES; Loading
app/mention/mention-list.ts +12 −10 Original line number Diff line number Diff line import {Component, ElementRef, Output, EventEmitter} from 'angular2/core'; import {isInputOrTextArea, getSelectionCoords} from './mention-utils'; import {isInputOrTextAreaElement, getContentEditableCaretCoords} from './mention-utils'; declare var getCaretCoordinates:any; /** * Angular 2 Mentions. * https://github.com/dmacfarlane/ng2-mentions * * Copyright (c) 2016 Dan MacFarlane */ @Component({ selector: 'mention-list', styles: [` Loading Loading @@ -31,23 +37,19 @@ export class MentionList { hidden = false; @Output() itemClick = new EventEmitter(); constructor(private _element: ElementRef) {} position(nativeParentElement) { position(nativeParentElement, iframe=null) { var coords = {top:0,left:0}; if (isInputOrTextArea(nativeParentElement)) { if (isInputOrTextAreaElement(nativeParentElement)) { coords = getCaretCoordinates(nativeParentElement, nativeParentElement.selectionStart); coords.top = nativeParentElement.offsetTop + coords.top; coords.top = nativeParentElement.offsetTop + coords.top + 16; coords.left = nativeParentElement.offsetLeft + coords.left; } else { var doc = document.documentElement; var scrollLeft = (window.pageXOffset || doc.scrollLeft) - (doc.clientLeft || 0); var scrollTop = (window.pageYOffset || doc.scrollTop) - (doc.clientTop || 0); var position = getSelectionCoords(window); coords = {top:position.y+scrollTop, left:position.x+scrollLeft}; coords = getContentEditableCaretCoords(nativeParentElement, iframe); } this._element.nativeElement.style.position = "absolute"; this._element.nativeElement.style.left = coords.left + 'px'; this._element.nativeElement.style.top = coords.top+16 + 'px'; this._element.nativeElement.style.top = coords.top + 'px'; } get activeItem() { return this.items[this.activeIndex]; Loading
app/mention/mention-utils.ts +125 −127 Original line number Diff line number Diff line // DOM element manipulation functions // DOM element manipulation functions... // function setValue(el, value) { if (isInputOrTextArea(el)) { //console.log("setValue", el.nodeName, value); if (isInputOrTextAreaElement(el)) { el.value = value; } else { //el.textContent = value; //el.appendChild(document.createTextNode(value)); el.innerHTML = value + (value.endsWith(" ") ? " " : ""); el.textContent = value; } } export function getValue(el) { return isInputOrTextArea(el) ? el.value : el.textContent; return isInputOrTextAreaElement(el) ? el.value : el.textContent; } export function insertValue(el, start, end, text) { export function insertValue(el, start, end, text, iframe, noRecursion=false) { //console.log("insertValue", el.nodeName, start, end, text, el); if (isTextElement(el)) { var val = getValue(el); setValue(el, val.substring(0, start) + text + val.substring(end, val.length)); setCaretPosition(el, start+text.length); setCaretPosition(el, start+text.length, iframe); } else if (!noRecursion) { var selObj = getWindowSelection(iframe); var selRange = selObj.getRangeAt(0); var position = selRange.startOffset; var anchorNode = selObj.anchorNode; insertValue(anchorNode, position-(end-start), position, text, iframe, true); } } export function isInputOrTextAreaElement(el) { return el!=null && (el.nodeName == 'INPUT' || el.nodeName == 'TEXTAREA'); }; export function isInputOrTextArea(el) { return el.nodeName == 'INPUT' || el.nodeName == 'TEXTAREA'; export function isTextElement(el) { return el!=null && (el.nodeName == 'INPUT' || el.nodeName == 'TEXTAREA' || el.nodeName=='#text'); }; export function getCaretPosition(el) { if (isInputOrTextArea(el)) { export 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); } } export function getCaretPosition(el, iframe=null) { //console.log("getCaretPosition", el); if (isInputOrTextAreaElement(el)) { var val = el.value; return val.slice(0, el.selectionStart).length; } else { return getCaretCharacterOffsetWithin(el); var selObj = getWindowSelection(iframe); //window.getSelection(); var selRange = selObj.getRangeAt(0); var position = selRange.startOffset; return position; } } export function setCaretPosition(el, pos) { if (isInputOrTextArea(el)) { // http://stackoverflow.com/questions/512528/set-cursor-position-in-html-textbox if(el.createTextRange) { let range = el.createTextRange(); range.move('character', pos); range.select(); } else if(el.selectionStart) { el.focus(); el.setSelectionRange(pos, pos); export function getContentEditableCaretCoords(nativeParentElement, iframe) { let ctx = iframe ? { iframe: iframe } : null; return getContentEditableCaretPositionMentIo(ctx); } else { el.focus(); // Based on ment.io functions... // function getDocument(iframe) { if (!iframe) { return document; } else { return iframe.contentWindow.document; } } else { // http://stackoverflow.com/questions/6249095/how-to-set-caretcursor-position-in-contenteditable-element-div let range = document.createRange(); let sel = window.getSelection(); range.setStart(el.firstChild, pos); range.collapse(true); sel.removeAllRanges(); sel.addRange(range); function getWindowSelection(iframe) { if (!iframe) { return window.getSelection(); } else { return iframe.contentWindow.getSelection(); } } // http://stackoverflow.com/questions/4811822/get-a-ranges-start-and-end-offsets-relative-to-its-parent-container/4812022#4812022 export function getCaretCharacterOffsetWithin(element) { var caretOffset = 0; var doc = element.ownerDocument || element.document; var win = doc.defaultView || doc.parentWindow; var sel; if (typeof win.getSelection != "undefined") { sel = win.getSelection(); if (sel.rangeCount > 0) { var range = win.getSelection().getRangeAt(0); var preCaretRange = range.cloneRange(); preCaretRange.selectNodeContents(element); preCaretRange.setEnd(range.endContainer, range.endOffset); caretOffset = preCaretRange.toString().length; } } else if ( (sel = doc.selection) && sel.type != "Control") { var textRange = sel.createRange(); var preCaretTextRange = doc.body.createTextRange(); preCaretTextRange.moveToElementText(element); preCaretTextRange.setEndPoint("EndToEnd", textRange); caretOffset = preCaretTextRange.text.length; } return caretOffset; } // http://stackoverflow.com/questions/6846230/coordinates-of-selected-text-in-browser-page export function getSelectionCoords(win) { win = win || window; var doc = win.document; var sel = doc.selection, range, rects, rect; var x = 0, y = 0; if (sel) { if (sel.type != "Control") { range = sel.createRange(); range.collapse(true); x = range.boundingLeft; y = range.boundingTop; } } else if (win.getSelection) { sel = win.getSelection(); if (sel.rangeCount) { range = sel.getRangeAt(0).cloneRange(); if (range.getClientRects) { range.collapse(true); rects = range.getClientRects(); if (rects.length > 0) { rect = rects[0]; } x = rect.left; y = rect.top; } // Fall back to inserting a temporary element if (x == 0 && y == 0) { var span = doc.createElement("span"); if (span.getClientRects) { // Ensure span has dimensions and position by // adding a zero-width space character span.appendChild( doc.createTextNode("\u200b") ); range.insertNode(span); rect = span.getClientRects()[0]; x = rect.left; y = rect.top; var spanParent = span.parentNode; spanParent.removeChild(span); // Glue any broken text nodes back together spanParent.normalize(); } } } } return { x: x, y: y }; } /* insertNodeAtCaret(el, start, end, text) { // var sel=window.getSelection(); // if (sel.rangeCount) { // var range = sel.getRangeAt(0); // range.collapse(false); // range.insertNode(node); // range.collapseAfter(node); // sel.setSingleRange(range); // } //var el = document.getElementById("editable"); var range = document.createRange(); var sel = window.getSelection(); //var range = sel.getRangeAt(0); range.setStart(el.firstChild, start); range.setEnd(el.firstChild, end); range.deleteContents(); //range.collapse(true); range.insertNode(document.createTextNode(text)); function getContentEditableCaretPositionMentIo(ctx/*, selectedNodePosition*/) { var markerTextChar = '\ufeff'; var markerId = 'sel_' + new Date().getTime() + '_' + Math.random().toString().substr(2); var doc = getDocument(ctx?ctx.iframe:null); var sel = getWindowSelection(ctx?ctx.iframe:null); var prevRange = sel.getRangeAt(0); // create new range and set postion using prevRange var 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 var markerEl = doc.createElement('span'); markerEl.id = markerId; markerEl.appendChild(doc.createTextNode(markerTextChar)); range.insertNode(markerEl); sel.removeAllRanges(); //sel.addRange(range); }*/ sel.addRange(prevRange); var coordinates = { left: 0, top: markerEl.offsetHeight }; localToGlobalCoordinates(ctx, markerEl, coordinates); markerEl.parentNode.removeChild(markerEl); return coordinates; } function localToGlobalCoordinates(ctx, element, coordinates) { var obj = element; var iframe = ctx ? ctx.iframe : null; while(obj) { 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 (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; } } }
app/mention/mention.ts +59 −47 Original line number Diff line number Diff line Loading @@ -16,6 +16,12 @@ const KEY_RIGHT = 39; const KEY_DOWN = 40; const KEY_2 = 50; /** * Angular 2 Mentions. * https://github.com/dmacfarlane/ng2-mentions * * Copyright (c) 2016 Dan MacFarlane */ @Component({ selector: '[mention]', template: '', Loading @@ -28,21 +34,26 @@ export class Mention { mentionStart:number; searchList: MentionList; escapePressed:boolean; iframe:any; // optional constructor(private _element: ElementRef, private _dcl: DynamicComponentLoader) {} @Input() set mention(items:string []){ this.items = items.sort(); } setIframe(iframe) { this.iframe = iframe; } stopEvent(event) { event.preventDefault(); event.stopPropagation(); event.stopImmediatePropagation(); } keyHandler(event) { let val = getValue(this._element.nativeElement); let pos = getCaretPosition(this._element.nativeElement); keyHandler(event, nativeElement=this._element.nativeElement) { let val = getValue(nativeElement); let pos = getCaretPosition(nativeElement, this.iframe); let charPressed = event.key; if (!charPressed) { let charCode = event.which || event.keyCode; Loading @@ -58,22 +69,27 @@ export class Mention { charPressed = String.fromCharCode(event.which || event.keyCode); } } //console.log(pos, val, event, charPressed); console.log("keyHandler", this.mentionStart, pos, val, charPressed, event); if (charPressed=="@") { this.mentionStart = pos; this.escapePressed = false; this.showSeachList(); this.showSearchList(nativeElement); } else if (this.mentionStart>=0 && !this.escapePressed) { if (event.keyCode!=KEY_SHIFT && pos>this.mentionStart) { if (event.keyCode === KEY_SPACE) { this.mentionStart = -1; } else if (event.keyCode === KEY_TAB || event.keyCode === KEY_ENTER) { else if (event.keyCode === KEY_BACKSPACE && pos>0) { this.searchList.hidden = this.escapePressed; pos--; } else if (!this.searchList.hidden) { if (event.keyCode === KEY_TAB || event.keyCode === KEY_ENTER) { this.stopEvent(event); this.searchList.hidden = true; insertValue(this._element.nativeElement, this.mentionStart, pos, "@"+this.searchList.activeItem+" "); insertValue(nativeElement, this.mentionStart, pos, "@"+this.searchList.activeItem+" ", this.iframe); this.mentionStart = -1; return false; } Loading @@ -93,12 +109,8 @@ export class Mention { this.searchList.activatePreviousItem(); return false; } else if (event.keyCode === KEY_BACKSPACE && pos>0) { this.searchList.hidden = this.escapePressed; pos--; } if (!this.searchList.hidden) { if (event.keyCode === KEY_LEFT || event.keyCode === KEY_RIGHT) { this.stopEvent(event); return false; Loading @@ -117,26 +129,26 @@ export class Mention { } } } } showSeachList() { showSearchList(nativeElement) { if (this.searchList==null) { this._dcl.loadNextToLocation(MentionList, this._element) .then((containerRef: ComponentRef) => { this.searchList = containerRef.instance; this.searchList.items = this.items; //matches; this.searchList.items = this.items; this.searchList.hidden = false; this.searchList.position(this._element.nativeElement); this.searchList.position(nativeElement, this.iframe); containerRef.instance['itemClick'].subscribe(ev => { let fakeKeydown = new KeyboardEvent('keydown', <KeyboardEventInit>{"keyCode":KEY_ENTER}); this.keyHandler(fakeKeydown); this.keyHandler(fakeKeydown, nativeElement); }); }); } else { this.searchList.activeIndex = 0; this.searchList.items = this.items; this.searchList.hidden = false; this.searchList.position(this._element.nativeElement); this.searchList.position(nativeElement, this.iframe); } } }