infusion
Version:
Infusion is an application framework for developing flexible stuff with JavaScript
439 lines (398 loc) • 17.7 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
*/
var fluid_3_0_0 = fluid_3_0_0 || {};
(function ($, fluid) {
"use strict";
// cf. ancient SVN-era version in bitbucket at https://bitbucket.org/fluid/infusion/src/adf319d9b279/branches/FLUID-2881/src/webapp/components/pager/js/Table.js
fluid.registerNamespace("fluid.table");
fluid.table.findColumnDef = function (columnDefs, key) {
return fluid.find_if(columnDefs, function (def) {
return def.key === key;
});
};
fluid.table.getRoots = function (target, dataOffset, index) {
target.shortRoot = index;
target.longRoot = fluid.pathUtil.composePath(dataOffset, target.shortRoot);
};
// TODO: This crazed variable expansion system was a sketch for what eventually became the "protoComponent expansion system" delivered in 1.x versions of
// Infusion. It in turn should be abolished when FLUID-4260 is implemented, allowing users to code with standard Fluid components and standard IoC references
fluid.table.expandPath = function (EL, shortRoot, longRoot) {
if (EL.charAt(0) === "*") {
return longRoot + EL.substring(1);
} else {
return EL.replace("*", shortRoot);
}
};
fluid.table.fetchValue = function (dataOffset, dataModel, index, valuebinding, roots) {
fluid.table.getRoots(roots, dataOffset, index);
var path = fluid.table.expandPath(valuebinding, roots.shortRoot, roots.longRoot);
return fluid.get(dataModel, path);
};
fluid.table.rowComparator = function (sortDir) {
return function (arec, brec) {
return (arec.value > brec.value ? 1 : (arec.value < brec.value ? -1 : 0)) * sortDir;
};
};
fluid.table.basicSorter = function (columnDefs, dataModel, dataOffset, model) {
var roots = {};
var columnDef = fluid.table.findColumnDef(columnDefs, model.sortKey);
var sortrecs = [];
for (var i = 0; i < model.totalRange; ++i) {
sortrecs[i] = {
index: i,
value: fluid.table.fetchValue(dataOffset, dataModel, i, columnDef.valuebinding, roots)
};
}
sortrecs.sort(fluid.table.rowComparator(model.sortDir));
return fluid.getMembers(sortrecs, "index");
};
fluid.table.IDforColumn = function (columnDef, keyPrefix, roots) {
var EL = columnDef.valuebinding;
var key = columnDef.key;
if (!EL) {
fluid.fail("Error in definition for column with key " + key + ": valuebinding is not set");
}
if (!key) {
var segs = fluid.model.parseEL(EL);
key = segs[segs.length - 1];
}
return {
ID: (keyPrefix || "") + key,
EL: fluid.table.expandPath(EL, roots.shortRoot, roots.longRoot)
};
};
fluid.table.bigHeaderForKey = function (key, options) {
// TODO: ensure this is shared properly
var id = options.rendererOptions.idMap["header:" + key];
var smallHeader = fluid.jById(id);
if (smallHeader.length === 0) {
return null;
}
var headerSortStylisticOffset = options.selectors.headerSortStylisticOffset;
var bigHeader = fluid.findAncestor(smallHeader, function (element) {
return $(element).is(headerSortStylisticOffset);
});
return bigHeader;
};
fluid.table.setSortHeaderClass = function (styles, element, sort) {
element = $(element);
element.removeClass(styles.ascendingHeader);
element.removeClass(styles.descendingHeader);
if (sort !== 0) {
element.addClass(sort === 1 ? styles.ascendingHeader : styles.descendingHeader);
// aria-sort property are specified in the W3C WAI spec, ascending, descending, none, other.
// since pager currently uses ascending and descending, we do not support the others.
// http://www.w3.org/WAI/PF/aria/states_and_properties#aria-sort
element.attr("aria-sort", sort === 1 ? "ascending" : "descending");
}
};
fluid.table.isCurrentColumnSortable = function (columnDefs, model) {
var columnDef = model.sortKey ? fluid.table.findColumnDef(columnDefs, model.sortKey) : null;
return columnDef ? columnDef.sortable : false;
};
fluid.table.setModelSortHeaderClass = function (columnDefs, newModel, options) {
var styles = options.styles;
var sort = fluid.table.isCurrentColumnSortable(columnDefs, newModel) ? newModel.sortDir : 0;
fluid.table.setSortHeaderClass(styles, fluid.table.bigHeaderForKey(newModel.sortKey, options), sort);
};
fluid.table.generateColumnClick = function (tableThat, options, model, columnDef) {
return function () {
if (columnDef.sortable === true) {
var model = tableThat.model;
var newModel = fluid.copy(model);
var styles = tableThat.options.styles;
var oldKey = model.sortKey;
if (columnDef.key !== model.sortKey) {
newModel.sortKey = columnDef.key;
newModel.sortDir = 1;
var oldBig = fluid.table.bigHeaderForKey(oldKey, options);
if (oldBig) {
fluid.table.setSortHeaderClass(styles, oldBig, 0);
}
} else if (newModel.sortKey === columnDef.key) {
newModel.sortDir = -1 * newModel.sortDir;
} else {
return false;
}
newModel.pageIndex = 0;
tableThat.applier.change("", newModel);
// fluid.table.setModelSortHeaderClass(newModel, options); - done during rerender, surely
}
return false;
};
};
fluid.table.fetchHeaderDecorators = function (decorators, columnDef) {
return decorators[columnDef.sortable ? "sortableHeader" : "unsortableHeader"];
};
fluid.table.generateHeader = function (tableThat, options, newModel) { // arg 2 is renderThat.options
var sortableColumnTxt = options.strings.sortableColumnText;
if (newModel.sortDir === 1) {
sortableColumnTxt = options.strings.sortableColumnTextAsc;
} else if (newModel.sortDir === -1) {
sortableColumnTxt = options.strings.sortableColumnTextDesc;
}
var columnDefs = tableThat.options.columnDefs;
return {
children:
fluid.transform(columnDefs, function (columnDef) {
return {
ID: fluid.table.IDforColumn(columnDef, options.keyPrefix, {}).ID,
value: columnDef.label,
decorators: [
{"jQuery": ["click", fluid.table.generateColumnClick(tableThat, options, newModel, columnDef)]},
{identify: "header:" + columnDef.key},
{type: "attrs", attributes: { title: (columnDef.key === newModel.sortKey) ? sortableColumnTxt : options.strings.sortableColumnText}}
].concat(fluid.table.fetchHeaderDecorators(options.decorators, columnDef))
};
})
};
};
fluid.table.expandVariables = function (value, opts) {
var togo = "";
var index = 0;
while (true) {
var nextindex = value.indexOf("${", index);
if (nextindex === -1) {
togo += value.substring(index);
break;
} else {
togo += value.substring(index, nextindex);
var endi = value.indexOf("}", nextindex + 2);
var EL = value.substring(nextindex + 2, endi);
if (EL === "VALUE") {
EL = opts.EL;
} else {
EL = fluid.table.expandPath(EL, opts.shortRoot, opts.longRoot);
}
var val = fluid.get(opts.dataModel, EL);
togo += val;
index = endi + 1;
}
}
return togo;
};
fluid.table.expandPaths = function (target, tree, opts) {
for (var i in tree) {
var val = tree[i];
if (fluid.isMarker(val, fluid.VALUE)) { // TODO, in theory, we could prevent copying of columnDefs
if (i === "valuebinding") {
target[i] = opts.EL;
} else {
target[i] = {"valuebinding" : opts.EL};
}
} else if (i === "valuebinding") {
target[i] = fluid.table.expandPath(tree[i], opts);
} else if (typeof (val) === "object") {
target[i] = val.length !== undefined ? [] : {};
fluid.table.expandPaths(target[i], val, opts);
} else if (typeof (val) === "string") {
target[i] = fluid.table.expandVariables(val, opts);
} else {
target[i] = tree[i];
}
}
return target;
};
fluid.table.expandColumnDefs = function (columnDefs, keyPrefix, dataModel, filteredRow, roots) {
var tree = fluid.transform(columnDefs, function (columnDef) {
var record = fluid.table.IDforColumn(columnDef, keyPrefix, roots);
var opts = $.extend({
dataModel: dataModel
}, roots, record);
var togo;
if (!columnDef.components) {
return {
ID: record.ID,
valuebinding: record.EL
};
} else if (typeof columnDef.components === "function") {
togo = columnDef.components(filteredRow.row, filteredRow.index);
} else {
togo = columnDef.components;
}
togo = fluid.table.expandPaths({}, togo, opts);
togo.ID = record.ID;
return togo;
});
return tree;
};
fluid.table.fetchDataModel = function (dataModel, dataOffset) {
return fluid.get(dataModel, dataOffset);
};
fluid.table.produceTree = function (tableThat, renderThat) {
var options = renderThat.options;
var columnDefs = tableThat.options.columnDefs;
var roots = {};
var tree = fluid.transform(tableThat.filtered,
function (filteredRow) {
fluid.table.getRoots(roots, tableThat.options.dataOffset, filteredRow.index);
if (columnDefs === "explode") {
return fluid.explode(filteredRow.row, roots.longRoot);
} else if (columnDefs.length) {
return fluid.table.expandColumnDefs(columnDefs, renderThat.options.keyPrefix, tableThat.dataModel, filteredRow, roots);
}
});
var fullTree = {};
fullTree[options.row] = tree;
if (typeof (columnDefs) === "object") {
fullTree[options.header] = fluid.table.generateHeader(tableThat, renderThat.options, tableThat.model);
}
return fullTree;
};
fluid.table.sortInvoker = function (tableThat, newModel) {
var columnDefs = tableThat.options.columnDefs;
var sorted = fluid.table.isCurrentColumnSortable(columnDefs, newModel) ?
tableThat.options.sorter(columnDefs, tableThat.options.dataModel, tableThat.options.dataOffset, newModel) : null;
tableThat.permutation = sorted;
};
fluid.table.onModelChange = function (tableThat, renderThat, newModel) {
renderThat.sortInvoker(newModel);
tableThat.dataModel = tableThat.fetchDataModel();
tableThat.filtered = tableThat.options.modelFilter(tableThat.dataModel, newModel, tableThat.permutation);
};
/** A body renderer implementation which uses the Fluid renderer to render a table section **/
fluid.defaults("fluid.table.selfRender", {
gradeNames: ["fluid.rendererComponent"],
listeners: {
onCreate: [{
"this": "{that}.root",
method: "addClass",
args: "{that}.options.styles.root"
}],
onIndexModelChange: [{
funcName: "fluid.table.onModelChange",
namespace: "onModelChange",
args: ["{fluid.table}", "{fluid.table.selfRender}", "{arguments}.0", "{arguments}.1"] // newModel, oldModel
}, {
func: "{that}.sortInvoker",
namespace: "sortInvoker",
args: "{arguments}.0"
}, {
priority: "last",
namespace: "refreshView",
func: "{that}.refreshView"
}],
afterRender: { // TODO, should this not be actually renderable?
funcName: "fluid.table.setModelSortHeaderClass",
args: ["{that}.options.columnDefs", "{fluid.table}.model", "{that}.options"]
}
},
modelListeners: {
"{fluid.table}.model": "{that}.events.onIndexModelChange.fire({change}.value, {change}.oldValue)"
},
events: {
onIndexModelChange: null
},
invokers: {
sortInvoker: {
funcName: "fluid.table.sortInvoker",
args: ["{fluid.table}", "{arguments}.0"] // newModel
},
produceTree: {
funcName: "fluid.table.produceTree",
args: ["{fluid.table}", "{fluid.table.selfRender}"]
}
},
selectors: {
root: ".flc-pager-body-template",
headerSortStylisticOffset: "{table}.options.selectors.headerSortStylisticOffset",
header: ".flc-table-header",
row: ".flc-table-row"
},
repeatingSelectors: ["header", "row"],
selectorsToIgnore: ["root", "headerSortStylisticOffset"],
styles: {
root: "fl-pager",
ascendingHeader: "{table}.options.styles.ascendingHeader",
descendingHeader: "{table}.options.styles.descendingHeader"
},
members: {
root: "{that}.dom.root"
},
decorators: {
sortableHeader: [],
unsortableHeader: []
},
keyStrategy: "id",
keyPrefix: "",
row: "row:", // should match selector name, deprecated after v1.5
header: "header:", // should match selector name, deprecated after v1.5
strings: "{table}.options.strings",
columnDefs: "{table}.options.columnDefs",
// Options passed upstream to the renderer
rendererFnOptions: {
templateSource: {node: "{that}.dom.root"},
renderTarget: "{that}.dom.root",
noexpand: true
},
rendererOptions: {
model: "{table}.options.dataModel",
idMap: {}
}
});
fluid.table.checkTotalRange = function (totalRange, pagerBar) {
if (totalRange === undefined && !pagerBar) {
fluid.fail("Error in Pager configuration - cannot determine total range, " +
" since not configured in model.totalRange and no PagerBar is configured");
}
};
fluid.defaults("fluid.table", {
gradeNames: ["fluid.viewComponent"],
mergePolicy: {
dataModel: "preserve",
columnDefs: "noexpand"
},
components: {
bodyRenderer: {
type: "fluid.table.selfRender",
container: "{table}.container"
}
},
listeners: {
onCreate: {
funcName: "fluid.table.checkTotalRange",
namespace: "checkTotalRange",
args: ["{that}.model.totalRange", "{that}.pagerBar"]
}
},
modelFilter: fluid.table.directModelFilter, // TODO: no implementation for this yet
sorter: fluid.table.basicSorter,
members: {
dataModel: {
expander: {
func: "{that}.fetchDataModel"
}
}
},
invokers: {
fetchDataModel: {
funcName: "fluid.table.fetchDataModel",
args: ["{that}.options.dataModel", "{that}.options.dataOffset"]
}
},
styles: {
ascendingHeader: "fl-pager-asc",
descendingHeader: "fl-pager-desc"
},
selectors: {
headerSortStylisticOffset: ".flc-pager-sort-header"
},
strings: {
sortableColumnText: "Select to sort",
sortableColumnTextDesc: "Select to sort in ascending, currently in descending order.",
sortableColumnTextAsc: "Select to sort in descending, currently in ascending order."
},
// Offset of the tree's "main" data from the overall dataModel root
dataOffset: "",
// strategy for generating a tree row, either "explode" or an array of columnDef objects
columnDefs: [] // [{key: "columnName", valuebinding: "*.valuePath", sortable: true/false}]
});
})(jQuery, fluid_3_0_0);