Skip to content

Instantly share code, notes, and snippets.

@jasonhofer
Last active February 20, 2022 03:15
Show Gist options
  • Save jasonhofer/d8d9f6d5feb160b9a28b to your computer and use it in GitHub Desktop.
Save jasonhofer/d8d9f6d5feb160b9a28b to your computer and use it in GitHub Desktop.
!(function () {
// Allow: <!--{ fullName }-->
// Instead of: <!-- ko text: fullName --><!-- /ko -->
// See: https://knockoutjs.com/documentation/binding-preprocessing.html
ko.bindingProvider.instance.preprocessNode = function (node) {
if (node.nodeType !== Node.COMMENT_NODE) {
return;
}
const match = node.nodeValue.match(/^[{](.*)[}]$/);
if (!match) { return; }
// Create a pair of comments to replace the single comment
const c1 = document.createComment(`ko text: ${match[1]}`);
const c2 = document.createComment('/ko');
node.parentNode.insertBefore(c1, node);
node.parentNode.replaceChild(c2, node);
// Tell Knockout about the new nodes so that it can apply bindings to them
return [c1, c2];
};
ko.extenders['type'] = function (target, type) {
var abbrev = {'int': 'integer', 'num': 'number', 'bool': 'boolean'};
type = abbrev[type] || type;
var cast = ko.extenders['type'].casters[type];
if (!cast) { throw new Error('Unknown type: ' + type); }
var read = function () { return cast(target()); };
if (ko.isWriteableObservable(target)) {
var computed = ko.pureComputed({
read: read,
write: function (newValue) { target(cast(newValue)); },
});
computed(target());
if ('integer' === type || 'number' === type) {
computed.increment = function (amt) { if (null == amt) { amt = 1; } return computed(computed() + cast(amt)); };
computed.decrement = function (amt) { if (null == amt) { amt = 1; } return computed(computed() - cast(amt)); };
}
} else {
var computed = ko.pureComputed(read);
}
computed.cast = cast;
return computed;
};
ko.extenders['type'].casters = {
'number': function (n) { return (+ko.unwrap(n) || 0); },
'integer': function (i) { return ~~ko.unwrap(i); },
'boolean': function (b) { return !!ko.unwrap(b); },
'string': function (s) { s = ko.unwrap(s); return '' + (0 === s || false === s ? s : (s || '')); }
};
ko.extenders['enum'] = function (target, validValues) {
target = target.extend({'type': 'string'});
var isValid = function (value) { return validValues.indexOf(value) >= 0; },
write = function (newValue) {
newValue = target.cast(newValue);
if (!isValid(newValue)) {
throw Error("Invalid enum value: '" + newValue + "'. Expected one of: '" + (validValues.join("', '")) + "'.");
}
target(newValue);
},
initialValue = target();
initialValue && write(initialValue); // Initialize with current value, but allow empty
var computed = ko.pureComputed({read: target, write: write});
computed.values = validValues;
computed.isValid = isValid;
return computed;
};
//
// Auto-casting observables.
//
ko.observableNumber = function (initialValue) {
return ko.observable(initialValue).extend({'type': 'number'});
};
ko.observableNumber.cast = ko.extenders['type'].casters['number'];
ko.observableInt = function (initialValue) {
return ko.observable(initialValue).extend({'type': 'integer'});
};
ko.observableInt.cast = ko.extenders['type'].casters['integer'];
ko.observableString = function (initialValue) {
return ko.observable(initialValue).extend({'type': 'string'});
};
ko.observableString.cast = ko.extenders['type'].casters['string'];
ko.observableBool = function (initialValue) {
return ko.observable(initialValue).extend({'type': 'boolean'});
};
ko.observableBool.cast = ko.extenders['type'].casters['boolean'];
ko.observableEnum = function (validValues, initialValue) {
return ko.observable(initialValue).extend({'enum': validValues});
};
//
// From: https://gist.github.com/lelandrichardson/9359228
// Old URL: http://tech.pro/blog/1863/10-knockout-binding-handlers-i-don-t-want-to-live-without
//
ko.bindingHandlers.stopBinding = {
init: function () { return { controlsDescendantBindings: true }; }
};
ko.virtualElements.allowedBindings.stopBinding = true;
ko.bindingHandlers.hidden = {
update: function (element, valueAccessor) {
ko.bindingHandlers.visible.update(element, function () {
return !ko.unwrap(valueAccessor());
});
}
};
ko.bindingHandlers.toggle = {
init: function (element, valueAccessor) {
var observable = valueAccessor();
ko.applyBindingsToNode(element, {
click: function () { observable(!observable()); }
});
}
};
ko.bindingHandlers.href = {
update: function (element, valueAccessor) {
ko.bindingHandlers.attr.update(element, function () {
return { href: valueAccessor() };
});
}
};
ko.bindingHandlers.src = {
update: function (element, valueAccessor) {
ko.bindingHandlers.attr.update(element, function () {
return { src: valueAccessor() };
});
}
};
ko.bindingHandlers.toJSON = {
update: function (element, valueAccessor) {
return ko.bindingHandlers.text.update(element, function () {
return ko.toJSON(valueAccessor(), null, 4);
});
}
};
/*
ko.bindingHandlers.instantValue = { // Same as textInput. Saving this for reference.
init: function (element, valueAccessor, allBindingsAccessor) {
handlers.value.init(element, valueAccessor,
ko.observable(ko.utils.extend(allBindingsAccessor(), {valueUpdate: 'afterkeydown'}))
);
},
update: handlers.value.update
};
*/
// See: https://github.com/mbest/knockout/issues/9
// Formerly "withlight"
ko.bindingHandlers['context'] = {
'init': function (element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) {
var bindingValue = valueAccessor();
if (typeof bindingValue !== 'object' || bindingValue === null) {
throw new Error('The "context" binding must be used with an object');
}
var innerContext = bindingContext['createChildContext'](bindingValue);
ko.applyBindingsToDescendants(innerContext, element);
return {'controlsDescendantBindings': true};
}
};
// my own
ko.bindingHandlers.id = {
update: function (element, valueAccessor) {
ko.bindingHandlers.attr.update(element, function () {
return { id: valueAccessor() };
});
}
};
ko.bindingHandlers.required = {
update: function (element, valueAccessor) {
var accessor = function () { return { required: valueAccessor() }; };
ko.bindingHandlers.attr.update(element, accessor);
ko.bindingHandlers.css.update(element, accessor);
}
};
ko.bindingHandlers.readOnly = {
update: function (element, valueAccessor) {
ko.bindingHandlers.attr.update(element, function () {
return { readOnly: valueAccessor() };
});
}
};
ko.bindingHandlers.enterKey = {
init: function (element, valueAccessor, allBindings, viewModel) {
ko.utils.registerEventHandler(element, 'keypress', function (event) {
if (13 === (event.which || event.keyCode)) {
ko.utils.triggerEvent(element, 'blur');
valueAccessor().call(viewModel, viewModel, event);
ko.utils.triggerEvent(element, 'focus');
return false;
}
return true;
});
}
};
/*
ko.bindingHandlers.fadeInVisible = {
init: function (element, valueAccessor) {
// Initially set the element to be instantly visible/hidden depending on the value
var value = valueAccessor();
$(element).toggle(ko.unwrap(value));
},
update: function (element, valueAccessor) {
// Whenever the value subsequently changes, slowly fade the element in or out
var value = valueAccessor();
ko.unwrap(value) ? $(element).fadeIn() : $(element).hide();
}
};
ko.bindingHandlers.fadeInHidden = {
init: function (element, valueAccessor) {
// Initially set the element to be instantly visible/hidden depending on the value
var value = valueAccessor();
$(element).toggle(!ko.unwrap(value));
},
update: function (element, valueAccessor) {
// Whenever the value subsequently changes, slowly fade the element in or out
var value = valueAccessor();
ko.unwrap(value) ? $(element).hide() : $(element).fadeIn();
}
};
*/
//
// HTML5
//
ko.bindingHandlers.contentEditable = {
init: function (element, valueAccessor) {
var value = valueAccessor();
element.setAttribute('contentEditable', true);
if (ko.isWritableObservable(value)) {
ko.utils.registerEventHandler(element, 'blur', function () {
value(element.innerText);
});
}
},
update: function (element, valueAccessor) {
ko.utils.setTextContent(element, ko.unwrap(valueAccessor()));
}
};
//
// Debugging
//
ko.bindingHandlers.consoleLog = {
init: function (element, valueAccessor, allBindingsAccessor) {
var value = valueAccessor(),
bindings = allBindingsAccessor();
if (!bindings.hasOwnProperty('unwrap') || bindings.unwrap) {
value = ko.unwrap(value);
}
console.log(value);
}
};
ko.bindingHandlers.consoleDebug = {
init: function (element, valueAccessor, allBindingsAccessor) {
var value = valueAccessor(),
bindings = allBindingsAccessor();
if (!bindings.hasOwnProperty('unwrap') || bindings.unwrap) {
value = ko.unwrap(value);
}
console.debug(value);
}
};
ko.bindingHandlers.consoleDir = {
init: function (element, valueAccessor, allBindingsAccessor) {
var value = valueAccessor(),
bindings = allBindingsAccessor();
if (!bindings.hasOwnProperty('unwrap') || bindings.unwrap) {
value = ko.unwrap(value);
}
console.dir(value);
}
};
ko.bindingHandlers.globalize = {
init: function (element, valueAccessor, allBindingsAccessor, viewModel) {
var value = valueAccessor(),
bindings = allBindingsAccessor();
if (!bindings.hasOwnProperty('unwrap') || bindings.unwrap) {
value = ko.unwrap(value);
}
window.koElement = element;
window.koValue = value;
window.koBindings = bindings;
window.koViewModel = viewModel;
}
};
//
// Vendor Dependent
//
ko.bindingHandlers.markdown = {
init: function () { return { controlsDescendantBindings: true }; },
update: function (element, valueAccessor) {
ko.utils.setHtml(element, markdownit().render(ko.unwrap(valueAccessor())));
}
};
ko.bindingHandlers.fromNow = {
update: function (element, valueAccessor) {
var fromNow, m, value;
value = ko.unwrap(valueAccessor());
fromNow = (function () {
try {
m = moment(value);
if (m.isValid()) {
return m.fromNow();
} else {
return '';
}
} catch (_error) {
return '';
}
})();
return ko.bindingHandlers.text.update(element, function () { return fromNow; });
}
};
ko.bindingHandlers.blockUi = {
update: function (element, valueAccessor, allBindingsAccessor) {
var bindings = allBindingsAccessor(),
message = bindings.message || null;
//$.blockUI.defaults.css = {};
if (ko.unwrap(valueAccessor())) {
$(element).block({
message: message,
overlayCSS: {
opacity: 0.25
}
});
} else {
$(element).unblock();
}
}
};
// FX
ko.bindingHandlers.textFadeIn = {
update: function (element, valueAccessor) {
var $el = $(element);
$el.hide();
ko.bindingHandlers.text.update(element, valueAccessor);
$el.fadeIn('slow');
}
};
// Simple Select2
// Basic usage: <select data-bind="value: myValue, options: myOptions, select2"></select>
ko.bindingHandlers.select2 = {
init: function (element, valueAccessor, allBindingsAccessor) {
var $el = $(element),
options = valueAccessor() || {},
bindings = allBindingsAccessor() || {};
$el.select2(options);
if (bindings.value && ko.isSubscribable(bindings.value)) {
bindings.value.subscribe(function () {
$el.trigger('change');
});
}
ko.utils.domNodeDisposal.addDisposeCallback(element, function () {
$el.select2('destroy');
});
}
};
function getSharedMixin(parentMixin) {
return {
alias: function (observable) {
var computed = ko.pureComputed({
read: function () { return observable(); },
write: function (value) { observable(value); return this; }
}, this);
return ko.utils.extend(computed, parentMixin);
},
empty: function () {
var computed = ko.pureComputed(function () {
return ko.utils.isEmpty(this());
}, this);
return ko.utils.extend(computed, parentMixin);
},
notEmpty: function () {
var computed = ko.pureComputed(function () {
return !ko.utils.isEmpty(this());
}, this);
return ko.utils.extend(computed, parentMixin);
}
};
}
var observableMixin = {
oneWay: function (observable) {
var reader = ko.observable(observable),
computed = ko.pureComputed({
read: function () { return reader()(); },
write: function (value) { this(value); reader(this); return this; }
}, this);
return ko.utils.extend(computed, observableMixin);
},
and: function () {
var computed = ko.pureComputed(function () {
if (!this()) return false;
for (var i in arguments) {
if (!ko.unwrap(arguments[i])) {
return false;
}
}
return true;
}, this);
return ko.utils.extend(computed, observableMixin);
},
or: function () {
var computed = ko.pureComputed(function () {
if (this()) return true;
for (var i in arguments) {
if (ko.unwrap(arguments[i])) {
return true;
}
}
return false;
}, this);
return ko.utils.extend(computed, observableMixin);
},
bool: function () {
var computed = ko.pureComputed(function () {
return !!this();
}, this);
return ko.utils.extend(computed, observableMixin);
},
not: function () {
var computed = ko.pureComputed(function () {
return !this();
}, this);
return ko.utils.extend(computed, observableMixin);
},
none: function () {
var computed = ko.pureComputed(function () {
return this() == null;
}, this);
return ko.utils.extend(computed, observableMixin);
},
equal: function (value) {
var computed = ko.pureComputed(function () {
return this() === ko.unwrap(value);
}, this);
return ko.utils.extend(computed, observableMixin);
},
gt: function (value) {
var computed = ko.pureComputed(function () {
return this() > ko.unwrap(value);
}, this);
return ko.utils.extend(computed, observableMixin);
},
gte: function (value) {
var computed = ko.pureComputed(function () {
return this() >= ko.unwrap(value);
}, this);
return ko.utils.extend(computed, observableMixin);
},
lt: function (value) {
var computed = ko.pureComputed(function () {
return this() < ko.unwrap(value);
}, this);
return ko.utils.extend(computed, observableMixin);
},
lte: function (value) {
var computed = ko.pureComputed(function () {
return this() <= ko.unwrap(value);
}, this);
return ko.utils.extend(computed, observableMixin);
},
add: function (amount) {
var computed = ko.pureComputed(function () {
return this() + ko.observableNumber.cast(ko.unwrap(amount));
}, this);
return ko.utils.extend(computed, observableMixin);
},
subtract: function (amount) {
var computed = ko.pureComputed(function () {
return this() - ko.observableNumber.cast(ko.unwrap(amount));
}, this);
return ko.utils.extend(computed, observableMixin);
},
match: function (regex) {
var computed = ko.pureComputed(function () {
return ko.unwrap(regex).test(this());
}, this);
return ko.utils.extend(computed, observableMixin);
},
ceil: function (divisor) {
var computed = ko.pureComputed(function () {
return Math.ceil(this() / ko.unwrap(divisor));
}, this);
return ko.utils.extend(computed, observableMixin);
},
floor: function (divisor) {
var computed = ko.pureComputed(function () {
return Math.floor(this() / ko.unwrap(divisor));
}, this);
return ko.utils.extend(computed, observableMixin);
},
get: function (property, defaultValue) {
var unwrap = ko.unwrap,
isNone = ko.utils.isNone,
computed = ko.pureComputed(function () {
var value = this();
if (isNone(value)) { return defaultValue; }
var parts = ko.observableString.cast(property).split('.'),
next;
while (parts.length) {
next = parts.shift();
if (typeof value !== 'object' || !value.hasOwnProperty(next)) {
return defaultValue;
}
value = unwrap(value[next]);
if (isNone(value)) { return defaultValue; }
}
return value;
}, this);
return ko.utils.extend(computed, observableMixin);
}
};
ko.utils.extend(observableMixin, getSharedMixin(observableMixin));
ko.utils.extend(ko.observable.fn, observableMixin);
// requires knockout-projections.js
var observableArrayMixin = {
filterBy: function (property, value) {
if (arguments.length === 1) {
value = true;
}
var computed = this.filter(function (item) {
return ko.unwrap(item[property]) === ko.unwrap(value);
});
return ko.utils.extend(computed, observableArrayMixin);
},
mapBy: function (property) {
var computed = this.map(function (item) { return ko.unwrap(item[property]); });
return ko.utils.extend(computed, observableArrayMixin);
},
paged: function (offset, limit) {
var computed = ko.pureComputed(function () {
var offsetVal = ko.unwrap(offset),
limitVal = ko.unwrap(limit);
return this().slice(offsetVal * limitVal, offsetVal * limitVal + limitVal);
}, this);
return ko.utils.extend(computed, observableArrayMixin);
},
count: function () {
var computed = ko.pureComputed(function () {
return this().length;
}, this);
return ko.utils.extend(computed, observableMixin);
},
contains: function (value) {
var computed = ko.pureComputed(function () {
return ko.utils.arrayIndexOf(this(), ko.unwrap(value)) >= 0;
}, this);
return ko.utils.extend(computed, observableMixin);
},
uniq: function () {
var computed = ko.pureComputed(function () {
return ko.utils.arrayGetDistinctValues(this());
}, this);
return ko.utils.extend(computed, observableArrayMixin);
},
intersect: function (other) {
var computed = ko.pureComputed(function () {
var args = [this()];
ko.utils.arrayPushAll(args, ko.utils.arrayMap(ko.utils.args(arguments), ko.unwrap));
return ko.utils.arrayIntersect.apply(null, args);
}, this);
return ko.utils.extend(computed, observableArrayMixin);
},
// http://stackoverflow.com/questions/12166982
subscribeArrayChanged: function (onAdded, onDeleted) {
var previousValue;
this.subscribe(function (oldValue) {
previousValue = oldValue.slice(0); // store a clone of the array
}, null, 'beforeChange');
this.subscribe(function (newValue) {
var edits = ko.utils.compareArrays(previousValue, newValue);
for (var i = 0, j = edits.length; i < j; ++i) {
var edit = edits[i];
switch (edit.status) {
case 'retained': break;
case 'deleted': if (onDeleted) { onDeleted(edit.value, edit.index); } break;
case 'added': if (onAdded) { onAdded(edit.value, edit.index); } break;
}
}
previousValue = void(0);
});
}
};
ko.utils.extend(observableArrayMixin, getSharedMixin(observableArrayMixin));
ko.utils.extend(ko.observableArray.fn, observableArrayMixin);
ko.extenders.min = function (target, min) {
return ko.pureComputed({
read: target,
write: function (value) {
var current = target();
value = Math.max(value, ko.unwrap(min));
if (value !== current) {
target(value);
}
}
});
};
ko.extenders.max = function (target, max) {
return ko.pureComputed({
read: target,
write: function (value) {
var current = target();
value = Math.min(value, ko.unwrap(max));
if (value !== current) {
target(value);
}
}
});
};
ko.utils.args = function (args, sliceAt) {
return Array.prototype.slice.call(args, 1 === arguments.length ? 0 : ~~sliceAt);
};
ko.utils.getType = function (obj) {
switch (true) {
case ko.utils.isArray(obj): return 'array';
case ko.utils.isFunction(obj): return 'function';
case ko.utils.isObject(obj): return 'object';
default: return gettype(ko.unwrap(obj));
}
};
// See https://github.com/kvz/locutus/blob/master/src/php/var/is_array.js
ko.utils.isArray = function (obj) {
obj = ko.unwrap(obj);
return !!obj && typeof obj === 'object' && typeof obj.length === 'number' && Object.prototype.toString.call(obj) === '[object Array]';
/* This seems pretty intense, but if you need to be absolutely sure and without a doubt, I guess it would be the only way.
var len = obj.length;
obj[obj.length] = 'bogus';
if (len !== obj.length) {
obj.length -= 1;
return true;
}
delete obj[obj.length];
return false;
*/
};
// See https://github.com/kvz/locutus/blob/master/src/php/var/is_object.js
ko.utils.isObject = function (obj) {
if (ko.utils.isArray(obj)) {
return false
}
obj = ko.unwrap(obj);
return obj !== null && typeof obj === 'object'
};
// See https://github.com/lodash/lodash => _.isFunction()
ko.utils.isFunction = function (obj) {
obj = ko.unwrap(obj);
if ('function' === typeof obj) {
return true;
}
var tag = ko.utils.isObject(obj) ? Object.prototype.toString.call(obj) : '';
return '[object Function]' === tag || '[object GeneratorFunction]' === tag;
};
ko.utils.isNone = function (obj) {
var undef;
obj = ko.unwrap(obj);
return (null === obj || undef === obj);
};
ko.utils.isEmptyObject = function (obj) {
var name;
for (name in ko.unwrap(obj)) {
return false;
}
return true;
};
ko.utils.isEmpty = function (obj) {
obj = ko.unwrap(obj);
if (ko.utils.isNone(obj) || '' === obj) {
return true;
}
if (ko.utils.isArray(obj)) {
return 0 === obj.length;
}
if (ko.utils.isObject(obj)) {
return ko.utils.isEmptyObject(obj);
}
return false;
};
ko.utils.arrayIntersect = function () {
var args = ko.utils.args(arguments),
intersect = function (a, b) {
var result = [];
a = a.slice(0).sort();
b = b.slice(0).sort();
while (a.length > 0 && b.length > 0) {
if (a[0] < b[0]) { a.shift(); }
else if (a[0] > b[0]) { b.shift(); }
else { /* they're equal */
result.push(a.shift());
b.shift();
}
}
return result;
},
a = args.shift() || [],
b = args.shift() || [],
result = intersect(a, b);
while (args.length > 0) {
result = intersect(result, args.shift() || []);
}
return result;
};
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment