From 460a7bb4b1e25a4cc1d830a72022f09938b1c3df Mon Sep 17 00:00:00 2001 From: Patrik Kullman Date: Tue, 3 Nov 2015 19:09:45 +0100 Subject: [PATCH] Initial, horrible state port of omnitable --- .gitignore | 1 + .jslintrc | 11 + CONTRIBUTING.md | 31 ++ LICENSE | 202 ++++++++++ bower.json | 36 ++ cosmoz-omnitable.css | 305 +++++++++++++++ cosmoz-omnitable.html | 262 +++++++++++++ cosmoz-omnitable.js | 846 ++++++++++++++++++++++++++++++++++++++++++ demo/index.html | 32 ++ index.html | 15 + 10 files changed, 1741 insertions(+) create mode 100644 .gitignore create mode 100644 .jslintrc create mode 100644 CONTRIBUTING.md create mode 100644 LICENSE create mode 100644 bower.json create mode 100644 cosmoz-omnitable.css create mode 100644 cosmoz-omnitable.html create mode 100644 cosmoz-omnitable.js create mode 100644 demo/index.html create mode 100644 index.html diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..fbe05fc9 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +bower_components/ diff --git a/.jslintrc b/.jslintrc new file mode 100644 index 00000000..5f9b7e0c --- /dev/null +++ b/.jslintrc @@ -0,0 +1,11 @@ +{ + "allowdollarproperty": true, + "edition": "cosmoz", + "indent": 1, + "maxerr": 100, + "nomen": true, + "this": true, + "todo": true, + "unparam": true, + "white": true +} \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..db8444cb --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,31 @@ +# Coding guidelines + +We follow the [Polymer](https://www.polymer-project.org) guidelines as well as [JSLint](http://jslint.com/) as best possible, but with some exceptions. + +Most importantly, we skip the check for `$` properties since Polymer populates `this.$` and we also skip the check for `_variable` since that's the way Polymer wants to do private properties and methods. Finally we allow the use of `this`. + +In addition to this we also choose to indent with tabs. + +## JSLint + +To lint the JavaScript, you will need to install [node-jslint](https://github.com/reid/node-jslint), download our [jslint edition](https://raw.githubusercontent.com/Neovici/JSLint/all-patches/jslint.js) and save it as `jslint-cosmoz.js` in the node-jslint lib folder. + +* Linux: `/usr/local/lib/node_modules/jslint/lib` +* Windows: `%APPDATA%\npm\node_modules\jslint\lib` + +## Sublime Text 3 + +The easiest way to conform to the guidelines is to run Sublime Text 3 and install the following packages: + +* SublimeLinter +* SublimeLinter-contrib-jslint +* TrailingSpaces + +### Settings + +* SublimeLinter: `"show_errors_on_save": true` +* TrailingSpaces: ` "trailing_spaces_trim_on_save": true` + +# Contributing code + +Just follow the [standard fork and pull request workflow](https://guides.github.com/activities/forking/). \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..08c571f1 --- /dev/null +++ b/LICENSE @@ -0,0 +1,202 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2015 Neovici AB + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + diff --git a/bower.json b/bower.json new file mode 100644 index 00000000..f6ab3f6f --- /dev/null +++ b/bower.json @@ -0,0 +1,36 @@ +{ + "name": "cosmoz-omnitable", + "main": "cosmoz-omnitable.html", + "version": "0.1.4", + "homepage": "https://github.com/Neovici/cosmoz-omnitable", + "authors": [ + "Neovici Development" + ], + "description": "Polymer component for grouped iron-lists", + "keywords": [ + "web-components", + "polymer", + "router", + "cosmoz" + ], + "license": "Apache 2.0", + "ignore": [ + "**/.*", + "node_modules", + "bower_components", + "test", + "tests" + ], + "dependencies": { + "polymer": "^1.2.0", + "iron-component-page": "PolymerElements/iron-component-page#~1.0.7", + "iron-flex-layout": "PolymerElements/iron-flex-layout#~1.0.4", + "cosmoz-grouped-list": "Neovici/cosmoz-grouped-list#*", + "cosmoz-web-worker": "Neovici/cosmoz-web-worker#*", + "paper-checkbox": "PolymerElements/paper-checkbox#~1.0.13" + }, + "resolutions": { + "polymer": "^1.2.0" + } +} + diff --git a/cosmoz-omnitable.css b/cosmoz-omnitable.css new file mode 100644 index 00000000..85fbda13 --- /dev/null +++ b/cosmoz-omnitable.css @@ -0,0 +1,305 @@ +:host { + display: block; + min-height: 150px; +} + +#omnitable { + background-color: #fff; +} + +content[select="header"] { + display: none; +} + +.header, +.cell { + @apply(--layout-flex); +} + +.cell { + font-size: 0.9em; + font-weight: 200; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.cell > * { + overflow: hidden; + text-overflow: ellipsis; +} + +#body { + overflow-y: auto; + min-height: 90px; +} + +#footer { + padding: 3px 0; + border-top: solid 1px #e8e8e8; + background-color: #f5f5f5; + min-height: 25px; + margin-bottom: 1px; /* Chrome overflow rendering bug? When only component in a view */ +} + +#header, #footer { + font-weight: 300; + font-size: 14px; + min-height: 20px; +} + +#footer > div:first-child { + padding-left: 3%; +} + +#footer > div:last-child { + padding-right: 3%; +} + + +#footer core-label .label { + margin-right: 10px; +} +#footer core-label paper-dropdown-menu { + padding-top: 2px; + margin: 0; +} + +#footer #sortLabel { + margin-right: 4%; +} + +.group-header { + height: 48px; + border-top: solid 1px #f1f1f1; + border-bottom: solid 1px #f1f1f1; +} + +.group-header.expanded { + background-color: #efefef; +} + +.group-header.expanded td { + border-top: solid 1px #efefef; + border-bottom: solid 1px #efefef; +} +.group-header.hidden { + display: none !important; +} + +.group-header h3 { + padding-left: 8px; + margin: 0; + font-size: 1.25em; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.group-header paper-button::shadow .button-content { + padding: 0.2em 0; +} + +.item-row { + height: 24px; + border-bottom: solid 1px #e2e2e2; + @apply(--layout-horizontal); + @apply(--layout-center); +} +.item-row .cell:last-of-type { + padding-right: 8px; +} +.item-row.selected { + background-color: #e1f8f9; +} +.item-row paper-icon-button { + padding: 0; +} +.item-extra-row > div { + padding: 0 20px; + line-height: 1.6em; +} + +.row-checkbox { + min-width: 32px; + max-width: 32px; + width: 32px; + padding: 1px; + text-align: center; + vertical-align: middle; +} + +cz-letterball { + padding-left: 8px; + width: 20px; + height: 20px; +} + +.simple-mobile-mode .row-checkbox { + min-width: 34px; + max-width: 34px; + width: 34px; +} +.simple-mobile-mode cz-letterball { + padding-left: 4px; + width: 28px; + height: 28px; +} + +.type-label div, +.simple-mobile-item div.type-label { + text-align: center; + display: inline-block; + background-color: #aaa; + color: white; + border-radius: 5px; + padding: 2px 5px; +} + +.type-amount { + min-width: 70px; + width: 100px; + max-width: 150px; + text-align: right; +} + +.type-documentId { + min-width: 80px; + width: 100px; + max-width: 200px; +} + +.header-type-date, +.type-date { + min-width: 85px; + width: 85px; + max-width: 140px; +} + +.header-type-datetime, +.type-datetime { + min-width: 180px; + width: 180px; + max-width: 180px; +} + +.type-default { + min-width: 120px; +} + +.header-type-number, +.type-number { + min-width: 30px; + width: 50px; + max-width: 80px; + text-align: right; +} + +.type-number paper-input-container input { + text-align: right; +} + +.type-number paper-input-container { + max-width: 100px; +} + +div.disabled { + margin: 0; + padding: 0; + width: 0; +} + +div.wrap { + white-space: normal; +} + +#emptyDataset { + color: #ccc; +} + +#emptyDataset > div { + margin: 0 5%; +} + +#emptyDataset iron-icon { + height: 96px; + min-height: 96px; + width: 96px; + min-width: 96px; + margin-right: 5%; +} + +#emptyDataset p { + font-size: 15px; + color: #ddd; + margin: 0; +} + +#emptyDataset h3 { + margin: 15px 0 3% 0; + white-space: nowrap; +} + +.simple-mobile-mode .group-header h3 { + margin: 0; + padding-right: 10px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.simple-mobile-item { + border-bottom: solid 1px #f9f9f9; + font-weight: 300; + font-size: 15px; + line-height: 1.4em; + height: 48px; +} + +.simple-mobile-mode #header, +.simple-mobile-item { + padding: 2px 4px; +} + +.cell.c0, +.header.c0, +.simple-mobile-item a { + padding-left: 8px; +} + +#header .header:first-of-type { + margin-right: -8px; +} + +.simple-mobile-item a { + text-decoration: none; + color: inherit; +} + +.simple-mobile-item .m { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + line-height: 1.4em; + min-width: initial; + max-width: initial; + width: initial; + text-align: left; +} + +.simple-mobile-item .m2 { + font-weight: 400; + font-size: 16px; +} + +.simple-mobile-item .m3 { + margin-right: 5px; + font-size: 14px; +} + +.width-setter { + border: 0; + height: 0; + overflow: hidden; + padding: 0; +} \ No newline at end of file diff --git a/cosmoz-omnitable.html b/cosmoz-omnitable.html new file mode 100644 index 00000000..b673933d --- /dev/null +++ b/cosmoz-omnitable.html @@ -0,0 +1,262 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/cosmoz-omnitable.js b/cosmoz-omnitable.js new file mode 100644 index 00000000..6974bbea --- /dev/null +++ b/cosmoz-omnitable.js @@ -0,0 +1,846 @@ +/*global cz, document, Polymer, window, d3, nv */ +(function () { + + "use strict"; + + Polymer({ + is: 'cosmoz-omnitable', + disabledHeaders: 0, + // help variable, by changing this variable, polymer gets a "kick" and refreshes. Same reasoning in similar variables below. + groupIndex: {}, + needs: {}, + properties: { + data: { + type: Array, + observer: '_dataChanged', + value: null + }, + filterKick: { + type: Number, + value: 0 + }, + filteredSortedGroupedItems: { + type: Object, + computed: 'filterSortedGroupedItems(sortedGroupedItems, filterKick)' + }, + forceMobile: { + type: Boolean, + value: false + }, + // The collection and the structure for the grouping of the data. + groupedItems: { + type: Array, + computed: 'groupItems(groupKick)' + }, + headersWithoutGroupOnHeader: { + type: Object, + computed: 'getHeadersWithoutGroupOnHeader(headers, groupOn, forceMobile)' + }, + // hide all groups except first + hideButFirst: { + type: Boolean, + value: true + }, + hideEmptyGroups: { + type: Boolean, + value: true + }, + headers: { + type: Array + }, + noData: { + type: Boolean, + computed: 'isEmpty(data)' + }, + selectedItems: { + type: Array + }, + // calculated decides mobile or application view mode. + simpleMobileMode: { + type: Boolean, + // computed: 'forceMobile || (((headersWithoutGroupOnHeader.length - disabledHeaders) <= 3) && disabledHeaders > 0)', + value: false + }, + sortedGroupedItems: { + type: Array, + computed: 'sortGroupedItems(groupedItems, sortOn)' + }, + sortOn: { + type: String, + value: "name" + }, + groupKick: { + type: Number, + value: 0 + }, + groupOn: { + type: String, + value: null, + observer: '_groupOnChanged' + }, + // collection of opened groups over the omnitable. This for minimizing overhead when sorting, allowing for bypassing closed groups. + toggledGroups: { + type: Object, + computed: 'toggleGroupVisibility(filteredSortedGroupedItems, toggleGroupKick)' + }, + + // private + _hasActions: { + type: Boolean, + value: false + } + }, + refreshKick: 0, + toggleGroupKick: 0, + visibilityKick: 0, + _dataChanged: function () { + this.needs.grouping = true; + this.needs.filtering = true; + this.needs.sorting = true; + // Delay the computing of groupedItems property until domReady, i.e. until the headers are defined. + if (this.headers) { + console.log('headers!'); + this.groupKick += 1; + } + this.fire('data-changed', { + action: 'replace', + data: this.data + }); + console.log('_dataChanged'); + }, + removeItem: function (item) { + var dataIndex, change = false; + // Removes item from selection if needed. + if (this.selectedItems) { + dataIndex = this.selectedItems.indexOf(item); + if (dataIndex > -1) { + this.selectedItems.splice(dataIndex, 1); + change = true; + } + } + dataIndex = this.data.indexOf(item); + if (dataIndex > -1) { + this.data.splice(dataIndex, 1); + change = true; + } + if (change && item.checked) { + delete item.checked; + } + return change; + }, + removeItems: function (items) { + var groupKick = false, i; + for (i = items.length - 1; i >= 0; i -= 1) { + groupKick = this.removeItem(items[i]) ? true : groupKick; + } + if (groupKick) { + this.groupKick += 1; + this.fire('data-changed', { + action: 'removeItem', + data: this.data + }); + } + }, + onAction: function (event, detail, element) { + detail.item.dispatchEvent(new window.CustomEvent('run', { + bubbles: true, + cancelable: true, + detail: { + omnitable: this, + items: this.selectedItems + } + })); + element.selected = undefined; + event.stopPropagation(); + }, + getSelection: function () { + return this.selectedItems; + }, + created: function () { + this.disabledHeaders = 0; + this.rendered = false; + this.mobileView = { + headers: [], + linkHeader: undefined + }; + this.needs = { + grouping: true, + filtering: true, + sorting: true + }; + }, + ready: function () { + this.needs = { + grouping: true, + filtering: true, + sorting: true + }; + // TODO: cz.state.app.addEventListener('resize', this.updateWidths.bind(this)); + }, + attached: function () { + this._hasActions = Polymer.dom(this.$.actions).getDistributedNodes().length > 0; + this.setHeadersFromMarkup(); + this.rendered = true; + }, + _groupOnChanged: function (oldValue, newValue) { + console.log('asdfsd'); + this.needs.grouping = true; + this.groupKick += 1; + }, + onAllCheckboxChange: function (event, detail) { + + if (event.target === null) { + return; + } + + console.log('onAllCheckboxChange', event.target, detail); + + var element = event.target, + checked = element.checked, + that = this; + + if (!this.groupedItems) { + return; + } + + this.groupedItems.forEach(function (group, index) { + group.checked = checked; + that.selectGroupItems(group); + }); + }, + onGroupCheckboxChange: function (event, detail, element) { + var group = event.target.templateInstance.model.groupModel; + this.selectGroupItems(group); + }, + onItemCheckboxChange: function (event, detail, element) { + var item = event.target.templateInstance.model.model; + this.selectItem(item); + }, + selectGroupItems: function (group) { + var that = this, + groupIndex = this.groupedItems.indexOf(group), + groupGroup = this.filteredSortedGroupedItems[groupIndex]; + + groupGroup.forEach(function (item, index) { + item.checked = group.checked; + if (!item.placeholder) { + that.selectItem(item); + } + }); + }, + selectItem: function (item) { + var itemIndex; + if (item.checked) { + this.selectedItems = this.selectedItems || []; + if (this.selectedItems.indexOf(item) === -1) { + this.selectedItems.push(item); + } + } else if (this.selectedItems) { + itemIndex = this.selectedItems.indexOf(item); + if (itemIndex > -1) { + this.selectedItems.splice(itemIndex, 1); + } + } + }, + toggleGroup: function (event, detail, element) { + var group = event.target.templateInstance.model.groupModel; + group.visible = !group.visible; + this.toggleGroupKick += 1; + }, + filterItem: function (item) { + var hide = false, that = this; + cz.loop.array(this.headers, function (headerIndex, header) { + if (header.filters !== undefined && header.filters.length > 0) { + var filterFail = true; + cz.loop.array(header.filters, function (headerFilterIndex, headerFilter) { + var itemVal = that.resolveProp(item, header.id); + if (itemVal === headerFilter.value) { + filterFail = false; + return false; + } + if (typeof itemVal === 'object' && that.renderObject(itemVal, false, header.type) === headerFilter.label) { + filterFail = false; + return false; + } + }); + if (filterFail) { + hide = true; + return false; + } + } + }); + item.visible = !hide; + }, + filterItems: function (event, detail, sender) { + this.needs.filtering = true; + this.filterKick += 1; + }, + clearHeaderValues: function () { + if (!this.headers) { + return; + } + this.headers.forEach(function (header) { + header.values = []; + }); + }, + renderItemProperty: function (item, header, ui) { + if (item === undefined || header === undefined) { + return ''; + } + // TODO: Cleaner solution? + if (item.placeholder) { + return ''; + } + var prop = this.resolveProp(item, header.id); + return this.renderObject(prop, ui, header.type); + }, + renderObject: function (obj, ui, type) { + if (obj === undefined || obj === '') { + return ''; + } + if (type) { + switch (type) { + case 'amount': + case 'money': + return cz.tools.money.render(obj, ui); + case 'date': + case 'datetime': + return 'date'; + // return cz.tools.date.render(obj, ui); + } + } + if (typeof obj !== 'object') { + return obj; + } + var ret = cz.tools.renderObject(obj, ui), uo = 'unknown object'; + if (ret === uo) { + console.warn(uo, obj); + } + return ret; + }, + setHeaderValues: function (item) { + var that = this; + cz.loop.array(this.headers, function (headerIndex, header) { + var value = that.resolveProp(item, header.id), hasValue = false, label = that.renderObject(value, false, header.type); + cz.loop.array(header.values, function (headerValueIndex, headerValue) { + if (headerValue.label === label) { + hasValue = true; + return false; + } + }); + if (!hasValue) { + header.values.push({ + label: label, + value: value + }); + } + header.values.sort(function (a, b) { + if (a.label < b.label) { + return -1; + } + return 1; + }); + }); + }, + dataRowChanged: function (event, detail, sender) { + var outerModel = sender.templateInstance.model, item; + cz.loop.object(outerModel, function (key, instance) { + if (key[0] === '@') { + item = instance.item; + return false; + } + }); + //item[outerModel.header.id] = sender.value; + this.fire('cz-data-row-changed', item); + this.fire('data-changed', { + action: 'updateItem', + data: this.data + }); + }, + disableColumn: function () { + var headerToDisable; + // disables/hides columns that for example does not fit in the current screen size. + cz.loop.array(this.headers, function (index, header) { + if (!header.disabled && (headerToDisable === undefined || headerToDisable.priority >= header.priority)) { + headerToDisable = header; + } + }); + if (headerToDisable) { + headerToDisable.disabled = true; + this.disabledHeaders += 1; + this.async(this.updateWidths); + } + }, + enableColumn: function () { + var headerToEnable; + cz.loop.array(this.headers, function (index, header) { + if (header.disabled && (headerToEnable === undefined || headerToEnable.priority < header.priority)) { + headerToEnable = header; + } + }); + headerToEnable.disabled = false; + this.disabledHeaders -= 1; + // Fake a resize bigger event, in the off chance that we go past + // the size of two columns in one resize, like maximizing a window + this.async(function () { + this.scalingUp = true; + this.updateWidths({ + detail: { + bigger: true, + second: true + } + }); + }); + }, + filterSortedGroupedItems: function (sortedGroupedItems, filterKick) { + console.log('filterSortedGroupedItems', sortedGroupedItems); + if (!this.sortedGroupedItems) { + return null; + } + + var that = this, + filteredGroupedItems = []; + + this.clearHeaderValues(); + + this.sortedGroupedItems.forEach(function (groupItems, index) { + filteredGroupedItems[index] = [{ placeholder: true }]; + cz.loop.array(groupItems, function (itemIndex, item) { + that.setHeaderValues(item); + if (that.needs.filtering) { + that.filterItem(item); + } + if (item.visible) { + filteredGroupedItems[index].push(item); + that.selectItem(item); + } + }); + }); + this.needs.filtering = false; + return filteredGroupedItems; + }, + getHeader: function (id) { + var foundHeader; + this.headers.some(function (header, index) { + if (header.id === id) { + foundHeader = header; + return true; + } + }); + return foundHeader; + }, + getHeadersWithoutGroupOnHeader: function (headers, groupOn, forceMobile) { + if (!headers) { + return; + } + if (forceMobile) { + return this.mobileView.headers; + } + var filteredHeaders = []; + headers.forEach(function (header, index) { + if (header.id !== groupOn) { + filteredHeaders.push(header); + } + }); + return filteredHeaders; + }, + groupItems: function (groupKick) { + // Computed properties are created and evaluated as soon as the component is created. + // This function needs the headers, but they are set in the ready callback (and cannot be set earlier as it needs the DOM children), + // so we might be in situations where the data is available but not the headers. + // (was the case in invoice view, the omnitable that display invoice rows). + if (!this.headers) { + return null; + } + if (!this.data || this.data.length === 0) { + this.needs.grouping = false; + this.needs.sorting = false; + this.needs.filtering = false; + return; + } + if (!this.needs.grouping) { + console.log('no grouping needed!', this.groupedItems); + return this.groupedItems; + } + console.log('groupItems'); + this._groupOnHeader = this.getHeader(this.groupOn); + + var groups = [], + itemStructure = {}, + that = this; + + if (that.groupOn) { + cz.loop.array(this.data, function (index, item) { + var groupOnValue = that.resolveProp(item, that.groupOn); + if (typeof groupOnValue === 'object') { + groupOnValue = that.renderObject(groupOnValue, false, that._groupOnHeader.type); + } + if (groupOnValue !== undefined) { + if (!itemStructure[groupOnValue]) { + itemStructure[groupOnValue] = []; + } + itemStructure[groupOnValue].push(item); + } + }); + } else { + itemStructure[''] = this.data.slice(0); + } + console.log(itemStructure); + Object.keys(itemStructure).forEach(function (key) { + groups.push({ + name: that._groupOnHeader, + id: key, + items: itemStructure[key], + visible: true + }); + }); + groups.sort(function (a, b) { + var v1 = that.resolveProp(a.items[0], that.groupOn), v2 = that.resolveProp(b.items[0], that.groupOn); + if (typeof v1 === 'object' && typeof v2 === 'object') { + return cz.tools.sortObject(v1, v2); + } + if (typeof v1 === 'number' && typeof v2 === 'number') { + return v1 - v2; + } + if (typeof v1 === 'string' && typeof v2 === 'string') { + return v1 < v2 ? -1 : 1; + } + if (typeof v1 === 'boolean' && typeof v2 === 'boolean') { + if (v1 === v2) { + return 0; + } + return v1 ? -1 : 1; + } + console.warn('unsupported sort', typeof v1, v1, typeof v2, v2); + return 0; + }); + if (this.hideButFirst && groups.length > 1) { + cz.loop.array(groups, function (index, group) { + if (index === 0) { + return; + } + group.visible = false; + }); + } + this.needs.grouping = false; + this.needs.sorting = true; + console.log(groups); + return groups; + }, + setHeadersFromMarkup: function () { + var ctx = this, + markupHeaders = Polymer.dom(this).querySelectorAll('header'), + mobileHeaders = [], newHeaders = []; + + markupHeaders.forEach(function (headerElement, index) { + var header = { + disabled: false, + editable: headerElement.hasAttribute('editable'), + id: headerElement.id, + linkbase: headerElement.getAttribute('linkbase'), + linkprop: headerElement.getAttribute('linkprop'), + name: Polymer.dom(headerElement).innerHTML, + priority: parseInt(headerElement.getAttribute('priority') || 0, 10), + type: headerElement.getAttribute('type') || 'default', + values: [], + filters: [], + wrap: headerElement.hasAttribute('wrap') + }; + mobileHeaders.push(header); + newHeaders.push(header); + }); + mobileHeaders.sort(function (a, b) { + if (a.priority === b.priority) { + return 0; + } + return a.priority > b.priority ? -1 : 1; + }); + mobileHeaders.splice(3, mobileHeaders.length - 3); + this.mobileView.headers = mobileHeaders; + mobileHeaders.some(function (header, index) { + header.mobile = true; + if (header.linkbase && !ctx.mobileView.linkHeader) { + ctx.mobileView.linkHeader = header; + return true; + } + }); + this.headers = newHeaders; + if (this.needs.grouping) { + this.groupKick += 1; + } + console.log(this.headers); + }, + _computeClass: function (data) { + console.log('_computeClass', data); + }, + sortGroupedItems: function (groupedItemsKick, sortOn) { + console.log('sortGroupedItems'); + if (!this.groupedItems) { + return null; + } + var items = [], + that = this, + lastIndex, + mappedItems; + + this.groupedItems.forEach(function (group, index) { + lastIndex = index; + if (group.items) { + if (!sortOn) { + items[index] = group.items; + } else if (group.items.map) { + // create a reduced version of the items array to transfer to the worker + // with item index and property to sort on + mappedItems = group.items.map(function (item, originalItemIndex) { + return { + index: originalItemIndex, + value: item[sortOn] + }; + }); + // Sort the reduced version of the array + that.$.sortWorker.process({ + meta: { + group: group, + index: index + }, + sortOn: 'value', + data: mappedItems + }, function (data) { + items[data.meta.index] = data.data.map(function (item, index) { + return group.items[item.index]; + }); + if (data.meta.group.visible || data.meta.index === lastIndex) { + that.needs.filtering = true; + that.filterKick += 1; + } + }); + } + } + }); + this.filterKick += 1; + return items; + }, + toggleExtraColumns: function (event, detail, sender) { + var item = event.target.templateInstance.model.model; + item.expanded = !item.expanded; + }, + toggleGroupVisibility: function (filteredSortedGroupedItems, toggleGroupKick) { + var groups = [], that = this, visibleGroups = []; + this.async(this.updateWidths); + if (filteredSortedGroupedItems === undefined) { + return; + } + cz.loop.array(filteredSortedGroupedItems, function (index, groupItems) { + if (groupItems.length > 1 || !that.hideEmptyGroups) { + groups.push(that.groupedItems[index]); + var items = []; + if (that.groupedItems[index].visible) { + items = groupItems; + } else { + items.push({ placeholder: true }); + } + visibleGroups.push(items); + } + }); + if (groups.length === 1 && !groups[0].visible) { + groups[0].visible = true; + this.toggleGroupKick += 1; + return {}; + } + return { + groups: groups, + items: visibleGroups + }; + }, + getNumFiltered: function (group) { + if (group === undefined || group === null) { + return; + } + var groupIndex = this.groupedItems.indexOf(group), filteredItems = this.filteredSortedGroupedItems[groupIndex].length; + return filteredItems - 1; + }, + /** + * Enable/disable columns to properly fit in the available space. + * + * @param {Event} event (optional) Resize event, required for "bigger" events + * (set event.detail.bigger = true) + * @memberOf element/cz-omnitable + */ + updateWidths: function (event) { + if (!this.rendered) { + return; + } + var body = this.$ ? this.$.body : null, bigger = event && event.detail && event.detail.bigger, coreList, fits, headerTds, widthSetter, widthTds; + if (!body) { + return; + } + coreList = Polymer.dom(body).querySelector('#coreList'); + fits = coreList.scrollWidth <= coreList.clientWidth; + /** + * To prevent infinite loops by multiple events, we need to check for 'bigger' events first + * to avoid triggering a 'disableColumn' action in the upscaling process. + * + * Also make sure that the body is not overflowing (fits), when receiving multiple resize up + * events during a "scale up", we can hit an async infinite loop otherwise. + * + * Finally, there's no point in trying to enable a header if there aren't any disabled ones, + * but we don't want to return since this event might be the final one - actually updating + * column widths. + */ + if (fits && bigger && this.disabledHeaders > 0) { + /** + * Only scale up if: + * * It's the first scale up step - a native 'resize' event without detail.second + * * it's the second scale up step - scalingup set by first event and detail.second + */ + /** + * Convert undefined to false + */ + event.detail.second = event.detail.second || false; + /** + * Make sure to sync scalingUp and detail.second since a mismatch can occur if a + * 'resize' triggers a scalingUp process that hasn't completed. + */ + if (this.scalingUp === event.detail.second) { + this.enableColumn(); + } + /** + * Discard any 'resize'-up events until the scale up is completed. + */ + return; + } + /** + * Reset scale-up status as soon as a non-'bigger' event occurs. + */ + this.scalingUp = false; + if (!fits) { + this.async(this.disableColumn); + return; + } + widthSetter = Polymer.dom(coreList).querySelector('.width-setter'); + if (widthSetter === null) { + return; + } + headerTds = Polymer.dom(this.$.header).querySelectorAll('.header'); + // not found in this.$ since it's not present in Simple Mobile Mode + widthTds = Polymer.dom(widthSetter).querySelectorAll('.cell'); + if (this.simpleMobileMode) { + cz.loop.array(headerTds, function (index, headerElement) { + headerElement.style.width = 'inherit'; + headerElement.style.maxWidth = 'inherit'; + }); + } else { + cz.loop.array(widthTds, function (index, element) { + var headerElement = headerTds[index], csElement = window.getComputedStyle(element, null), newWidth = element.clientWidth - parseInt(csElement.getPropertyValue('padding-left'), 10) - parseInt(csElement.getPropertyValue('padding-right'), 10); + headerElement.style.width = newWidth + 'px'; + headerElement.style.maxWidth = newWidth + 'px'; + }); + } + }, + renderLink: function (header, model) { + var link; + if (!header || !model) { + return ''; + } + if (!header.linkbase || !header.linkprop) { + return '#!/invalid/link'; + } + if (header.linkbase[0] === '#') { + // static url + link = header.linkbase; + } else { + link = this.resolveProp(model, header.linkbase); + } + return link + this.resolveProp(model, header.linkprop); + }, + //TODO: @memberOf behavior/cz-common-behavior + /** + * Helper method for Polymer 1.0+ templates - check if variable + * is undefined, null, empty Array list or empty String. + * @param {Object} obj variable + * @return {Boolean} true if "empty", false otherwise + * @memberOf element/cz-omnitable + */ + isEmpty: function (obj) { + if (obj === undefined || obj === null) { + return true; + } + if (obj instanceof Array && obj.length === 0) { + return true; + } + if (typeof obj === 'string' && obj.length === 0) { + return true; + } + return false; + }, + //TODO: @memberOf behavior/cz-common-behavior + /** + * Resolve a JS object path to its property value + * @param {Object} item The JS object + * @param {String} path The (recursive) object property such as "counterParty.name" + * @return {mixed} The value of the object property + * @memberOf element/cz-omnitable + */ + resolveProp: function (item, path) { + if (item === undefined || path === undefined) { + return ''; + } + // TODO: Cleaner solution ? + if (item.placeholder && Object.keys(item).length === 1) { + return ''; + } + if (item.hasOwnProperty(path)) { + return item[path]; + } + var firstDotIndex = path.indexOf('.'), firstProp, restOfPropPath; + if (firstDotIndex > 0) { + firstProp = path.substring(0, firstDotIndex); + restOfPropPath = path.substring(firstDotIndex + 1); + return this.resolveProp(item[firstProp], restOfPropPath); + } + console.warn('item does not have property/path', item, path); + return ''; + }, + + _computeClasses: function (type, headerType, index) { + return [ + type, + "c" + index, + "header-type-" + headerType + ].join(" "); + }, + + _computeHeaderClasses: function (headerType, index) { + return [ + "header", + "c" + index, + "header-type-" + headerType + ].join(" "); + }, + + _computeItemClasses: function (item) { + var classes = [ + 'item-row' + ]; + if (item.checked) { + classes.push('selected'); + } + if (item.placeholder) { + classes.push('width-setter'); + } + return classes.join(' '); + }, + + _getClass: function (className, arg) { + if (arg) { + return className; + } + return ''; + }, + + // TODO: Generalize into behavior, more args + _allTrue: function (arg1, arg2) { + return arg1 && arg2; + } + }); +}()); \ No newline at end of file diff --git a/demo/index.html b/demo/index.html new file mode 100644 index 00000000..3d339b68 --- /dev/null +++ b/demo/index.html @@ -0,0 +1,32 @@ + + + + + + + Cosmoz Omnitable + + + + + + + + + \ No newline at end of file diff --git a/index.html b/index.html new file mode 100644 index 00000000..92199857 --- /dev/null +++ b/index.html @@ -0,0 +1,15 @@ + + + + + + + Cosmoz Omnitable + + + + + + + + \ No newline at end of file