Last active
August 29, 2015 13:57
-
-
Save miguelludert/9533997 to your computer and use it in GitHub Desktop.
Rockout (for knockout)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
var utils = { | |
isValid: function (toValidate, deep, show) { | |
if (deep === undefined) { | |
deep = true; | |
} | |
var deepValid = true; | |
if (deep) { | |
var validateThis = ko.utils.unwrapObservable(toValidate); | |
var childrenToTest = _(validateThis).where(function (item) { | |
return ko.isObservable(item) || _.isObject(item); | |
}); | |
if (show) { | |
// loop through each child explictly, allowing each child to display its error messages | |
childrenToTest.each(function (item) { | |
var isValidChild = ko.validation.utils.isValid(item, deep, true); | |
if (!isValidChild) { | |
deepValid = false; | |
} | |
}); | |
} else { | |
// if show is false, we do not care about displaying error messages, leave after the first invalid item is found | |
deepValid = childrenToTest.all(function (item) { | |
return ko.validation.utils.isValid(item, deep, false); | |
}); | |
} | |
} | |
if (show & ko.isObservable(toValidate) && _.isFunction(toValidate.isValid)) { | |
toValidate.isModified(true); | |
} | |
return deepValid && (_.isFunction(toValidate.isValid) ? toValidate.isValid() : true); | |
}, | |
validate: function (toValidate, deep) { | |
return ko.validation.utils.isValid(toValidate, deep, true); | |
} | |
}; | |
_.extend(ko.validation.utils, utils); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Copyright Miguel Ludert 2014 | |
// MIT License - http://opensource.org/licenses/MIT | |
// https://gist.github.com/thinkingsites/9533997 | |
define(["knockout", "lodash"], function (ko, _) { | |
var isString = _.isString; | |
function round(newValueAsNum, precision) { | |
if (isNaN(newValueAsNum) || isNaN(precision)) { | |
return newValueAsNum; | |
} else { | |
var roundingMultiplier = Math.pow(10, precision); | |
return Math.round(newValueAsNum * roundingMultiplier) / roundingMultiplier; | |
} | |
} | |
var presets = { | |
'int' : function(val){ | |
return presets['number'](val, { precision: 0 }); | |
}, | |
'number': function (val, options) { | |
var precision = options && options.precision ? options.precision : undefined; | |
val = parseFloat(val); | |
val = _.isNaN(val) ? undefined : val; | |
val = round(val, precision); | |
return val; | |
}, | |
'date' : function (val) { | |
if (!_.isDate(val)) { | |
if (isString(val) || _.isNumber(val)) { | |
// take into account those messed up MS dates | |
if(isString(val) && val.match(/Date\(-?\d+\)/)){ | |
// strip out the milliseconds and set the date | |
val = val.match(/-?\d+/); | |
} | |
val = new Date(val); | |
} else { | |
val = undefined; | |
} | |
} | |
return val; | |
}, | |
'bool': function (val) { | |
return (val ? true : false); | |
} | |
}; | |
ko.extenders.castAs = function (target, options) { | |
var callback; | |
if (_.isFunction(options)) { | |
// if the options is not a direct fu | |
callback = options; | |
} else if (isString(options)) { | |
// if the options is a string preset, look for it | |
if (_.has(presets, options)) { | |
callback = presets[options]; | |
} else { | |
throw "Cannot find casting preset " + options; | |
} | |
} else if(_.isPlainObject(options)) { | |
// if the options are an object check to see if we're getting a callback or a constructor | |
if(_.has(options,'callback')){ | |
//strip out the call back and pass the options into the callback on execute | |
callback = options['callback']; | |
} else if(_.has(options,'constructor')) { | |
// if the options have a constructor create an constructor callback | |
callback = function (val) { | |
if (_.isUndefined(val)) { | |
return undefined; | |
} else { | |
return new this(val); | |
} | |
}.bind(options.constructor); // do a bind so we don't lose the constructor later | |
} | |
//omit the callback and constructor properties from the options, we don't want to pass them into the callback once it executes | |
options = _.omit(options,'callback','constructor'); | |
} else { | |
// don't know what to do with these options, throw an error | |
throw "Cannot use options passed into castAs extender: " + options; | |
} | |
var result = ko.computed({ | |
read: target, //always return the original observables value | |
write: function (newValue) { | |
var current = target(), | |
valueToWrite = callback(newValue, options); | |
//only write if it changed | |
if (valueToWrite !== current) { | |
target(valueToWrite); | |
} else { | |
//if the rounded value is the same, but a different value was written, force a notification for the current field | |
if (newValue !== current) { | |
target.notifySubscribers(valueToWrite); | |
} | |
} | |
} | |
}).extend({ notify: 'always' }); | |
//initialize with current value to make sure it is rounded appropriately | |
result(target()); | |
//return the new computed observable | |
return result; | |
}; | |
}); | |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Copyright Miguel Ludert 2014 | |
// MIT License - http://opensource.org/licenses/MIT | |
// https://gist.github.com/thinkingsites/9533997 | |
"use strict"; | |
define(["knockout", "jquery", "lodash"], function (ko, $, _) { | |
var | |
defaultOptions = { | |
jQueryStyles: true, | |
showOverlay: true, | |
draggable: false, | |
resizable : false, | |
modalId: "rockout-modal-window", | |
parent: "body", | |
overlayId: "rockout-modal-overlay", | |
style: { | |
minHeight: "150px", | |
minWidth: "200px" | |
} | |
}, | |
jqClasses = { | |
contentClass: '.ui-dialog-content', | |
closeClass: '.ui-dialog-titlebar-close', | |
titleClass : '.ui-dialog-titlebar' | |
}, | |
rockoutClasses = { | |
contentClass: '.rockout-ui-modal-content', | |
closeClass : '.rockout-ui-modal-close', | |
titleClass: '.rockout-ui-modal-header' | |
}, | |
currentModal; | |
function rockoutTempate(title) { | |
return [ | |
"<div class='rockout-ui-modal-header'>", | |
"<span class='rockout-ui-modal-header-text'>", | |
title, | |
"</span>", | |
"<button class='rockout-ui-modal-close'>Close</button>", | |
"</div>", | |
"<div class='rockout-ui-modal-content'></div>", | |
].join(''); | |
} | |
function jqueryTemplate(title) { | |
return [ | |
// the dialog's absolute position must be overridden to static to work with our positioning | |
'<div class="ui-dialog ui-widget ui-widget-content ui-corner-all ui-front" tabindex="-1" role="dialog" aria-describedby="dialog" style="position:static">', | |
'<div class="ui-dialog-titlebar ui-widget-header ui-corner-all ui-helper-clearfix">', | |
'<span class="ui-dialog-title">', | |
title, | |
'</span>', | |
'<button type="button" class="ui-button ui-widget ui-state-default ui-corner-all ui-button-icon-only ui-dialog-titlebar-close" role="button" aria-disabled="false" title="close">', | |
'<span class="ui-button-icon-primary ui-icon ui-icon-closethick"></span>', | |
'<span class="ui-button-text">close</span>', | |
'</button>', | |
'</div>', | |
'<div class="ui-dialog-content ui-widget-content">', | |
'</div>', | |
'</div' | |
].join(''); | |
} | |
function ensureElement(id, appendTo) { | |
var element = $("#" + id); | |
if (!element.length) { | |
element = $("<div></div>") | |
.attr("id", id) | |
.appendTo(appendTo); | |
} | |
return element; | |
}; | |
function resizeModal() { | |
// make sure we are using jquery | |
var $this = $(this); | |
// setting margins has to happen in a different call to allow the prior css to set properly | |
$this.css({ | |
top: "50%", | |
left: "50%", | |
marginTop: "-" + ($this.outerHeight() / 2).toString() + "px", | |
marginLeft: "-" + ($this.outerWidth() / 2).toString() + "px", | |
}); | |
} | |
function closeModal() { | |
if (currentModal) { | |
currentModal.modal.hide(); | |
currentModal.overlay.hide(); | |
currentModal = undefined; | |
} | |
}; | |
ko.bindingHandlers.modal = { | |
init: function (element, valueAccessor, allBindings) { | |
$(element).on("click", function () { | |
var bindings = allBindings() || {}, | |
options = _.extend({}, defaultOptions, bindings.modalOptions), | |
modal = ensureElement(bindings.modalId || "rockout-modal-window", options.parent), | |
overlay = ensureElement(bindings.modalId || "rockout-modal-overlay", options.parent), | |
classes = options.jQueryStyles ? jqClasses : rockoutClasses, | |
templateFunction = options.jQueryStyles ? jqueryTemplate : rockoutTempate, | |
modalContent; | |
if (options.showOverlay) { | |
// set up overlay | |
overlay.css({ | |
top: 0, | |
bottom: 0, | |
left: 0, | |
right: 0, | |
position: "fixed", | |
zIndex: 999999, // six digits | |
backgroundColor: "rgba(0,0,0,0.6)", | |
display: "block" | |
}); | |
} | |
// set up the modal window, use html to clean the content and reset the modal | |
modal.html(templateFunction(options.title || " ")).css(_.extend({ | |
position: "fixed", | |
zIndex: 9999999, // seven digits | |
display: "block", | |
}, options.style)); | |
// resize the current modal after initial setup | |
resizeModal.call(modal); | |
// set the current modal so we can close them easily | |
currentModal = { | |
modal: modal, | |
overlay : overlay | |
}; | |
var modalBindings = valueAccessor(); | |
if (_.isString(modalBindings)) { | |
// of the value is a string, convert it to an object with template bindings to access the afterRender event | |
modalBindings = ({ | |
template: function(){ | |
return this; | |
}.bind({ | |
name: modalBindings, | |
afterRender : resizeModal.bind(modal) | |
}) | |
}) | |
} else { | |
// if the new bindings contain a template of a module, append the after render | |
if (_.has(modalBindings, "template") || _.has(modalBindings, "module")) { | |
var key = _.has(modalBindings, "template") ? "template" : "module"; | |
// if template is a string then turn it into an object | |
if (_.isString(modalBindings[key])) { | |
modalBindings[key] = { | |
name: modalBindings[key], | |
afterRender : resizeModal.bind(modal) | |
}; | |
} else { | |
// if the modal is an object retain the original afterRender and inject your own call | |
var originalAfterRender = modalBindings[key].afterRender; | |
modalBindings[key].afterRender = function (elements) { | |
if(_.isFunction(originalAfterRender)){ | |
originalAfterRender(elements); | |
} | |
resizeModal.call(modal); | |
} | |
} | |
} | |
// to use applyBindingAccessorsToNode, all bindings must be wrapped in a function if not already a function | |
_.each(modalBindings, function (item, key) { | |
if(!_.isFunction(item)){ | |
modalBindings[key] = function () { | |
// there is a strange bug with the bind function that changes the native string into a string object and breaks templates and modules | |
return _.isString(this) ? this.toString() : this; | |
}.bind(item); | |
} | |
}); | |
} | |
// if the modal is to be draggable, set it up here | |
if (options.draggable) { | |
modal.draggable({ | |
handle: classes.titleClass | |
}); | |
} | |
// if the modal is to be resizable, set it up here | |
if (options.resizable) { | |
modal.resizable(); | |
} | |
// grab the modal content so we can bind the content to it | |
modalContent = $(classes.contentClass, modal); | |
// toggle display the close modal button and bind the click event | |
$(classes.closeClass, modal).toggle(_.isUndefined(options.closeButton) ? true : false).on("click", function () { | |
ko.rockout.closeModal(); | |
}); | |
// don't forget that because we're using jquery for node construction, we have to access the jquery node collection | |
ko.applyBindingAccessorsToNode(modalContent[0], modalBindings); | |
}); | |
return { controlsDescendantBindings: true }; | |
}, | |
options : defaultOptions | |
}; | |
// if the rockout object doesn't exist | |
ko.rockout = ko.rockout || {}; | |
ko.rockout.closeModal = closeModal; | |
// all rockout modules should return the rockout object | |
return ko.rockout; | |
}); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Copyright Miguel Ludert 2014 | |
// MIT License - http://opensource.org/licenses/MIT | |
// https://gist.github.com/thinkingsites/9533997 | |
"use sctrict"; | |
define(["knockout", "_"], function (ko, _) { | |
// namespacing | |
ko.rockout = ko.rockout || {}; | |
ko.rockout.utils = ko.rockout.utils || {}; | |
function patch(target) { | |
var | |
data = _.extend.apply(undefined, [{}].concat(_.rest(_.toArray(arguments)))), | |
targetKeys = _.keys(target), | |
dataKeys = _.keys(data); | |
_(targetKeys).intersection(dataKeys).where(function (item) { | |
// get the intersection of the observable keys | |
return ko.isWriteableObservable(self[item]); | |
}).each(function (item) { | |
// set the observable | |
target[item](data[item]); | |
}); | |
}; | |
// stand alone method for patching | |
ko.rockout.utils.patch = patch; | |
// adds a "patch" method to the target object that merges all the data from a source object into the observables where the keys match | |
ko.rockout.utils.addPatching = function (target, initial) { | |
var inProgress = ko.observable(false); | |
target.patch = function (data) { | |
inProgress(true); | |
patch(target, data); | |
inProgress(false); | |
}; | |
// patch.pause signals to the consuming function that patching is in progress | |
target.patch.inProgress = inProgress; | |
// if there are initial values, run the patch now | |
if (initial) { | |
target.patch(initial); | |
} | |
}; | |
return ko.rockout; | |
}); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment