Skip to content

Instantly share code, notes, and snippets.

@danmeyers
Last active August 3, 2020 23:28
Show Gist options
  • Select an option

  • Save danmeyers/b1295c1ce2ae82b29fc96f5908559313 to your computer and use it in GitHub Desktop.

Select an option

Save danmeyers/b1295c1ce2ae82b29fc96f5908559313 to your computer and use it in GitHub Desktop.
New Twiddle
import Component from '@ember/component';
import { schedule } from '@ember/runloop';
function format(gtin) {
if (!gtin) {
return '';
}
// If this is not an all-numeric gtin, don't even try to add spacing.
if (Number.isNaN(Number(gtin))) {
return gtin;
}
switch (gtin.length) {
// GTIN-8: XXXX XXXX
case 8:
return `${gtin.slice(0, 4)} ${gtin.slice(4)}`;
// GTIN-12: X XXXXX XXXXX X
case 12:
return `${gtin[0]} ${gtin.slice(1, 6)} ${gtin.slice(6, 11)} ${gtin[11]}`;
// GTIN-13: X XXXXXX XXXXXX
case 13:
return `${gtin[0]} ${gtin.slice(1, 7)} ${gtin.slice(7)}`;
// GTIN-14: X XX XXXXX XXXXX X
case 14:
return `${gtin[0]} ${gtin.slice(1, 3)} ${gtin.slice(3, 8)} ${gtin.slice(8, 13)} ${gtin[13]}`;
default:
return gtin;
}
}
function unformat(formattedGtin) {
return formattedGtin.replace(/\D/g, '');
}
/**
* For a newly formatted display value, determine where the the cursor
* must be moved based on its current position. In the new formatted value, it should be
* after the same exact digit that it is after in the current display value.
*
* @param {string} currentDisplayValue The value as it is displayed, which may need updated formatting.
* @param {Number} currentCursorPosition The current position of the cursor in the input box.
* @returns {Number} The new position of the cursor in the updated, formatted value string.
*/
function findNewCursorPosition(currentDisplayValue, currentCursorPosition) {
const digitsBeforeCursor = currentDisplayValue.slice(0, currentCursorPosition).match(/\d/g);
const numDigitsBeforeCursor = (digitsBeforeCursor || []).length;
// Traverse the new formatted value, counting digits until reaching the
// same amount of digits that we had before the cursor in the current display value.
const newFormattedValue = format(unformat(currentDisplayValue));
let digitsCounted = 0;
let newCursorPosition = 0;
for (let char of newFormattedValue) {
if (digitsCounted === numDigitsBeforeCursor) {
break;
}
if (char.match('[0-9]')) {
digitsCounted++;
}
newCursorPosition++;
}
return newCursorPosition;
}
export default Component.extend({
value: null, // passed in
displayValue: null, // set internally
deletedSpaceCharacter: false, // set internally
didInsertElement(...args) {
this._super(...args);
this.setUpListeners();
},
willDestroyElement(...args) {
this._super(...args);
this.teardownListeners();
},
didReceiveAttrs() {
this._super();
this.set('displayValue', format(this.value));
},
/***********************************************************************
| Event Handling |
| |
| - Input: Reformat display value and set internal property value |
| when input changes. |
| – KeyDown: |
| - Left/Right arrow, to skip over formatted space characters |
| - Backspace, to delete an additional character if a space |
| character is being deleted |
| - Copy/Cut: Remove space characters from copied value |
| |
***********************************************************************/
/*
* Event Handling: Setup / Teardown
*/
setUpListeners() {
// Binding `this` provides the handler access to the component as its context.
this._inputEventListener = this.inputEventListener.bind(this);
this._copyEventListener = this.copyEventListener.bind(this);
this._cutEventListener = this.cutEventListener.bind(this);
this._keyDownEventListener = this.keyDownEventListener.bind(this);
const inputElement = this.element.querySelector('input');
inputElement.addEventListener('input', this._inputEventListener);
inputElement.addEventListener('copy', this._copyEventListener);
inputElement.addEventListener('cut', this._cutEventListener);
inputElement.addEventListener('keydown', this._keyDownEventListener);
},
teardownListeners() {
const inputElement = this.element.querySelector('input');
inputElement.removeEventListener('input', this._inputEventListener);
inputElement.removeEventListener('copy', this._copyEventListener);
inputElement.removeEventListener('cut', this._cutEventListener);
inputElement.removeEventListener('keydown', this._keyDownEventListener);
},
/*
* Event Handling: Listeners
*/
// Format the new input value. Update visible and internal state.
inputEventListener(e) {
const inputElement = e.target;
let cursorPosition = inputElement.selectionStart;
let inputValue = inputElement.value;
const formattedValue = format(unformat(inputValue));
const newCursorPosition = findNewCursorPosition(inputValue, cursorPosition);
this.updateAndRenderValue({ inputElement, formattedValue, cursorPosition : newCursorPosition });
},
keyDownEventListener(e) {
// Modifier keys can have complex, browser-specific effects that seem dangerous
// to approximate. E.g., holding shift with the left key selects a range, or
// cmd/ctrl + left moves to the front of the field.
//
// We're solving for the 95% case here, to help sellers understand that the spaces
// are not a part of the actual value.
if (e.altKey || e.ctrlKey || e.metaKey || e.shiftKey) {
return;
}
const inputElement = e.target;
const inputValue = inputElement.value;
let cursorPosition = inputElement.selectionStart;
// If moving left to become right-adjacent to a space, skip over the space.
if(e.key == 'ArrowLeft' && inputValue[cursorPosition - 2] === ' ') {
e.preventDefault();
cursorPosition -= 2;
inputElement.setSelectionRange(cursorPosition, cursorPosition);
}
// If moving right to become right-adjacent to a space, skip over the space.
if (e.key == 'ArrowRight' && inputValue[cursorPosition] === ' ') {
e.preventDefault();
cursorPosition += 2;
inputElement.setSelectionRange(cursorPosition, cursorPosition);
}
// If deleting a space, also delete the prior digit.
// E.g., `1234 |5678` becomes `123|567`.
if (e.key === 'Backspace' && inputValue[cursorPosition - 1] === ' ') {
e.preventDefault();
const cursorPositionWithoutDeletedChars = cursorPosition - 2;
const inputValueWithoutDeletedChars = inputValue.slice(0, cursorPositionWithoutDeletedChars) + inputValue.slice(cursorPosition);
// Re-format the value and find the new cursor position.
const formattedValue = format(unformat(inputValueWithoutDeletedChars));
cursorPosition = findNewCursorPosition(inputValueWithoutDeletedChars, cursorPositionWithoutDeletedChars);
// We've modified the value by deleting a digit. Update visible and internal state.
this.updateAndRenderValue({ inputElement, formattedValue, cursorPosition });
}
},
// For the selected range, unformat the data and copy to clipboard.
copyEventListener(e) {
e.preventDefault();
let inputElement = e.target;
let selectionStart = inputElement.selectionStart;
let selectionEnd = inputElement.selectionEnd;
let selectedUnformattedText = unformat(inputElement.value.slice(selectionStart, selectionEnd));
e.clipboardData.setData('Text', selectedUnformattedText);
},
// The same as copy, but we need to also delete the selected text.
cutEventListener(e) {
this.copyEventListener(e);
const inputElement = e.target;
const selectionStart = inputElement.selectionStart;
const selectionEnd = inputElement.selectionEnd;
const formattedValue = `${this.displayValue.slice(0, selectionStart)}${this.displayValue.slice(selectionEnd, this.displayValue.length)}`;
this.updateAndRenderValue({ inputElement, formattedValue, cursorPosition : selectionStart });
},
/*
* Event Handling: Helpers
*/
/**
* Updates the visible and internal property values. Then, repositions the cursor.
*
* @param {inputElement} The GTIN <input> DOM element.
* @param {formattedValue} The new formatted value to display.
* The internal property value will be set to the `unformat`ed formattedValue.
* @param {cursorPosition} After updating the display value in the input element,
where to place the cursor.
*/
updateAndRenderValue({ inputElement, formattedValue, cursorPosition }) {
this.set('displayValue', formattedValue);
this.set('value', unformat(formattedValue));
// Ember will render the input box with the new display value. After, we need to
// set the position of the cursor, otherwise it will be at the end of the string.
schedule('afterRender', this, () => {
inputElement.setSelectionRange(cursorPosition, cursorPosition);
});
},
});
import Controller from '@ember/controller';
export default class ApplicationController extends Controller {
appName = '123456789012';
}
<h1>Welcome to {{this.appName}}</h1>
<br>
<br>
{{outlet}}
<br>
<br>
{{my-input value=this.appName}}
{{input}}
{{input value=this.displayValue}}
{
"version": "0.17.1",
"EmberENV": {
"FEATURES": {},
"_TEMPLATE_ONLY_GLIMMER_COMPONENTS": false,
"_APPLICATION_TEMPLATE_WRAPPER": true,
"_JQUERY_INTEGRATION": true
},
"options": {
"use_pods": false,
"enable-testing": false
},
"dependencies": {
"jquery": "https://cdnjs.cloudflare.com/ajax/libs/jquery/3.5.1/jquery.js",
"ember": "3.18.1",
"ember-template-compiler": "3.18.1",
"ember-testing": "3.18.1"
},
"addons": {
"@glimmer/component": "1.0.0"
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment