From 249e9f1ce166408f74e79b8b1f3493f77275a4a0 Mon Sep 17 00:00:00 2001 From: Sumedha Pramod Date: Mon, 31 Jul 2017 16:15:47 -0700 Subject: [PATCH] Chore: Duplicating any util.js methods used into annotatorUtil.js (#260) --- src/lib/annotations/AnnotationDialog.js | 3 +- src/lib/annotations/AnnotationService.js | 2 +- .../__tests__/annotatorUtil-test.js | 154 +++++++++++++++++- src/lib/annotations/annotatorUtil.js | 141 ++++++++++++++++ src/lib/annotations/doc/DocHighlightDialog.js | 7 +- .../doc/__tests__/DocHighlightDialog-test.js | 2 +- 6 files changed, 300 insertions(+), 9 deletions(-) diff --git a/src/lib/annotations/AnnotationDialog.js b/src/lib/annotations/AnnotationDialog.js index 8e1adf5b7..27b22868e 100644 --- a/src/lib/annotations/AnnotationDialog.js +++ b/src/lib/annotations/AnnotationDialog.js @@ -3,7 +3,6 @@ import EventEmitter from 'events'; import * as annotatorUtil from './annotatorUtil'; import * as constants from './annotationConstants'; import { CLASS_ACTIVE, CLASS_HIDDEN } from '../constants'; -import { decodeKeydown } from '../util'; import { ICON_CLOSE, ICON_DELETE } from '../icons/icons'; const CLASS_ANNOTATION_PLAIN_HIGHLIGHT = 'bp-plain-highlight'; @@ -329,7 +328,7 @@ class AnnotationDialog extends EventEmitter { keydownHandler(event) { event.stopPropagation(); - const key = decodeKeydown(event); + const key = annotatorUtil.decodeKeydown(event); if (key === 'Escape') { this.hide(); } else { diff --git a/src/lib/annotations/AnnotationService.js b/src/lib/annotations/AnnotationService.js index a537b1dbc..ce3ceef66 100644 --- a/src/lib/annotations/AnnotationService.js +++ b/src/lib/annotations/AnnotationService.js @@ -2,7 +2,7 @@ import 'whatwg-fetch'; import EventEmitter from 'events'; import autobind from 'autobind-decorator'; import Annotation from './Annotation'; -import { getHeaders } from '../util'; +import { getHeaders } from './annotatorUtil'; const ANONYMOUS_USER = { id: '0', diff --git a/src/lib/annotations/__tests__/annotatorUtil-test.js b/src/lib/annotations/__tests__/annotatorUtil-test.js index b32369fb7..a20385f0b 100644 --- a/src/lib/annotations/__tests__/annotatorUtil-test.js +++ b/src/lib/annotations/__tests__/annotatorUtil-test.js @@ -18,7 +18,10 @@ import { repositionCaret, isPending, validateThreadParams, - eventToLocationHandler + eventToLocationHandler, + decodeKeydown, + getHeaders, + replacePlaceholders } from '../annotatorUtil'; import { STATES, @@ -400,4 +403,153 @@ describe('lib/annotations/annotatorUtil', () => { expect(annotator.isChanged).to.be.true; }); }); + + describe('decodeKeydown()', () => { + it('should return empty when no key', () => { + assert.equal( + decodeKeydown({ + key: '' + }), + '' + ); + }); + it('should return empty when modifier and key are same', () => { + assert.equal( + decodeKeydown({ + key: 'Control', + ctrlKey: true + }), + '' + ); + }); + it('should return correct with ctrl modifier', () => { + assert.equal( + decodeKeydown({ + key: '1', + ctrlKey: true + }), + 'Control+1' + ); + }); + it('should return correct with shift modifier', () => { + assert.equal( + decodeKeydown({ + key: '1', + shiftKey: true + }), + 'Shift+1' + ); + }); + it('should return correct with meta modifier', () => { + assert.equal( + decodeKeydown({ + key: '1', + metaKey: true + }), + 'Meta+1' + ); + }); + it('should return space key', () => { + assert.equal( + decodeKeydown({ + key: ' ' + }), + 'Space' + ); + }); + it('should return right arrow key', () => { + assert.equal( + decodeKeydown({ + key: 'Right' + }), + 'ArrowRight' + ); + }); + it('should return left arrow key', () => { + assert.equal( + decodeKeydown({ + key: 'Left' + }), + 'ArrowLeft' + ); + }); + it('should return up arrow key', () => { + assert.equal( + decodeKeydown({ + key: 'Up' + }), + 'ArrowUp' + ); + }); + it('should return down arrow key', () => { + assert.equal( + decodeKeydown({ + key: 'Down' + }), + 'ArrowDown' + ); + }); + it('should return esc key', () => { + assert.equal( + decodeKeydown({ + key: 'U+001B' + }), + 'Escape' + ); + }); + it('should decode correct UTF8 key', () => { + assert.equal( + decodeKeydown({ + key: 'U+0041' + }), + 'A' + ); + }); + }); + + /* eslint-disable no-undef */ + describe('getHeaders()', () => { + it('should return correct headers', () => { + const sharedLink = 'https://sharename'; + const fooHeader = 'bar'; + const token = 'someToken'; + const headers = getHeaders({ foo: fooHeader }, token, sharedLink); + expect(headers.foo).to.equal(fooHeader); + expect(headers.Authorization).to.equal(`Bearer ${token}`); + expect(headers.BoxApi).to.equal(`shared_link=${sharedLink}`); + expect(headers['X-Box-Client-Name']).to.equal(__NAME__); + expect(headers['X-Box-Client-Version']).to.equal(__VERSION__); + }); + + it('should return correct headers with password', () => { + const headers = getHeaders({ foo: 'bar' }, 'token', 'https://sharename', 'password'); + assert.equal(headers.foo, 'bar'); + assert.equal(headers.Authorization, 'Bearer token'); + assert.equal(headers.BoxApi, 'shared_link=https://sharename&shared_link_password=password'); + assert.equal(headers['X-Box-Client-Name'], __NAME__); + assert.equal(headers['X-Box-Client-Version'], __VERSION__); + }); + }); + + describe('replacePlaceholders()', () => { + it('should replace only the placeholder with the custom value in the given string', () => { + expect(replacePlaceholders('{1} highlighted', ['Bob'])).to.equal('Bob highlighted'); + }); + + it('should replace all placeholders with the custom value in the given string', () => { + expect(replacePlaceholders('{1} highlighted {2}', ['Bob', 'Suzy'])).to.equal('Bob highlighted Suzy'); + }); + + it('should replace only placeholders that have custom value in the given string', () => { + expect(replacePlaceholders('{1} highlighted {2}', ['Bob'])).to.equal('Bob highlighted {2}'); + }); + + it('should respect the order of placeholders when given an arbitrary order', () => { + expect(replacePlaceholders('{2} highlighted {1}', ['Bob', 'Suzy'])).to.equal('Suzy highlighted Bob'); + }); + + it('should replace with the same value if the placeholder is repeated', () => { + expect(replacePlaceholders('{2} highlighted {2}', ['Bob', 'Suzy'])).to.equal('Suzy highlighted Suzy'); + }); + }); }); diff --git a/src/lib/annotations/annotatorUtil.js b/src/lib/annotations/annotatorUtil.js index 1ba94184a..ce4ad0f33 100644 --- a/src/lib/annotations/annotatorUtil.js +++ b/src/lib/annotations/annotatorUtil.js @@ -1,6 +1,14 @@ +import 'whatwg-fetch'; import { CLASS_ACTIVE, CLASS_HIDDEN, CLASS_INVISIBLE } from '../constants'; import { TYPES, SELECTOR_ANNOTATION_CARET, PENDING_STATES } from './annotationConstants'; +const HEADER_CLIENT_NAME = 'X-Box-Client-Name'; +const HEADER_CLIENT_VERSION = 'X-Box-Client-Version'; +/* eslint-disable no-undef */ +const CLIENT_NAME = __NAME__; +const CLIENT_VERSION = __VERSION__; +/* eslint-enable no-undef */ + const AVATAR_COLOR_COUNT = 9; // 9 colors defined in Box React UI avatar code const THREAD_PARAMS = [ 'annotatedElement', @@ -379,3 +387,136 @@ export function eventToLocationHandler(locationFunction, callback) { } }; } + +//------------------------------------------------------------------------------ +// General Util Methods +//------------------------------------------------------------------------------ + +/** + * Function to decode key down events into keys + * + * @param {Event} event - Keydown event + * @return {string} Decoded keydown key + */ +export function decodeKeydown(event) { + let modifier = ''; + + // KeyboardEvent.key is the new spec supported in Chrome, Firefox and IE. + // KeyboardEvent.keyIdentifier is the old spec supported in Safari. + // Priority is given to the new spec. + let key = event.key || event.keyIdentifier || ''; + + // Get the modifiers on their own + if (event.ctrlKey) { + modifier = 'Control'; + } else if (event.shiftKey) { + modifier = 'Shift'; + } else if (event.metaKey) { + modifier = 'Meta'; + } + + // The key and keyIdentifier specs also include modifiers. + // Since we are manually getting the modifiers above we do + // not want to trap them again here. + if (key === modifier) { + key = ''; + } + + // keyIdentifier spec returns UTF8 char codes + // Need to convert them back to ascii. + if (key.indexOf('U+') === 0) { + if (key === 'U+001B') { + key = 'Escape'; + } else { + key = String.fromCharCode(key.replace('U+', '0x')); + } + } + + // If nothing was pressed just return + if (!key) { + return ''; + } + + // Special casing for space bar + if (key === ' ') { + key = 'Space'; + } + + // Edge bug which outputs "Esc" instead of "Escape" + // https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/5290772/ + if (key === 'Esc') { + key = 'Escape'; + } + + // keyIdentifier spec does not prefix the word Arrow. + // Newer key spec does it automatically. + if (key === 'Right' || key === 'Left' || key === 'Down' || key === 'Up') { + key = `Arrow${key}`; + } + + if (modifier) { + modifier += '+'; + } + + return modifier + key; +} + +/** + * Builds a list of required XHR headers. + * + * @param {Object} [headers] - Optional headers + * @param {string} [token] - Optional auth token + * @param {string} [sharedLink] - Optional shared link + * @param {string} [password] - Optional shared link password + * @return {Object} Headers + */ +export function getHeaders(headers = {}, token = '', sharedLink = '', password = '') { + /* eslint-disable no-param-reassign */ + if (token) { + headers.Authorization = `Bearer ${token}`; + } + if (sharedLink) { + headers.BoxApi = `shared_link=${sharedLink}`; + + if (password) { + headers.BoxApi = `${headers.BoxApi}&shared_link_password=${password}`; + } + } + + // Following headers are for API analytics + if (CLIENT_NAME) { + headers[HEADER_CLIENT_NAME] = CLIENT_NAME; + } + + if (CLIENT_VERSION) { + headers[HEADER_CLIENT_VERSION] = CLIENT_VERSION; + } + + /* eslint-enable no-param-reassign */ + return headers; +} + +/** + * Replaces variable place holders specified between {} in the string with + * specified custom value. Localizes strings that include variables. + * + * @param {string} string - String to be interpolated + * @param {string[]} placeholderValues - Custom values to replace into string + * @return {string} Properly translated string with replaced custom variable + */ +export function replacePlaceholders(string, placeholderValues) { + const regex = /\{\d+\}/g; + + if (!string || !string.length) { + return string; + } + + return string.replace(regex, (match) => { + // extracting the index that is supposed to replace the matched placeholder + const placeholderIndex = parseInt(match.replace(/^\D+/g, ''), 10) - 1; + + /* eslint-disable no-plusplus */ + return placeholderValues[placeholderIndex] ? placeholderValues[placeholderIndex] : match; + /* eslint-enable no-plusplus */ + }); +} diff --git a/src/lib/annotations/doc/DocHighlightDialog.js b/src/lib/annotations/doc/DocHighlightDialog.js index ebba86e7d..584d122a1 100644 --- a/src/lib/annotations/doc/DocHighlightDialog.js +++ b/src/lib/annotations/doc/DocHighlightDialog.js @@ -3,7 +3,6 @@ import AnnotationDialog from '../AnnotationDialog'; import * as annotatorUtil from '../annotatorUtil'; import * as docAnnotatorUtil from './docAnnotatorUtil'; import { CLASS_HIDDEN, CLASS_ACTIVE } from '../../constants'; -import { replacePlaceholders, decodeKeydown } from '../../util'; import { ICON_HIGHLIGHT, ICON_HIGHLIGHT_COMMENT } from '../../icons/icons'; import * as constants from '../annotationConstants'; @@ -36,7 +35,7 @@ class DocHighlightDialog extends AnnotationDialog { // Will be displayed as '{name} highlighted' if (annotation.text === '' && annotation.user.id !== '0') { const highlightLabelEl = this.highlightDialogEl.querySelector(`.${CLASS_HIGHLIGHT_LABEL}`); - highlightLabelEl.textContent = replacePlaceholders(__('annotation_who_highlighted'), [ + highlightLabelEl.textContent = annotatorUtil.replacePlaceholders(__('annotation_who_highlighted'), [ annotation.user.name ]); annotatorUtil.showElement(highlightLabelEl); @@ -242,7 +241,7 @@ class DocHighlightDialog extends AnnotationDialog { // be 'Some User' if (annotatorUtil.isPlainHighlight(annotations) && annotations[0].user.id !== '0') { const highlightLabelEl = this.highlightDialogEl.querySelector(`.${CLASS_HIGHLIGHT_LABEL}`); - highlightLabelEl.textContent = replacePlaceholders(__('annotation_who_highlighted'), [ + highlightLabelEl.textContent = annotatorUtil.replacePlaceholders(__('annotation_who_highlighted'), [ annotations[0].user.name ]); annotatorUtil.showElement(highlightLabelEl); @@ -309,7 +308,7 @@ class DocHighlightDialog extends AnnotationDialog { */ keydownHandler(event) { event.stopPropagation(); - if (decodeKeydown(event) === 'Enter') { + if (annotatorUtil.decodeKeydown(event) === 'Enter') { this.mousedownHandler(event); } super.keydownHandler(event); diff --git a/src/lib/annotations/doc/__tests__/DocHighlightDialog-test.js b/src/lib/annotations/doc/__tests__/DocHighlightDialog-test.js index c0c99b036..23faf3cd6 100644 --- a/src/lib/annotations/doc/__tests__/DocHighlightDialog-test.js +++ b/src/lib/annotations/doc/__tests__/DocHighlightDialog-test.js @@ -5,7 +5,7 @@ import AnnotationDialog from '../../AnnotationDialog'; import * as annotatorUtil from '../../annotatorUtil'; import * as docAnnotatorUtil from '../docAnnotatorUtil'; import { CLASS_HIDDEN, CLASS_ACTIVE } from '../../../constants'; -import * as util from '../../../util'; +import * as util from '../../annotatorUtil'; import * as constants from '../../annotationConstants'; let dialog;