can
Version:
MIT-licensed, client-side, JavaScript framework that makes building rich web applications easy.
259 lines (231 loc) • 8.49 kB
JavaScript
steal('can/view', './elements', 'can/view/live', 'can/util/string', function (can, elements, live) {
/**
* Helper(s)
*/
var pendingHookups = [],
tagChildren = function (tagName) {
var newTag = elements.tagMap[tagName] || "span";
if (newTag === "span") {
//innerHTML in IE doesn't honor leading whitespace after empty elements
return "@@!!@@";
}
return "<" + newTag + ">" + tagChildren(newTag) + "</" + newTag + ">";
},
contentText = function (input, tag) {
// If it's a string, return.
if (typeof input === 'string') {
return input;
}
// If has no value, return an empty string.
if (!input && input !== 0) {
return '';
}
// If it's an object, and it has a hookup method.
var hook = (input.hookup &&
// Make a function call the hookup method.
function (el, id) {
input.hookup.call(input, el, id);
}) ||
// Or if it's a `function`, just use the input.
(typeof input === 'function' && input);
// Finally, if there is a `function` to hookup on some dom,
// add it to pending hookups.
if (hook) {
if (tag) {
return "<" + tag + " " + can.view.hook(hook) + "></" + tag + ">";
} else {
pendingHookups.push(hook);
}
return '';
}
// Finally, if all else is `false`, `toString()` it.
return "" + input;
},
// Returns escaped/sanatized content for anything other than a live-binding
contentEscape = function (txt, tag) {
return (typeof txt === 'string' || typeof txt === 'number') ?
can.esc(txt) :
contentText(txt, tag);
},
// A flag to indicate if .txt was called within a live section within an element like the {{name}}
// within `<div {{#person}}{{name}}{{/person}}/>`.
withinTemplatedSectionWithinAnElement = false,
emptyHandler = function () {};
var lastHookups;
can.extend(can.view, {
live: live,
// called in text to make a temporary
// can.view.lists function that can be called with
// the list to iterate over and the template
// used to produce the content within the list
setupLists: function () {
var old = can.view.lists,
data;
can.view.lists = function (list, renderer) {
data = {
list: list,
renderer: renderer
};
return Math.random();
};
// sets back to the old data
return function () {
can.view.lists = old;
return data;
};
},
getHooks: function () {
var hooks = pendingHookups.slice(0);
lastHookups = hooks;
pendingHookups = [];
return hooks;
},
onlytxt: function (self, func) {
return contentEscape(func.call(self));
},
/**
* @function can.view.txt
* @hide
*
* A helper function used to insert the
* value of the contents of a magic tag into
* a template's output. It detects if an observable value is
* read and will setup live binding.
*
* @signature `can.view.txt(escape, tagName, status, self, func)`
*
* @param {Number} 1 if the content returned should be escaped, 0 if otherwise.
* @param {String} tagName the name of the tag the magic tag is most immediately
* within. Ex: `"li"`.
* @param {String|Number} status A flag indicates which part of a tag the
* magic tag is within. Status can be:
*
* - _STRING_ - The name of the attribute the magic tag is within. Ex: `"class"`
* - `1` - The magic tag is within a tag like `<div <%= %>>`
* - `0` - The magic tag is outside (or between) tags like `<div><%= %></div>`
*
* @param {*} self The `this` of the current context template. `func` is called with
* self as this.
*
* @param {function} func The "wrapping" function. For
* example: `<%= task.attr('name') %>` becomes
* `(function(){return task.attr('name')})
*
*/
txt: function (escape, tagName, status, self, func) {
// the temporary tag needed for any live setup
var tag = (elements.tagMap[tagName] || "span"),
// should live-binding be setup
setupLiveBinding = false,
// the compute's value
value,
listData,
compute,
unbind = emptyHandler,
attributeName;
// Are we currently within a live section within an element like the {{name}}
// within `<div {{#person}}{{name}}{{/person}}/>`.
if (withinTemplatedSectionWithinAnElement) {
value = func.call(self);
} else {
// If this magic tag is within an attribute or an html element,
// set the flag to true so we avoid trying to live bind
// anything that func might be setup.
// TODO: the scanner should be able to set this up.
if (typeof status === "string" || status === 1) {
withinTemplatedSectionWithinAnElement = true;
}
// Sets up a listener so we know any can.view.lists called
// when func is called
var listTeardown = can.view.setupLists();
//
unbind = function () {
compute.unbind("change", emptyHandler);
};
// Create a compute that calls func and looks for dependencies.
// By passing `false`, this compute can not be a dependency of other
// computes. This is because live-bits are nested, but
// handle their own updating. For example:
// {{#if items.length}}{{#items}}{{.}}{{/items}}{{/if}}
// We do not want `{{#if items.length}}` changing the DOM if
// `{{#items}}` text changes.
compute = can.compute(func, self, false);
// Bind to get and temporarily cache the value of the compute.
compute.bind("change", emptyHandler);
// Call the "wrapping" function and get the binding information
listData = listTeardown();
// Get the value of the compute
value = compute();
// Let people know we are no longer within an element.
withinTemplatedSectionWithinAnElement = false;
// If we should setup live-binding.
setupLiveBinding = compute.computeInstance.hasDependencies;
}
if (listData) {
unbind();
return "<" + tag + can.view.hook(function (el, parentNode) {
live.list(el, listData.list, listData.renderer, self, parentNode);
}) + "></" + tag + ">";
}
// If we had no observes just return the value returned by func.
if (!setupLiveBinding || typeof value === "function") {
unbind();
return ((withinTemplatedSectionWithinAnElement || escape === 2 || !escape) ?
contentText :
contentEscape)(value, status === 0 && tag);
}
// the property (instead of innerHTML elements) to adjust. For
// example options should use textContent
var contentProp = elements.tagToContentPropMap[tagName];
// The magic tag is outside or between tags.
if (status === 0 && !contentProp) {
var selfClosing = !!elements.selfClosingTags[tag];
// Return an element tag with a hookup in place of the content
return "<" + tag + can.view.hook(
// if value is an object, it's likely something returned by .safeString
escape && typeof value !== "object" ?
// If we are escaping, replace the parentNode with
// a text node who's value is `func`'s return value.
function (el, parentNode) {
live.text(el, compute, parentNode);
unbind();
} :
// If we are not escaping, replace the parentNode with a
// documentFragment created as with `func`'s return value.
function (el, parentNode) {
live.html(el, compute, parentNode);
unbind();
//children have to be properly nested HTML for buildFragment to work properly
}) + ( selfClosing ? "/>" : ">" + tagChildren(tag) + "</" + tag + ">" );
// In a tag, but not in an attribute
} else if (status === 1) {
// remember the old attr name
pendingHookups.push(function (el) {
live.attributes(el, compute, compute());
unbind();
});
return compute();
} else if (escape === 2) { // In a special attribute like src or style
attributeName = status;
pendingHookups.push(function (el) {
live.specialAttribute(el, attributeName, compute);
unbind();
});
return compute();
} else { // In an attribute...
attributeName = status === 0 ? contentProp : status;
// if the magic tag is inside the element, like `<option><% TAG %></option>`,
// we add this hookup to the last element (ex: `option`'s) hookups.
// Otherwise, the magic tag is in an attribute, just add to the current element's
// hookups.
(status === 0 ? lastHookups : pendingHookups)
.push(function (el) {
live.attribute(el, attributeName, compute);
unbind();
});
return live.attributePlaceholder;
}
}
});
return can;
});