From 9ec535562d3ff492b59ab733a8d619040ff47ef9 Mon Sep 17 00:00:00 2001 From: Sendil Kumar Date: Sat, 28 Oct 2017 07:59:22 +0200 Subject: [PATCH] Linting adding mouse events --- elements/howto-slider/README.md | 36 ++ elements/howto-slider/demo.html | 14 + elements/howto-slider/howto-slider.e2etest.js | 194 +++++++++++ elements/howto-slider/howto-slider.js | 318 ++++++++++++++++++ .../howto-slider/howto-slider.unittest.js | 128 +++++++ package-lock.json | 50 +-- 6 files changed, 715 insertions(+), 25 deletions(-) create mode 100644 elements/howto-slider/README.md create mode 100644 elements/howto-slider/demo.html create mode 100644 elements/howto-slider/howto-slider.e2etest.js create mode 100644 elements/howto-slider/howto-slider.js create mode 100644 elements/howto-slider/howto-slider.unittest.js diff --git a/elements/howto-slider/README.md b/elements/howto-slider/README.md new file mode 100644 index 00000000..2a6beb1f --- /dev/null +++ b/elements/howto-slider/README.md @@ -0,0 +1,36 @@ +## Summary {: #summary } + +A `` represents a slider in a form. The slider allows user +to select a value from a range of value. + +The element attempts to self apply the attributes `role="slider"` and +`tabindex="0"` when it is first created. The `role` attribute helps assistive +technology like a screen reader tell the user what kind of control this is. +The `tabindex` attribute opts the element into the tab order, making it keyboard +focusable and operable. To learn more about these two topics, check out +[What can ARIA do?][what-aria] and [Using tabindex][using-tabindex]. + +When the slider is moved, it sets `value` attribute. In addition, +the element sets an `aria-valuenow` attribute to corresponding value. Clicking on +the slider will change its value based on the distance in which it is clicked. +Also, on pressing `right` / `up` arrow key the value is increased by `1`. +On pressing `left` / `down` arrow key the value is decreased by `1` respectively. +You can also press `pageUp` / `pageDown` to increase / decrease the value by `10` +respectively. This also gives an option to press `home` / `end` button to go to +`minimum` and `maximum` values respectively. + +Warning: Just because you _can_ build a custom element slider, doesn't +necessarily mean that you _should_. As this example shows, you will need to add +your own keyboard, labeling, and ARIA support. It's also important to note that +the native `
` element will NOT submit values from a custom element. You +will need to wire that up yourself using AJAX or a hidden `` field. + +## Reference {: #reference } + +- [Checkbox Pattern in ARIA Authoring Practices 1.1][checkbox-pattern] +- [What can ARIA Do?][what-aria] +- [Using tabindex][using-tabindex] + +[checkbox-pattern]: https://www.w3.org/TR/wai-aria-practices/examples/slider/slider-1.html +[what-aria]: https://developers.google.com/web/fundamentals/accessibility/semantics-aria/#what_can_aria_do +[using-tabindex]: https://developers.google.com/web/fundamentals/accessibility/focus/using-tabindex diff --git a/elements/howto-slider/demo.html b/elements/howto-slider/demo.html new file mode 100644 index 00000000..820469d5 --- /dev/null +++ b/elements/howto-slider/demo.html @@ -0,0 +1,14 @@ + + + diff --git a/elements/howto-slider/howto-slider.e2etest.js b/elements/howto-slider/howto-slider.e2etest.js new file mode 100644 index 00000000..f0a0e710 --- /dev/null +++ b/elements/howto-slider/howto-slider.e2etest.js @@ -0,0 +1,194 @@ +/** + * Copyright 2017 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/* eslint max-len: ["off"] */ + +const helper = require('../../tools/selenium-helper.js'); +const expect = require('chai').expect; +const {Key} = require('selenium-webdriver'); + +describe('howto-slider', function() { + let success; + let value; + const findSlider = _ => { + window.expectedSlider = document.querySelector('[role=slider]'); + return window.expectedSlider; + }; + + const initialize = value => { + window.expectedSlider.setAttribute('aria-valuenow', `${value}`); + window.expectedSlider.value = value; + return window.expectedSlider.getAttribute('aria-valuenow') === `${value}`; + }; + + const isInitialized = _ => { + let isAriaValuenow = window.expectedSlider.getAttribute('aria-valuenow') === '0'; + return isAriaValuenow && window.expectedSlider.value === 0; + }; + + const isChanged = value => { + let isAriaValuenow = window.expectedSlider.getAttribute('aria-valuenow') === `${value}`; + return isAriaValuenow && window.expectedSlider.value === value; + }; + + beforeEach(function() { + value = 0; + return this.driver.get(`${this.address}/howto-slider/demo.html`) + .then(_ => helper.waitForElement(this.driver, 'howto-slider')); + }); + + it('should increase the value by 1 on [arrow-up]', async function() { + await this.driver.executeScript(findSlider); + success = await helper.pressKeyUntil(this.driver, Key.TAB, _ => document.activeElement === window.expectedSlider); + expect(success).to.be.true; + success = await this.driver.executeScript(isInitialized); + expect(success).to.be.true; + await this.driver.actions().sendKeys(Key.ARROW_UP).perform(); + success = await this.driver.executeScript(isChanged, value+1); + expect(success).to.be.true; + }); + + it('should increase the value by 1 on [arrow-right]', async function() { + await this.driver.executeScript(findSlider); + success = await helper.pressKeyUntil(this.driver, Key.TAB, _ => document.activeElement === window.expectedSlider); + expect(success).to.be.true; + success = await this.driver.executeScript(isInitialized); + expect(success).to.be.true; + await this.driver.actions().sendKeys(Key.ARROW_RIGHT).perform(); + success = await this.driver.executeScript(isChanged, value+1); + expect(success).to.be.true; + }); + + it('should increase the value by 10 on [page-up]', async function() { + await this.driver.executeScript(findSlider); + success = await helper.pressKeyUntil(this.driver, Key.TAB, _ => document.activeElement === window.expectedSlider); + expect(success).to.be.true; + success = await this.driver.executeScript(isInitialized); + expect(success).to.be.true; + await this.driver.actions().sendKeys(Key.PAGE_UP).perform(); + success = await this.driver.executeScript(isChanged, value+10); + expect(success).to.be.true; + }); + + it('should decrease the value by 1 on [arrow-left]', async function() { + await this.driver.executeScript(findSlider); + success = await helper.pressKeyUntil(this.driver, Key.TAB, _ => document.activeElement === window.expectedSlider); + expect(success).to.be.true; + value = 10; + success = await this.driver.executeScript(initialize, value); + expect(success).to.be.true; + await this.driver.actions().sendKeys(Key.ARROW_LEFT).perform(); + success = await this.driver.executeScript(isChanged, value-1); + expect(success).to.be.true; + }); + + it('should decrease the value by 1 on [arrow-down]', async function() { + await this.driver.executeScript(findSlider); + success = await helper.pressKeyUntil(this.driver, Key.TAB, _ => document.activeElement === window.expectedSlider); + expect(success).to.be.true; + value = 10; + success = await this.driver.executeScript(initialize, value); + expect(success).to.be.true; + await this.driver.actions().sendKeys(Key.ARROW_DOWN).perform(); + success = await this.driver.executeScript(isChanged, value-1); + expect(success).to.be.true; + }); + + it('should decrease the value by 10 on [page-down]', async function() { + await this.driver.executeScript(findSlider); + success = await helper.pressKeyUntil(this.driver, Key.TAB, _ => document.activeElement === window.expectedSlider); + expect(success).to.be.true; + value = 50; + success = await this.driver.executeScript(initialize, value); + expect(success).to.be.true; + await this.driver.actions().sendKeys(Key.PAGE_DOWN).perform(); + success = await this.driver.executeScript(isChanged, value-10); + expect(success).to.be.true; + }); + + it('should set the value to minimum on [home]', async function() { + await this.driver.executeScript(findSlider); + success = await helper.pressKeyUntil(this.driver, Key.TAB, _ => document.activeElement === window.expectedSlider); + expect(success).to.be.true; + value = 99; + success = await this.driver.executeScript(initialize, value); + expect(success).to.be.true; + await this.driver.actions().sendKeys(Key.HOME).perform(); + success = await this.driver.executeScript(isChanged, 0); + expect(success).to.be.true; + }); + + it('should set the value to maximum on [end]', async function() { + await this.driver.executeScript(findSlider); + success = await helper.pressKeyUntil(this.driver, Key.TAB, _ => document.activeElement === window.expectedSlider); + expect(success).to.be.true; + value = 10; + success = await this.driver.executeScript(initialize, value); + expect(success).to.be.true; + await this.driver.actions().sendKeys(Key.END).perform(); + success = await this.driver.executeScript(isChanged, 100); + expect(success).to.be.true; + }); + + it('should set the value on mousemove', async function() { + const slider = await this.driver.executeScript(findSlider); + success = await helper.pressKeyUntil(this.driver, Key.TAB, _ => document.activeElement === window.expectedSlider); + expect(success).to.be.true; + value = 10; + success = await this.driver.executeScript(initialize, value); + expect(success).to.be.true; + await this.driver.actions().mouseDown(slider).perform(); + await this.driver.actions().mouseMove(slider, {x: 100, y: 0}).perform(); + await this.driver.actions().mouseUp(slider).perform(); + success = await this.driver.executeScript(isChanged, 100); + expect(success).to.be.true; + }); +}); + +describe('howto-slider pre-upgrade', function() { + let success; + + beforeEach(function() { + return this.driver.get(`${this.address}/howto-slider/demo.html?nojs`); + }); + + it('should handle attributes set before upgrade', async function() { + await this.driver.executeScript(_ => window.expectedSlider = document.querySelector('howto-slider')); + await this.driver.executeScript(_ => window.expectedSlider.setAttribute('value', 0)); + + await this.driver.executeScript(_ => _loadJavaScript()); + await helper.waitForElement(this.driver, 'howto-slider'); + success = await this.driver.executeScript(_ => + window.expectedSlider.value === 0 && + window.expectedSlider.getAttribute('aria-valuenow') === '0' + ); + expect(success).to.be.true; + }); + + it('should handle instance properties set before upgrade', async function() { + await this.driver.executeScript(_ => window.expectedSlider = document.querySelector('howto-slider')); + await this.driver.executeScript(_ => window.expectedSlider.value = 0); + + await this.driver.executeScript(_ => _loadJavaScript()); + await helper.waitForElement(this.driver, 'howto-slider'); + success = await this.driver.executeScript(_ => + window.expectedSlider.hasAttribute('value') && + window.expectedSlider.hasAttribute('aria-valuenow') && + window.expectedSlider.getAttribute('aria-valuenow') === '0' + ); + expect(success).to.be.true; + }); +}); + diff --git a/elements/howto-slider/howto-slider.js b/elements/howto-slider/howto-slider.js new file mode 100644 index 00000000..eeecd171 --- /dev/null +++ b/elements/howto-slider/howto-slider.js @@ -0,0 +1,318 @@ +/** + * Copyright 2017 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +(function() { + /** + * Define key codes to help with handling keyboard events. + */ + const KEYCODE = { + PAGE_UP: 33, + PAGE_DOWN: 34, + END: 35, + HOME: 36, + LEFT_ARROW: 37, + UP_ARROW: 38, + RIGHT_ARROW: 39, + DOWN_ARROW: 40, + }; + + /** + * Cloning contents from a <template> element is more performant + * than using innerHTML because it avoids addtional HTML parse costs. + */ + const template = document.createElement('template'); + template.innerHTML = ` + +
+
+ +
+ `; + + // HIDE + // ShadyCSS will rename classes as needed to ensure style scoping. + ShadyCSS.prepareTemplate(template, 'howto-slider'); + // /HIDE + + class HowToSlider extends HTMLElement { + static get observedAttributes() { + return ['value']; + } + /** + * The element's constructor is run anytime a new instance is created. + * Instances are created either by parsing HTML, calling + * document.createElement('howto-checkbox'), or calling new HowToCheckbox(); + * The construtor is a good place to create shadow DOM, though you should + * avoid touching any attributes or light DOM children as they may not + * be available yet. + */ + constructor() { + super(); + this.attachShadow({mode: 'open'}); + this.shadowRoot.appendChild(template.content.cloneNode(true)); + } + + /** + * `connectedCallback()` fires when the element is inserted into the DOM. + * It's a good place to set the initial `role`, `tabindex`, `aria-valuemin`, `aria-valuemax`, `aria-valuenow`, `value` internal state, + * and install event listeners. + */ + connectedCallback() { + // HIDE + // Shim Shadow DOM styles. This needs to be run in `connectedCallback()` + // because if you shim Custom Properties (CSS variables) the element + // will need access to its parent node. + ShadyCSS.styleElement(this); + // /HIDE + + if (!this.hasAttribute('role')) + this.setAttribute('role', 'slider'); + if (!this.hasAttribute('tabindex')) + this.setAttribute('tabindex', 0); + if (!this.hasAttribute('aria-valuenow')) + this.setAttribute('aria-valuenow', 0); + if (!this.hasAttribute('aria-valuemin')) + this.setAttribute('aria-valuemin', 0); + if (!this.hasAttribute('aria-valuemax')) + this.setAttribute('aria-valuemax', 100); + if (!this.hasAttribute('value')) + this.setAttribute('value', 0); + + this.shadowRoot.querySelector('howto-label').innerHTML = this.value; + + // A user may set a property on an _instance_ of an element, + // before its prototype has been connected to this class. + // The `_upgradeProperty()` method will check for any instance properties + // and run them through the proper class setters. + // See the [lazy properites](/web/fundamentals/architecture/building-components/best-practices#lazy-properties) + // section for more details. + // this upgrade property to be added + + this.addEventListener('keyup', this._onKeyUp); + this.addEventListener('click', this._onClick); + this.addEventListener('mousedown', this._onMousedown); + } + + _upgradeProperty(prop) { + if (this.hasOwnProperty(prop)) { + let value = this[prop]; + delete this[prop]; + this[prop] = value; + } + } + + /** + * `disconnectedCallback()` fires when the element is removed from the DOM. + * It's a good place to do clean up work like releasing references and + * removing event listeners. + */ + disconnectedCallback() { + this.removeEventListener('keyup', this._onKeyUp); + this.removeEventListener('click', this._onClick); + this.removeEventListener('mousedown', this._onMousedown); + this.removeEventListener('mouseup', this._onMouseup); + this.removeEventListener('mousemove', this._onMousemove); + } + + /** + * Properties and their corresponding attributes should mirror one another. + * The property setter for `checked` handles truthy/falsy values and + * reflects those to the state of the attribute. See the [avoid + * reentrancy](/web/fundamentals/architecture/building-components/best-practices#avoid-reentrancy) + * section for more details. + */ + set value(value) { + this.setAttribute('value', value); + } + + get value() { + return parseInt(this.getAttribute('value')); + } + + get valuemax() { + return parseInt(this.getAttribute('aria-valuemax')); + } + + get valuemin() { + return parseInt(this.getAttribute('aria-valuemin')); + } + + /** + * `attributeChangedCallback()` is called when any of the attributes in the + * `observedAttributes` array are changed. It's a good place to handle + * side effects, like setting ARIA attributes. + */ + attributeChangedCallback(name, oldValue, newValue) { + this.setAttribute(`aria-${name}now`, newValue); + } + + _onKeyUp(event) { + // Don’t handle modifier shortcuts typically used by assistive technology. + if (event.altKey) + return; + + switch (event.keyCode) { + case KEYCODE.RIGHT_ARROW: + case KEYCODE.UP_ARROW: + event.preventDefault(); + this._increaseValue(1); + break; + case KEYCODE.LEFT_ARROW: + case KEYCODE.DOWN_ARROW: + event.preventDefault(); + this._decreaseValue(1); + break; + case KEYCODE.HOME: + event.preventDefault(); + this._setMinValue(); + break; + case KEYCODE.END: + event.preventDefault(); + this._setMaxValue(); + break; + case KEYCODE.PAGE_UP: + event.preventDefault(); + this._increaseValue(10); + break; + case KEYCODE.PAGE_DOWN: + event.preventDefault(); + this._decreaseValue(10); + break; + // Any other key press is ignored and passed back to the browser. + default: + return; + } + } + + _onClick(event) { + const diffX = event.pageX - this.offsetLeft; + const containerWidth = + this.shadowRoot.querySelector('.slider-container').offsetWidth; + this.value = + parseInt(((this.valuemax - this.valuemin) * diffX) / containerWidth); + if (this.value <= this.valuemin) { + this._setMinValue(); + } else if (this.value >= this.valuemax) { + this._setMaxValue(); + } else { + this._changeValue(this.value); + } + } + + _onMousedown(event) { + this.addEventListener('mousemove', this._onMousemove); + this.addEventListener('mouseup', this._onMouseup); + event.preventDefault(); + event.stopPropagation(); + } + + _onMousemove(event) { + this._onClick(event); + event.preventDefault(); + event.stopPropagation(); + } + + _onMouseup(event) { + this.removeEventListener('mousemove', this._onMousemove); + this.removeEventListener('mouseup', this._onMouseup); + event.preventDefault(); + event.stopPropagation(); + } + + _increaseValue(value) { + if (this.value + value <= this.valuemax) { + this._changeValue(this.value+value); + } else { + this._setMaxValue(); + } + } + + _decreaseValue(value) { + if (this.value - value >= this.valuemin) { + this._changeValue(this.value-value); + } else { + this._setMinValue(); + } + } + + _setMinValue() { + this._changeValue(this.valuemin); + } + + _setMaxValue() { + this._changeValue(this.valuemax); + } + + _changePosition() { + const containerWidth = + this.shadowRoot.querySelector('.slider-container').offsetWidth; + const sliderWidth = + this.shadowRoot.querySelector('.slider').offsetWidth; + const left = + Math.round((this.value * containerWidth) + / (this.valuemax - this.valuemin)) - (sliderWidth / 2); + this.shadowRoot.querySelector('.slider').style.left = left + 'px'; + } + + + /** + * `_changeValue(value)` calls the `value` setter and sets its value. + * Because `_changeValue(value)` is only caused by a user action, it will + * also dispatch a change event. This event bubbles in order to let the + * change to its listeners. + */ + _changeValue(value) { + this.value = value; + this._changePosition(); + this.shadowRoot.querySelector('howto-label').innerHTML = this.value; + this.dispatchEvent(new CustomEvent('change', { + detail: { + value: this.value, + }, + bubbles: true, + })); + } + } + + window.customElements.define('howto-slider', HowToSlider); +})(); diff --git a/elements/howto-slider/howto-slider.unittest.js b/elements/howto-slider/howto-slider.unittest.js new file mode 100644 index 00000000..1be1805b --- /dev/null +++ b/elements/howto-slider/howto-slider.unittest.js @@ -0,0 +1,128 @@ +/** + * Copyright 2017 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/* eslint max-len: ["off"] */ + +(function() { + const expect = chai.expect; + + describe('howto-slider', function() { + before(howtoComponents.before()); + after(howtoComponents.after()); + beforeEach(function() { + this.container.innerHTML = ``; + return howtoComponents.waitForElement('howto-slider') + .then(_ => { + this.slider = this.container.querySelector('howto-slider'); + }); + }); + + it('should add a [role] to the slider', function() { + expect(this.slider.getAttribute('role')).to.equal('slider'); + }); + + it('should add a [tabindex] to the slider', function() { + expect(this.slider.getAttribute('tabindex')).to.equal('0'); + }); + + it('should add a [aria-valuenow] to the slider', function() { + expect(this.slider.getAttribute('aria-valuenow')).to.equal('0'); + }); + + it('should add a [aria-valuemax] to the slider', function() { + expect(this.slider.getAttribute('aria-valuemax')).to.equal('100'); + }); + + it('should add a [aria-valuemin] to the slider', function() { + expect(this.slider.getAttribute('aria-valuemin')).to.equal('0'); + }); + + describe('value', function() { + it('should set [value] and [valuenow] when calling ' + + '_changeValue(value)', function() { + expect(this.slider.value).to.equal(0); + this.slider._changeValue(10); + expect(this.slider.getAttribute('aria-valuenow')).to.equal('10'); + expect(this.slider.value).to.equal(10); + }); + + it('should set [value] and [valuenow] when setting .value', function() { + expect(this.slider.value).to.equal(0); + this.slider.value = 10; + expect(this.slider.getAttribute('aria-valuenow')).to.equal('10'); + expect(this.slider.value).to.equal(10); + this.slider.value = 50; + expect(this.slider.getAttribute('aria-valuenow')).to.equal('50'); + expect(this.slider.value).to.equal(50); + }); + + it('should set [value] and [valuenow] when calling ' + + '_increaseValue(value)', function() { + expect(this.slider.value).to.equal(0); + this.slider._increaseValue(10); + expect(this.slider.getAttribute('aria-valuenow')).to.equal('10'); + expect(this.slider.value).to.equal(10); + }); + + it('should set [value] and [valuenow] when calling ' + + '_increaseValue(value) within range', function() { + expect(this.slider.value).to.equal(0); + this.slider.value = 100; + this.slider._increaseValue(10); + expect(this.slider.getAttribute('aria-valuenow')).to.equal('100'); + expect(this.slider.value).to.equal(100); + }); + + it('should set [value] and [valuenow] when calling ' + + '_decreaseValue(value)', function() { + expect(this.slider.value).to.equal(0); + this.slider.value = 100; + this.slider._decreaseValue(10); + expect(this.slider.getAttribute('aria-valuenow')).to.equal('90'); + expect(this.slider.value).to.equal(90); + }); + + it('should set [value] and [valuenow] when calling ' + + '_decreaseValue(value) within range', function() { + expect(this.slider.value).to.equal(0); + this.slider._decreaseValue(10); + expect(this.slider.getAttribute('aria-valuenow')).to.equal('0'); + expect(this.slider.value).to.equal(0); + }); + + it('should set minimum [value] and [valuenow] when calling ' + + '_setMinValue()', function() { + expect(this.slider.value).to.equal(0); + this.slider.value = 99; + expect(this.slider.value).to.equal(99); + this.slider._setMinValue(); + const minValue = this.slider.getAttribute('aria-valuemin'); + expect(this.slider.getAttribute('aria-valuenow')).to.equal(minValue); + expect(this.slider.value).to.equal(parseInt(minValue)); + }); + + it('should set maximum [value] and [valuenow] when calling ' + + '_setMaxValue()', function() { + expect(this.slider.value).to.equal(0); + this.slider.value = 1; + expect(this.slider.value).to.equal(1); + this.slider._setMaxValue(); + const maxValue = this.slider.getAttribute('aria-valuemax'); + expect(this.slider.getAttribute('aria-valuenow')).to.equal(maxValue); + expect(this.slider.value).to.equal(parseInt(maxValue)); + }); + }); + }); +})(); diff --git a/package-lock.json b/package-lock.json index 663b8062..8424b1de 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1826,6 +1826,15 @@ "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", "dev": true }, + "string_decoder": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.0.3.tgz", + "integrity": "sha512-4AH6Z5fzNNBcH+6XDMfA/BTt87skxqJlO0lAh3Dker5zThcAxG6mKz+iGu308UKoPPQ8Dcqx/4JhujzltRa+hQ==", + "dev": true, + "requires": { + "safe-buffer": "5.1.1" + } + }, "string-width": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", @@ -1837,15 +1846,6 @@ "strip-ansi": "3.0.1" } }, - "string_decoder": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.0.3.tgz", - "integrity": "sha512-4AH6Z5fzNNBcH+6XDMfA/BTt87skxqJlO0lAh3Dker5zThcAxG6mKz+iGu308UKoPPQ8Dcqx/4JhujzltRa+hQ==", - "dev": true, - "requires": { - "safe-buffer": "5.1.1" - } - }, "strip-ansi": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", @@ -4024,22 +4024,22 @@ } } }, - "string-width": { - "version": "1.0.2", + "string_decoder": { + "version": "1.0.1", "bundled": true, "dev": true, "requires": { - "code-point-at": "1.1.0", - "is-fullwidth-code-point": "1.0.0", - "strip-ansi": "3.0.1" + "safe-buffer": "5.0.1" } }, - "string_decoder": { - "version": "1.0.1", + "string-width": { + "version": "1.0.2", "bundled": true, "dev": true, "requires": { - "safe-buffer": "5.0.1" + "code-point-at": "1.1.0", + "is-fullwidth-code-point": "1.0.0", + "strip-ansi": "3.0.1" } }, "stringstream": { @@ -6541,22 +6541,22 @@ } } }, - "string-width": { - "version": "1.0.2", + "string_decoder": { + "version": "1.0.1", "bundled": true, "dev": true, "requires": { - "code-point-at": "1.1.0", - "is-fullwidth-code-point": "1.0.0", - "strip-ansi": "3.0.1" + "safe-buffer": "5.0.1" } }, - "string_decoder": { - "version": "1.0.1", + "string-width": { + "version": "1.0.2", "bundled": true, "dev": true, "requires": { - "safe-buffer": "5.0.1" + "code-point-at": "1.1.0", + "is-fullwidth-code-point": "1.0.0", + "strip-ansi": "3.0.1" } }, "stringstream": {