Skip to content

Instantly share code, notes, and snippets.

@runspired
Last active January 28, 2022 01:22
Show Gist options
  • Save runspired/1bdaaf166d0ed07642afacd4bdcdba0d to your computer and use it in GitHub Desktop.
Save runspired/1bdaaf166d0ed07642afacd4bdcdba0d to your computer and use it in GitHub Desktop.
Box and Mask
import Component from '@glimmer/component';
export default class extends Component {}
import Controller from '@ember/controller';
import { tracked } from "@glimmer/tracking";
export default class ApplicationController extends Controller {
appName = 'Ember Twiddle';
@tracked
value = null;
}
import { helper } from '@ember/component/helper';
import { tracked } from "@glimmer/tracking";
import { dependentKeyCompat } from "@ember/object/compat";
class Box {
constructor(context, path) {
this.context = context;
this.path = path;
}
@dependentKeyCompat
get value() {
return this.context[this.path];
}
set value(v) {
this.context[this.path] = v;
}
update(value) {
}
}
export default helper(function box([context, path]/*, hash*/) {
return new Box(context, path);
});
import { helper } from '@ember/component/helper';
export default helper(function inc(params/*, hash*/) {
return params[0] + params[1];
});
import { helper } from '@ember/component/helper';
import { tracked } from "@glimmer/tracking";
import { dependentKeyCompat } from "@ember/object/compat";
const Masks = new WeakMap();
class Mask {
@tracked current;
constructor(context, path, config = {}) {
this.context = context;
this.path = path;
this.pattern = config.pattern || "MM/DD/YYYY";
this.current = this.context[this.path] || "";
}
get upstream() {
return this.context[this.path];
}
get isValid() {
return this.validate(this.current);
}
format(value) {
const { pattern } = this;
let val = "";
if (!value) {
return val;
}
for (let i = 0; i < pattern.length; i++) {
let char = value.length > i ? value.charAt(i) : "";
let expected = pattern.charAt(i);
if (expected === "M" || expected === "D" || expected === "Y") {
val += char;
} else if (char === expected) {
val += char;
} else if (value.length === i) {
val += expected;
}
}
return val;
}
validate(value) {
const { pattern } = this;
if (this.partialMatch(value) && value.length === pattern.length) {
let year = "";
let month = "";
let day = "";
for (let i = 0; i < pattern.length; i++) {
let expected = pattern.charAt(i);
let char = value.charAt(i);
if (expected === "M") {
month += char;
} else if (expected === "D") {
day += char;
} else if (expected === "Y") {
year += char;
}
}
let monthNum = Number(month);
let dayNum = Number(day);
let isFebruary = monthNum === 2;
let isShortMonth = [4, 6, 9, 11].includes(monthNum);
if (isFebruary) {
let isLeapYear = Number(year) % 4 === 0;
let isCenturyYear = year.endsWith("00");
let isCenturyLeapYear = isCenturyYear && Number(year) % 400 === 0;
if (isCenturyYear) {
return isCenturyLeapYear? dayNum <= 29 : dayNum <= 28;
} else {
return isLeapYear ? dayNum <= 29 : dayNum <= 28;
}
} else if (isShortMonth) {
return dayNum <= 30;
}
return true;
}
return false;
}
partialMatch(value) {
const { pattern } = this;
const isFullMonth = pattern.includes("MM");
const isFullDay = pattern.includes("DD");
const isFullYear = pattern.includes("YYYY");
let val = "";
if (!value) {
return true;
}
if (value.length > pattern.length) {
return false;
}
let buff = "";
for (let i = 0, j = 0; i < pattern.length; j++, i++) {
if (value.length <= i) {
return true;
}
let char = value.charAt(j);
let expected = pattern.charAt(i);
console.log({ expected, char, buff });
// handle months
if (expected === "M") {
if (isFullMonth) {
if (buff === "M") {
let prev = value.charAt(j - 1);
if (prev === "0") {
if (!/[1-9]/.test(char)) {
return false; // 01 09 but not 00 0a etc.
}
} else {
if (!/[0-2]/.test(char)) {
return false; // 10-12 but not 1a 13 etc.
}
}
buff = "";
} else {
if (!/[0-1]/.test(char)) {
return false; // 0 1 but not 2 a etc.
}
buff = "M";
}
} else {
if (i > 0) {
let prev = value.charAt(j - 1);
if (prev === "1") {
if (!/[0-2]/.test(char)) {
return false; // 10-12 but not 1a 13 etc.
}
} else {
if (!/[1-9]/.test(char)) {
return false; // 1 9 but not 0 a etc.
}
}
} else {
if (!/[1-9]/.test(char)) {
return false; // 1 9 but not 0 a etc.
}
}
}
// handle days
} else if (expected === "D") {
if (isFullDay) {
if (buff === "D") {
let prev = value.charAt(j - 1);
let num = Number(prev + char);
if (!/[0-9]/.test(char) || isNaN(num) || num === 0 || num > 31) {
return false;
}
buff = "";
} else {
if (!/[0-3]/.test(char)) {
return false;
}
buff = "D";
}
} else {
let num;
if (i > 0) {
let prev = value.charAt(j - 1);
if (/[1-3]/.test(prev)) {
num = Number(prev + char);
}
} else {
num = Number(char);
}
if (!/[0-9]/.test(char) || isNaN(num) || num === 0 || num > 31) {
return false;
}
}
} else if (expected === "Y") {
if (buff === "") {
if (!/[1-2]/.test(char)) {
return false;
}
buff = "Y";
} else if (buff === "Y") {
let prev = value.charAt(i - 1);
if (prev === "1") {
if (char !== "9") {
return false;
}
} else if (prev === "2") {
if (char !== "0") {
return false;
}
}
buff = "YY";
} else if (buff === "YY" || buff === "YYY") {
if (!/[0-9]/.test(char)) {
return false;
}
buff = buff === "YY" ? "YYY" : "";
} else {
return false;
}
} else if (expected !== char) {
return false;
}
}
return true;
}
@dependentKeyCompat
get value() {
return this.current;
}
set value(v) {
const partialMatch = this.partialMatch(v);
if (partialMatch && this.validate(v)) {
// update if we fully match
this.current = this.context[this.path] = v;
return;
} else if (partialMatch) {
// buffer partial inputs
if (v.length > this.current.length) {
this.current = this.format(v);
return;
} else {
this.current = v;
return;
}
} else {
// reject invalid inputs other than deletes
return;
}
}
}
export default helper(function mask([context, path], config) {
let masksFor = Masks.get(context);
if (!masksFor) {
masksFor = new Map();
Masks.set(context, masksFor);
}
let mask = masksFor.get(path);
if (!mask) {
mask = new Mask(context, path, config);
masksFor.set(path, mask);
}
return mask;
});
<h1>Welcome to {{this.appName}}</h1>
<br>
Value Is: {{this.value}}
<br>
{{#let (box this "value") as |boxed|}}
<PlusBtn @value={{boxed.value}} />
{{/let}}
<PlusBtn @value={{this.value}} />
{{#let (mask this "value") as |masked|}}
<br>
Is Valid: {{if masked.isValid "true" "false"}}<br>
Upstream: {{masked.upstream}}<br>
<input type="date" />
<input type="month" />
<Input
@value={{masked.value}}
@type="tel"
placeholder={{masked.pattern}}
/>
{{/let}}
<br>
{{outlet}}
<br>
<br>
<button {{on "click" (fn (mut @value) (inc @value 1))}}>+</button>
<button {{on "click" (fn (mut @value) (inc @value -1))}}>-</button>
<Input @value={{@value}} />
{
"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