infusion
Version:
Infusion is an application framework for developing flexible stuff with JavaScript
358 lines (321 loc) • 13.3 kB
JavaScript
/*
Copyright The Infusion copyright holders
See the AUTHORS.md file at the top-level directory of this distribution and at
https://github.com/fluid-project/infusion/raw/master/AUTHORS.md.
Licensed under the Educational Community License (ECL), Version 2.0 or the New
BSD license. You may not use this file except in compliance with one these
Licenses.
You may obtain a copy of the ECL 2.0 License and BSD License at
https://github.com/fluid-project/infusion/raw/master/Infusion-LICENSE.txt
*/
/** This file contains functions which depend on the presence of a browser-style DOM document
* and which depend on the contents of Fluid.js **/
;
/** Sets the value to the DOM element and triggers the change event on the element.
* Note: when using jQuery val() function to change the node value, the change event would
* not be fired automatically, it requires to be initiated by the user.
*
* @param {jQueryable} node - A selector, a DOM node, or a jQuery instance
* @param {String|Number|Array} value - A string of text, a number, or an array of strings
* corresponding to the value of each matched element to set in the node
*/
fluid.changeElementValue = function (node, value) {
node = $(node);
fluid.value(node, value);
node.change();
};
// TODO: Most of these utilities below are not general purpose, and should either be packaged with individual components
// such as the Reorderer if they are still appropriate, or else abolished once the new renderer is fully rolled out
// DOM Utilities.
// TODO: Replace with element.closest
/**
* Finds the nearest ancestor of the element that matches a predicate
* @param {Element} element - DOM element
* @param {Function} test - A function (predicate) accepting a DOM element, returning a truthy value representing a match
* @return {Element|undefined} - The first element parent for which the predicate returns truthy - or undefined if no parent matches
*/
fluid.findAncestor = function (element, test) {
element = fluid.unwrap(element);
while (element) {
if (test(element)) {
return element;
}
element = element.parentNode;
}
};
fluid.findForm = function (node) {
return fluid.findAncestor(node, function (element) {
return element.nodeName.toLowerCase() === "form";
});
};
/* A utility with the same signature as jQuery.text and jQuery.html, but without the API irregularity
* that treats a single argument of undefined as different to no arguments */
// in jQuery 1.7.1, jQuery pulled the same dumb trick with $.text() that they did with $.val() previously,
// see comment in fluid.value below
fluid.each(["text", "html"], function (method) {
fluid[method] = function (node, newValue) {
node = $(node);
return newValue === undefined ? node[method]() : node[method](newValue);
};
});
fluid.BINDING_ROOT_KEY = "fluid-binding-root";
/* Recursively find any data stored under a given name from a node upwards
* in its DOM hierarchy **/
fluid.findData = function (elem, name) {
while (elem) {
var data = $.data(elem, name);
if (data) {
return data;
}
elem = elem.parentNode;
}
};
fluid.bindFossils = function (node, data, fossils) {
$.data(node, fluid.BINDING_ROOT_KEY, {data: data, fossils: fossils});
};
fluid.boundPathForNode = function (node, fossils) {
node = fluid.unwrap(node);
var key = node.name || node.id;
var record = fossils[key];
return record ? record.EL : null;
};
/* relevant, the changed value received at the given DOM node */
fluid.applyBoundChange = function (node, newValue, applier) {
node = fluid.unwrap(node);
if (newValue === undefined) {
newValue = fluid.value(node);
}
if (node.nodeType === undefined && node.length > 0) {
node = node[0];
} // assume here that they share name and parent
var root = fluid.findData(node, fluid.BINDING_ROOT_KEY);
if (!root) {
fluid.fail("Bound data could not be discovered in any node above " + fluid.dumpEl(node));
}
var name = node.name;
var fossil = root.fossils[name];
if (!fossil) {
fluid.fail("No fossil discovered for name " + name + " in fossil record above " + fluid.dumpEl(node));
}
var EL = root.fossils[name].EL;
if (applier) {
applier.fireChangeRequest({path: EL, value: newValue, source: "DOM:" + node.id});
} else {
fluid.set(root.data, EL, newValue);
}
};
/*
* Returns a jQuery object given the id of a DOM node. In the case the element
* is not found, will return an empty list.
*/
fluid.jById = function (id, dokkument) {
dokkument = dokkument && dokkument.nodeType === 9 ? dokkument : document;
var element = fluid.byId(id, dokkument);
var togo = element ? $(element) : [];
togo.selector = "#" + id;
togo.context = dokkument;
return togo;
};
/**
* Returns an DOM element quickly, given an id
*
* @param {Object} id - The id of the DOM node to find
* @param {Document} [dokkument] - (optional) The document in which it is to be found (if left empty, use the current document)
* @return {Object} - The DOM element with this id, or null, if none exists in the document.
*/
fluid.byId = function (id, dokkument) {
dokkument = dokkument && dokkument.nodeType === 9 ? dokkument : document;
var el = dokkument.getElementById(id);
if (el) {
// Use element id property here rather than attribute, to work around FLUID-3953
if (el.id !== id) {
fluid.fail("Problem in document structure - picked up element " +
fluid.dumpEl(el) + " for id " + id +
" without this id - most likely the element has a name which conflicts with this id");
}
return el;
} else {
return null;
}
};
/**
* Returns the document to which an element belongs, or the element itself if it is already a document
*
* @param {jQuery|Element} element - The element to return the document for
* @return {Document} - The document in which it is to be found
*/
fluid.getDocument = function (element) {
var node = fluid.unwrap(element);
// DOCUMENT_NODE - guide to node types at https://developer.mozilla.org/en/docs/Web/API/Node/nodeType
return node.nodeType === 9 ? node : node.ownerDocument;
};
// END DOM UTILITIES which are probably not general purpose
/** Fluid ARIA Labeller **/
fluid.defaults("fluid.ariaLabeller", {
gradeNames: ["fluid.viewComponent"],
labelAttribute: "aria-label",
liveRegionMarkup: "<div class=\"liveRegion fl-hidden-accessible\" aria-live=\"polite\"></div>",
liveRegionId: "fluid-ariaLabeller-liveRegion",
invokers: {
generateLiveElement: {
funcName: "fluid.ariaLabeller.generateLiveElement",
args: "{that}"
},
update: {
funcName: "fluid.ariaLabeller.update",
args: ["{that}", "{arguments}.0"]
}
},
listeners: {
onCreate: {
func: "{that}.update",
args: [null]
}
}
});
fluid.ariaLabeller.update = function (that, newOptions) {
newOptions = newOptions || that.options;
that.container.attr(that.options.labelAttribute, newOptions.text);
if (newOptions.dynamicLabel) {
var live = fluid.jById(that.options.liveRegionId);
if (live.length === 0) {
live = that.generateLiveElement();
}
live.text(newOptions.text);
}
};
fluid.ariaLabeller.generateLiveElement = function (that) {
var liveEl = $(that.options.liveRegionMarkup);
liveEl.prop("id", that.options.liveRegionId);
$("body").append(liveEl);
return liveEl;
};
var LABEL_KEY = "aria-labelling";
fluid.getAriaLabeller = function (element) {
element = $(element);
var that = fluid.getScopedData(element, LABEL_KEY);
return that;
};
/* Manages an ARIA-mediated label attached to a given DOM element. An
* aria-labelledby attribute and target node is fabricated in the document
* if they do not exist already, and a view component is returned exposing a method
* "update" that allows the text to be updated. */
fluid.updateAriaLabel = function (element, text, options) {
options = $.extend({}, options || {}, {text: text});
var that = fluid.getAriaLabeller(element);
if (!that) {
that = fluid.ariaLabeller(element, options);
fluid.setScopedData(element, LABEL_KEY, that);
} else {
that.update(options);
}
return that;
};
/* "Global Dismissal Handler" for the entire page. Attaches a click handler to the
* document root that will cause dismissal of any elements (typically dialogs) which
* have registered themselves. Dismissal through this route will automatically clean up
* the record - however, the dismisser themselves must take care to deregister in the case
* dismissal is triggered through the dialog interface itself. This component can also be
* automatically configured by fluid.deadMansBlur by means of the "cancelByDefault" option */
fluid.dismissList = {};
$(document).click(function (event) {
var target = fluid.resolveEventTarget(event);
while (target) {
if (fluid.dismissList[target.id]) {
return;
}
target = target.parentNode;
}
fluid.each(fluid.dismissList, function (dismissFunc, key) {
dismissFunc(event);
delete fluid.dismissList[key];
});
});
// TODO: extend a configurable equivalent of the above dealing with "focusin" events
/* Accepts a free hash of nodes and an optional "dismissal function".
* If dismissFunc is set, this "arms" the dismissal system, such that when a click
* is received OUTSIDE any of the hierarchy covered by "nodes", the dismissal function
* will be executed.
*/
fluid.globalDismissal = function (nodes, dismissFunc) {
fluid.each(nodes, function (node) {
// Don't bother to use the real id if it is from a foreign document - we will never receive events
// from it directly in any case - and foreign documents may be under the control of malign fiends
// such as tinyMCE who allocate the same id to everything
var id = fluid.unwrap(node).ownerDocument === document ? fluid.allocateSimpleId(node) : fluid.allocateGuid();
if (dismissFunc) {
fluid.dismissList[id] = dismissFunc;
}
else {
delete fluid.dismissList[id];
}
});
};
/* Sets an interation on a target control, which manages a blur interaction for a possibly composite region.
*
* A timed blur listener is set on the control, which waits for a short period of
* time (options.delay, defaults to 150ms) to discover whether the reason for the
* blur interaction is that either a focus or click is being serviced on a nominated
* set of "exclusions" (options.exclusions, a free hash of elements or jQueries).
* If no such event is received within the window, options.handler will be called
* with the argument "control", to service whatever interaction is required of the
* blur.
*/
fluid.deadMansBlur = function (control, options) {
// TODO: This should be rewritten as a proper component
var that = {options: $.extend(true, {}, fluid.defaults("fluid.deadMansBlur"), options)};
that.blurPending = false;
that.lastCancel = 0;
that.canceller = function (event) {
fluid.log("Cancellation through " + event.type + " on " + fluid.dumpEl(event.target));
that.lastCancel = Date.now();
that.blurPending = false;
};
that.noteProceeded = function () {
fluid.globalDismissal(that.options.exclusions);
};
that.reArm = function () {
fluid.globalDismissal(that.options.exclusions, that.proceed);
};
that.addExclusion = function (exclusions) {
fluid.globalDismissal(exclusions, that.proceed);
};
that.proceed = function (event) {
fluid.log("Direct proceed through " + event.type + " on " + fluid.dumpEl(event.target));
that.blurPending = false;
that.options.handler(control);
};
fluid.each(that.options.exclusions, function (exclusion) {
exclusion = $(exclusion);
fluid.each(exclusion, function (excludeEl) {
$(excludeEl).on("focusin", that.canceller).
on("fluid-focus", that.canceller).
click(that.canceller).mousedown(that.canceller);
// Mousedown is added for FLUID-4212, as a result of Chrome bug 6759, 14204
});
});
if (!that.options.cancelByDefault) {
$(control).on("focusout", function (event) {
fluid.log("Starting blur timer for element " + fluid.dumpEl(event.target));
var now = Date.now();
fluid.log("back delay: " + (now - that.lastCancel));
if (now - that.lastCancel > that.options.backDelay) {
that.blurPending = true;
}
setTimeout(function () {
if (that.blurPending) {
that.options.handler(control);
}
}, that.options.delay);
});
}
else {
that.reArm();
}
return that;
};
fluid.defaults("fluid.deadMansBlur", {
gradeNames: "fluid.function",
delay: 150,
backDelay: 100
});