Skip to content

Instantly share code, notes, and snippets.

@tim-evans
Last active February 18, 2020 15:49
Show Gist options
  • Save tim-evans/0abc070ec485d09ad40322d74141b3e4 to your computer and use it in GitHub Desktop.
Save tim-evans/0abc070ec485d09ad40322d74141b3e4 to your computer and use it in GitHub Desktop.
duration-field
import Component from '@ember/component';
import { set } from '@ember/object';
import { tryInvoke, isBlank } from '@ember/utils';
const UP = 38;
const DOWN = 40;
const BACKSPACE = 8;
function lpad(string, count, filler) {
if (string.length < count) {
while (string.length < count) {
string = `${filler}${string}`;
}
}
return string;
}
function formatDuration(format, milliseconds) {
if (milliseconds == null) {
return '';
}
let hours = Math.min(Math.floor(milliseconds / 3600000), 23);
let minutes = Math.min(
Math.floor(
(milliseconds - (hours * 3600000)) / 60000
),
59
);
let seconds = Math.min(
Math.floor(
(milliseconds - (hours * 3600000) - (minutes * 60000)) / 1000
),
59
);
return format
.replace('hh', lpad(hours + '', 2, '0'))
.replace('mm', lpad(minutes + '', 2, '0'))
.replace('ss', lpad(seconds + '', 2, '0'));
}
function parseDuration(format, string) {
// Do we have other formats that may be pasted in here
// cf. 1h22m
let parts = (string || '').split(':').map(part => {
let multiplier = part.length === 1 ? 10 : 1;
return parseInt(part, 10) * multiplier;
});
let units = format.split(':');
let duration = 0;
for (let i = 0, len = units.length; i < len; i++) {
let unit = units[i];
let value = parts[i];
if (unit === 'hh') {
if (!isNaN(value) && value <= 23) {
duration += 3600000 * value;
} else {
return false;
}
} else if (unit === 'mm') {
if (!isNaN(value) && value <= 59) {
duration += 60000 * value;
} else {
return false;
}
} else if (unit === 'ss') {
if (!isNaN(value) && value <= 59) {
duration += 1000 * value;
} else {
return false;
}
}
}
return duration;
}
export default Component.extend({
classNames: ['duration-field'],
/**
Called whenever the user changes the value.
@event onchange
@param {String} value The string
*/
/**
The `name` property of the `input` element.
@property name
@type String
@default null
*/
name: null,
/**
The `format` that the duration should be displayed in.
@property format
@type String
@default 'hh:mm:ss'
*/
format: 'hh:mm:ss',
/**
Whether or not the field is disabled.
@property disabled
@type Boolean
@default false
*/
disabled: false,
didRender() {
this._updateDisplayValue(this._getValue());
},
_getValue() {
if (this.isFocused) {
let input = this.element.querySelector('input');
return input.value;
} else {
return formatDuration(this.format, this.value);
}
},
_setValue(value) {
let duration = parseDuration(this.format, value);
if (isBlank(value) || value == null || duration === 0) {
this.onchange(null);
} else if (duration !== false) {
this.onchange(duration);
}
this._updateDisplayValue(value);
},
_updateDisplayValue(displayValue) {
let input = this.element.querySelector('input');
let selectionStart = input.selectionStart;
let selectionEnd = input.selectionEnd;
if (
displayValue.match(/\d{2}$/) &&
selectionStart === selectionEnd &&
selectionStart === displayValue.length &&
this.intent !== "delete" &&
displayValue.split(":").length < 3
) {
displayValue += ':';
selectionStart = selectionEnd = selectionStart + 1;
}
set(this, 'displayValue', displayValue || '');
input.value = displayValue || '';
if (this.isFocused) {
input.selectionStart = selectionStart;
input.selectionEnd = selectionEnd;
}
},
actions: {
handleArrowKeys(evt) {
this.set('intent', evt.which === BACKSPACE ? "delete": "insert");
if (evt.which === UP || evt.which === DOWN) {
let input = this.element.querySelector('input');
let cursor = input.selectionStart;
let direction = evt.which === UP ? 1 : -1;
let parts = this.format.split(':').map(part => {
return {
format: part,
start: this.format.indexOf(part),
end: this.format.indexOf(part) + part.length + 1
};
});
let unit = parts.find(function (part) {
return cursor >= part.start && cursor < part.end;
});
let duration = 0;
if (unit.format === 'hh') {
duration += 3600000;
} else if (unit.format === 'mm') {
duration += 60000;
} else if (unit.format === 'ss') {
duration += 1000;
}
this._setValue(
formatDuration(
this.format,
Math.max(this.value + duration * direction, 0)
)
);
return false;
}
},
restrict(evt) {
if (evt.which === 32) {
return false;
}
if (evt.which <= 40 || evt.metaKey) {
return true;
}
return /[:\d]/.test(String.fromCharCode(evt.which));
},
reformat() {
let input = this.element.querySelector('input');
this._setValue(input.value);
},
focus() {
set(this, 'isFocused', true);
},
blur() {
set(this, 'isFocused', false);
this._updateDisplayValue(this._getValue());
tryInvoke(this, 'onblur');
}
}
});
import Ember from 'ember';
export default Ember.Controller.extend({
appName: 'Ember Twiddle',
fieldName: "duration",
placeholder: "00:00:00",
model: Ember.computed(function () {
return {
duration: 200000
};
})
});
{{duration-field
value=(get model fieldName)
onchange=(action (mut (get model fieldName)))
name=fieldName
placeholder=placeholder
}}
{{model.duration}}
<input type='text'
id={{inputId}}
name={{name}}
placeholder={{placeholder}}
autocomplete="off"
onkeydown={{action 'handleArrowKeys'}}
onkeypress={{action 'restrict'}}
onfocus={{action 'focus'}}
onblur={{action 'blur'}}
onpaste={{action 'reformat'}}
onchange={{action 'reformat'}}
oninput={{action 'reformat'}}
disabled={{disabled}}>
{
"version": "0.15.1",
"EmberENV": {
"FEATURES": {}
},
"options": {
"use_pods": false,
"enable-testing": false
},
"dependencies": {
"jquery": "https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.js",
"ember": "3.4.3",
"ember-template-compiler": "3.4.3",
"ember-testing": "3.4.3"
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment