diff --git a/src/Linkifier.test.ts b/src/Linkifier.test.ts index ca393c21..36f825ba 100644 --- a/src/Linkifier.test.ts +++ b/src/Linkifier.test.ts @@ -8,9 +8,9 @@ import { Linkifier } from './Linkifier'; import { LinkMatcher } from './Types'; class TestLinkifier extends Linkifier { - constructor(document: Document, rows: HTMLElement[]) { + constructor() { Linkifier.TIME_BEFORE_LINKIFY = 0; - super(document, rows); + super(); } public get linkMatchers(): LinkMatcher[] { return this._linkMatchers; } @@ -25,89 +25,105 @@ describe('Linkifier', () => { let linkifier: TestLinkifier; beforeEach(done => { - rows = []; jsdom.env('', (err, w) => { window = w; document = window.document; - linkifier = new TestLinkifier(document, rows); - container = document.createElement('div'); - document.body.appendChild(container); + linkifier = new TestLinkifier(); done(); }); }); - function addRow(text: string) { - const element = document.createElement('div'); - element.textContent = text; - container.appendChild(element); - rows.push(element); - } - - function clickElement(element: Node) { - const event = document.createEvent('MouseEvent'); - event.initMouseEvent('click', true, true, window, 0, 0, 0, 0, 0, false, false, false, false, 0, null); - element.dispatchEvent(event); - } - - function assertLinkifiesEntireRow(uri: string, done: MochaDone) { - addRow(uri); - linkifier.linkifyRow(0); - setTimeout(() => { - assert.equal((rows[0].firstChild).tagName, 'A'); - assert.equal((rows[0].firstChild).textContent, uri); + describe('before attachToDom', () => { + it('should allow link matcher registration', done => { + assert.doesNotThrow(() => { + const linkMatcherId = linkifier.registerLinkMatcher(/foo/, () => {}); + assert.isTrue(linkifier.deregisterLinkMatcher(linkMatcherId)); done(); - }, 0); - } - - describe('http links', () => { - it('should allow ~ character in URI path', done => assertLinkifiesEntireRow('http://foo.com/a~b#c~d?e~f', done)); - }); - - describe('validationCallback', () => { - it('should enable link if true', done => { - addRow('test'); - linkifier.registerLinkMatcher(/test/, () => done(), { - validationCallback: (url, cb) => { - cb(true); - assert.equal((rows[0].firstChild).tagName, 'A'); - setTimeout(() => clickElement(rows[0].firstChild), 0); - } }); - linkifier.linkifyRow(0); - }); - - it('should disable link if false', done => { - addRow('test'); - linkifier.registerLinkMatcher(/test/, () => assert.fail(), { - validationCallback: (url, cb) => { - cb(false); - assert.equal((rows[0].firstChild).tagName, 'A'); - setTimeout(() => clickElement(rows[0].firstChild), 0); - } - }); - linkifier.linkifyRow(0); - // Allow time for the click to be performed - setTimeout(() => done(), 10); }); }); - describe('priority', () => { - it('should order the list from highest priority to lowest #1', () => { - const aId = linkifier.registerLinkMatcher(/a/, () => {}, { priority: 1 }); - const bId = linkifier.registerLinkMatcher(/b/, () => {}, { priority: -1 }); - assert.deepEqual(linkifier.linkMatchers.map(lm => lm.id), [aId, 0, bId]); + describe('after attachToDom', () => { + beforeEach(() => { + rows = []; + linkifier.attachToDom(document, rows); + container = document.createElement('div'); + document.body.appendChild(container); }); - it('should order the list from highest priority to lowest #2', () => { - const aId = linkifier.registerLinkMatcher(/a/, () => {}, { priority: -1 }); - const bId = linkifier.registerLinkMatcher(/b/, () => {}, { priority: 1 }); - assert.deepEqual(linkifier.linkMatchers.map(lm => lm.id), [bId, 0, aId]); + function addRow(text: string) { + const element = document.createElement('div'); + element.textContent = text; + container.appendChild(element); + rows.push(element); + } + + function clickElement(element: Node) { + const event = document.createEvent('MouseEvent'); + event.initMouseEvent('click', true, true, window, 0, 0, 0, 0, 0, false, false, false, false, 0, null); + element.dispatchEvent(event); + } + + function assertLinkifiesEntireRow(uri: string, done: MochaDone) { + addRow(uri); + linkifier.linkifyRow(0); + setTimeout(() => { + assert.equal((rows[0].firstChild).tagName, 'A'); + assert.equal((rows[0].firstChild).textContent, uri); + done(); + }, 0); + } + + describe('http links', () => { + it('should allow ~ character in URI path', done => assertLinkifiesEntireRow('http://foo.com/a~b#c~d?e~f', done)); }); - it('should order items of equal priority in the order they are added', () => { - const aId = linkifier.registerLinkMatcher(/a/, () => {}, { priority: 0 }); - const bId = linkifier.registerLinkMatcher(/b/, () => {}, { priority: 0 }); - assert.deepEqual(linkifier.linkMatchers.map(lm => lm.id), [0, aId, bId]); + describe('validationCallback', () => { + it('should enable link if true', done => { + addRow('test'); + linkifier.registerLinkMatcher(/test/, () => done(), { + validationCallback: (url, cb) => { + cb(true); + assert.equal((rows[0].firstChild).tagName, 'A'); + setTimeout(() => clickElement(rows[0].firstChild), 0); + } + }); + linkifier.linkifyRow(0); + }); + + it('should disable link if false', done => { + addRow('test'); + linkifier.registerLinkMatcher(/test/, () => assert.fail(), { + validationCallback: (url, cb) => { + cb(false); + assert.equal((rows[0].firstChild).tagName, 'A'); + setTimeout(() => clickElement(rows[0].firstChild), 0); + } + }); + linkifier.linkifyRow(0); + // Allow time for the click to be performed + setTimeout(() => done(), 10); + }); + }); + + describe('priority', () => { + it('should order the list from highest priority to lowest #1', () => { + const aId = linkifier.registerLinkMatcher(/a/, () => {}, { priority: 1 }); + const bId = linkifier.registerLinkMatcher(/b/, () => {}, { priority: -1 }); + assert.deepEqual(linkifier.linkMatchers.map(lm => lm.id), [aId, 0, bId]); + }); + + it('should order the list from highest priority to lowest #2', () => { + const aId = linkifier.registerLinkMatcher(/a/, () => {}, { priority: -1 }); + const bId = linkifier.registerLinkMatcher(/b/, () => {}, { priority: 1 }); + assert.deepEqual(linkifier.linkMatchers.map(lm => lm.id), [bId, 0, aId]); + }); + + it('should order items of equal priority in the order they are added', () => { + const aId = linkifier.registerLinkMatcher(/a/, () => {}, { priority: 0 }); + const bId = linkifier.registerLinkMatcher(/b/, () => {}, { priority: 0 }); + assert.deepEqual(linkifier.linkMatchers.map(lm => lm.id), [0, aId, bId]); + }); }); }); }); diff --git a/src/Linkifier.ts b/src/Linkifier.ts index 0f9d9a21..f47554ea 100644 --- a/src/Linkifier.ts +++ b/src/Linkifier.ts @@ -49,19 +49,32 @@ export class Linkifier { private _rowTimeoutIds: number[]; private _nextLinkMatcherId = HYPERTEXT_LINK_MATCHER_ID; - constructor(document: Document, rows: HTMLElement[]) { - this._document = document; - this._rows = rows; + constructor() { this._rowTimeoutIds = []; this._linkMatchers = []; this.registerLinkMatcher(strictUrlRegex, null, { matchIndex: 1 }); } + /** + * Attaches the linkifier to the DOM, enabling linkification. + * @param document The document object. + * @param rows The array of rows to apply links to. + */ + public attachToDom(document: Document, rows: HTMLElement[]) { + this._document = document; + this._rows = rows; + } + /** * Queues a row for linkification. * @param {number} rowIndex The index of the row to linkify. */ public linkifyRow(rowIndex: number): void { + // Don't attempt linkify if not yet attached to DOM + if (!this._document) { + return; + } + const timeoutId = this._rowTimeoutIds[rowIndex]; if (timeoutId) { clearTimeout(timeoutId); diff --git a/src/xterm.js b/src/xterm.js index fb7d0cf7..2c6ad38e 100644 --- a/src/xterm.js +++ b/src/xterm.js @@ -219,7 +219,7 @@ function Terminal(options) { this.parser = new Parser(this.inputHandler, this); // Reuse renderer if the Terminal is being recreated via a Terminal.reset call. this.renderer = this.renderer || null; - this.linkifier = this.linkifier || null;; + this.linkifier = this.linkifier || new Linkifier(); // user input states this.writeBuffer = []; @@ -645,7 +645,7 @@ Terminal.prototype.open = function(parent) { this.rowContainer.classList.add('xterm-rows'); this.element.appendChild(this.rowContainer); this.children = []; - this.linkifier = new Linkifier(document, this.children); + this.linkifier.attachToDom(document, this.children); // Create the container that will hold helpers like the textarea for // capturing DOM Events. Then produce the helpers.