├── .eslintrc ├── .gitignore ├── README.md ├── dev ├── dev.aspx ├── index.html ├── index.js └── src │ ├── ChoiceFieldDemo │ ├── ChoiceFieldDemo.html │ ├── ChoiceFieldDemo.js │ └── ChoiceFieldDemo.less │ ├── ColumnPickerDemo │ ├── ColumnPickerDemo.html │ └── ColumnPickerDemo.js │ ├── DateTimeFieldDemo │ ├── DateTimeFieldDemo.html │ └── DateTimeFieldDemo.js │ ├── ListPickerDemo │ ├── ListPickerDemo.html │ └── ListPickerDemo.js │ ├── LookupFieldDemo │ ├── LookupFieldDemo.html │ └── LookupFieldDemo.js │ ├── PersonaCardDemo │ ├── PersonaCardDemo.html │ ├── PersonaCardDemo.js │ └── PersonaCardDemo.less │ ├── SPFilterPanelDemo │ ├── SPFilterPanelDemo.html │ └── SPFilterPanelDemo.js │ ├── SPPeoplePickerDemo │ ├── SPPeoplePickerDemo.html │ └── SPPeoplePickerDemo.js │ └── setup │ ├── common.js │ └── setup.js ├── dist ├── SPWidgets.js ├── SPWidgets.js.map ├── SPWidgets.min.js └── SPWidgets.min.js.map ├── jsdoc.conf.json ├── package.json ├── src ├── collections │ ├── ListColumnsCollection.js │ └── ListItemsCollection.js ├── index.js ├── models │ ├── ListColumnModel.js │ ├── ListItemModel.js │ ├── ListModel.js │ └── UserProfileModel.js ├── spapi │ ├── getCurrentUser.js │ ├── getList.js │ ├── getListColumns.js │ ├── getListContentTypes.js │ ├── getListFormCollection.js │ ├── getListItems.js │ ├── getSiteListCollection.js │ ├── getSiteUrl.js │ ├── getSiteWebUrl.js │ ├── getUserProfile.js │ ├── resolvePrincipals.js │ ├── rest │ │ ├── ensureUser.js │ │ ├── getContextInfo.js │ │ ├── getCurrentUser.js │ │ ├── getListItems.js │ │ ├── getWebUrlFromPageUrl.js │ │ ├── searchPeoplePicker.js │ │ └── updateListItems.js │ ├── searchPrincipals.js │ └── updateListItems.js ├── sputils │ ├── apiFetch.js │ ├── cache.js │ ├── constants.js │ ├── doesMsgHaveError.js │ ├── fillTemplate.js │ ├── getCamlLogical.js │ ├── getDateString.js │ ├── getFullUrl.js │ ├── getMsgError.js │ ├── getNodesFromXml.js │ ├── getSPVersion.js │ ├── parseDateString.js │ ├── parseLookupFieldValue.js │ ├── parsePeopleField.js │ ├── restUtils.js │ └── xmlEscape.js └── widgets │ ├── BooleanField │ ├── BooleanField.html │ ├── BooleanField.js │ └── BooleanField.less │ ├── ChoiceField │ ├── ChoiceField.html │ ├── ChoiceField.js │ ├── ChoiceField.less │ └── ChoiceItem │ │ ├── ChoiceItem.html │ │ ├── ChoiceItem.js │ │ └── ChoiceItem.less │ ├── ColumnPicker │ └── ColumnPicker.js │ ├── ContentTypeField │ └── ContentTypeField.js │ ├── DateTimeField │ ├── DateTimeField.html │ ├── DateTimeField.js │ └── DateTimeField.less │ ├── FilterPanel │ ├── ColumnSelector │ │ ├── ColumnSelector.html │ │ ├── ColumnSelector.js │ │ ├── ColumnSelector.less │ │ └── column.html │ ├── FilterColumn │ │ ├── FilterColumn.html │ │ ├── FilterColumn.js │ │ └── FilterColumn.less │ ├── FilterColumnAttachmentsField │ │ └── FilterColumnAttachmentsField.js │ ├── FilterColumnBooleanField │ │ └── FilterColumnBooleanField.js │ ├── FilterColumnChoiceField │ │ └── FilterColumnChoiceField.js │ ├── FilterColumnContentTypeField │ │ └── FilterColumnContentTypeField.js │ ├── FilterColumnDateTimeField │ │ └── FilterColumnDateTimeField.js │ ├── FilterColumnLookupField │ │ └── FilterColumnLookupField.js │ ├── FilterColumnNumberField │ │ └── FilterColumnNumberField.js │ ├── FilterColumnTextField │ │ └── FilterColumnTextField.js │ ├── FilterColumnUserField │ │ └── FilterColumnUserField.js │ ├── FilterModel.js │ ├── FilterPanel.html │ ├── FilterPanel.js │ ├── FilterPanel.less │ └── FiltersCollection.js │ ├── ItemPicker │ └── ItemPicker.js │ ├── List │ ├── List.html │ └── List.js │ ├── ListItem │ ├── ListItem.js │ ├── ListItem.less │ ├── ListItemFull.html │ └── ListItemSimple.html │ ├── ListPicker │ └── ListPicker.js │ ├── LookupField │ ├── LookupField.html │ ├── LookupField.js │ ├── LookupField.less │ └── SelectedItem │ │ ├── SelectedItem.html │ │ ├── SelectedItem.js │ │ └── SelectedItem.less │ ├── Message │ ├── Message.html │ ├── Message.js │ └── Message.less │ ├── PeoplePicker │ ├── PeoplePicker.html │ ├── PeoplePicker.js │ ├── PeoplePicker.less │ ├── PeoplePickerPersona │ │ ├── PeoplePickerPersona.html │ │ ├── PeoplePickerPersona.js │ │ └── PeoplePickerPersona.less │ ├── PeoplePickerREST.js │ ├── PeoplePickerUserProfileModel.js │ ├── Result │ │ ├── Result.html │ │ ├── Result.js │ │ └── Result.less │ └── ResultGroup │ │ ├── ResultGroup.html │ │ └── ResultGroup.js │ ├── Persona │ ├── Persona.html │ ├── Persona.js │ └── Persona.less │ ├── PersonaCard │ ├── PersonaCard.html │ ├── PersonaCard.js │ ├── PersonaCard.less │ ├── PersonaCardActionDetails │ │ ├── PersonaCardActionDetails.html │ │ ├── PersonaCardActionDetails.js │ │ └── detailLine.html │ └── PersonaCardActions │ │ ├── PersonaCardActions.html │ │ ├── PersonaCardActions.js │ │ └── action.html │ └── TextField │ ├── TextField.html │ ├── TextField.js │ └── TextField.less ├── test ├── .jshintrc ├── index.html ├── server │ ├── mock.soap.GetList.js │ ├── mock.soap.GetListFormCollection.js │ ├── mock.soap.SearchPrincipals.js │ ├── mock.soap.WebUrlFromPageUrl.js │ ├── mock.soap.getListItems.js │ └── soapMsgs │ │ ├── error.ErrorCode.bad.xml │ │ ├── error.ErrorCode.good.xml │ │ ├── error.copyResult.xml │ │ ├── error.faultcode.xml │ │ ├── forms.GetListFormCollection.response.success.xml │ │ ├── list.GetList.response.success.xml │ │ ├── list.GetListItems.response.success.xml │ │ ├── list.GetListItemsChangesSinceTokenResponse.response.success.xml │ │ ├── login.operation.response.cannotBeNull.xml │ │ ├── login.operation.response.noError.xml │ │ ├── login.operation.response.passwordNotMatch.xml │ │ ├── people.searchPrincipals.response.success.xml │ │ └── web.webUrlFromPageUrl.response.success.xml ├── setup │ ├── jasmine-boot.js │ └── requirejs.config.js ├── specs │ ├── jquery.SPWidgets.js │ ├── jsutils │ │ └── Compose.js │ ├── models │ │ ├── ListColumnModel.js │ │ ├── ListItemModel.js │ │ └── ListModel.js │ ├── spapi │ │ ├── getList.js │ │ ├── getListColumns.js │ │ ├── getListFormCollection.js │ │ ├── getListItems.js │ │ └── searchPrincipals.js │ └── sputils │ │ ├── doesMsgHaveError.js │ │ └── getMsgError.js ├── suite.js └── test.SPWidgets.aspx └── tools ├── copy.process.minifyHtml.js └── server.js /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "eslint:recommended" 4 | ], 5 | "parserOptions": { 6 | "ecmaVersion": 6, 7 | "sourceType": "module" 8 | }, 9 | "parser": "babel-eslint", 10 | "env": { 11 | "browser": true, 12 | "node": true, 13 | "es6": true 14 | }, 15 | "plugins": [], 16 | "rules": { 17 | "no-console": 0, 18 | "no-underscore-dangle": 0, 19 | "quotes": [2, "double"] 20 | } 21 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | #* 2 | *$ 3 | *.BAK 4 | *.Z 5 | *.bak 6 | *.class 7 | *.elc 8 | *.ln 9 | *.log 10 | *.o 11 | *.obj 12 | *.olb 13 | *.old 14 | *.orig 15 | *.pyc 16 | *.pyo 17 | *.rej 18 | *~ 19 | ,* 20 | .#* 21 | .DS_Store 22 | .del-* 23 | .deployables 24 | .make.state 25 | .nse_depinfo 26 | CVS.adm 27 | RCS 28 | RCSLOG 29 | SCCS 30 | _$* 31 | .settings* 32 | .project* 33 | my.* 34 | me.* 35 | BUILD* 36 | _BUILD* 37 | vendor 38 | node_modules 39 | 40 | 41 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | SPWidgets 2 | ========= 3 | 4 | SharePoint Custom UI Widgets that make building custom User Interfaces easier. All widgets are self-contained and use the office-ui-fabric styling. 5 | 6 | ## CURRENT STATUS 7 | 8 | **THIS VERSION (3.0.0) REMAINS IN **BETA** AS THE DOCUMENTATION HAS NOT BEEN CREATED AND THERE ARE NO TEST CASES. WIDGETS, HOWEVER, HAVE PROVEN (UNDER MY OWN PROJECTS) TO BE STABLE ENOUGH FOR USAGE.** 9 | 10 | Version 3.0 was a large effort that removed jQuery and jQuery UI from the code base and instead adopted native JavaScript APIs and the new Office UI Fabric for styling. It also replaced the use of Grunt with npm script and Webpack. My goal with this project has always been to have a set of SharePoint widgets I can use in any context and independent of any framework. To that extent, v3.0 has proven to do that as I have used them directly within SharePoint as a "drop in" library as well as in a project that uses VueJS. 11 | 12 | At this point, I'm still not unsure if I will dedicate any more time to the activities I believe are needed remove the `beta` indicator from v3.0. I'm leaning towards moving these widgets to Custom Elements and fully embrace the web platform in providing framework/library agnostic widgets. 13 | 14 | 15 | ## example: 16 | 17 | ```html 18 | 19 | 20 | 21 | 22 | 23 | SP Widgets 24 | 25 | 26 | 27 | 28 | 29 | 30 | 34 | 35 | 36 | ``` 37 | 38 | To play with the available widgets, see this bin: 39 | 40 | http://jsbin.com/sesayohecu/edit?html,js,output 41 | 42 | __HOWEVER:__ note that several widgets will likely fail, since they require a SharePoint env. to access its APIs) 43 | 44 | 45 | Documentation 46 | ------------- 47 | 48 | All widgets are built into the `dist/SPWidgets.js` bundle. When using this file directly on a page (as the example above shows), all widgets will be available under `window.SPWidgets.default`. This path is an object containing all of the exported utilities and widgets (see `src/index.js` for a full list) 49 | 50 | Full documentation is TBD... 51 | 52 | Currently, I mainly use this library as a dependency into some other projects, thus documentation (outside of the jsdocs in each widgets) has not been a focus. 53 | 54 | 55 | License 56 | ------- 57 | 58 | - MIT http://www.opensource.org/licenses/mit-license.php 59 | 60 | 61 | Contributions 62 | ------------- 63 | 64 | Contributions are welcomed. 65 | 66 | 67 | 68 | Developing 69 | ---------- 70 | 71 | The `npm server:sp` task assists with running an environment that allows for connection to a real sharepoint instance to use its APIs. Note that this is achieve by staring a separate instance of Chrome with security turned off. 72 | 73 | > __IMPORTANT__: Do not use this version of chrome for regular internet browsing. 74 | 75 | Before staring development, create a file under `dev/` folder named `my.sp.dev.js`. In this file, add the following: 76 | 77 | ```javascript 78 | window._spPageContextInfo = { 79 | webServerRelativeUrl: "/sites/your-site-name", 80 | webAbsoluteUrl: "https://my-sharepoint-site-here.sharepoint.com/sites/your-site-name" 81 | }; 82 | ``` 83 | 84 | Change the above to include your SharePoint tenant information. Now run: 85 | 86 | ```bash 87 | npm run serve:sp 88 | ``` 89 | 90 | All content is now being served from the `dev/` folder. Ensure that you access your sharepoint URL defined above from the same browser instance that is displaying the development content. 91 | 92 | 93 | ## Contributors 94 | 95 | - [@TerryMooreII](https://github.com/TerryMooreII) 96 | - [@donsuhr](https://github.com/donsuhr) 97 | 98 | 99 | -------------------------------------------------------------------------------- /dev/dev.aspx: -------------------------------------------------------------------------------- 1 | <%-- SPWidgetsDemos --%> 2 | <%-- Generated by yeoman sharepoint-spa --%> 3 | <%@ Page language="C#" MasterPageFile="~masterurl/default.master" 4 | Inherits="Microsoft.SharePoint.WebPartPages.WebPartPage,Microsoft.SharePoint,Version=12.0.0.0,Culture=neutral,PublicKeyToken=71e9bce111e9429c" %> 5 | <%@ Register 6 | Tagprefix="SharePoint" 7 | Namespace="Microsoft.SharePoint.WebControls" 8 | Assembly="Microsoft.SharePoint, Version=12.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" %> 9 | <%@ Register 10 | Tagprefix="Utilities" 11 | Namespace="Microsoft.SharePoint.Utilities" 12 | Assembly="Microsoft.SharePoint, Version=12.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" %> 13 | <%@ Import Namespace="Microsoft.SharePoint" %> 14 | <%@ Register 15 | Tagprefix="WebPartPages" 16 | Namespace="Microsoft.SharePoint.WebPartPages" 17 | Assembly="Microsoft.SharePoint, Version=12.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" %> 18 | SPWidgetsDemos 19 | SPWidgetsDemos 20 | 21 | 22 | 23 | SPWidgetsDemos 24 | 25 | 26 | 27 | 33 | 34 | 35 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 |
62 |
63 | 70 |
71 |
72 |
@BUILD | @DATE
73 |
74 | 75 | 78 | 79 |
80 | -------------------------------------------------------------------------------- /dev/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Title 6 | 7 | 8 | 9 | 10 | 11 |
12 |
13 | 16 |
17 |
18 |
@BUILD | @DATE
19 |
20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /dev/index.js: -------------------------------------------------------------------------------- 1 | import "./src/setup/setup.js" -------------------------------------------------------------------------------- /dev/src/ChoiceFieldDemo/ChoiceFieldDemo.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
Status field from Task List
4 |
5 |
6 |
7 | 8 |
9 |
Single select using custom list of choices
10 |
11 |
12 |
13 | 14 |
15 |
Tasks "Status" column, but overwritten to allow selection of multiple values. "Completed" should be preselected
16 |
17 |
18 |
19 | 20 |
21 |
Choice Fields using "layout" option of "inline"
22 |
23 |
24 |
25 |
-------------------------------------------------------------------------------- /dev/src/ChoiceFieldDemo/ChoiceFieldDemo.less: -------------------------------------------------------------------------------- 1 | .demo-ChoiceFieldDemo { 2 | margin-top: 1em; 3 | 4 | .demo { 5 | margin: auto; 6 | margin-top: 1em; 7 | border-bottom: 1px solid; 8 | width: 80%; 9 | padding: 2em; 10 | } 11 | 12 | } -------------------------------------------------------------------------------- /dev/src/ColumnPickerDemo/ColumnPickerDemo.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
-------------------------------------------------------------------------------- /dev/src/ColumnPickerDemo/ColumnPickerDemo.js: -------------------------------------------------------------------------------- 1 | import Widget from "common-micro-libs/src/jsutils/Widget" 2 | import dataStore from "common-micro-libs/src/jsutils/dataStore" 3 | import objectExtend from "common-micro-libs/src/jsutils/objectExtend" 4 | import fillTemplate from "common-micro-libs/src/jsutils/fillTemplate" 5 | import parseHTML from "common-micro-libs/src/jsutils/parseHTML" 6 | 7 | import ListPickerDemoTemplate from "./ColumnPickerDemo.html" 8 | 9 | import ColumnPicker from "../../../src/widgets/ColumnPicker/ColumnPicker" 10 | 11 | const PRIVATE = dataStore.create(); 12 | 13 | /** 14 | * ColumnPickerDemo Widget 15 | * 16 | * @class ColumnPickerDemo 17 | * @extends Widget 18 | * 19 | * @param {Object} options 20 | */ 21 | const ColumnPickerDemo = Widget.extend(/** @lends ColumnPickerDemo.prototype */{ 22 | init: function (options) { 23 | var inst = { 24 | opt: objectExtend({}, ColumnPickerDemo.defaults, options) 25 | }; 26 | 27 | PRIVATE.set(this, inst); 28 | 29 | this.$ui = parseHTML( 30 | fillTemplate(this.getTemplate(), inst.opt) 31 | ).firstChild; 32 | 33 | setupDemo1.call(this); 34 | 35 | this.onDestroy(function () { 36 | 37 | 38 | // Destroy all Compose object 39 | Object.keys(inst).forEach(function (prop) { 40 | if (inst[prop]) { 41 | // Widgets 42 | if (inst[prop].destroy) { 43 | inst[prop].destroy(); 44 | 45 | // DOM events 46 | } else if (inst[prop].remove) { 47 | inst[prop].remove(); 48 | 49 | // EventEmitter events 50 | } else if (inst[prop].off) { 51 | inst[prop].off(); 52 | } 53 | 54 | inst[prop] = undefined; 55 | } 56 | }); 57 | 58 | PRIVATE.delete(this); 59 | }.bind(this)); 60 | }, 61 | 62 | /** 63 | * returns the widget's template 64 | * @return {String} 65 | */ 66 | getTemplate: function () { 67 | return ListPickerDemoTemplate; 68 | } 69 | }); 70 | 71 | function setupDemo1() { 72 | 73 | var inst = PRIVATE.get(this); 74 | 75 | inst.demo1 = ColumnPicker.create({ 76 | listName: "Tasks" 77 | }); 78 | inst.demo1.appendTo(this.getEle()); 79 | 80 | var $out = parseHTML('
').firstChild; 81 | this.getEle().appendChild($out); 82 | 83 | inst.demo1.on("item-selected", function(list){ 84 | $out.textContent = JSON.stringify(list, null, 2); 85 | }); 86 | } 87 | 88 | 89 | ColumnPickerDemo.defaults = {}; 90 | 91 | export default ColumnPickerDemo; 92 | -------------------------------------------------------------------------------- /dev/src/DateTimeFieldDemo/DateTimeFieldDemo.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
-------------------------------------------------------------------------------- /dev/src/DateTimeFieldDemo/DateTimeFieldDemo.js: -------------------------------------------------------------------------------- 1 | import Widget from "common-micro-libs/src/jsutils/Widget" 2 | import dataStore from "common-micro-libs/src/jsutils/dataStore" 3 | import objectExtend from "common-micro-libs/src/jsutils/objectExtend" 4 | import fillTemplate from "common-micro-libs/src/jsutils/fillTemplate" 5 | import parseHTML from "common-micro-libs/src/jsutils/parseHTML" 6 | import DateTimeField from "../../../src/widgets/DateTimeField/DateTimeField" 7 | import DateTimeFieldDemoTemplate from "./DateTimeFieldDemo.html" 8 | 9 | var 10 | PRIVATE = dataStore.create(), 11 | 12 | /** 13 | * Widget description 14 | * 15 | * @class DateTimeFieldDemo 16 | * @extends Widget 17 | * 18 | * @param {Object} options 19 | */ 20 | DateTimeFieldDemo = /** @lends DateTimeFieldDemo.prototype */{ 21 | init: function (options) { 22 | var inst = { 23 | opt: objectExtend({}, DateTimeFieldDemo.defaults, options) 24 | }; 25 | 26 | PRIVATE.set(this, inst); 27 | 28 | this.$ui = parseHTML( 29 | fillTemplate(DateTimeFieldDemoTemplate, inst.opt) 30 | ).firstChild; 31 | 32 | inst.uiFind = this.$ui.querySelector.bind(this.$ui); 33 | 34 | setupDemo1.call(this); 35 | 36 | this.onDestroy(function () { 37 | Object.keys(inst).forEach(function(prop){ 38 | if (inst[prop] && inst[prop].destroy) { 39 | inst[prop].destroy(); 40 | } 41 | }); 42 | PRIVATE.delete(this); 43 | }.bind(this)); 44 | } 45 | }; 46 | 47 | function setupDemo1(){ 48 | var inst = PRIVATE.get(this); 49 | var $demo1Cntr = inst.uiFind("#DateTimeField_demo1"); 50 | 51 | inst.demo1 = DateTimeField.create({ 52 | column: { 53 | DisplayName: "Due Date", 54 | Description: "the date the item is due" 55 | } 56 | }); 57 | inst.demo1.appendTo($demo1Cntr); 58 | } 59 | 60 | DateTimeFieldDemo = Widget.extend(DateTimeFieldDemo); 61 | DateTimeFieldDemo.defaults = {}; 62 | 63 | export default DateTimeFieldDemo; 64 | -------------------------------------------------------------------------------- /dev/src/ListPickerDemo/ListPickerDemo.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
-------------------------------------------------------------------------------- /dev/src/ListPickerDemo/ListPickerDemo.js: -------------------------------------------------------------------------------- 1 | import Widget from "common-micro-libs/src/jsutils/Widget" 2 | import dataStore from "common-micro-libs/src/jsutils/dataStore" 3 | import objectExtend from "common-micro-libs/src/jsutils/objectExtend" 4 | import fillTemplate from "common-micro-libs/src/jsutils/fillTemplate" 5 | import parseHTML from "common-micro-libs/src/jsutils/parseHTML" 6 | 7 | import ListPickerDemoTemplate from "./ListPickerDemo.html" 8 | 9 | import ListPicker from "../../../src/widgets/ListPicker/ListPicker" 10 | 11 | const PRIVATE = dataStore.create(); 12 | 13 | /** 14 | * ListPickerDemo Widget 15 | * 16 | * @class ListPickerDemo 17 | * @extends Widget 18 | * 19 | * @param {Object} options 20 | */ 21 | const ListPickerDemo = Widget.extend(/** @lends ListPickerDemo.prototype */{ 22 | init: function (options) { 23 | var inst = { 24 | opt: objectExtend({}, ListPickerDemo.defaults, options) 25 | }; 26 | 27 | PRIVATE.set(this, inst); 28 | 29 | this.$ui = parseHTML( 30 | fillTemplate(this.getTemplate(), inst.opt) 31 | ).firstChild; 32 | 33 | setupDemo1.call(this); 34 | 35 | this.onDestroy(function () { 36 | 37 | 38 | // Destroy all Compose object 39 | Object.keys(inst).forEach(function (prop) { 40 | if (inst[prop]) { 41 | // Widgets 42 | if (inst[prop].destroy) { 43 | inst[prop].destroy(); 44 | 45 | // DOM events 46 | } else if (inst[prop].remove) { 47 | inst[prop].remove(); 48 | 49 | // EventEmitter events 50 | } else if (inst[prop].off) { 51 | inst[prop].off(); 52 | } 53 | 54 | inst[prop] = undefined; 55 | } 56 | }); 57 | 58 | PRIVATE.delete(this); 59 | }.bind(this)); 60 | }, 61 | 62 | /** 63 | * returns the widget's template 64 | * @return {String} 65 | */ 66 | getTemplate: function () { 67 | return ListPickerDemoTemplate; 68 | } 69 | }); 70 | 71 | function setupDemo1() { 72 | 73 | var inst = PRIVATE.get(this); 74 | 75 | inst.demo1 = ListPicker.create(); 76 | inst.demo1.appendTo(this.getEle()); 77 | 78 | var $out = parseHTML('
').firstChild; 79 | this.getEle().appendChild($out); 80 | 81 | inst.demo1.on("item-selected", function(list){ 82 | $out.textContent = JSON.stringify(list, null, 2); 83 | }); 84 | } 85 | 86 | 87 | ListPickerDemo.defaults = {}; 88 | 89 | export default ListPickerDemo; 90 | -------------------------------------------------------------------------------- /dev/src/LookupFieldDemo/LookupFieldDemo.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 |
Type of column set to LookupMulti
5 |
6 |
7 |
Type of column set to Lookup
8 |
9 | 10 |
-------------------------------------------------------------------------------- /dev/src/LookupFieldDemo/LookupFieldDemo.js: -------------------------------------------------------------------------------- 1 | import Widget from "common-micro-libs/src/jsutils/Widget" 2 | import dataStore from "common-micro-libs/src/jsutils/dataStore" 3 | import objectExtend from "common-micro-libs/src/jsutils/objectExtend" 4 | import fillTemplate from "common-micro-libs/src/jsutils/fillTemplate" 5 | import parseHTML from "common-micro-libs/src/jsutils/parseHTML" 6 | import LookupField from "../../../src/widgets/LookupField/LookupField" 7 | import getListColumns from "../../../src/spapi/getListColumns" 8 | import LookupFieldDemoTemplate from "./LookupFieldDemo.html" 9 | 10 | import common from "../setup/common"; 11 | 12 | var 13 | PRIVATE = dataStore.create(), 14 | 15 | /** 16 | * Widget description 17 | * 18 | * @class LookupFieldDemo 19 | * @extends Widget 20 | * 21 | * @param {Object} options 22 | */ 23 | LookupFieldDemo = /** @lends LookupFieldDemo.prototype */{ 24 | init: function (options) { 25 | var inst = { 26 | opt: objectExtend({}, LookupFieldDemo.defaults, options) 27 | }; 28 | 29 | PRIVATE.set(this, inst); 30 | 31 | this.$ui = parseHTML( 32 | fillTemplate(LookupFieldDemoTemplate, inst.opt) 33 | ).firstChild; 34 | 35 | setupDemo1.call(this); 36 | setupDemo2.call(this); 37 | 38 | this.onDestroy(function () { 39 | Object.keys(inst).forEach(function(prop){ 40 | if (inst[prop] && inst[prop].destroy) { 41 | inst[prop].destroy(); 42 | } 43 | }); 44 | PRIVATE.delete(this); 45 | }.bind(this)); 46 | } 47 | }; 48 | 49 | function setupDemo1() { 50 | var inst = PRIVATE.get(this); 51 | 52 | getListColumns({ 53 | listName: "Tasks", 54 | webURL: common.getWebURL() 55 | }) 56 | .then(function(columns){ 57 | inst.demo1 = LookupField.create({ 58 | column: columns.getColumn("Predecessors") 59 | }); 60 | inst.demo1.appendTo(this.getEle().querySelector("#lookupFieldDemo_1")); 61 | 62 | }.bind(this))["catch"](function(e){ 63 | console.log(e); // jshint ignore:line 64 | }); 65 | } 66 | 67 | 68 | function setupDemo2() { 69 | var inst = PRIVATE.get(this); 70 | 71 | getListColumns({ 72 | listName: "Tasks", 73 | webURL: common.getWebURL() 74 | }) 75 | .then(function(columns){ 76 | let col = columns.getColumn("Predecessors"); 77 | col.Type = "Lookup"; 78 | 79 | inst.demo1 = LookupField.create({ 80 | column: columns.getColumn("Predecessors") 81 | }); 82 | inst.demo1.appendTo(this.getEle().querySelector("#lookupFieldDemo_2")); 83 | 84 | }.bind(this))["catch"](function(e){ 85 | console.log(e); // jshint ignore:line 86 | }); 87 | } 88 | 89 | LookupFieldDemo = Widget.extend(LookupFieldDemo); 90 | LookupFieldDemo.defaults = {}; 91 | 92 | export default LookupFieldDemo; 93 | -------------------------------------------------------------------------------- /dev/src/PersonaCardDemo/PersonaCardDemo.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
-------------------------------------------------------------------------------- /dev/src/PersonaCardDemo/PersonaCardDemo.js: -------------------------------------------------------------------------------- 1 | import Widget from "common-micro-libs/src/jsutils/Widget" 2 | import dataStore from "common-micro-libs/src/jsutils/dataStore" 3 | import objectExtend from "common-micro-libs/src/jsutils/objectExtend" 4 | import fillTemplate from "common-micro-libs/src/jsutils/fillTemplate" 5 | import parseHTML from "common-micro-libs/src/jsutils/parseHTML" 6 | 7 | import getUserProfile from "../../../src/spapi/getUserProfile" 8 | import PeoplePicker from "../../../src/widgets/PeoplePicker/PeoplePicker" 9 | import PersonaCard from "../../../src/widgets/PersonaCard/PersonaCard" 10 | 11 | import PersonaCardDemoTemplate from "./PersonaCardDemo.html" 12 | import "./PersonaCardDemo.less" 13 | 14 | //========================================================================== 15 | const PRIVATE = dataStore.create(); 16 | 17 | 18 | /** 19 | * PersonaCardDemo Widget 20 | * 21 | * @class PersonaCardDemo 22 | * @extends Widget 23 | * 24 | * @param {Object} options 25 | */ 26 | const PersonaCardDemo = Widget.extend(/** @lends PersonaCardDemo.prototype */{ 27 | init(options) { 28 | var inst = { 29 | opt: objectExtend({}, this.getFactory().defaults, options) 30 | }; 31 | 32 | PRIVATE.set(this, inst); 33 | 34 | let $ui = this.$ui = this.getTemplate(); 35 | 36 | if (typeof $ui === "string") { 37 | $ui = this.$ui = parseHTML(fillTemplate($ui, inst)).firstChild; 38 | } 39 | 40 | let uiFind = $ui.querySelector.bind($ui); 41 | let $cardHolder = uiFind(".card"); 42 | 43 | inst.peoplePicker = PeoplePicker.create({ 44 | allowMultiples: false, 45 | showSelected: false 46 | }); 47 | inst.peoplePicker.appendTo(uiFind(".picker")); 48 | 49 | inst.peoplePicker.on("select", (/** @type PeoplePickerUserProfileModel */person) => { 50 | inst.peoplePicker.hideResults(); 51 | 52 | if (inst.personaCard) { 53 | inst.personaCard.destroy(); 54 | inst.personaCard = null; 55 | } 56 | 57 | getUserProfile({ 58 | accountName: person.AccountName, 59 | webURL: person.webURL 60 | }).then((personProfile) => { 61 | inst.personaCard = PersonaCard.create({ 62 | userProfile: personProfile 63 | }); 64 | inst.personaCard.appendTo($cardHolder); 65 | }); 66 | }); 67 | 68 | 69 | this.onDestroy(() => { 70 | // Destroy all Compose object 71 | Object.keys(inst).forEach(function (prop) { 72 | if (inst[prop]) { 73 | [ 74 | "destroy", // Compose 75 | "remove", // DOM Events Listeners 76 | "off" // EventEmitter Listeners 77 | ].some((method) => { 78 | if (inst[prop][method]) { 79 | inst[prop][method](); 80 | return true; 81 | } 82 | }); 83 | 84 | inst[prop] = undefined; 85 | } 86 | }); 87 | 88 | PRIVATE['delete'](this); 89 | }); 90 | }, 91 | 92 | /** 93 | * returns the widget's template 94 | * @return {String} 95 | */ 96 | getTemplate(){ 97 | return PersonaCardDemoTemplate; 98 | } 99 | }); 100 | 101 | PersonaCardDemo.defaults = {}; 102 | 103 | export default PersonaCardDemo; 104 | -------------------------------------------------------------------------------- /dev/src/PersonaCardDemo/PersonaCardDemo.less: -------------------------------------------------------------------------------- 1 | .spwidgets-PersonaCardDemo { 2 | margin-top: 2em; 3 | padding: 0 3em; 4 | 5 | .picker, 6 | .card { 7 | display: inline-block; 8 | width: ~"calc(49% - 4em)"; 9 | box-sizing: border-box; 10 | vertical-align: top; 11 | padding: 1em; 12 | } 13 | } -------------------------------------------------------------------------------- /dev/src/SPFilterPanelDemo/SPFilterPanelDemo.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
-------------------------------------------------------------------------------- /dev/src/SPFilterPanelDemo/SPFilterPanelDemo.js: -------------------------------------------------------------------------------- 1 | import Widget from "common-micro-libs/src/jsutils/Widget"; 2 | import dataStore from "common-micro-libs/src/jsutils/dataStore"; 3 | import objectExtend from "common-micro-libs/src/jsutils/objectExtend"; 4 | import fillTemplate from "common-micro-libs/src/jsutils/fillTemplate"; 5 | import parseHTML from "common-micro-libs/src/jsutils/parseHTML"; 6 | import xmlEscape from "common-micro-libs/src/jsutils/xmlEscape" 7 | import FilterPanel from "../../../src/widgets/FilterPanel/FilterPanel"; 8 | import SPFilterPanelDemoTemplate from "./SPFilterPanelDemo.html"; 9 | 10 | var PRIVATE = dataStore.create(); 11 | 12 | /** 13 | * Widget description 14 | * 15 | * @class SPFilterPanelDemo 16 | * @extends Widget 17 | */ 18 | var SPFilterPanelDemo = /** @lends SPFilterPanelDemo.prototype */{ 19 | init: function (options) { 20 | var inst = { 21 | opt: objectExtend({}, SPFilterPanelDemo.defaults, options) 22 | }; 23 | 24 | PRIVATE.set(this, inst); 25 | 26 | this.$ui = parseHTML( 27 | fillTemplate(SPFilterPanelDemoTemplate, inst.opt) 28 | ).firstChild; 29 | 30 | inst.uiFind = this.$ui.querySelector.bind(this.$ui); 31 | 32 | setupDemo1.call(this); 33 | 34 | this.onDestroy(function () { 35 | PRIVATE.delete(this); 36 | }.bind(this)); 37 | } 38 | }; 39 | 40 | function setupDemo1(){ 41 | var inst = PRIVATE.get(this); 42 | var filterPanel = FilterPanel.create({listName: "Tasks", bodyHeight: `${window.innerHeight - 200}px`}); 43 | var cntr = inst.uiFind("#spfilterpaneldemo_1"); 44 | var out = inst.uiFind("#spfilterpaneldemo_1_out"); 45 | 46 | filterPanel.appendTo(cntr); 47 | filterPanel.getEle().style.width = "50%"; 48 | filterPanel.getEle().style.marginLeft = "20%"; 49 | 50 | filterPanel.on("*", function(evName){ 51 | out.appendChild( 52 | parseHTML('
Event: ' + evName + '
') 53 | ); 54 | 55 | if (evName === 'find') { 56 | out.appendChild( 57 | parseHTML( 58 | '
' + JSON.stringify(
59 |                         filterPanel.getFilters().slice(),
60 |                         null,
61 |                         2
62 |                     ) + "\r\n URL PARAMS: " +
63 |                     filterPanel.getFilters().toURLParams() +
64 |                     "\r\n URL PARAMS W/abreviated keys: " +
65 |                     filterPanel.getFilters().toURLParams({
66 |                         stringifyProperties: [
67 |                             {column: "c"},
68 |                             {values: 'v'},
69 |                             {logicalOperator: 'lO'}
70 |                         ]
71 |                     }) +
72 |                     "\r\n toCAMLQuery: " +
73 |                     xmlEscape.escape(filterPanel.getFilters().toCAMLQuery()) +
74 |                     '
' 75 | ) 76 | ); 77 | } 78 | }); 79 | 80 | window.filterPanel1 = filterPanel; 81 | console.info("window.filterPanel1 created"); 82 | 83 | this.onDestroy(function() { 84 | filterPanel.destroy(); 85 | window.filterPanel1 = null; 86 | }); 87 | } 88 | 89 | SPFilterPanelDemo = Widget.extend(SPFilterPanelDemo); 90 | SPFilterPanelDemo.defaults = {}; 91 | 92 | export default SPFilterPanelDemo; 93 | -------------------------------------------------------------------------------- /dev/src/SPPeoplePickerDemo/SPPeoplePickerDemo.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 |
5 |

6 | Instance of People Picker does not allow multiples to be selected. Search for suggestions occur as soon as a value is typed. 7 |

8 |
9 | 10 | 11 |
12 |
-------------------------------------------------------------------------------- /dev/src/SPPeoplePickerDemo/SPPeoplePickerDemo.js: -------------------------------------------------------------------------------- 1 | import Widget from "common-micro-libs/src/jsutils/Widget"; 2 | import parseHTML from "common-micro-libs/src/jsutils/parseHTML"; 3 | import domFind from "common-micro-libs/src/domutils/domFind"; 4 | import SPPeoplePicker from "../../../src/widgets/PeoplePicker/PeoplePicker"; 5 | import template from "./SPPeoplePickerDemo.html"; 6 | 7 | import common from "../setup/common"; 8 | 9 | var $CONSOLE_LOG_OUT, 10 | log = function(data){ 11 | $CONSOLE_LOG_OUT.appendChild( 12 | parseHTML('
' + data + '
') 13 | ); 14 | }; 15 | 16 | export default Widget.extend({ 17 | init: function(){ 18 | this.$ui = parseHTML(template).firstChild; 19 | $CONSOLE_LOG_OUT = domFind(this.$ui, "#SPPeoplePicker_console_out").shift(); 20 | 21 | initDemo1.call(this); 22 | initDemo2.call(this); 23 | } 24 | }); 25 | 26 | function initDemo1() { 27 | var picker = SPPeoplePicker.create({ 28 | webURL: common.getWebURL() 29 | }); 30 | picker.appendTo(domFind(this.$ui, "#SPPeoplePickerDemo1")[0]); 31 | } 32 | 33 | function initDemo2() { 34 | var picker = SPPeoplePicker.create({ 35 | allowMultiples: false, 36 | minLength: 0, 37 | webURL: common.getWebURL() 38 | }); 39 | picker.appendTo(domFind(this.$ui, "#SPPeoplePickerDemo2")[0]); 40 | 41 | picker.on("remove", function(person){ 42 | log("Event Triggered: remove:\n" + 43 | JSON.stringify(person || {}, null, 2) 44 | .replace("<", "<") 45 | .replace(">", ">") 46 | ); 47 | }); 48 | 49 | picker.on("select", function(person){ 50 | log("Event Triggered: select:\n" + 51 | JSON.stringify(person || {}, null, 2) 52 | .replace("<", "<") 53 | .replace(">", ">") 54 | ); 55 | }); 56 | } 57 | 58 | -------------------------------------------------------------------------------- /dev/src/setup/common.js: -------------------------------------------------------------------------------- 1 | 2 | export default { 3 | getWebURL: function(){ 4 | return location.search.substr(1); 5 | } 6 | }; -------------------------------------------------------------------------------- /dev/src/setup/setup.js: -------------------------------------------------------------------------------- 1 | import page from "page" 2 | import parseHTML from "common-micro-libs/src/jsutils/parseHTML" 3 | 4 | import SPPeoplePickerDemo from '../SPPeoplePickerDemo/SPPeoplePickerDemo' 5 | import SPFilterPanelDemo from '../SPFilterPanelDemo/SPFilterPanelDemo' 6 | import LookupFieldDemo from '../LookupFieldDemo/LookupFieldDemo' 7 | import ListPickerDemo from '../ListPickerDemo/ListPickerDemo' 8 | import ColumnPickerDemo from '../ColumnPickerDemo/ColumnPickerDemo' 9 | import DateTimeFieldDemo from '../DateTimeFieldDemo/DateTimeFieldDemo' 10 | import ChoiceFieldDemo from '../ChoiceFieldDemo/ChoiceFieldDemo' 11 | import PersonaCardDemo from '../PersonaCardDemo/PersonaCardDemo' 12 | 13 | 14 | //============================================================== 15 | 16 | 17 | let currentDemo; 18 | let demoCntr = document.querySelector("#spwidgets_dev_demo"); 19 | let demoSelector = document.querySelector("#demo_selector"); 20 | let demoComponents = { 21 | SPPeoplePickerDemo, 22 | SPFilterPanelDemo, 23 | LookupFieldDemo, 24 | ListPickerDemo, 25 | ColumnPickerDemo, 26 | DateTimeFieldDemo, 27 | ChoiceFieldDemo, 28 | PersonaCardDemo 29 | }; 30 | 31 | Object.keys(demoComponents).forEach((demoName) => { 32 | demoSelector.appendChild(parseHTML('')); 33 | }); 34 | 35 | demoSelector.addEventListener("change", function(){ 36 | location.hash = `#/${demoSelector.value}`; 37 | }); 38 | 39 | page.base("/#"); 40 | page("/:demoName", function(ctx){ 41 | if (currentDemo) { 42 | currentDemo.destroy(); 43 | currentDemo = null; 44 | } 45 | 46 | let demoName = ctx.params.demoName; 47 | 48 | if (demoComponents[demoName]) { 49 | currentDemo = demoComponents[demoName].create(); 50 | currentDemo.appendTo(demoCntr); 51 | } 52 | }); 53 | page.start(); 54 | 55 | demoSelector.style.display = ""; 56 | 57 | -------------------------------------------------------------------------------- /jsdoc.conf.json: -------------------------------------------------------------------------------- 1 | { 2 | "source": { 3 | "include": [ 4 | "./src/models", 5 | "./src/collections", 6 | "./src/widgets", 7 | "./node_modules/common-micro-libs/src" 8 | ], 9 | "includePattern": ".+\\.js(doc)?$", 10 | "excludePattern": "(^|\\/|\\\\)_" 11 | }, 12 | "opts": { 13 | "recurse": true, 14 | "destination": "./_DOCS/" 15 | } 16 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "SPWidgets", 3 | "version": "3.0.0-beta.1", 4 | "description": "SharePoint widgets and tools for custom client side web solutions", 5 | "main": "src/index.js", 6 | "scripts": { 7 | "open": "opener http://127.0.0.1:8184", 8 | "serve": "webpack-dev-server --config node_modules/project-base/configs/webpack.dev.js --progress --hot --color --port 8184 --entry ./dev/index.js", 9 | "serve:sp": "opener chrome.exe --user-data-dir=\"C:/chromeDevSession\" --disable-web-security http://127.0.0.1:8184 && npm run serve -- --no-open", 10 | "build": "webpack --config node_modules/project-base/configs/webpack.dev.js", 11 | "build:ie": "webpack --config node_modules/project-base/configs/webpack.prod.js --entry ./dev/index.js --output-path ./dev --output-filename ie-test-bundle.js", 12 | "build:prod": "webpack --config node_modules/project-base/configs/webpack.prod.js", 13 | "build:prod:min": "webpack --config node_modules/project-base/configs/webpack.prod.uglify.js", 14 | "build:apiDocs": "jsdoc -c node_modules/project-base/configs/jsdoc.conf.json", 15 | "dist": "npm run build:prod&&npm run build:prod:min", 16 | "setup:dev": "node node_modules/project-base/scripts/create-dev", 17 | "test": "tape -r @std/esm test/**/*.js", 18 | "lint": "eslint src/**/*.js", 19 | "lint:fix": "eslint src/**/*.js --fix" 20 | }, 21 | "repository": { 22 | "type": "git", 23 | "url": "git@github.com:purtuga/SPWidgets.git" 24 | }, 25 | "author": { 26 | "name": "Paul Tavares" 27 | }, 28 | "homepage": "http://purtuga.github.io/SPWidgets", 29 | "license": "MIT", 30 | "dependencies": { 31 | "common-micro-libs": "purtuga/common-micro-libs#release/v2x", 32 | "flatpickr": "^1.9.1", 33 | "observable-data": "github:purtuga/observable-data#release/v2x" 34 | }, 35 | "devDependencies": { 36 | "opener": "^1.4.1", 37 | "page": "^1.7.1", 38 | "project-base": "github:purtuga/project-base#beta/v2.0.0" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/collections/ListColumnsCollection.js: -------------------------------------------------------------------------------- 1 | import objectExtend from "common-micro-libs/src/jsutils/objectExtend" 2 | import Collection from "observable-data/src/ObservableArray" 3 | import dataStore from "common-micro-libs/src/jsutils/dataStore" 4 | 5 | 6 | var PRIVATE = dataStore.create(); 7 | 8 | /** 9 | * A collection of List Columns 10 | * 11 | * @class ListColumnsCollection 12 | * @extends Collection 13 | * 14 | * @param {Array} itemsList 15 | * @param {Object} options 16 | * @param {Object} options.listDef 17 | */ 18 | export default Collection.extend({ 19 | init: function(itemsList, options){ 20 | Collection.prototype.init.call(this, itemsList); 21 | 22 | var opt = objectExtend({}, { 23 | listDef: null 24 | }, options); 25 | 26 | PRIVATE.set(this, opt); 27 | }, 28 | 29 | /** 30 | * Returns an object with the definition for the given column 31 | * 32 | * @param {String} name 33 | * Name of column - external or internal. 34 | * 35 | * @return {ListColumnModel} 36 | */ 37 | getColumn: function(name){ 38 | var col; 39 | this.some(function(thisCol){ 40 | if (thisCol.Name === name || thisCol.DisplayName === name || thisCol.StaticName === name){ 41 | col = thisCol; 42 | } 43 | }); 44 | return col; 45 | }, 46 | 47 | /** 48 | * returns the ListModel for the list for which the collection was requested. 49 | * 50 | * @return {ListModel} 51 | */ 52 | getList: function(){ 53 | return PRIVATE.get(this).listDef; 54 | } 55 | 56 | }); 57 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import getMsgError from "./sputils/getMsgError" 2 | import doesMsgHaveError from "./sputils/doesMsgHaveError" 3 | import xmlEscape from "./sputils/xmlEscape" 4 | import fillTemplate from "./sputils/fillTemplate" 5 | import getCamlLogical from "./sputils/getCamlLogical" 6 | import getSPVersion from "./sputils/getSPVersion" 7 | import parseDateString from "./sputils/parseDateString" 8 | import parseLookupFieldValue from "./sputils/parseLookupFieldValue" 9 | import getDateString from "./sputils/getDateString" 10 | import getNodesFromXml from "./sputils/getNodesFromXml" 11 | import getList from "./spapi/getList" 12 | import getListColumns from "./spapi/getListColumns" 13 | import getListFormCollection from "./spapi/getListFormCollection" 14 | import getListItems from "./spapi/getListItems" 15 | import getSiteListCollection from "./spapi/getSiteListCollection" 16 | import getSiteWebUrl from "./spapi/getSiteWebUrl" 17 | import getUserProfile from "./spapi/getUserProfile" 18 | import resolvePrincipals from "./spapi/resolvePrincipals" 19 | import searchPrincipals from "./spapi/searchPrincipals" 20 | import updateListItems from "./spapi/updateListItems" 21 | 22 | import ChoiceField from "./widgets/ChoiceField/ChoiceField" 23 | import DateTimeField from "./widgets/DateTimeField/DateTimeField" 24 | import List from "./widgets/List/List" 25 | import ListItem from "./widgets/ListItem/ListItem" 26 | import LookupField from "./widgets/LookupField/LookupField" 27 | import Message from "./widgets/Message/Message" 28 | import PeoplePicker from "./widgets/PeoplePicker/PeoplePicker" 29 | import Persona from "./widgets/Persona/Persona" 30 | import PersonaCard from "./widgets/PersonaCard/PersonaCard" 31 | import TextField from "./widgets/TextField/TextField" 32 | 33 | export default { 34 | getMsgError: getMsgError, 35 | doesMsgHaveError: doesMsgHaveError, 36 | xmlEscape: xmlEscape, 37 | fillTemplate: fillTemplate, 38 | getCamlLogical: getCamlLogical, 39 | getSPVersion: getSPVersion, 40 | parseDateString: parseDateString, 41 | parseLookupFieldValue: parseLookupFieldValue, 42 | getDateString: getDateString, 43 | getNodesFromXml: getNodesFromXml, 44 | getList: getList, 45 | getListColumns: getListColumns, 46 | getListFormCollection: getListFormCollection, 47 | getListItems: getListItems, 48 | getSiteListCollection: getSiteListCollection, 49 | getSiteWebUrl: getSiteWebUrl, 50 | getUserProfile: getUserProfile, 51 | resolvePrincipals: resolvePrincipals, 52 | searchPrincipals: searchPrincipals, 53 | updateListItems: updateListItems, 54 | 55 | ChoiceField: ChoiceField, 56 | DateTimeField: DateTimeField, 57 | List: List, 58 | ListItem: ListItem, 59 | LookupField: LookupField, 60 | Message: Message, 61 | PeoplePicker: PeoplePicker, 62 | Persona: Persona, 63 | PersonaCard: PersonaCard, 64 | TextField: TextField 65 | }; 66 | -------------------------------------------------------------------------------- /src/models/ListItemModel.js: -------------------------------------------------------------------------------- 1 | import ObservableObject from "observable-data/src/ObservableObject" 2 | import objectExtend from "common-micro-libs/src/jsutils/objectExtend" 3 | import dataStore from "common-micro-libs/src/jsutils/dataStore" 4 | import parseHTML from "common-micro-libs/src/jsutils/parseHTML" 5 | import domFind from "common-micro-libs/src/domutils/domFind" 6 | 7 | 8 | var PRIVATE = dataStore.stash; 9 | 10 | /** 11 | * Model for SharePoint List Items (rows). Object returned will include all of 12 | * the properties that were given on input (row). In addition, if `options` 13 | * are provided on input and those have a `CAMLViewFields`, then the model 14 | * will have one attribute for each - even if those were not included in the 15 | * `itemData` (SharePoint does not return empty attributes) 16 | * 17 | * @class ListItemModel 18 | * @extends ObservableObject 19 | * 20 | * @param {Object} itemData 21 | * An object with the properties for the model 22 | * @param {Object} [options] 23 | * An object with the options used to get the row from SP 24 | * 25 | */ 26 | const ListItemModel = ObservableObject.extend(/** @lends ListItemModel.prototype */{ 27 | init: function(itemData, options){ 28 | if (PRIVATE.has(this)) { 29 | return; 30 | } 31 | 32 | ObservableObject.prototype.init.call(this, itemData, options); 33 | 34 | var opt = objectExtend({}, { 35 | listName: "", 36 | webURL: "" 37 | }, options); 38 | 39 | // If options has CAMLViewFields, then ensure the model has 40 | // those fields defined as attributes 41 | if (opt && opt.CAMLViewFields) { 42 | domFind(parseHTML(opt.CAMLViewFields), "FieldRef").forEach(fieldEle => { 43 | let fieldName = fieldEle.getAttribute("Name"); 44 | if (fieldName && !this.hasOwnProperty(fieldName)) { 45 | this[fieldName] = ""; 46 | } 47 | }); 48 | } 49 | 50 | PRIVATE.set(this, opt); 51 | this.onDestroy(() => PRIVATE["delete"](this)); 52 | }, 53 | 54 | /** 55 | * Returns an object with the `listName` and `webURL` 56 | * attributes needed to retrieve list information. Data 57 | * will only be available if provided on input when model 58 | * was initialized. 59 | * 60 | * @returns {Object} 61 | */ 62 | getListInfo: function(){ 63 | return PRIVATE.get(this); 64 | } 65 | }); 66 | 67 | export default ListItemModel -------------------------------------------------------------------------------- /src/spapi/getListContentTypes.js: -------------------------------------------------------------------------------- 1 | import objectExtend from "common-micro-libs/src/jsutils/objectExtend" 2 | import getSiteWebUrl from "./getSiteWebUrl" 3 | import cache from "../sputils/cache"; 4 | import getNodesFromXml from "../sputils/getNodesFromXml"; 5 | import apiFetch from "../sputils/apiFetch"; 6 | 7 | //======================================================================== 8 | 9 | /** 10 | * Retrieves the list of content types for a given list. 11 | * 12 | * @param {Object} options 13 | * 14 | * @param {String} options.listName 15 | * list Name or ID 16 | * 17 | * @param {String} [options.webURL=currentSite] 18 | * The url to the site where list is located. Defaults to current site. 19 | * 20 | * @param {Boolean} [options.cache=true] 21 | * If true (default), content will be cached. 22 | * 23 | * @return {Promise} 24 | * Resolved with an array-of-object with the content types. 25 | * 26 | * @see https://msdn.microsoft.com/en-us/library/lists.lists.getlistcontenttypes.aspx 27 | * 28 | * @example Content type object 29 | * 30 | * { 31 | * Description: "Track a work item that you or your team needs to complete.", 32 | * ID: "0x010800719988A683552B489A4C7F1E2288B466", 33 | * Name: "Task", 34 | * Scope: "https://tenant.sharepoint.com/sites/sitea/Lists/Tasks", 35 | * Version: "16" 36 | * } 37 | */ 38 | const getListContentTypes = function(options) { 39 | let opt = objectExtend({}, getListContentTypes.defaults, options); 40 | 41 | return getSiteWebUrl(opt.webURL).then(function(webURL) { 42 | opt.cacheKey = opt.webURL + "?getListContentTypes=" + opt.listName; 43 | 44 | // IF cache was requested and we have it cached, resolve now 45 | if (opt.cache && cache.isCached(opt.cacheKey)) { 46 | return JSON.parse(JSON.stringify(cache.get(opt.cacheKey))); 47 | } 48 | 49 | return apiFetch(webURL + "_vti_bin/Lists.asmx", { 50 | method: "POST", 51 | headers: { 52 | "Content-Type": "text/xml;charset=UTF-8" 53 | }, 54 | body: "" + 55 | "" + 56 | opt.listName + "" 57 | }) 58 | .then(function(response){ 59 | let contentTypes = getNodesFromXml({ 60 | xDoc: response.content, 61 | nodeName: "ContentType" 62 | }); 63 | 64 | if (opt.cache) { 65 | cache(opt.cacheKey, contentTypes); 66 | } 67 | 68 | return JSON.parse(JSON.stringify(contentTypes)); 69 | }); 70 | }); 71 | }; 72 | 73 | getListContentTypes.defaults = { 74 | listName: "", 75 | webURL: "", 76 | cache: true 77 | }; 78 | 79 | export default getListContentTypes; 80 | -------------------------------------------------------------------------------- /src/spapi/rest/ensureUser.js: -------------------------------------------------------------------------------- 1 | import objectExtend from "common-micro-libs/src/jsutils/objectExtend" 2 | import getContextInfo from "./getContextInfo" 3 | import apiFetch from "../../sputils/apiFetch" 4 | import { 5 | getRestHeaders, 6 | processUserInfo } from "../../sputils/restUtils" 7 | import cache from "../../sputils/cache" 8 | import UserProfileModel from "../../models/UserProfileModel" 9 | 10 | //=========================================================================== 11 | 12 | /** 13 | * Ensures that a given user is added to the current site, which then returns the 14 | * `ID` of that user as present in the User information table. 15 | * 16 | * @param options 17 | * @param {String} options.logonName 18 | * @param {String} [options.webURL] 19 | * @param {Boolean} [options.cache=true] 20 | * @param {UserProfileModel} [options.UserProfileModel=UserProfileModel] 21 | * 22 | * @return {Promise} 23 | * 24 | * @see https://msdn.microsoft.com/en-us/library/office/dn499819%28v=office.15%29.aspx?f=255&MSPPError=-2147217396#bk_WebEnsureUser 25 | * @see https://msdn.microsoft.com/en-us/library/office/dn531432.aspx#bk_User 26 | */ 27 | export default function ensureUser (options) { 28 | const opt = objectExtend({ 29 | logonName: "", 30 | webURL: "", 31 | cache: true, 32 | UserProfileModel 33 | }, options); 34 | 35 | return getContextInfo(opt.webURL) 36 | .then(contextInfo => { 37 | opt.webURL = contextInfo.WebFullUrl + "/"; 38 | const cacheKey = `${opt.webURL}?${ opt.logonName }`; 39 | 40 | if (opt.cache && cache.get(cacheKey)) { 41 | return cache.get(cacheKey).then(response => processApiResponse(response, opt)); 42 | } 43 | else if (!opt.cache) { 44 | cache.clear(cacheKey); 45 | } 46 | 47 | const apiRequest = apiFetch(`${ contextInfo.WebFullUrl }/_api/web/ensureuser`, { 48 | method: "POST", 49 | headers: getRestHeaders(contextInfo, true), 50 | body: JSON.stringify({ logonName: opt.logonName }) 51 | }); 52 | 53 | if (opt.cache) { 54 | cache.set(cacheKey, apiRequest); 55 | 56 | apiRequest.catch(e => { 57 | cache.clear(cacheKey); 58 | console.log(e); // eslint-disable-line 59 | }); 60 | } 61 | 62 | return apiRequest.then(response => processApiResponse(response, opt)); 63 | }); 64 | } 65 | 66 | function processApiResponse(response, opt) { 67 | return opt.UserProfileModel.create(processUserInfo(response.content.d), opt); 68 | } 69 | 70 | // SAMPLE RESPONSE: 71 | // 72 | // { 73 | // "d": { 74 | // "__metadata": { 75 | // "id": "https://tenant.sharepoint.com/sites/siteName/_api/Web/GetUserById(11)", 76 | // "uri": "https://tenant.sharepoint.com/sites/siteName/_api/Web/GetUserById(11)", 77 | // "type": "SP.User" 78 | // }, 79 | // "Alerts": {"__deferred": {"uri": "https://tenant.sharepoint.com/sites/siteName/_api/Web/GetUserById(11)/Alerts"}}, 80 | // "Groups": {"__deferred": {"uri": "https://tenant.sharepoint.com/sites/siteName/_api/Web/GetUserById(11)/Groups"}}, 81 | // "Id": 11, 82 | // "IsHiddenInUI": false, 83 | // "LoginName": "i:0#.f|membership|paul.tavares@tenantname.com", 84 | // "Title": "Paul Tavares", 85 | // "PrincipalType": 1, 86 | // "Email": "paultavares@tenantname.com", 87 | // "IsEmailAuthenticationGuestUser": false, 88 | // "IsShareByEmailGuestUser": false, 89 | // "IsSiteAdmin": true, 90 | // "UserId": { 91 | // "__metadata": {"type": "SP.UserIdInfo"}, 92 | // "NameId": "10033fff8524baa1", 93 | // "NameIdIssuer": "urn:federation:microsoftonline" 94 | // } 95 | // } 96 | // } 97 | 98 | -------------------------------------------------------------------------------- /src/spapi/rest/getListItems.js: -------------------------------------------------------------------------------- 1 | import objectExtend from "common-micro-libs/src/jsutils/objectExtend" 2 | import getContextInfo from "./getContextInfo" 3 | import apiFetch from "../../sputils/apiFetch" 4 | import { getRestHeaders, processResults } from "../../sputils/restUtils" 5 | import ListItemModel from "../../models/ListItemModel" 6 | import ListItemsCollection from "../../collections/ListItemsCollection" 7 | import {IS_GUID_RE} from "../../sputils/constants"; 8 | 9 | 10 | //================================================================== 11 | const encodeURIComponent = window.encodeURIComponent; 12 | 13 | 14 | /** 15 | * Method to retrieve data from a SharePoint lists 16 | * 17 | * @function 18 | * 19 | * @param {Object} options 20 | * 21 | * @param {String} options.list 22 | * The list name or ID 23 | * 24 | * @param {String} [options.web=__current_web__] 25 | * 26 | * @param {String} [options.select=""] 27 | * 28 | * @param {String} [options.filter=""] 29 | * 30 | * @param {String} [options.orderBy=""] 31 | * 32 | * @param {String} [options.expand=""] 33 | * 34 | * @param {Boolean} [options.ListItemsCollection=ListItemsCollection] 35 | * 36 | * @param {Boolean} [options.ListItemModel=ListItemModel] 37 | * The model to be used for each row retrieved. Model constructor must 38 | * support a .create() method. 39 | * 40 | * @return {Promise} 41 | * Promise is resolved with a Collection, or rejected with an Error object 42 | * 43 | * @example 44 | * 45 | * getListItems({list: "tasks"}) 46 | */ 47 | export function getListItems(options) { 48 | const opt = objectExtend({}, getListItems.defaults, options); 49 | 50 | return getContextInfo(opt.web) 51 | .then(contextInfo => { 52 | let requestUrl = `${ contextInfo.WebFullUrl }/_api/web/lists${ 53 | IS_GUID_RE.test(opt.list) ? 54 | `(guid'${opt.list.replace(/[{}]/g, "")}')` : 55 | `/getbytitle('${encodeURIComponent(opt.list)}')` 56 | }/items?`; 57 | 58 | // FIXME: should encodeURIComponent() be used for below options? 59 | 60 | if (opt.filter) { 61 | requestUrl+= `&$filter=${opt.filter}`; 62 | } 63 | 64 | if (opt.select) { 65 | requestUrl+= `&$select=${opt.select}`; 66 | } 67 | 68 | if (opt.orderBy) { 69 | requestUrl+= `&$orderby=${opt.orderBy}`; 70 | } 71 | 72 | if (opt.expand) { 73 | requestUrl+= `&$expand=${opt.expand}`; 74 | } 75 | 76 | opt.requestUrl = requestUrl; 77 | opt.isREST = true; 78 | 79 | return apiFetch(requestUrl, { 80 | method: "GET", 81 | headers: getRestHeaders(contextInfo) 82 | }) 83 | .then(fetchResponse => { 84 | return ListItemsCollection.create( // FIXME: convert to Class 85 | fetchResponse.content.value.map(item => { 86 | processResults(item); 87 | return new opt.ListItemModel(item, opt); 88 | }) 89 | ) 90 | }); 91 | }); 92 | } 93 | export default getListItems; 94 | 95 | /** 96 | * Default options for `getListItems` REST method 97 | * 98 | * @type {{list: string, web: string, select: string, filter: string, expand: string, orderBy: string, ListItemsCollection, ListItemModel}} 99 | */ 100 | getListItems.defaults = { 101 | list: "", 102 | web: "", 103 | select: "", 104 | filter: "", 105 | expand: "", 106 | orderBy: "", 107 | ListItemsCollection, 108 | ListItemModel 109 | }; 110 | -------------------------------------------------------------------------------- /src/spapi/rest/getWebUrlFromPageUrl.js: -------------------------------------------------------------------------------- 1 | /* global _spPageContextInfo */ 2 | import Promise from "common-micro-libs/src/jsutils/es6-promise" 3 | import getFullUrl from "../../sputils/getFullUrl" 4 | import { getRestHeaders } from "../../sputils/restUtils" 5 | import cache from "../../sputils/cache" 6 | import apiFetch from "../../sputils/apiFetch" 7 | 8 | /** 9 | * Returns the Site Web url for a given page url. 10 | * Url returned will end with a forward slash (`/`). 11 | * Example: 12 | * 13 | * https://yourtenant.sharepoint.com/sites/A/ 14 | * 15 | * @param {String} [pageUrl=location.href] 16 | * current serving page will be used if left empty 17 | * 18 | * @return {Promise} 19 | */ 20 | export default function getWebUrlFromPageUrl(pageUrl) { 21 | let isThisPage = false; 22 | 23 | if (!pageUrl) { 24 | pageUrl = location.href; 25 | isThisPage = true; 26 | } 27 | 28 | // Get only the pure url up to the page... no URL params or hash. 29 | if (pageUrl.indexOf("?") > -1) { 30 | pageUrl = pageUrl.substr(0, pageUrl.indexOf("?")); 31 | 32 | } 33 | else if (pageUrl.indexOf("#") > -1) { 34 | pageUrl = pageUrl.substr(0, pageUrl.indexOf("#")); 35 | } 36 | 37 | pageUrl = getFullUrl(pageUrl); 38 | const cacheKey = `getWebUrlFromPageUrl():${ pageUrl }`.toLowerCase(); 39 | 40 | if (cache.get(cacheKey)) { 41 | return cache.get(cacheKey); 42 | } 43 | 44 | let siteUrl = ""; 45 | 46 | // DO we have _spPageContextInfo to work with? Then use it to locate the web URL in 47 | // one of several params. We'll then use that to query SP or resolve this request if 48 | // the current page URL is being used. 49 | if (typeof _spPageContextInfo !== "undefined") { 50 | ["webAbsoluteUrl", "webServerRelativeUrl"].some(function(attr){ 51 | if (_spPageContextInfo[attr]) { 52 | siteUrl = _spPageContextInfo[attr]; 53 | return true; 54 | } 55 | }); 56 | } 57 | 58 | // If it is the current page, then try to determine the siteUrl 59 | // based on variables set by SharePoint 60 | if (isThisPage && siteUrl) { 61 | siteUrl = getFullUrl(siteUrl); 62 | cache.set(cacheKey, Promise.resolve(siteUrl)); 63 | return cache.get(cacheKey); 64 | } 65 | 66 | // Last resolve - make an API call 67 | const apiRequest = apiFetch( 68 | `${ getFullUrl(`${ siteUrl }/_api`) }sp.web.getweburlfrompageurl(@v)?@v='${ encodeURIComponent(pageUrl) }'`, 69 | { 70 | method: "GET", 71 | headers: getRestHeaders() 72 | } 73 | ) 74 | .then(response => getFullUrl(response.content.value)); 75 | 76 | cache.set(cacheKey, apiRequest); 77 | apiRequest.catch(e => { 78 | cache.clear(cacheKey); 79 | console.log(e); // eslint-disable-line 80 | }); 81 | 82 | return apiRequest; 83 | } 84 | -------------------------------------------------------------------------------- /src/spapi/rest/updateListItems.js: -------------------------------------------------------------------------------- 1 | import objectExtend from "common-micro-libs/src/jsutils/objectExtend" 2 | import getContextInfo from "./getContextInfo" 3 | import apiFetch from "../../sputils/apiFetch" 4 | import { getRestHeaders } from "../../sputils/restUtils" 5 | import ListItemModel from "../../models/ListItemModel" 6 | import {IS_GUID_RE} from "../../sputils/constants"; 7 | 8 | 9 | //================================================================== 10 | const encodeURIComponent = window.encodeURIComponent; 11 | 12 | ////// FIXME: support Array of updates (bulk) 13 | ////// FIXME: support updates defined as as string (pass it to `body` of request as is) 14 | 15 | 16 | /** 17 | * Makes updates to list items 18 | * 19 | * @function 20 | * 21 | * @param {Object} options 22 | * 23 | * @param {String} options.list 24 | * The list name or ID 25 | * 26 | * @param {String|Object|Array} options.updates 27 | * 28 | * @param {String} [options.type="update"] 29 | * The type of update... Possible values are: 30 | * - `update` (`PATCH` will be used) 31 | * - `create` (`POST` will be used) 32 | * 33 | * @param {String} [options.web=__current_web__] 34 | * 35 | * @param {ListItemModel} [options.ListItemModel=ListItemModel] 36 | * 37 | * @return {Promise} 38 | * 39 | * @example 40 | * 41 | * FIXME: example here 42 | */ 43 | export function updateListItems(options) { 44 | const opt = objectExtend({}, updateListItems.defaults, options); 45 | 46 | if (Array.isArray(opt.update)) { 47 | throw new Error("options.updates as an Array not yet supported!"); 48 | } 49 | 50 | return getContextInfo(opt.web) 51 | .then(contextInfo => { 52 | const isCreate = opt.type.toLowerCase() === "create"; 53 | 54 | let requestUrl = `${ contextInfo.WebFullUrl }/_api/web/lists${ 55 | IS_GUID_RE.test(opt.list) ? 56 | `(guid'${opt.list.replace(/[{}]/g, "")}')` : 57 | `/getbytitle('${encodeURIComponent(opt.list)}')` 58 | }/items`; 59 | 60 | if (!isCreate) { 61 | requestUrl += `(${opt.updates.ID})` 62 | } 63 | 64 | requestUrl += "?"; 65 | 66 | // FIXME: should encodeURIComponent() be used for below options? 67 | 68 | if (opt.filter) { 69 | requestUrl+= `&$filter=${opt.filter}`; 70 | } 71 | 72 | if (opt.select) { 73 | requestUrl+= `&$select=${opt.select}`; 74 | } 75 | 76 | if (opt.orderBy) { 77 | requestUrl+= `&$orderby=${opt.orderBy}`; 78 | } 79 | 80 | if (opt.expand) { 81 | requestUrl+= `&$expand=${opt.expand}`; 82 | } 83 | 84 | opt.requestUrl = requestUrl; 85 | opt.isREST = true; 86 | 87 | const headers = getRestHeaders(contextInfo); 88 | 89 | if (!isCreate) { 90 | headers["X-HTTP-Method"] = "MERGE"; // FIXME: these should be input options for greater flexibility 91 | headers["If-Match"] = "*"; 92 | } 93 | 94 | return apiFetch(requestUrl, { 95 | method: "POST", 96 | headers, 97 | body: JSON.stringify(opt.updates) 98 | }).then(fetchResponse => { 99 | return new opt.ListItemModel(fetchResponse.content, opt); 100 | }); 101 | }); 102 | } 103 | export default updateListItems; 104 | 105 | 106 | 107 | /** 108 | * Default options for `updateListItems` REST method 109 | * 110 | * @type {{list: string, web: string, select: string, filter: string, expand: string, orderBy: string, ListItemsCollection, ListItemModel}} 111 | */ 112 | updateListItems.defaults = { 113 | list: "", 114 | web: "", 115 | type: "update", 116 | updates: null, 117 | ListItemModel 118 | }; 119 | -------------------------------------------------------------------------------- /src/sputils/apiFetch.js: -------------------------------------------------------------------------------- 1 | import fetchPolyfill from "common-micro-libs/src/jsutils/es7-fetch" 2 | import parseXML from "common-micro-libs/src/jsutils/parseXML" 3 | import Promise from "common-micro-libs/src/jsutils/es6-promise" 4 | import doesMsgHaveError from "./doesMsgHaveError" 5 | import getMsgError from "./getMsgError" 6 | 7 | var fetch = fetchPolyfill.fetch; 8 | 9 | /** 10 | * Handles API calls to SharePoint using the low level ES7 fetch() api, 11 | * thus is has the same input signature. Response will be processed for 12 | * Sharepoint Status errors and then data parsed, returning instead an 13 | * object. 14 | * 15 | * @param {String|Request} input 16 | * @param {Object} init 17 | * 18 | * @return {Promise} 19 | * Promise is resolved with an object containing the following: 20 | * 21 | * { 22 | * content: {}, // XMLDocument 23 | * msgType: 'xml', // String 24 | * response: response // A Response object 25 | * } 26 | * 27 | */ 28 | var apiFetch = function(input, init){ 29 | return fetch(input, init) 30 | .then(parseApiResponse) 31 | .then(checkForSharePointErrors) 32 | .then(checkForHttpErrors); 33 | }, 34 | 35 | /** 36 | * Checks the HTTP resposne to see if there was an HTTP error. 37 | * 38 | * @private 39 | * 40 | * @param response 41 | * 42 | * @returns {*} 43 | */ 44 | checkForHttpErrors = function(response) { 45 | var res = response.status ? response : response.response ? response.response : {}; 46 | 47 | // If server returned an error code, then reject promise 48 | if (res.status >= 200 && res.status < 300) { 49 | return response; 50 | 51 | } else { 52 | var error = new Error(`HTTP ${ res.status }: ${ res.statusText } (${ res.url })`); 53 | error.response = response; 54 | return Promise.reject(error); 55 | } 56 | }, 57 | 58 | /** 59 | * Parses the API response into either XML or JSON 60 | * 61 | * @private 62 | * 63 | * @param response 64 | * @returns {*} 65 | */ 66 | parseApiResponse = function(response){ 67 | // If the message return is JSON, then parse that. 68 | if (response.headers.map["content-type"].join("").toLowerCase().indexOf("application/json") !== -1) { 69 | return response.json().then(content => ({ 70 | content, 71 | msgType: "json", 72 | response: response 73 | })); 74 | } 75 | 76 | // Get the response text and then parse it. 77 | return response.text().then(function(responseString){ 78 | /** 79 | * A sharepoint API response 80 | * 81 | * @typedef {Object} ApiFetchResponse 82 | * 83 | * @property {Document} content 84 | * @property {String} msgType 85 | * Valid value: `xml` 86 | * @property {Object} response 87 | * API fetch response 88 | */ 89 | return { 90 | content: parseXML(responseString), 91 | msgType: responseString ? "xml" : "", // responseString could be empty - example: HTTP 403 92 | response: response 93 | }; 94 | }); 95 | }, 96 | 97 | /** 98 | * Checks the API response for any SharePoint processing errors. 99 | * 100 | * @private 101 | * 102 | * @param {Object} response 103 | * 104 | * @returns {*} 105 | */ 106 | checkForSharePointErrors = function(response){ 107 | if (response.msgType === "xml"){ 108 | if (doesMsgHaveError(response.content)) { 109 | var error = new Error(getMsgError(response.content)); 110 | error.response = response; 111 | return Promise.reject(error); 112 | } 113 | } 114 | 115 | return response; 116 | }; 117 | 118 | export default apiFetch; 119 | 120 | -------------------------------------------------------------------------------- /src/sputils/cache.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Simple caching function. 3 | * @function 4 | * 5 | * @param {Sting} key 6 | * @param {Object} value 7 | * 8 | * @return {undefined} 9 | * 10 | * Methods: 11 | * 12 | * cache("myKey") // getter. Same as cache.get() 13 | * cache("myKey", "value") // Setter. Same as cache.set(); 14 | * cache.clear(key) 15 | * cache.clearAll() 16 | * cache.get(key), 17 | * cache.set(key, value), 18 | * cache.isCached(key) 19 | * 20 | * Dependencies: 21 | * 22 | * none 23 | * 24 | */ 25 | var cache = (function(){ 26 | 27 | var cacheData = {}, 28 | fnCaller = function cache(key, value){ 29 | 30 | if (!key) { 31 | 32 | return; 33 | 34 | } 35 | 36 | // Getter 37 | if (typeof value === "undefined"){ 38 | 39 | return fnCaller.get(key); 40 | 41 | } 42 | 43 | // Setter 44 | return fnCaller.set(key, value); 45 | 46 | }; 47 | 48 | /** 49 | * Clear specific key from cache. 50 | * @function cache.clear 51 | * @param {String} key 52 | */ 53 | fnCaller.clear = function(key){ 54 | 55 | delete cacheData[key]; 56 | 57 | }; 58 | /** 59 | * Clears all cached data 60 | * @fucntion cache.clearAll 61 | */ 62 | fnCaller.clearAll = function(){ 63 | 64 | cacheData = {}; 65 | 66 | }; 67 | /** 68 | * Gets a cached piece of data 69 | * @function cache.get 70 | * @param {String} key 71 | */ 72 | fnCaller.get = function(key) { 73 | 74 | return cacheData[key]; 75 | 76 | }; 77 | /** 78 | * Caches a piece of data. 79 | * @function cache.set 80 | * @param {String} key 81 | * @param {*} value 82 | */ 83 | fnCaller.set = function(key, value) { 84 | 85 | cacheData[key] = value; 86 | return value; 87 | 88 | }; 89 | /** 90 | * Returns a boolean indicating if the give key has cached data. 91 | * @function cache.isCached 92 | * @param {String} key 93 | * @return {Boolean} 94 | */ 95 | fnCaller.isCached = function(key){ 96 | 97 | if (cacheData.hasOwnProperty(key)) { 98 | 99 | return true; 100 | 101 | } 102 | 103 | return false; 104 | }; 105 | 106 | return fnCaller; 107 | 108 | })(); //end: cache method. 109 | 110 | export default cache; 111 | 112 | 113 | -------------------------------------------------------------------------------- /src/sputils/constants.js: -------------------------------------------------------------------------------- 1 | import {objectKeys} from "common-micro-libs/src/jsutils/runtime-aliases" 2 | /** 3 | * The typical REST API headers for JSON data structures. Includes `odata=verbose` 4 | * 5 | * @type {{"Content-Type": string, Accept: string}} 6 | */ 7 | export const REST_HEADERS = { 8 | "Content-Type": "application/json;odata=verbose", 9 | "Accept": "application/json;odata=verbose" 10 | }; 11 | 12 | /** 13 | * The typical REST API headers, but with `odata=nometadata` - ideal for working 14 | * with Create/Update operation and not have to define the item type 15 | * @type {{"Content-Type": string, Accept: string}} 16 | */ 17 | export const REST_HEADERS_NO_METADATA = Object.assign({}, REST_HEADERS); 18 | objectKeys(REST_HEADERS_NO_METADATA) 19 | .forEach(key => REST_HEADERS_NO_METADATA[key] = REST_HEADERS_NO_METADATA[key].replace("verbose", "nometadata")); 20 | 21 | 22 | /** 23 | * RegExp to validate GUID's 24 | * 25 | * @type {RegExp} 26 | */ 27 | export const IS_GUID_RE = /^(\{{0,1}([0-9a-fA-F]){8}-([0-9a-fA-F]){4}-([0-9a-fA-F]){4}-([0-9a-fA-F]){4}-([0-9a-fA-F]){12}\}{0,1})$/; 28 | -------------------------------------------------------------------------------- /src/sputils/doesMsgHaveError.js: -------------------------------------------------------------------------------- 1 | import domFind from "common-micro-libs/src/domutils/domFind"; 2 | 3 | /** 4 | * Checks if an xml message has an error. Taken from 5 | * SPWidgets. 6 | * 7 | * @param {XMLDocument} xmlMsg 8 | * 9 | * @return {Boolean} 10 | */ 11 | export default function(xmlMsg) { 12 | 13 | // BACKWARD COMPATIBILITY 14 | // if xmlMsg seems to be a jQuery object, then get it native element 15 | if (xmlMsg.jquery) { 16 | xmlMsg = xmlMsg[0]; 17 | } 18 | 19 | // if xmlDocument does not support querySelector, throw error 20 | if (!xmlMsg.querySelector) { 21 | throw new Error("input is not an XML Document!"); 22 | } 23 | 24 | var spErrCode = domFind(xmlMsg, "ErrorCode"), 25 | response = false; 26 | 27 | // If we don't have elements, then check other stuff 28 | // that sharepoint can return in error conditions 29 | if (!spErrCode.length) { 30 | // Any "fauldcode" nodes? 31 | if (domFind(xmlMsg, "faultcode").length) { 32 | return true; 33 | } 34 | 35 | // Any CopyResult nodes with ErrorMessage 36 | if (domFind(xmlMsg, "CopyResult[ErrorMessage]").length){ 37 | return true; 38 | } 39 | 40 | return response; 41 | } 42 | 43 | spErrCode.some(function(errorCodeEle){ 44 | var errorCodeString = errorCodeEle.textContent; 45 | if (errorCodeString !== "0x00000000" && errorCodeString !== "NoError" ) { 46 | response = true; 47 | return true; 48 | } 49 | }); 50 | 51 | return response; 52 | } 53 | 54 | -------------------------------------------------------------------------------- /src/sputils/fillTemplate.js: -------------------------------------------------------------------------------- 1 | import fillTemplate from "common-micro-libs/src/jsutils/fillTemplate"; 2 | export default fillTemplate; 3 | -------------------------------------------------------------------------------- /src/sputils/getDateString.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | /** 4 | * Returns a date string in the format expected by Sharepoint 5 | * Date/time fields. Usefull in doing filtering queries. 6 | * 7 | * Credit: Matt (twitter @iOnline247) 8 | * {@see http://spservices.codeplex.com/discussions/349356} 9 | * 10 | * @param {Date} [dateObj=Date()] 11 | * @param {String} [formatType='local'] 12 | * Possible formats: local, utc 13 | * 14 | * @return {String} a date string. 15 | * 16 | */ 17 | var getDateString = function( dateObj, formatType ) { 18 | 19 | formatType = String(formatType || "local").toLowerCase(); 20 | dateObj = dateObj || new Date(); 21 | 22 | function pad( n ) { 23 | 24 | return n < 10 ? "0" + n : n; 25 | 26 | } 27 | 28 | var ret = ""; 29 | 30 | if (formatType === "utc") { 31 | 32 | ret = dateObj.getUTCFullYear() + "-" + 33 | pad( dateObj.getUTCMonth() + 1 ) + "-" + 34 | pad( dateObj.getUTCDate() ) + "T" + 35 | pad( dateObj.getUTCHours() ) + ":" + 36 | pad( dateObj.getUTCMinutes() )+ ":" + 37 | pad( dateObj.getUTCSeconds() )+ "Z"; 38 | 39 | } else { 40 | 41 | ret = dateObj.getFullYear() + "-" + 42 | pad( dateObj.getMonth() + 1 ) + "-" + 43 | pad( dateObj.getDate() ) + "T" + 44 | pad( dateObj.getHours() ) + ":" + 45 | pad( dateObj.getMinutes() )+ ":" + 46 | pad( dateObj.getSeconds() ); 47 | 48 | } 49 | 50 | return ret; 51 | 52 | }; //end: SPGetDateString() 53 | 54 | export default getDateString; 55 | 56 | -------------------------------------------------------------------------------- /src/sputils/getFullUrl.js: -------------------------------------------------------------------------------- 1 | const DOCUMENT_LOCATION = document.location; 2 | 3 | /** 4 | * Returns the full URL (starting with `http...` for a given page address 5 | * 6 | * @param {String} pageAddress 7 | * @param {Boolean} [noEndSlash=false] 8 | * By default, the returned url will be ensured to end with a `/`. set this 9 | * param to `true` to not append this character if needed. 10 | * 11 | * @returns {string} 12 | */ 13 | export default function getFullUrl(pageAddress, noEndSlash) { 14 | 15 | // if URL does not end with "/" then insert it 16 | if (pageAddress && !noEndSlash && pageAddress.charAt(pageAddress.length - 1) !== "/") { 17 | pageAddress += "/"; 18 | } 19 | 20 | if (pageAddress.toLowerCase().indexOf("http") > -1) { 21 | return pageAddress; 22 | } 23 | 24 | pageAddress = DOCUMENT_LOCATION.protocol + "//" + 25 | DOCUMENT_LOCATION.hostname + 26 | ( Number(DOCUMENT_LOCATION.port) !== 80 && 27 | Number(DOCUMENT_LOCATION.port) > 0 ? 28 | ":" + DOCUMENT_LOCATION.port : 29 | "" 30 | ) + 31 | pageAddress; 32 | 33 | return pageAddress; 34 | } 35 | -------------------------------------------------------------------------------- /src/sputils/getMsgError.js: -------------------------------------------------------------------------------- 1 | import domFind from "common-micro-libs/src/domutils/domFind"; 2 | import parseXML from "common-micro-libs/src/jsutils/parseXML"; 3 | 4 | /** 5 | * Given a sharepoint webservices response, this method will 6 | * look to see if it contains an error and return that error 7 | * formated as a string. 8 | * 9 | * @param {XMLDocument|String} xmlMsg 10 | * 11 | * @return {String} errorMessage 12 | * 13 | */ 14 | export default function getMsgError(xmlMsg){ 15 | 16 | if (typeof xmlMsg === "string") { 17 | xmlMsg = parseXML(xmlMsg); 18 | 19 | // Backwards compatible 20 | // if xmlMsg is a jquery object, get the native ele 21 | } else if (xmlMsg && xmlMsg.jquery) { 22 | xmlMsg = xmlMsg[0]; 23 | } 24 | 25 | // if xmlDocument does not support querySelector, throw error 26 | if (!xmlMsg.querySelector) { 27 | throw new Error("input is not an XML Document!"); 28 | } 29 | 30 | var error = "", 31 | spErr = domFind(xmlMsg, "ErrorCode"), 32 | count = 0; 33 | 34 | if (!spErr.length) { 35 | spErr = domFind(xmlMsg, "faultcode"); 36 | } 37 | 38 | // See if any Elements with ErrorMessage attribute 39 | if (!spErr.length) { 40 | spErr = domFind(xmlMsg, "CopyResult[ErrorMessage]"); 41 | 42 | if (spErr.length) { 43 | spErr.forEach(function(thisErr){ 44 | count += 1; 45 | error += "(" + count + ") " + 46 | (thisErr.getAttribute("ErrorCode") || "unknown") + 47 | ": " + 48 | thisErr.getAttribute("ErrorMessage") + "\n"; 49 | }); 50 | return count + " error(s) encountered! \n" + error; 51 | } 52 | } 53 | 54 | if (!spErr.length) { 55 | return ""; 56 | } 57 | 58 | // Loop through and get all errors. 59 | spErr.forEach(function(thisErr){ 60 | var textContent = thisErr.textContent; 61 | if ( textContent !== "0x00000000" ) { 62 | count += 1; 63 | error += "(" + count + ") " + textContent + ": " + 64 | domFind(thisErr.parentNode, "*") 65 | .filter(function(ele){ 66 | return ele !== thisErr; 67 | }) 68 | .reduce(function(text, ele){ 69 | return text + " " + ele.textContent; 70 | }, "") + 71 | "\n"; 72 | } 73 | }); 74 | 75 | error = count + " error(s) encountered! \n" + error; 76 | return error; 77 | } 78 | 79 | -------------------------------------------------------------------------------- /src/sputils/getSPVersion.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | /* global SP, _spPageContextInfo */ 4 | 5 | /** 6 | * Returns the SharePoint version number. This is accomplished by 7 | * looking for the SP namespace and if it is define, parsing the 8 | * SP.ClientSchemeversions value. 9 | * 10 | * @param {Boolean} returnExternal 11 | * If true, then the external version (ex. 2007, 2010) is 12 | * returned. Default is to return the internal version number 13 | * (ex. 12, 14) 14 | * 15 | * @return {String} 16 | * 17 | */ 18 | var getSPVersion = function getSPVersion(returnExternal) { 19 | 20 | // Some approaches below taken from: 21 | // http://sharepoint.stackexchange.com/questions/74978/can-i-tell-what-version-of-sharepoint-is-being-used-from-javascript 22 | 23 | var versionMap = { 24 | 12: "2007", 25 | 14: "2010", 26 | 15: "2013" 27 | }, 28 | version = 12, 29 | foundIt = false; 30 | 31 | // If the SP variable is defined, then its at least SP2010 32 | if (typeof SP !== "undefined") { 33 | 34 | version = 14; 35 | 36 | if (SP.ClientSchemaVersions) { 37 | 38 | if (SP.ClientSchemaVersions.currentVersion) { 39 | 40 | version = parseInt(SP.ClientSchemaVersions.currentVersion); 41 | foundIt = true; 42 | 43 | } 44 | 45 | } 46 | 47 | if (!foundIt && (typeof _spPageContextInfo !== "undefined")) { 48 | 49 | version = parseInt(_spPageContextInfo.webUIVersion); 50 | 51 | if (version === 4) { 52 | 53 | version = 14; 54 | 55 | } 56 | 57 | } 58 | 59 | } 60 | 61 | // TODO: implement method detailed by Jeremy Thake: http://www.jeremythake.com/2013/08/get-sharepoint-version-number-of-your-platform-quickly/ 62 | // Queries: /_vti_pvt/service.cnf ... Works in SP2010 / 2013, 2007 as well. 63 | // OUTPUT: 64 | // vti_encoding:SR|utf8-nl 65 | // ti_extenderversion:SR|16.0.0.1216 66 | 67 | if (returnExternal) { 68 | 69 | version = versionMap[version] || version; 70 | 71 | } 72 | 73 | return version; 74 | 75 | }; //end: getSPVersion(); 76 | 77 | export default getSPVersion; 78 | 79 | 80 | -------------------------------------------------------------------------------- /src/sputils/parseDateString.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Parses a date string in ISO 8601 format into a Date object. 3 | * Date format supported on input: 4 | * 2013-09-01T01:00:00 5 | * 2013-09-01T01:00:00Z 6 | * 2013-09-01T01:00:00Z+05:00 7 | * 8 | * @param {String} dateString 9 | * The date string to be parsed. 10 | * 11 | * @return {Date|Null} 12 | * If unable to parse string, a Null value will be returned. 13 | * 14 | * @see {https://github.com/csnover/js-iso8601} 15 | * Method was developed using some of the code from js-iso8601 16 | * project on github by csnover. 17 | * 18 | */ 19 | var parseDateString = function parseDateString(dateString) { 20 | 21 | var dtObj = null, 22 | re, dtPieces, i, j, numericKeys, minOffset; 23 | 24 | if (!dateString) { 25 | 26 | return dtObj; 27 | 28 | } 29 | 30 | // let's see if Date.parse() can do it? 31 | // We append 'T00:00' to the date string case it is 32 | // only in format YYYY-MM-DD 33 | dtObj = Date.parse( 34 | dateString.length === 10 ? 35 | (dateString + "T00:00") : 36 | dateString 37 | ); 38 | 39 | if (dtObj) { 40 | 41 | return new Date(dtObj); 42 | 43 | } 44 | 45 | // Once we parse the date string, these locations 46 | // in the array must be Numbers. 47 | numericKeys = [ 1, 4, 5, 6, 7, 10, 11 ]; 48 | 49 | // Define regEx 50 | re = /^(\d{4}|[+\-]\d{6})(?:-(\d{2})(?:-(\d{2}))?)?(?:T(\d{2}):(\d{2})(?::(\d{2})(?:\.(\d{3}))?)?(?:(Z)|([+\-])(\d{2})(?::(\d{2}))?)?)?$/; // eslint-disable-line 51 | 52 | // dtPieces: 53 | // [0] 54 | // [1] YYYY 55 | // [2] MM 56 | // [3] DD 57 | // [4] HH 58 | // [5] mm 59 | // [6] ss 60 | // [7] msec 61 | // [8] Z 62 | // [9] +|- 63 | // [10] Z HH 64 | // [11] Z mm 65 | dtPieces = dateString.match(re); 66 | 67 | 68 | if( !dtPieces ){ 69 | 70 | return dtObj; 71 | 72 | } 73 | 74 | for(i=0,j=numericKeys.length; i} 10 | * Array of objects. Each object has two keys; `Title` and `ID` 11 | * 12 | * @example 13 | * 14 | * parseLookupFieldValue("1;#item one title;#2;#item two title"); 15 | * // Returns: 16 | * [ 17 | * { 18 | * ID: "1", 19 | * Title: "item one title" 20 | * }, 21 | * { 22 | * ID: "2", 23 | * Title: "item two title" 24 | * } 25 | * ] 26 | */ 27 | const parseLookupFieldValue = function(lookupValue) { 28 | var response = [], 29 | valueTokens = String(lookupValue).split(";#"), 30 | total = valueTokens.length, 31 | i, vId, vTitle; 32 | 33 | if (!lookupValue) { 34 | return response; 35 | } 36 | 37 | for (i=0; i} 16 | */ 17 | const parsePeopleField = function(peopleString, PersonModel) { 18 | PersonModel = PersonModel || parsePeopleField.defaults.PersonModel; 19 | 20 | return parseLookupFieldValue(String(peopleString || "")).map(function(person){ 21 | var personInfo = { 22 | ID: person.id || "", 23 | Name: person.title || "" 24 | }; 25 | 26 | // If the Name field seems to have data that is returned when you 27 | // expand the field during the API call, then parse that now into 28 | // individual attributes... See for more info. on these attributes: 29 | // http://msdn.microsoft.com/en-us/library/cc264031%28v=office.14%29.aspx 30 | // O365 seems to return some additional values from what is documented. 31 | // Example of expanded values in an array (from o365): 32 | // [ 33 | // "First Last", 34 | // "i:0#.f|membership|somename@domain.com", 35 | // "someName@domain.com", 36 | // "", 37 | // "First Last", 38 | // "https://someDomain-my.sharepoint.com:443/User%20Photos/.....jpg", // Not using it now 39 | // "", // Not using it now 40 | // "" // Not using it now 41 | // ] 42 | if (personInfo.Name.indexOf(",#") > -1) { 43 | let additionalAttributes = [ 44 | "Name", 45 | "AccountName", 46 | "Email", 47 | "SIP", 48 | "DisplayName" 49 | //, 50 | //"UserPhoto" // not adding it to the info object because this normally points to the "my site" which requires additional login 51 | ]; 52 | 53 | personInfo.Name.split(/,#/g).forEach(function(expandedValue, index){ 54 | if (additionalAttributes[index]) { 55 | personInfo[additionalAttributes[index]] = String(expandedValue || "").replace(/,,/g, ",") 56 | } 57 | }); 58 | } 59 | 60 | // Create the model and populate with the attr. from above. 61 | return PersonModel.create(personInfo); 62 | }); 63 | }; 64 | 65 | /** 66 | * Defaults for the function 67 | * 68 | * @name parsePeopleField.defaults 69 | * @type {Object} 70 | */ 71 | parsePeopleField.defaults = { 72 | PersonModel: UserProfileModel 73 | }; 74 | 75 | export default parsePeopleField; 76 | 77 | 78 | -------------------------------------------------------------------------------- /src/sputils/restUtils.js: -------------------------------------------------------------------------------- 1 | import objectExtend from "common-micro-libs/src/jsutils/objectExtend" 2 | import {objectDefineProperty} from "common-micro-libs/src/jsutils/runtime-aliases" 3 | import { REST_HEADERS, REST_HEADERS_NO_METADATA } from "./constants" 4 | import apiFetch from "./apiFetch"; 5 | import getContextInfo from "../spapi/rest/getContextInfo"; 6 | 7 | //=========================================================== 8 | 9 | /** 10 | * Returns an object with the standard JSON headers for a SharePoint REST call. 11 | * 12 | * @param {ContextWebInformation} [contextInfo] 13 | * @param {Boolean} [oDataVerbose=false] 14 | * If set to `true`, then the REST headers that indicate `odata=verbose` will be used. 15 | * Default is to use `odata=nometadata`. 16 | * 17 | * @return {Object} 18 | */ 19 | export function getRestHeaders (contextInfo, oDataVerbose = false) { 20 | return objectExtend( 21 | {}, 22 | oDataVerbose ? REST_HEADERS : REST_HEADERS_NO_METADATA, 23 | contextInfo ? {"X-RequestDigest": contextInfo.FormDigestValue} : null 24 | ); 25 | } 26 | 27 | 28 | /** 29 | * Given a user info object returnd by SP REST, this method will augument that data and 30 | * return back a UserProfileModel instance. 31 | * 32 | * @param {Object} userInfo 33 | * 34 | * @returns {UserProfileModel} 35 | */ 36 | export function processUserInfo (userInfo) { 37 | if (userInfo.Title && !userInfo.FirstName) { 38 | [ userInfo.FirstName, userInfo.LastName ] = userInfo.Title.split(" "); 39 | } 40 | return userInfo; 41 | } 42 | 43 | /** 44 | * Processes REST API response results. Does: 45 | * 46 | * - Added a `.load()` method to any property value that has a `__deferred` object 47 | * 48 | * @param {Array|Object} results 49 | */ 50 | export function processResults(results) { 51 | results = Array.isArray(results) ? results : [results]; 52 | for (let x=0, t=results.length; x < t; x++) { 53 | if (results[x]){ 54 | for (let attrName in results[x]) { 55 | if (results[x].hasOwnProperty(attrName) && results[x][attrName] && results[x][attrName].__deferred) { 56 | objectDefineProperty( 57 | results[x][attrName], 58 | "load", 59 | { configurable: true, writable: true, value: fetchDeferredUri } 60 | ); 61 | } 62 | } 63 | } 64 | } 65 | } 66 | 67 | 68 | function fetchDeferredUri () { 69 | return getContextInfo(this.__deferred.uri.substr(0, this.__deferred.uri.indexOf("_api"))) 70 | .then(contextInfo => { 71 | return apiFetch(this.__deferred.uri, { 72 | method: "GET", 73 | headers: getRestHeaders(contextInfo, true) 74 | }); 75 | }); 76 | } 77 | -------------------------------------------------------------------------------- /src/sputils/xmlEscape.js: -------------------------------------------------------------------------------- 1 | import xmlEscape from "common-micro-libs/src/jsutils/xmlEscape"; 2 | // FIXME: remove references to this file and replace with 'common-micro-libs/src/jsutils/xmlEscape' 3 | export default xmlEscape; 4 | 5 | -------------------------------------------------------------------------------- /src/widgets/BooleanField/BooleanField.html: -------------------------------------------------------------------------------- 1 |
2 | {{opt.column.DisplayName}} 3 | 4 | 8 | {{opt.column.description}} 9 |
10 | -------------------------------------------------------------------------------- /src/widgets/BooleanField/BooleanField.less: -------------------------------------------------------------------------------- 1 | .spwidgets-BooleanField { 2 | position: relative; 3 | 4 | &-displayName { 5 | display: block; 6 | } 7 | 8 | //================================= 9 | // modifiers 10 | //================================= 11 | &--noLabel &-displayName { 12 | display: none; 13 | } 14 | 15 | &--noDescription .ms-Toggle-description { 16 | display: none; 17 | } 18 | } -------------------------------------------------------------------------------- /src/widgets/ChoiceField/ChoiceField.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 | 5 |
6 | {{column.Description}} 7 |
8 |
9 |
10 |
11 | -------------------------------------------------------------------------------- /src/widgets/ChoiceField/ChoiceField.less: -------------------------------------------------------------------------------- 1 | .spwidgets-ChoiceField { 2 | 3 | &-choices { 4 | padding: 0.5em; 5 | overflow: auto; 6 | border: 1px solid #eaeaea; 7 | min-height: 5em; 8 | 9 | &::-webkit-scrollbar { 10 | width: 0.5em; 11 | background-color: #F5F5F5; 12 | } 13 | 14 | &::-webkit-scrollbar-thumb { 15 | -webkit-box-shadow: inset 0 0 6px rgba(0,0,0,.3); 16 | background-color : #555; 17 | } 18 | } 19 | 20 | //--------------------------- 21 | // NO MODIFIERS 22 | //--------------------------- 23 | &--noLabel .ms-ChoiceFieldGroup-title { 24 | display: none; 25 | } 26 | 27 | &--noDescription &-description { 28 | display: none; 29 | } 30 | 31 | &--inline &-choices > * { 32 | display: inline-block; 33 | margin-right: 1em; 34 | padding-right: 0.5em; 35 | 36 | &:last-child { 37 | margin-right: 0; 38 | } 39 | } 40 | 41 | &--noSelectedCount &-selectedCount { 42 | display: none; 43 | } 44 | } -------------------------------------------------------------------------------- /src/widgets/ChoiceField/ChoiceItem/ChoiceItem.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 | 7 |
8 |
9 | -------------------------------------------------------------------------------- /src/widgets/ChoiceField/ChoiceItem/ChoiceItem.less: -------------------------------------------------------------------------------- 1 | .spwidgets-ChoiceField-ChoiceItem { 2 | padding-left: 0.5em; 3 | padding-right: 0.5em; 4 | 5 | &-label { 6 | display: block; 7 | } 8 | } -------------------------------------------------------------------------------- /src/widgets/ContentTypeField/ContentTypeField.js: -------------------------------------------------------------------------------- 1 | import dataStore from "common-micro-libs/src/jsutils/dataStore" 2 | import Deferred from "common-micro-libs/src/jsutils/Deferred" 3 | import Promise from "common-micro-libs/src/jsutils/es6-promise" 4 | import ChoiceField from "../ChoiceField/ChoiceField" 5 | import getListContentTypes from "../../spapi/getListContentTypes" 6 | 7 | //================================================================= 8 | const PRIVATE = dataStore.create(); 9 | const ChoiceFieldPrototype = ChoiceField.prototype; 10 | 11 | /** 12 | * Widget picker for List content types. Note that the `value` of the selected 13 | * options will be the Content Type internal ID, which in order to be used 14 | * when a query, one would have to use the `ContentTypeId` field of the list. 15 | * 16 | * @class ContentTypeField 17 | * @extends ChoiceField 18 | */ 19 | export default ChoiceField.extend({ 20 | init(options) { 21 | if (options && !options.choiceList) { 22 | options.choiceList = []; 23 | } 24 | ChoiceFieldPrototype.init.call(this, options); 25 | const deferred = Deferred.create(); 26 | const state = { 27 | onReady: deferred.promise.then(() => this) 28 | }; 29 | PRIVATE.set(this, state); 30 | 31 | ChoiceFieldPrototype.onReady.call(this) 32 | .then(() => getListContentTypes({listName: options.listName, webURL: options.webURL})) 33 | .then(contentTypes => { 34 | contentTypes.forEach(contentType => { 35 | contentType.title = contentType.Name; 36 | contentType.value = contentType.ID; 37 | }); 38 | state.onReady = Promise.resolve(this); 39 | this.setChoices.call(this, contentTypes); 40 | deferred.resolve(); 41 | }); 42 | 43 | this.onDestroy(() => PRIVATE.delete(state)); 44 | }, 45 | 46 | onReady() { 47 | return PRIVATE.get(this).onReady; 48 | }, 49 | 50 | setSelected(...vals) { 51 | return this.onReady().then(() => ChoiceFieldPrototype.setSelected.call(this, ...vals)); 52 | } 53 | }); 54 | -------------------------------------------------------------------------------- /src/widgets/DateTimeField/DateTimeField.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 | 5 |
6 | 7 |
8 |
9 | 10 |
11 |
12 | {{column.Description}} 13 |
14 | -------------------------------------------------------------------------------- /src/widgets/DateTimeField/DateTimeField.less: -------------------------------------------------------------------------------- 1 | .spwidgets-DateTimeField { 2 | position: relative; 3 | box-sizing: border-box; 4 | 5 | &-inputHolder { 6 | position: relative; 7 | } 8 | 9 | &-calIcon, 10 | &-clearIcon { 11 | position: absolute; 12 | top: 0; 13 | right: 54px; 14 | border: 1px solid #c8c8c8; 15 | min-width: 32px; 16 | } 17 | 18 | &-clearIcon { 19 | right: 0; 20 | } 21 | 22 | &--noLabel .ms-Label { 23 | display: none; 24 | } 25 | &--noDescription .ms-TextField-description { 26 | display: none; 27 | } 28 | &--inlinePicker &-calIcon { 29 | display: none; 30 | } 31 | } -------------------------------------------------------------------------------- /src/widgets/FilterPanel/ColumnSelector/ColumnSelector.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | {{labels.select}} 4 |
5 |
6 |
Loading...
7 |
8 | 16 |
-------------------------------------------------------------------------------- /src/widgets/FilterPanel/ColumnSelector/ColumnSelector.less: -------------------------------------------------------------------------------- 1 | .spwidgets-FilterPanel-ColumnSelector { 2 | 3 | @class-name-ms-icon-checkmark: ms-Icon--CheckMark; 4 | 5 | //----------------------------------------------------------- 6 | 7 | box-sizing: border-box; 8 | position: absolute; 9 | top: 0; 10 | bottom: 0; 11 | left: 0; 12 | right: 0; 13 | //width: 100%; 14 | //height: 100%; 15 | border-style: solid; 16 | border-width: 1px; 17 | 18 | &-header { 19 | padding: 0.5em; 20 | border-width: 1px; 21 | border-style: solid; 22 | } 23 | 24 | &-body { 25 | box-sizing: border-box; 26 | position: absolute; 27 | height: 100%; 28 | width: 100%; 29 | top: 0; 30 | left: 0; 31 | padding: 3em 0.5em 3.5em; 32 | 33 | &-content { 34 | box-sizing: border-box; 35 | height: 100%; 36 | overflow: auto; 37 | } 38 | } 39 | 40 | &-footer { 41 | box-sizing: border-box; 42 | position: absolute; 43 | bottom: 0; 44 | left: 0; 45 | width: 100%; 46 | padding: 0.5em; 47 | text-align: right; 48 | } 49 | 50 | &-col { 51 | box-sizing: border-box; 52 | display: inline-block; 53 | width: ~"calc((100% / 3) - 1em)"; 54 | cursor: pointer; 55 | margin: 0.5em 0.5em 0em 0em; 56 | position: relative; 57 | 58 | &:last-child { 59 | margin-right: 0; 60 | } 61 | 62 | &-icon, 63 | &-title { 64 | padding: 0.5em; 65 | box-sizing: border-box; 66 | } 67 | 68 | &-icon { 69 | position: absolute; 70 | left: 0; 71 | top: 0; 72 | height: 100%; 73 | width: 2.5em; 74 | padding: 0.5em; 75 | text-align: center; 76 | 77 | .@{class-name-ms-icon-checkmark} { 78 | display: none; 79 | } 80 | } 81 | 82 | &-title { 83 | width: 100%; 84 | padding-left: 3em; 85 | text-overflow: ellipsis; 86 | overflow: hidden; 87 | white-space: nowrap; 88 | } 89 | 90 | // MODIFIER 91 | &--selected { 92 | color: #0078d7; // same as ms-Link 93 | } 94 | &--selected &-icon .@{class-name-ms-icon-checkmark} { 95 | display: inline-block; 96 | } 97 | } 98 | 99 | 100 | //--------------------- 101 | // MODIFIERS 102 | //--------------------- 103 | &--1-col &-col { 104 | width: ~"calc(100% - 1em)"; 105 | } 106 | &--2-col &-col { 107 | width: ~"calc(50% - 1em)"; 108 | } 109 | } -------------------------------------------------------------------------------- /src/widgets/FilterPanel/ColumnSelector/column.html: -------------------------------------------------------------------------------- 1 | 2 |
{{DisplayName}}
3 |
4 | 5 |
6 |
-------------------------------------------------------------------------------- /src/widgets/FilterPanel/FilterColumn/FilterColumn.html: -------------------------------------------------------------------------------- 1 |
2 |

3 | 4 | 5 | 6 | {{column.DisplayName}}

7 |
8 | 15 | 19 | 24 |
25 |
26 |
27 | 28 |
29 |
30 |
31 |
{{labels.keywordsInfo}}
32 | {{labels.options}} 33 |
34 | 35 | 43 |
-------------------------------------------------------------------------------- /src/widgets/FilterPanel/FilterColumn/FilterColumn.less: -------------------------------------------------------------------------------- 1 | .spwidgets-FilterPanel-FilterColumn { 2 | box-sizing: border-box; 3 | padding: 0.5em 2.5em 1em 0.5em; 4 | border-bottom-width: 1px; 5 | border-bottom-style: solid; 6 | position: relative; 7 | 8 | &:last-child, 9 | &:only-child { 10 | margin-bottom: 0.5em; 11 | } 12 | 13 | &:last-child { 14 | border-bottom: none; 15 | } 16 | 17 | &:only-child &-move { 18 | display: none; 19 | } 20 | 21 | &:hover { 22 | background-color: #F4F4F4; 23 | transition: background-color 0.5s 0s; 24 | } 25 | 26 | &-title { 27 | margin: 0; 28 | } 29 | &-title > * { 30 | vertical-align: middle; 31 | } 32 | 33 | &-dirtyIndicator { 34 | color: #C8C8C8; 35 | font-size: 8px; 36 | } 37 | 38 | &-options { 39 | font-size: 0.8em; 40 | opacity: 0.7; 41 | display: none; 42 | padding-top: 0.5em; 43 | text-align: right; 44 | 45 | &:hover { 46 | opacity: 1; 47 | } 48 | } 49 | 50 | &-input { 51 | margin-top: 0.5em; 52 | } 53 | 54 | &-info { 55 | position: relative; 56 | min-height: 1.2em; 57 | 58 | &-keywords { 59 | width: 70%; 60 | } 61 | 62 | &-optLink { 63 | position: absolute; 64 | right: 0; 65 | top: 0; 66 | } 67 | } 68 | 69 | &-move { 70 | position: absolute; 71 | top: 0; 72 | right: 0; 73 | height: 100%; 74 | width: 1.5em; 75 | visibility: hidden; 76 | 77 | > a { 78 | display: block; 79 | height: 49.8%; 80 | box-sizing: border-box; 81 | padding-top: 70%; 82 | text-align: center; 83 | font-size: 1.3em; 84 | cursor: pointer; 85 | 86 | &:hover { 87 | background-color: #EAEAEA; 88 | } 89 | } 90 | 91 | } 92 | &:hover &-move { 93 | visibility: visible; 94 | } 95 | 96 | 97 | // Choice fields: make Choices holder white 98 | .spwidgets-ChoiceField-choices { 99 | background-color: white; 100 | } 101 | 102 | //------------------------------ 103 | // MODIFIERS 104 | //------------------------------ 105 | 106 | // ==> show the options for column 107 | &--showOptions &-options { 108 | display: block; 109 | } 110 | &--showOptions &-info-optLink { 111 | color: #A6A6A6; 112 | } 113 | 114 | // ==> No Options Link 115 | &--noOptionsToggle &-info-optLink { 116 | display: none; 117 | } 118 | 119 | 120 | // ==> Hide the input 121 | &--hideInput &-input-holder { 122 | display: none; 123 | } 124 | &--hideInput &-info { 125 | display: none; 126 | } 127 | 128 | 129 | // ==> Column is dirty (something was changed/entered) 130 | &--isDirty &-dirtyIndicator { 131 | color: #107C10; 132 | } 133 | } -------------------------------------------------------------------------------- /src/widgets/FilterPanel/FilterColumnAttachmentsField/FilterColumnAttachmentsField.js: -------------------------------------------------------------------------------- 1 | import FilterColumn from "../FilterColumn/FilterColumn" 2 | import objectExtend from "common-micro-libs/src/jsutils/objectExtend" 3 | import dataStore from "common-micro-libs/src/jsutils/dataStore" 4 | import ChoiceField from "../../ChoiceField/ChoiceField" 5 | 6 | 7 | //==================================================================== 8 | const PRIVATE = dataStore.stash; 9 | 10 | /** 11 | * Filter column allowing the user to filter items based on whether 12 | * they have attachments or not. 13 | * 14 | * @class FilterColumnAttachmentsField 15 | * @extends FilterColumn 16 | * 17 | * @param {Object} options 18 | */ 19 | let FilterColumnAttachmentsField = /** @lends FilterColumnAttachmentsField.prototype */{ 20 | init: function (options) { 21 | FilterColumn.prototype.init.call(this, 22 | objectExtend({}, this.getFactory().defaults, options) 23 | ); 24 | 25 | var 26 | inst = PRIVATE.get(this), 27 | opt = inst.opt, 28 | labels = opt.labels, 29 | attachmentsField = inst.inputWdg = ChoiceField.create({ 30 | layout: "inline", 31 | hideLabel: true, 32 | column: opt.column, 33 | labels: labels 34 | }); 35 | 36 | attachmentsField.setChoices([labels.any, labels.yes, labels.no]); 37 | 38 | attachmentsField.on("change", function(){ 39 | this.evalDirtyState(); 40 | }.bind(this)); 41 | 42 | inst.inputWdg.setValue(options.selected || ""); 43 | inst.inputWdg.appendTo(inst.inputHolder); 44 | 45 | this.setCompareOperatorDefault("Eq"); 46 | this.setKeywordInfo(opt.labels.attachmentsInfo); 47 | } 48 | }; 49 | 50 | FilterColumnAttachmentsField = FilterColumn.extend(FilterColumnAttachmentsField); 51 | FilterColumnAttachmentsField.defaults = { 52 | layout: "inline", 53 | selected: "", 54 | labels: { 55 | any: "Any", 56 | yes: "Yes", 57 | no: "No" 58 | } 59 | }; 60 | 61 | export default FilterColumnAttachmentsField; 62 | -------------------------------------------------------------------------------- /src/widgets/FilterPanel/FilterColumnBooleanField/FilterColumnBooleanField.js: -------------------------------------------------------------------------------- 1 | import FilterColumn from "../FilterColumn/FilterColumn"; 2 | import BooleanField from "../../BooleanField/BooleanField"; 3 | import dataStore from "common-micro-libs/src/jsutils/dataStore"; 4 | import objectExtend from "common-micro-libs/src/jsutils/objectExtend"; 5 | 6 | const PRIVATE = dataStore.stash; 7 | 8 | /** 9 | * A text field column for filter panel 10 | * 11 | * @class FilterColumnBooleanField 12 | * @extends FilterColumn 13 | * 14 | * @param {Object} options 15 | * In addition to the options required/supported by 16 | * [FilterColumn]{@link FilterColumn}, this Widget supports this additional 17 | * set documented below 18 | * 19 | */ 20 | const FilterColumnBooleanField = FilterColumn.extend(/** @lends FilterColumnBooleanField.prototype */{ 21 | init: function (options) { 22 | FilterColumn.prototype.init.call(this, 23 | objectExtend({}, this.getFactory().defaults, options) 24 | ); 25 | 26 | const inst = PRIVATE.get(this); 27 | const opt = inst.opt; 28 | const inputWdg = inst.inputWdg = BooleanField.create({ 29 | column: opt.column, 30 | value: opt.value, 31 | hideLabel: true, 32 | hideDescription: true 33 | }); 34 | 35 | inputWdg.on("change", () => this.evalDirtyState()); 36 | inputWdg.appendTo(inst.inputHolder); 37 | this.removeCompareOperators("Contains"); 38 | this.setCompareOperatorDefault("Eq"); 39 | this.setKeywordInfo(""); 40 | this.evalDirtyState(); 41 | }, 42 | 43 | appendTo(ele) { 44 | FilterColumn.prototype.appendTo.call(this, ele); 45 | this.evalDirtyState(); 46 | } 47 | }); 48 | 49 | 50 | export default FilterColumnBooleanField; 51 | -------------------------------------------------------------------------------- /src/widgets/FilterPanel/FilterColumnChoiceField/FilterColumnChoiceField.js: -------------------------------------------------------------------------------- 1 | import FilterColumn from "../FilterColumn/FilterColumn"; 2 | import ChoiceField from "../../ChoiceField/ChoiceField"; 3 | import objectExtend from "common-micro-libs/src/jsutils/objectExtend"; 4 | import dataStore from "common-micro-libs/src/jsutils/dataStore"; 5 | import fillTemplate from "common-micro-libs/src/jsutils/fillTemplate"; 6 | 7 | var 8 | PRIVATE = dataStore.stash, 9 | 10 | /** 11 | * Widget description 12 | * 13 | * @class FilterColumnChoiceField 14 | * @extends FilterColumn 15 | * 16 | * @param {Object} options 17 | */ 18 | FilterColumnChoiceField = /** @lends FilterColumnChoiceField.prototype */{ 19 | init: function (options) { 20 | FilterColumn.prototype.init.call(this, 21 | objectExtend({}, this.getFactory().defaults, options) 22 | ); 23 | 24 | var 25 | inst = PRIVATE.get(this), 26 | opt = inst.opt, 27 | column = opt.column, 28 | choice; 29 | 30 | // Change the type temporarily so that the widget is 31 | // created with Checkboxes 32 | choice = inst.inputWdg = ChoiceField.create( 33 | objectExtend( 34 | { 35 | column: column, 36 | hideLabel: true, 37 | isMulti: true 38 | }, 39 | options 40 | ) 41 | ); 42 | 43 | choice.appendTo(inst.inputHolder); 44 | choice.on("change", function(){ 45 | this.evalDirtyState(); 46 | 47 | var totalSelected = choice.getValue().length; 48 | 49 | if (totalSelected) { 50 | this.setKeywordInfo(fillTemplate(opt.labels.totalSelected, {total: totalSelected})); 51 | 52 | } else { 53 | this.setKeywordInfo(""); 54 | } 55 | }.bind(this)); 56 | 57 | if (column.Type === "Choice") { 58 | this.setCompareOperatorDefault("Eq"); 59 | } 60 | 61 | this.setKeywordInfo(""); 62 | }, 63 | 64 | setFilter: function(filter){ 65 | var inst = PRIVATE.get(this); 66 | inst.setFieldCommonFilters.call(this, filter); 67 | return inst.inputWdg.setSelected(filter.values).then(() => this.evalDirtyState()); 68 | } 69 | }; 70 | 71 | FilterColumnChoiceField = FilterColumn.extend(FilterColumnChoiceField); 72 | FilterColumnChoiceField.defaults = {}; 73 | 74 | export default FilterColumnChoiceField; 75 | -------------------------------------------------------------------------------- /src/widgets/FilterPanel/FilterColumnContentTypeField/FilterColumnContentTypeField.js: -------------------------------------------------------------------------------- 1 | import FilterColumn from "../FilterColumn/FilterColumn"; 2 | import ContentTypeField from "../../ContentTypeField/ContentTypeField"; 3 | import objectExtend from "common-micro-libs/src/jsutils/objectExtend"; 4 | import dataStore from "common-micro-libs/src/jsutils/dataStore"; 5 | import fillTemplate from "common-micro-libs/src/jsutils/fillTemplate"; 6 | 7 | const PRIVATE = dataStore.stash; 8 | 9 | 10 | /** 11 | * Widget description 12 | * 13 | * @class FilterColumnContentTypeField 14 | * @extends FilterColumn 15 | * 16 | * @param {Object} options 17 | */ 18 | const FilterColumnContentTypeField = FilterColumn.extend(/** @lends FilterColumnContentTypeField.prototype */{ 19 | init: function (options) { 20 | FilterColumn.prototype.init.call(this, 21 | objectExtend({}, this.getFactory().defaults, options) 22 | ); 23 | 24 | const inst = PRIVATE.get(this); 25 | const opt = inst.opt; 26 | const column = opt.column; 27 | 28 | // Change the type temporarily so that the widget is 29 | // created with Checkboxes 30 | const choice = inst.inputWdg = ContentTypeField 31 | .extend({ 32 | setChoices(choices) { 33 | choices = choices || []; 34 | return ContentTypeField.prototype.setChoices.call(this, choices.map(choice => { 35 | choice.value = choice.title; 36 | return choice; 37 | })); 38 | } 39 | }).create( 40 | objectExtend( 41 | { 42 | column: column, 43 | hideLabel: true, 44 | isMulti: true 45 | }, 46 | options 47 | ) 48 | ); 49 | 50 | choice.appendTo(inst.inputHolder); 51 | choice.on("change", function(){ 52 | this.evalDirtyState(); 53 | 54 | const totalSelected = choice.getValue().length; 55 | 56 | if (totalSelected) { 57 | this.setKeywordInfo(fillTemplate(opt.labels.totalSelected, {total: totalSelected})); 58 | 59 | } else { 60 | this.setKeywordInfo(""); 61 | } 62 | }.bind(this)); 63 | 64 | this.removeCompareOperators("Contains"); 65 | this.setCompareOperatorDefault("Eq"); 66 | this.setKeywordInfo(""); 67 | }, 68 | 69 | setFilter: function(filter){ 70 | const inst = PRIVATE.get(this); 71 | inst.setFieldCommonFilters.call(this, filter); 72 | return inst.inputWdg.setSelected(filter.values).then(() => this.evalDirtyState()); 73 | } 74 | }); 75 | 76 | // FilterColumnContentTypeField.defaults = {}; 77 | 78 | export default FilterColumnContentTypeField; 79 | -------------------------------------------------------------------------------- /src/widgets/FilterPanel/FilterColumnDateTimeField/FilterColumnDateTimeField.js: -------------------------------------------------------------------------------- 1 | import FilterColumn from "../FilterColumn/FilterColumn"; 2 | import DateTimeField from "../../DateTimeField/DateTimeField"; 3 | import objectExtend from "common-micro-libs/src/jsutils/objectExtend"; 4 | import dataStore from "common-micro-libs/src/jsutils/dataStore"; 5 | import Promise from "common-micro-libs/src/jsutils/es6-promise"; 6 | 7 | //------------------------------------------------------------------- 8 | const PRIVATE = dataStore.stash; 9 | const FilterColumnPrototype = FilterColumn.prototype; 10 | 11 | 12 | /** 13 | * Filter Panel date/time field 14 | * 15 | * @class FilterColumnDateTimeField 16 | * @extends FilterColumn 17 | * 18 | * @param {Object} options 19 | */ 20 | const FilterColumnDateTimeField = FilterColumn.extend(/** @lends FilterColumnDateTimeField.prototype */{ 21 | init: function (options) { 22 | FilterColumnPrototype.init.call(this, 23 | objectExtend({}, this.getFactory().defaults, options) 24 | ); 25 | 26 | var 27 | inst = PRIVATE.get(this), 28 | opt = inst.opt, 29 | inputWdg = inst.inputWdg = DateTimeField.create({ 30 | column: opt.column, 31 | hideLabel: true, 32 | hideDescription: true, 33 | allowMultiples: true 34 | }); 35 | 36 | inputWdg.on("change", this.evalDirtyState.bind(this)); 37 | inputWdg.appendTo(inst.inputHolder); 38 | inputWdg.pipe(this, "DateTimeField"); 39 | 40 | this.setKeywordInfo(""); 41 | this.removeCompareOperators("Contains"); 42 | this.addCompareOperators([ 43 | {title: opt.labels.after, value: "Gt"}, 44 | {title: opt.labels.before, value: "Lt"} 45 | ]); 46 | this.setCompareOperatorDefault("Eq"); 47 | }, 48 | 49 | getValue: function(){ 50 | let dateValue = PRIVATE.get(this).inputWdg.getValue(); 51 | if (!dateValue || !dateValue.dateObj) { 52 | return []; 53 | } 54 | return [ dateValue.dateObj.toISOString() ]; 55 | }, 56 | 57 | setFilter: function(filter){ 58 | let inst = PRIVATE.get(this); 59 | let values = filter.values; 60 | 61 | inst.setFieldCommonFilters(filter); 62 | inst.inputWdg.setValue(Array.isArray(values) ? values[0] : values); 63 | this.evalDirtyState(); 64 | return Promise.resolve(); 65 | } 66 | }); 67 | 68 | FilterColumnDateTimeField.defaults = {}; 69 | 70 | export default FilterColumnDateTimeField; 71 | -------------------------------------------------------------------------------- /src/widgets/FilterPanel/FilterColumnLookupField/FilterColumnLookupField.js: -------------------------------------------------------------------------------- 1 | import FilterColumn from "../FilterColumn/FilterColumn"; 2 | import LookupField from "../../LookupField/LookupField"; 3 | import objectExtend from "common-micro-libs/src/jsutils/objectExtend"; 4 | import dataStore from "common-micro-libs/src/jsutils/dataStore"; 5 | 6 | var 7 | PRIVATE = dataStore.stash, 8 | 9 | /** 10 | * Filter panel lookup field 11 | * 12 | * @class FilterColumnLookupField 13 | * @extends FilterColumn 14 | * 15 | * @param {Object} options 16 | */ 17 | FilterColumnLookupField = /** @lends FilterColumnLookupField.prototype */{ 18 | init: function (options) { 19 | FilterColumn.prototype.init.call(this, 20 | objectExtend({}, this.getFactory().defaults, options) 21 | ); 22 | 23 | var 24 | inst = PRIVATE.get(this), 25 | opt = inst.opt, 26 | inputWdg = inst.inputWdg = LookupField.create({ 27 | column: opt.column, 28 | hideLabel: true, 29 | hideDescription: true, 30 | allowMultiples: true, 31 | choicesZIndex: opt.zIndex 32 | }); 33 | 34 | inputWdg.on("item:selected", this.evalDirtyState.bind(this)); 35 | inputWdg.on("item:unselected", this.evalDirtyState.bind(this)); 36 | inputWdg.appendTo(inst.inputHolder); 37 | inputWdg.pipe(this, "LookupField:"); 38 | 39 | this.setKeywordInfo(""); 40 | }, 41 | 42 | getValue: function(){ 43 | return PRIVATE.get(this).inputWdg.getSelected(); 44 | }, 45 | 46 | setFilter: function(filter){ 47 | var inst = PRIVATE.get(this); 48 | inst.setFieldCommonFilters(filter); 49 | return inst.inputWdg.setSelected(filter.values); 50 | } 51 | }; 52 | 53 | FilterColumnLookupField = FilterColumn.extend(FilterColumnLookupField); 54 | FilterColumnLookupField.defaults = {}; 55 | 56 | export default FilterColumnLookupField; 57 | -------------------------------------------------------------------------------- /src/widgets/FilterPanel/FilterColumnNumberField/FilterColumnNumberField.js: -------------------------------------------------------------------------------- 1 | import FilterColumnTextField from "../FilterColumnTextField/FilterColumnTextField"; 2 | import objectExtend from "common-micro-libs/src/jsutils/objectExtend"; 3 | import dataStore from "common-micro-libs/src/jsutils/dataStore"; 4 | 5 | const PRIVATE = dataStore.stash; 6 | const FilterColumnTextFieldPrototype = FilterColumnTextField.prototype; 7 | const toPrct = value => String(Number(value) * 100); 8 | const toDecimal = value => String(Number(value) / 100); 9 | 10 | /** 11 | * Widget description 12 | * 13 | * @class FilterColumnNumberField 14 | * @extends FilterColumnTextField 15 | * 16 | * @param {Object} options 17 | */ 18 | const FilterColumnNumberField = FilterColumnTextField.extend(/** @lends FilterColumnNumberField.prototype */{ 19 | init: function (options) { 20 | if (options && options.value) { 21 | options.value = toPrct(options.value); 22 | } 23 | 24 | FilterColumnTextFieldPrototype.init.call(this, 25 | objectExtend({}, this.getFactory().defaults, options) 26 | ); 27 | 28 | const labels = PRIVATE.get(this).opt.labels; 29 | 30 | this.addCompareOperators([ 31 | { 32 | value: "Gt", 33 | title: labels.greaterThan 34 | }, 35 | { 36 | value: "Lt", 37 | title: labels.lessThan 38 | } 39 | ]); 40 | 41 | this.setCompareOperatorDefault("Eq"); 42 | }, 43 | 44 | isPercent() { 45 | return !!(PRIVATE.get(this).opt.column || {}).Percentage; 46 | }, 47 | 48 | setFilter(filter) { 49 | if (this.isPercent()){ 50 | if (filter && filter.values) { 51 | filter.values.forEach((decimalValue, i) => filter.values[i] = toPrct(decimalValue)); 52 | } 53 | } 54 | FilterColumnTextFieldPrototype.setFilter.call(this, filter); 55 | }, 56 | 57 | getValue() { 58 | const response = FilterColumnTextFieldPrototype.getValue.call(this); 59 | return this.isPercent() ? toDecimal(response) : response; 60 | } 61 | }); 62 | 63 | FilterColumnNumberField.defaults = {}; 64 | 65 | export default FilterColumnNumberField; 66 | -------------------------------------------------------------------------------- /src/widgets/FilterPanel/FilterColumnTextField/FilterColumnTextField.js: -------------------------------------------------------------------------------- 1 | import FilterColumn from "../FilterColumn/FilterColumn"; 2 | import TextField from "../../TextField/TextField"; 3 | import dataStore from "common-micro-libs/src/jsutils/dataStore"; 4 | import objectExtend from "common-micro-libs/src/jsutils/objectExtend"; 5 | 6 | var 7 | PRIVATE = dataStore.stash, 8 | 9 | /** 10 | * A text field column for filter panel 11 | * 12 | * @class FilterColumnTextField 13 | * @extends FilterColumn 14 | * 15 | * @param {Object} options 16 | * In addition to the options required/supported by 17 | * [FilterColumn]{@link FilterColumn}, this Widget supports this additional 18 | * set documented below 19 | * 20 | * // FIXME document options 21 | */ 22 | FilterColumnTextField = /** @lends FilterColumnTextField.prototype */{ 23 | init: function (options) { 24 | FilterColumn.prototype.init.call(this, 25 | objectExtend({}, this.getFactory().defaults, options) 26 | ); 27 | 28 | var inst = PRIVATE.get(this), 29 | opt = inst.opt; 30 | 31 | inst.inputWdg = TextField.create({ 32 | column: opt.column, 33 | hideLabel: true, 34 | hideDescription: true, 35 | placeholder: opt.inputKeywords 36 | }); 37 | 38 | inst.inputWdg.on("change", function() { 39 | this.evalDirtyState(); 40 | }.bind(this)); 41 | 42 | inst.inputWdg.appendTo(inst.inputHolder); 43 | }, 44 | 45 | getKeywords: function(){ 46 | var 47 | opt = PRIVATE.get(this).opt, 48 | delimiter = opt.delimeter || ";", 49 | reIgnore = opt.ignoreKeywords; 50 | 51 | return this.getValue() 52 | .split(delimiter) 53 | .map(function(keyword){ 54 | return keyword.trim(); 55 | }) 56 | .filter(function(keyword){ 57 | return (keyword && !reIgnore.test(keyword)); 58 | }); 59 | } 60 | }; 61 | 62 | FilterColumnTextField = FilterColumn.extend(FilterColumnTextField); 63 | FilterColumnTextField.defaults = { 64 | delimiter: ";", 65 | ignoreKeywords: new RegExp() 66 | }; 67 | 68 | export default FilterColumnTextField; 69 | -------------------------------------------------------------------------------- /src/widgets/FilterPanel/FilterColumnUserField/FilterColumnUserField.js: -------------------------------------------------------------------------------- 1 | import FilterColumn from "../FilterColumn/FilterColumn"; 2 | import PeoplePicker from "../../PeoplePicker/PeoplePicker"; 3 | import objectExtend from "common-micro-libs/src/jsutils/objectExtend"; 4 | import dataStore from "common-micro-libs/src/jsutils/dataStore"; 5 | 6 | //============================================================================ 7 | const PRIVATE = dataStore.stash; 8 | 9 | /** 10 | * Filter Panel User field. 11 | * 12 | * @class FilterColumnUserField 13 | * @extends FilterColumn 14 | * 15 | * @param {Object} options 16 | */ 17 | const FilterColumnUserField = FilterColumn.extend(/** @lends FilterColumnUserField.prototype */{ 18 | init: function (options) { 19 | const opt = objectExtend({}, this.getFactory().defaults, options); 20 | FilterColumn.prototype.init.call(this, opt); 21 | 22 | const inst = PRIVATE.get(this); 23 | const userSelectionMode = opt.column ? opt.column.UserSelectionMode : null; 24 | const peoplePicker = inst.inputWdg = opt.PeoplePickerWidget.create({ 25 | resultsZIndex: inst.opt.zIndex, 26 | type: userSelectionMode && (userSelectionMode === "PeopleOnly" || String(userSelectionMode) === "0") ? "User" : "All" 27 | }); 28 | 29 | ["remove", "select"].forEach(function(evName){ 30 | peoplePicker.on(evName, this.evalDirtyState.bind(this)); 31 | }.bind(this)); 32 | 33 | peoplePicker.appendTo(inst.inputHolder); 34 | this.setKeywordInfo(""); 35 | }, 36 | 37 | getValue: function(){ 38 | return PRIVATE.get(this).inputWdg.getSelected(); 39 | }, 40 | 41 | setFilter: function(filter){ 42 | var inst = PRIVATE.get(this); 43 | 44 | inst.setFieldCommonFilters.call(this, filter); 45 | 46 | return inst.inputWdg 47 | .add(filter.values) 48 | .then(function(){ 49 | this.evalDirtyState(); 50 | }.bind(this)); 51 | } 52 | }); 53 | 54 | FilterColumnUserField.defaults = { 55 | PeoplePickerWidget: PeoplePicker 56 | }; 57 | 58 | export default FilterColumnUserField; 59 | -------------------------------------------------------------------------------- /src/widgets/FilterPanel/FilterPanel.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | {{labels.title}} 4 | 9 |
10 | 11 |
12 |
13 | 14 | 30 |
31 |
-------------------------------------------------------------------------------- /src/widgets/FilterPanel/FilterPanel.less: -------------------------------------------------------------------------------- 1 | .spwidgets-FilterPanel { 2 | 3 | .border-1px-solid(@prop: border) { 4 | @{prop}-width: 1px; 5 | @{prop}-style: solid; 6 | } 7 | 8 | position: relative; 9 | box-sizing: border-box; 10 | 11 | button { 12 | min-width: 0; 13 | } 14 | 15 | &-header { 16 | padding: 0.5em; 17 | position: relative; 18 | 19 | &-title { 20 | display: block; 21 | padding-right: 4em; 22 | } 23 | 24 | &-close { 25 | display: block; 26 | position: absolute; 27 | top: 0; 28 | right: 0; 29 | height: 100%; 30 | border: none; 31 | 32 | .ms-Button-icon { 33 | display: inline-block; 34 | } 35 | } 36 | } 37 | 38 | &-main { 39 | box-sizing: border-box; 40 | position: relative; 41 | .border-1px-solid() 42 | } 43 | 44 | &-body { 45 | box-sizing: border-box; 46 | min-height: 15em; 47 | } 48 | 49 | &-footer { 50 | box-sizing: border-box; 51 | position: relative; 52 | padding-top: 20px; 53 | 54 | &-actions { 55 | padding: 0.5em; 56 | text-align: right; 57 | .border-1px-solid(border-top); 58 | } 59 | } 60 | 61 | // ==> No Header 62 | &--noHeader &-header { 63 | display: none; 64 | } 65 | 66 | // ==> Fixed height filter panel 67 | &--fixHeight &-body { 68 | overflow-y: auto; 69 | } 70 | 71 | // ==> No Find button 72 | &--noFindButton &-footer-action-find { 73 | display: none; 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/widgets/ItemPicker/ItemPicker.js: -------------------------------------------------------------------------------- 1 | import Picker from "common-micro-libs/src/widgets/Picker/Picker" 2 | import domAddClass from "common-micro-libs/src/domutils/domAddClass" 3 | 4 | //============================================================================== 5 | 6 | /** 7 | * Same as Picker, but applies styles using UI Fabric 8 | * 9 | * @class ItemPicker 10 | * @extend Picker 11 | */ 12 | export default Picker.extend({ 13 | init: function (options) { 14 | Picker.prototype.init.call(this, options); 15 | 16 | var CSS_MS_FONT_M = "ms-font-m"; 17 | var CSS_MS_ICON = "ms-Icon ms-Icon"; 18 | var CSS_PICKER = "Picker"; 19 | var $ui = this.getEle(); 20 | var uiFind = $ui.querySelector.bind($ui); 21 | var $ele; 22 | 23 | domAddClass($ui, CSS_MS_FONT_M); 24 | domAddClass($ui, "ms-borderColor-neutralSecondary--hover"); 25 | domAddClass(this.getPopupWidget().getEle(), CSS_MS_FONT_M); 26 | 27 | $ele = uiFind(`.${CSS_PICKER}-clear`); 28 | $ele.textContent = ""; 29 | domAddClass($ele, `${CSS_MS_ICON}--ChromeClose`); 30 | 31 | $ele = uiFind(`.${CSS_PICKER}-showMenu`); 32 | $ele.textContent = ""; 33 | domAddClass($ele, `${CSS_MS_ICON}--ChevronDownMed`); 34 | } 35 | }); 36 | -------------------------------------------------------------------------------- /src/widgets/List/List.html: -------------------------------------------------------------------------------- 1 |
    -------------------------------------------------------------------------------- /src/widgets/ListItem/ListItem.less: -------------------------------------------------------------------------------- 1 | .spwidgets-ListItem { 2 | @MS_PRIMARY_TEXT: ms-ListItem-primaryText; 3 | 4 | &-simple { 5 | .@{MS_PRIMARY_TEXT} { 6 | padding-right: 0; 7 | } 8 | } 9 | 10 | &--hover { 11 | background-color: #eaeaea; 12 | cursor: pointer; 13 | outline: 1px solid transparent 14 | } 15 | 16 | &.is-selected .ms-ListItem-primaryText { 17 | color: #0078d7; 18 | } 19 | } -------------------------------------------------------------------------------- /src/widgets/ListItem/ListItemFull.html: -------------------------------------------------------------------------------- 1 |
  • 2 | {{_ui.primary}} 3 | {{_ui.secondary}} 4 | {{_ui.tertiary}} 5 | {{_ui.meta}} 6 | 7 |
    8 | 9 |
    10 |
    11 |
    12 |
    13 |
    14 |
    15 |
  • 16 | -------------------------------------------------------------------------------- /src/widgets/ListItem/ListItemSimple.html: -------------------------------------------------------------------------------- 1 |
  • 2 | {{_ui.primary}} 3 |
    4 |
  • -------------------------------------------------------------------------------- /src/widgets/LookupField/LookupField.html: -------------------------------------------------------------------------------- 1 |
    2 |
    3 | 4 |
    5 |
    6 | {{labels.clear}} 7 | {{_selectedCountUI}} 8 | 9 |
    10 |
    11 |
    12 |
    13 |
    {{column.Description}}
    14 |
    15 |
    -------------------------------------------------------------------------------- /src/widgets/LookupField/LookupField.less: -------------------------------------------------------------------------------- 1 | .spwidgets-LookupField { 2 | 3 | .scrollBar() { 4 | &::-webkit-scrollbar { 5 | width: 0.5em; 6 | background-color: #F5F5F5; 7 | } 8 | 9 | &::-webkit-scrollbar-thumb { 10 | -webkit-box-shadow: inset 0 0 6px rgba(0,0,0,.3); 11 | background-color : #555; 12 | } 13 | } 14 | 15 | background-color: white; 16 | 17 | &-items { 18 | border: 1px solid #c8c8c8; 19 | position: relative; 20 | box-sizing: border-box; 21 | 22 | &:hover { 23 | border-color: #767676; 24 | } 25 | 26 | &-count { 27 | display: inline-block; 28 | vertical-align: middle; 29 | padding: 0 1em; 30 | font-style: italic; 31 | } 32 | 33 | &-clear { 34 | vertical-align: middle; 35 | display: inline-block; 36 | cursor: pointer; 37 | } 38 | } 39 | 40 | &-selected { 41 | max-height: 15em; 42 | overflow-y: auto; 43 | 44 | .scrollBar() 45 | } 46 | 47 | 48 | &-input { 49 | position: relative; 50 | box-sizing: border-box; 51 | 52 | & > input { 53 | box-sizing: border-box; 54 | display: inline-block; 55 | border: 0; 56 | height: 38px; 57 | outline: none; 58 | padding-left: 8px; 59 | } 60 | 61 | &-choices { 62 | .scrollBar(); 63 | 64 | box-sizing: border-box; 65 | display: none; 66 | position: absolute; 67 | top: 100%; 68 | left: 0; 69 | min-height: 2em; 70 | border: 1px solid #ababab; 71 | border-top: none; 72 | padding: 0.5em 1em; 73 | box-shadow: 0 0 5px 0 rgba(0,0,0,.4); 74 | background-color: white; 75 | z-index: 5; 76 | max-width: 100%; 77 | max-height: 20em; 78 | overflow-y: auto; 79 | } 80 | } 81 | 82 | 83 | //--------------------------- 84 | // NO MODIFIERS 85 | //--------------------------- 86 | 87 | //==> Hide label 88 | &--noLabel &-label { 89 | display: none; 90 | } 91 | 92 | //==> Hide description 93 | &--noDescription &-description { 94 | display: none; 95 | } 96 | 97 | //==> -displayInline 98 | &--displayInline &-selected, 99 | &--displayInline &-selected > *, 100 | &--displayInline &-input { 101 | display: inline-block; 102 | } 103 | &--displayInline &-selected { 104 | max-height: none; 105 | } 106 | &--displayInline &-input-choices { 107 | max-width: 450px; 108 | } 109 | 110 | } -------------------------------------------------------------------------------- /src/widgets/LookupField/SelectedItem/SelectedItem.html: -------------------------------------------------------------------------------- 1 |
    2 |
    3 | {{item.Title}} 4 |
    5 | 10 |
    -------------------------------------------------------------------------------- /src/widgets/LookupField/SelectedItem/SelectedItem.js: -------------------------------------------------------------------------------- 1 | import Widget from "common-micro-libs/src/jsutils/Widget"; 2 | import EventEmitter from "common-micro-libs/src/jsutils/EventEmitter"; 3 | import dataStore from "common-micro-libs/src/jsutils/dataStore"; 4 | import objectExtend from "common-micro-libs/src/jsutils/objectExtend"; 5 | import fillTemplate from "common-micro-libs/src/jsutils/fillTemplate"; 6 | import parseHTML from "common-micro-libs/src/jsutils/parseHTML"; 7 | import domAddEventListener from "common-micro-libs/src/domutils/domAddEventListener"; 8 | import SelectedItemTemplate from "./SelectedItem.html"; 9 | import "./SelectedItem.less"; 10 | 11 | var 12 | PRIVATE = dataStore.create(), 13 | 14 | CSS_CLASS_REMOVE_BUTTON = "spwidgets-LookupField-SelectedItem-remove", 15 | 16 | /** 17 | * Displays a selected item from the LookupField list of choices. 18 | * 19 | * @class SelectedItem 20 | * @extends Widget 21 | * @extends EventEmitter 22 | * 23 | * @param {Object} options 24 | * 25 | * @fires SelectedItem#remove 26 | */ 27 | SelectedItem = /** @lends SelectedItem.prototype */{ 28 | init: function (options) { 29 | var inst = { 30 | opt: objectExtend({}, this.getFactory().defaults, options) 31 | }; 32 | 33 | PRIVATE.set(this, inst); 34 | 35 | this.$ui = parseHTML( 36 | fillTemplate(this.getTemplate(), inst.opt) 37 | ).firstChild; 38 | 39 | inst.$removeBtn = this.$ui.querySelector("." + CSS_CLASS_REMOVE_BUTTON); 40 | 41 | domAddEventListener(inst.$removeBtn, "click", function(ev){ 42 | ev.stopPropagation(); 43 | /** 44 | * User clicked on the Remove button. The `item` object provided on input 45 | * will be given to listeners of this event. 46 | * 47 | * @event SelectedItem#remove 48 | * @type {Object} 49 | */ 50 | this.emit("remove", inst.opt.item); 51 | }.bind(this)); 52 | 53 | this.onDestroy(function () { 54 | PRIVATE.delete(this); 55 | }.bind(this)); 56 | }, 57 | 58 | /** 59 | * Returns the template for the widget. 60 | * 61 | * @return {String} 62 | */ 63 | getTemplate: function(){ 64 | return SelectedItemTemplate; 65 | }, 66 | 67 | /** 68 | * Same as user clicking the remove button of the selected item 69 | */ 70 | remove: function(){ 71 | PRIVATE.get(this).$removeBtn.click(); 72 | }, 73 | 74 | /** 75 | * Returns the value for this selected item (default is the item passed on input) 76 | * 77 | * @return {Object} 78 | */ 79 | getValue: function(){ 80 | return PRIVATE.get(this).opt.item; 81 | } 82 | }; 83 | 84 | SelectedItem = EventEmitter.extend(Widget, SelectedItem); 85 | SelectedItem.defaults = { 86 | item: null 87 | }; 88 | 89 | export default SelectedItem; 90 | -------------------------------------------------------------------------------- /src/widgets/LookupField/SelectedItem/SelectedItem.less: -------------------------------------------------------------------------------- 1 | .spwidgets-LookupField-SelectedItem { 2 | position: relative; 3 | margin: 4px; 4 | padding: 0; 5 | border: 1px solid transparent; 6 | 7 | &:hover { 8 | border-color: #C8C8C8; 9 | } 10 | 11 | &-info { 12 | padding-right: 34px; 13 | padding-left: 0.5em; 14 | background-color: #f4f4f4; 15 | min-height: 32px; 16 | line-height: 32px; 17 | white-space: nowrap; 18 | text-overflow: ellipsis; 19 | overflow: hidden; 20 | } 21 | 22 | &-remove { 23 | position: absolute; 24 | height: 100%; 25 | top: 0; 26 | right: 0; 27 | min-width: 0; 28 | 29 | .ms-Button-icon { 30 | display: inline-block; 31 | } 32 | 33 | &:hover { 34 | background-color: #eaeaea; 35 | color: #333; 36 | } 37 | } 38 | } -------------------------------------------------------------------------------- /src/widgets/Message/Message.html: -------------------------------------------------------------------------------- 1 |
    2 | 3 | 4 | 5 |
    6 |
    7 | {{opt.message}} 8 | {{opt.more}} 9 |
    10 |
    11 |
    12 |
    -------------------------------------------------------------------------------- /src/widgets/Message/Message.less: -------------------------------------------------------------------------------- 1 | .spwidgets-message { 2 | position: relative; 3 | padding: 0.5em; 4 | 5 | &-iconHolder { 6 | display: block; 7 | position: absolute; 8 | font-size: 1.5em; 9 | line-height: 1em; 10 | } 11 | 12 | &-msgHolder { 13 | padding-left: 2em; 14 | min-height: 2em; 15 | 16 | &-msg > * { 17 | display: inline; 18 | 19 | } 20 | 21 | } 22 | 23 | &-msg { 24 | &-showMore { 25 | display: none; 26 | cursor: pointer; 27 | } 28 | 29 | &-more { 30 | display: none; 31 | white-space: pre-wrap; 32 | } 33 | } 34 | 35 | //-------------------------------- 36 | // modifiers 37 | //-------------------------------- 38 | 39 | &--hasMore &-msg-showMore { 40 | display: inline; 41 | 42 | } 43 | 44 | &--showMore &-msg-more { 45 | display: block; 46 | } 47 | } -------------------------------------------------------------------------------- /src/widgets/PeoplePicker/PeoplePicker.html: -------------------------------------------------------------------------------- 1 |
    2 | 7 | 8 |
    9 |
    10 | 11 |
    12 |
    13 | 14 | 15 |
    16 |

    {{labels.searchInfoMsg}}

    17 |

    {{labels.searchingMsg}}

    18 |
    19 |
    20 |
    21 | -------------------------------------------------------------------------------- /src/widgets/PeoplePicker/PeoplePicker.less: -------------------------------------------------------------------------------- 1 | .spwidgets-PeoplePicker { 2 | 3 | // Overrides needed due to sharepoint styles (in 2013) 4 | button { 5 | min-width: 0; 6 | padding: initial; 7 | margin-left: auto; 8 | font-size: initial; 9 | } 10 | 11 | &-searchFieldCntr { 12 | min-width: 180px; 13 | 14 | input { 15 | min-width: 100%; 16 | } 17 | } 18 | 19 | &-suggestions { 20 | min-width: 250px; 21 | position: absolute; 22 | z-index: 5; 23 | 24 | &-groups { 25 | padding: 1px; // needed to avoid unnecessary scroll bar 26 | overflow-y: auto; 27 | overflow-x: hidden; 28 | max-height: 20em; 29 | 30 | &::-webkit-scrollbar { 31 | width: 0.5em; 32 | background-color: #F5F5F5; 33 | } 34 | 35 | &::-webkit-scrollbar-thumb { 36 | -webkit-box-shadow: inset 0 0 6px rgba(0,0,0,.3); 37 | background-color : #555; 38 | } 39 | } 40 | } 41 | 42 | &-searchInfo { 43 | border-top: 1px solid #eaeaea; 44 | padding: 1em; 45 | 46 | &-iconHolder { 47 | position: absolute; 48 | } 49 | 50 | > p { 51 | padding-left: 2em; 52 | margin: 0; 53 | } 54 | 55 | &-searchingMsg { 56 | display: none; 57 | } 58 | } 59 | 60 | &-busy { 61 | display: none; 62 | } 63 | 64 | // Override UI Fabric so that each persona card has 65 | // a max-width of 190px instead of fixed width 66 | .ms-Persona { 67 | &-details > * { 68 | width: auto; 69 | max-width: 190px; 70 | } 71 | } 72 | 73 | 74 | 75 | //----------------------------------------- 76 | // MODIFIERS 77 | //----------------------------------------- 78 | &-suggestions.is-searching .ms-Icon--Search { 79 | display: none; 80 | } 81 | &-suggestions.is-searching &-searchInfo-msg { 82 | display: none; 83 | } 84 | 85 | &-suggestions.is-searching &-searchInfo-searchingMsg { 86 | display: block; 87 | } 88 | &-suggestions.is-searching &-busy { 89 | display: inline-block; 90 | -webkit-animation: spwidgets-PeoplePicker-rotate 0.8s linear infinite; 91 | animation: spwidgets-PeoplePicker-rotate 0.8s linear infinite; 92 | } 93 | 94 | 95 | &--suggestionsRight &-suggestions { 96 | right: 0; 97 | } 98 | 99 | //--------------------------------------- 100 | // ANIMATIONS 101 | //--------------------------------------- 102 | @-webkit-keyframes spwidgets-PeoplePicker-rotate { 103 | from { 104 | -ms-transform: rotate(0deg); 105 | -moz-transform: rotate(0deg); 106 | -webkit-transform: rotate(0deg); 107 | -o-transform: rotate(0deg); 108 | transform: rotate(0deg); 109 | } 110 | to { 111 | -ms-transform: rotate(360deg); 112 | -moz-transform: rotate(360deg); 113 | -webkit-transform: rotate(360deg); 114 | -o-transform: rotate(360deg); 115 | transform: rotate(360deg); 116 | } 117 | } 118 | @keyframes spwidgets-PeoplePicker-rotate { 119 | from { 120 | -ms-transform: rotate(0deg); 121 | -moz-transform: rotate(0deg); 122 | -webkit-transform: rotate(0deg); 123 | -o-transform: rotate(0deg); 124 | transform: rotate(0deg); 125 | } 126 | to { 127 | -ms-transform: rotate(360deg); 128 | -moz-transform: rotate(360deg); 129 | -webkit-transform: rotate(360deg); 130 | -o-transform: rotate(360deg); 131 | transform: rotate(360deg); 132 | } 133 | } 134 | } -------------------------------------------------------------------------------- /src/widgets/PeoplePicker/PeoplePickerPersona/PeoplePickerPersona.html: -------------------------------------------------------------------------------- 1 |
    2 | {{PersonaTemplateHtml}} 3 |
    4 | -------------------------------------------------------------------------------- /src/widgets/PeoplePicker/PeoplePickerPersona/PeoplePickerPersona.js: -------------------------------------------------------------------------------- 1 | import Persona from "../../Persona/Persona" 2 | import fillTemplate from "common-micro-libs/src/jsutils/fillTemplate" 3 | import domAddClass from "common-micro-libs/src/domutils/domAddClass" 4 | import objectExtend from "common-micro-libs/src/jsutils/objectExtend" 5 | 6 | import PeoplePickerPersonaTemplate from "./PeoplePickerPersona.html" 7 | import "./PeoplePickerPersona.less"; 8 | 9 | //========================================================================= 10 | 11 | /** 12 | * A Selected Persona widget for the people picker, which includes 13 | * a button to remove the entry from the selected list. 14 | * 15 | * @class PeoplePickerPersona 16 | * @extends Persona 17 | * 18 | * @triggers PeoplePickerPersona#remove 19 | */ 20 | let PeoplePickerPersona = /** @lends PeoplePickerPersona.prototype **/{ 21 | init: function (options) { 22 | let opt = objectExtend({}, this.getFactory().defaults, options); 23 | 24 | Persona.prototype.init.call(this, opt); 25 | 26 | let evActionClickListener = this.on("action-click", () => { 27 | /** 28 | * User clicked the remove button on the people picker persona. 29 | * 30 | * @event PeoplePickerPersona#remove 31 | */ 32 | this.emit("remove"); 33 | }); 34 | 35 | this.onDestroy(function(){ 36 | evActionClickListener.off(); 37 | }); 38 | 39 | if (String(this.getUserProfile().ID).toLowerCase() === "") { 40 | this.setAsCurrentUser(); 41 | this.setPresence("noPresence"); 42 | } 43 | }, 44 | 45 | getTemplate: function(){ 46 | return fillTemplate( 47 | PeoplePickerPersonaTemplate, 48 | { PersonaTemplateHtml: Persona.prototype.getTemplate.call(this) } 49 | ); 50 | }, 51 | 52 | /** 53 | * Used to highlight the persona that it is not a specific user, but 54 | * rather the pseudo entry that point to the currently logged in user. 55 | */ 56 | setAsCurrentUser: function(){ 57 | domAddClass(this.getEle(), "is-currentUserEntry"); 58 | } 59 | }; 60 | 61 | PeoplePickerPersona = Persona.extend(PeoplePickerPersona); 62 | PeoplePickerPersona.defaults = objectExtend({}, PeoplePickerPersona.defaults, { 63 | variant: "token", 64 | hideAction: false 65 | }); 66 | 67 | export default PeoplePickerPersona; 68 | -------------------------------------------------------------------------------- /src/widgets/PeoplePicker/PeoplePickerPersona/PeoplePickerPersona.less: -------------------------------------------------------------------------------- 1 | .spwidgets-PeoplePicker-persona { 2 | 3 | display: inline-block; 4 | 5 | &.is-currentUserEntry .ms-Persona-primaryText { 6 | color: #005A9E; 7 | font-style: italic; 8 | padding-right: 0.5em; // so that last letter is not truncated due to italics 9 | } 10 | } -------------------------------------------------------------------------------- /src/widgets/PeoplePicker/PeoplePickerREST.js: -------------------------------------------------------------------------------- 1 | import objectExtend from "common-micro-libs/src/jsutils/objectExtend" 2 | import dataStore from "common-micro-libs/src/jsutils/dataStore" 3 | import PeoplePicker from "./PeoplePicker" 4 | import searchPeoplePicker from "../../spapi/rest/searchPeoplePicker" 5 | import ensureUser from "../../spapi/rest/ensureUser" 6 | 7 | // PeoplePicker widget based on SharePoint's REST api 8 | const PRIVATE = dataStore.create(); 9 | const PeoplePickerREST = PeoplePicker.extend({}); 10 | 11 | PeoplePickerREST.defaults = objectExtend({}, PeoplePickerREST.defaults, { 12 | apiSearch: searchPeoplePicker, 13 | UserProfileModel: PeoplePicker.defaults.UserProfileModel.extend({ 14 | resolvePrincipal: function(){ 15 | if (this.ID && this.ID !== "-1") { 16 | return Promise.resolve(this); 17 | } 18 | 19 | let inst; 20 | 21 | if (PRIVATE.has(this)) { 22 | inst = PRIVATE.get(this); 23 | } else { 24 | inst = { resolvePromise: null }; 25 | PRIVATE.set(this, inst); 26 | } 27 | 28 | if (inst.resolvePromise) { 29 | return inst.resolvePromise; 30 | } 31 | 32 | inst.resolvePromise = ensureUser({ 33 | webURL: this.webURL, 34 | logonName: this.AccountName || this.LoginName 35 | }).then(response => { 36 | this.ID = response.ID; 37 | return this; 38 | }); 39 | 40 | return inst.resolvePromise 41 | } 42 | }) 43 | }); 44 | 45 | export default PeoplePickerREST; -------------------------------------------------------------------------------- /src/widgets/PeoplePicker/PeoplePickerUserProfileModel.js: -------------------------------------------------------------------------------- 1 | import Promise from "common-micro-libs/src/jsutils/es6-promise" 2 | import dataStore from "common-micro-libs/src/jsutils/dataStore" 3 | 4 | import UserProfileModel from "../../models/UserProfileModel"; 5 | import resolvePrincipals from "../../spapi/resolvePrincipals" 6 | 7 | 8 | //======================================================================= 9 | const PRIVATE = dataStore.create(); 10 | 11 | /** 12 | * People picker user profile model used to model each user profile 13 | * 14 | * @class PeoplePickerUserProfileModel 15 | * @extends UserProfileModel 16 | */ 17 | let PeoplePickerUserProfileModel = UserProfileModel.extend(/** @lends PeoplePickerProfileModel.prototype */{ 18 | /** 19 | * The web URL from where this user was retrieved. Used in resolved principal 20 | * @type {String} 21 | */ 22 | webURL: "", 23 | 24 | /** 25 | * Returns the AccountName url encoded. 26 | * 27 | * @returns {string} 28 | */ 29 | getAccountNameUrlEncoded: function(){ 30 | return encodeURIComponent(this.AccountName); 31 | }, 32 | 33 | /** 34 | * Resolves the person against the site (`webURL`) by calling 35 | * the `ResolvePrincipal` API. API is only called if `ID` is `-1` 36 | * 37 | * @returns {Promise} 38 | */ 39 | resolvePrincipal: function(){ 40 | if (this.ID && this.ID !== "-1") { 41 | return Promise.resolve(this); 42 | } 43 | 44 | let inst; 45 | 46 | if (PRIVATE.has(this)) { 47 | inst = PRIVATE.get(this); 48 | } else { 49 | inst = { resolvePromise: null }; 50 | PRIVATE.set(this, inst); 51 | } 52 | 53 | if (inst.resolvePromise) { 54 | return inst.resolvePromise; 55 | } 56 | 57 | inst.resolvePromise = resolvePrincipals({ 58 | webURL: this.webURL, 59 | principalKeys: this.AccountName 60 | }).then(function(userList){ 61 | // See Issue #42 for the possibility of the results returning 62 | // multiples, even when only one principalKey was provided on 63 | // input to the API. 64 | // https://github.com/purtuga/SPWidgets/issues/42 65 | userList.some(function(resolvedUser){ 66 | if ( 67 | resolvedUser.ID && 68 | String(resolvedUser.ID) !== "-1" && 69 | ( 70 | ( 71 | resolvedUser.AccountName && 72 | resolvedUser.AccountName === this.AccountName 73 | ) || 74 | ( 75 | resolvedUser.Email && 76 | resolvedUser.Email === this.Email 77 | ) || 78 | ( 79 | resolvedUser.DisplayName && 80 | resolvedUser.DisplayName === this.DisplayName 81 | ) 82 | 83 | ) 84 | ) { 85 | this.UserInfoID = this.ID = resolvedUser.ID; 86 | return true; 87 | } 88 | 89 | }.bind(this)); 90 | 91 | return this; 92 | }.bind(this)); 93 | 94 | return inst.resolvePromise 95 | } 96 | }); 97 | 98 | export default PeoplePickerUserProfileModel; 99 | -------------------------------------------------------------------------------- /src/widgets/PeoplePicker/Result/Result.html: -------------------------------------------------------------------------------- 1 |
    2 | {{PersonaTemplateHtml}} 3 |
    -------------------------------------------------------------------------------- /src/widgets/PeoplePicker/Result/Result.js: -------------------------------------------------------------------------------- 1 | import Persona from "../../Persona/Persona" 2 | import fillTemplate from "common-micro-libs/src/jsutils/fillTemplate" 3 | import domHasClass from "common-micro-libs/src/domutils/domHasClass" 4 | import domAddClass from "common-micro-libs/src/domutils/domAddClass" 5 | import domRemoveClass from "common-micro-libs/src/domutils/domRemoveClass" 6 | 7 | import ResultTemplate from "./Result.html" 8 | import "./Result.less"; 9 | 10 | 11 | //============================================================================ 12 | 13 | const CSS_CLASS_MS_PICKER_RESULT = "spwidgets-PeoplePicker-Result"; 14 | const CSS_CLASS_MS_PICKER_RESULT_FOCUS = CSS_CLASS_MS_PICKER_RESULT + "--focus"; 15 | 16 | /** 17 | * A People picker suggestion result 18 | * 19 | * @class Result 20 | * @extends Persona 21 | * 22 | * @param {Object} options 23 | * @param {Object} options.userProfile 24 | */ 25 | let Result = { 26 | init: function(){ 27 | Persona.prototype.init.apply(this, arguments); 28 | if (this.getUserProfile().ID === "") { 29 | this.setAsCurrentUser(); 30 | } 31 | this.setSize("sm"); 32 | }, 33 | // Returns the People Picker Result wrapper with the persona template inside. 34 | getTemplate: function(){ 35 | return fillTemplate( 36 | ResultTemplate, 37 | { PersonaTemplateHtml: Persona.prototype.getTemplate.call(this) } 38 | ); 39 | }, 40 | 41 | /** 42 | * Highlights he result items 43 | */ 44 | setFocus: function(){ 45 | domAddClass(this.getEle(), CSS_CLASS_MS_PICKER_RESULT_FOCUS); 46 | }, 47 | 48 | /** 49 | * Removes the highlight of the item 50 | */ 51 | removeFocus: function(){ 52 | domRemoveClass(this.getEle(), CSS_CLASS_MS_PICKER_RESULT_FOCUS); 53 | }, 54 | 55 | /** 56 | * Returns a boolean indicating if item is currently focused 57 | */ 58 | hasFocus: function(){ 59 | return domHasClass(this.getEle(), CSS_CLASS_MS_PICKER_RESULT_FOCUS); 60 | }, 61 | 62 | /** 63 | * Used to highlight the persona that it is not a specific user, but 64 | * rather the pseudo entry that point to the currently logged in user. 65 | */ 66 | setAsCurrentUser: function(){ 67 | domAddClass(this.getEle(), "is-currentUserEntry"); 68 | } 69 | }; 70 | 71 | Result = Persona.extend(Result); 72 | 73 | export default Result; 74 | 75 | -------------------------------------------------------------------------------- /src/widgets/PeoplePicker/Result/Result.less: -------------------------------------------------------------------------------- 1 | .spwidgets-PeoplePicker-Result { 2 | &.is-currentUserEntry .ms-Persona-primaryText { 3 | color: #005A9E; 4 | font-style: italic; 5 | } 6 | 7 | &--focus { 8 | background-color: #eaeaea; 9 | outline: 1px solid transparent; 10 | } 11 | 12 | //&--no-button &-resultAction { 13 | // display: none; 14 | //} 15 | } 16 | 17 | -------------------------------------------------------------------------------- /src/widgets/PeoplePicker/ResultGroup/ResultGroup.html: -------------------------------------------------------------------------------- 1 |
    2 |
    {{groupTitle}}
    3 |
    4 |
    5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/widgets/Persona/Persona.html: -------------------------------------------------------------------------------- 1 |
    2 |
    3 |
    {{Initials}}
    4 |
    5 | 6 |
    7 |
    8 |
    9 |
    10 |
    {{Name}}
    11 |
    {{Title}}
    12 |
    {{Office}}
    13 |
    14 |
    15 |
    16 | 17 |
    18 |
    -------------------------------------------------------------------------------- /src/widgets/Persona/Persona.less: -------------------------------------------------------------------------------- 1 | .spwidgets-Persona { 2 | 3 | .ms-Image-image { 4 | width: 100%; 5 | } 6 | 7 | .ms-Persona-initials:empty:before { 8 | content: "?"; 9 | } 10 | 11 | //-------------------------- 12 | // MODIFIERS 13 | //-------------------------- 14 | &--noDetails .ms-Persona-details { 15 | display: none; 16 | } 17 | 18 | &--noAction .ms-Persona-actionIcon { 19 | display: none; 20 | } 21 | 22 | // hide image if forced to initials 23 | &--showInitials .ms-Persona-image { 24 | display: none; 25 | } 26 | 27 | &.ms-Persona--nopresence .ms-Persona-presence { 28 | display: none; 29 | } 30 | } -------------------------------------------------------------------------------- /src/widgets/PersonaCard/PersonaCard.html: -------------------------------------------------------------------------------- 1 |
    2 |
    3 |
    4 |
    5 |
    -------------------------------------------------------------------------------- /src/widgets/PersonaCard/PersonaCard.less: -------------------------------------------------------------------------------- 1 | .spwidgets-PersonaCard { 2 | 3 | } -------------------------------------------------------------------------------- /src/widgets/PersonaCard/PersonaCardActionDetails/PersonaCardActionDetails.html: -------------------------------------------------------------------------------- 1 |
    2 | {{detailsHTML}} 3 |
    -------------------------------------------------------------------------------- /src/widgets/PersonaCard/PersonaCardActionDetails/PersonaCardActionDetails.js: -------------------------------------------------------------------------------- 1 | import Widget from "common-micro-libs/src/jsutils/Widget" 2 | import EventEmitter from "common-micro-libs/src/jsutils/EventEmitter" 3 | import dataStore from "common-micro-libs/src/jsutils/dataStore" 4 | import objectExtend from "common-micro-libs/src/jsutils/objectExtend" 5 | import fillTemplate from "common-micro-libs/src/jsutils/fillTemplate" 6 | import parseHTML from "common-micro-libs/src/jsutils/parseHTML" 7 | 8 | import PersonaCardActionDetailsTemplate from "./PersonaCardActionDetails.html" 9 | import detailLineTemplate from "./detailLine.html" 10 | 11 | //========================================================================== 12 | const PRIVATE = dataStore.create(); 13 | 14 | 15 | /** 16 | * PersonaCardActionDetails Widget Display the content of an action when the 17 | * user clicks on it. 18 | * 19 | * @class PersonaCardActionDetails 20 | * @extends Widget 21 | * @extends EventEmitter 22 | * 23 | * @param {Object} options 24 | * @param {Array} options.details 25 | * Each Detail object can have the following properties: 26 | * 27 | * - `label`: The label for the value 28 | * - `value`: the value for detail item. Could be a string of HTML 29 | * 30 | */ 31 | const PersonaCardActionDetails = EventEmitter.extend(Widget).extend(/** @lends PersonaCardActionDetails.prototype */{ 32 | init(options) { 33 | var inst = { 34 | opt: objectExtend({}, this.getFactory().defaults, options) 35 | }; 36 | 37 | PRIVATE.set(this, inst); 38 | 39 | let $ui = this.$ui = this.getTemplate(); 40 | 41 | if (typeof $ui === "string") { 42 | $ui = this.$ui = parseHTML(fillTemplate($ui, { 43 | detailsHTML: fillTemplate(this.getDetailLineTemplate(), inst.opt.details) 44 | })).firstChild; 45 | } 46 | 47 | 48 | this.onDestroy(() => { 49 | // Destroy all Compose object 50 | Object.keys(inst).forEach(function (prop) { 51 | if (inst[prop]) { 52 | [ 53 | "destroy", // Compose 54 | "remove", // DOM Events Listeners 55 | "off" // EventEmitter Listeners 56 | ].some((method) => { 57 | if (inst[prop][method]) { 58 | inst[prop][method](); 59 | return true; 60 | } 61 | }); 62 | 63 | inst[prop] = undefined; 64 | } 65 | }); 66 | 67 | PRIVATE["delete"](this); 68 | }); 69 | }, 70 | 71 | /** 72 | * returns the widget's template 73 | * @return {String} 74 | */ 75 | getTemplate(){ 76 | return PersonaCardActionDetailsTemplate; 77 | }, 78 | 79 | /** 80 | * Returns the HTML template for an insividual item 81 | * 82 | * @return {String} 83 | */ 84 | getDetailLineTemplate(){ 85 | return detailLineTemplate; 86 | } 87 | }); 88 | 89 | PersonaCardActionDetails.defaults = { 90 | details: null // Array 91 | }; 92 | 93 | export default PersonaCardActionDetails; 94 | -------------------------------------------------------------------------------- /src/widgets/PersonaCard/PersonaCardActionDetails/detailLine.html: -------------------------------------------------------------------------------- 1 |
    2 | {{label}} 3 | {{value}} 4 |
    -------------------------------------------------------------------------------- /src/widgets/PersonaCard/PersonaCardActions/PersonaCardActions.html: -------------------------------------------------------------------------------- 1 |
      2 | {{_actionsHTML}} 3 |
    -------------------------------------------------------------------------------- /src/widgets/PersonaCard/PersonaCardActions/action.html: -------------------------------------------------------------------------------- 1 |
  • 2 | 3 | {{title}} 4 |
  • -------------------------------------------------------------------------------- /src/widgets/TextField/TextField.html: -------------------------------------------------------------------------------- 1 |
    2 | 3 | 4 | {{column.Description}} 5 |
    -------------------------------------------------------------------------------- /src/widgets/TextField/TextField.less: -------------------------------------------------------------------------------- 1 | .spwidgets-TextField { 2 | 3 | input.ms-TextField-field { 4 | min-width: 100%; 5 | } 6 | 7 | &--noLabel .ms-Label { 8 | display: none; 9 | } 10 | 11 | &--noDescription .ms-TextField-description { 12 | display: none; 13 | } 14 | } -------------------------------------------------------------------------------- /test/.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "globals": { 3 | "SP": true, 4 | "window": true, 5 | "document": true, 6 | "location": true, 7 | "alert": true, 8 | "tinymce": true, 9 | "unescape": true, 10 | "prompt": true, 11 | "confirm": true, 12 | "define": true, 13 | "jQuery": true, 14 | "describe": true, 15 | "it": true, 16 | "expect": true, 17 | "beforeEach": true, 18 | "afterEach": true 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /test/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | SPWidgets Test Suite 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /test/server/mock.soap.GetList.js: -------------------------------------------------------------------------------- 1 | define([ 2 | "text!./soapMsgs/list.GetList.response.success.xml" 3 | ], function( 4 | getListResponseSuccessXML 5 | ){ 6 | 7 | return { 8 | 9 | // requests must have 'auto_respond' in the request message 10 | install: function(){ 11 | 12 | jasmine.Ajax.stubRequest( 13 | /.*\/_vti_bin\/Lists\.asmx/, // url 14 | /.* 31 | // 32 | // 33 | // https://site.sharepoint.com/sites/mysite/Shared%20Documents/page.apsxp 34 | // 35 | // 36 | // 37 | 38 | }); 39 | -------------------------------------------------------------------------------- /test/server/mock.soap.getListItems.js: -------------------------------------------------------------------------------- 1 | define([ 2 | "text!./soapMsgs/list.GetListItems.response.success.xml", 3 | "text!./soapMsgs/list.GetListItemsChangesSinceTokenResponse.response.success.xml" 4 | ], function( 5 | getListItemsResponseSuccessXML, 6 | getListItemsChangesSinceTokenResponseSuccessXML 7 | ){ 8 | 9 | return { 10 | // requests must have 'auto_respond' in the request message 11 | install: function(){ 12 | 13 | jasmine.Ajax.stubRequest( 14 | /.*\/_vti_bin\/Lists\.asmx/, // url 15 | /.* 2 | 4 | 5 | 6 | 0 7 | 8 | 0x85300000 9 | Some error message 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /test/server/soapMsgs/error.ErrorCode.good.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 0 7 | 8 | 0x00000000 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /test/server/soapMsgs/error.copyResult.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | err101 8 | Some Message here 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /test/server/soapMsgs/error.faultcode.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | soap:Server 7 | Exception of type 'Microsoft.SharePoint.SoapServer.SoapServerException' was thrown. 8 | 9 | 10 | The specified name is already in use. 11 | 0x81020067 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /test/server/soapMsgs/forms.GetListFormCollection.response.success.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 |
    9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /test/server/soapMsgs/list.GetListItems.response.success.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /test/server/soapMsgs/login.operation.response.cannotBeNull.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | soap:Server 7 | Server was unable to process request. ---> Value cannot be null. 8 | Parameter name: userName 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /test/server/soapMsgs/login.operation.response.noError.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | FedAuth 8 | NoError 9 | 1800 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /test/server/soapMsgs/login.operation.response.passwordNotMatch.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | PasswordNotMatch 8 | 0 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /test/server/soapMsgs/people.searchPrincipals.response.success.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | i:0#.f|membership|joe.doe@test.com 8 | 11 9 | Joe Doe 10 | joe.doe@test.com 11 | true 12 | User 13 | 14 | 15 | i:0#.f|membership|amy.smith@test.com 16 | 12 17 | Amy Smith 18 | amy.smith@test.com 19 | true 20 | User 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /test/server/soapMsgs/web.webUrlFromPageUrl.response.success.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | https://tenant.sharepoint.com/sites/test 6 | 7 | 8 | -------------------------------------------------------------------------------- /test/setup/requirejs.config.js: -------------------------------------------------------------------------------- 1 | require.config({ 2 | baseUrl: "../", 3 | urlArgs: '@BUILD', 4 | paths: { 5 | jquery: 'vendor/jquery/dist/jquery', 6 | 'jquery-ui': 'vendor/jquery-ui/jquery-ui', 7 | less: 'vendor/require-less/less', 8 | lessc: 'vendor/require-less/lessc', 9 | normalize: 'vendor/require-less/normalize', 10 | text: 'vendor/requirejs-text/text', 11 | 'vendor/jsutils': 'vendor/common-micro-libs/src/jsutils', 12 | 'vendor/domutils': 'vendor/common-micro-libs/src/domutils' 13 | }, 14 | shim: { 15 | 'jquery-ui': { 16 | deps: ['jquery'] 17 | }, 18 | 'SPWidgets': { 19 | deps: ['jquery', 'jquery-ui'] 20 | } 21 | }, 22 | less: { 23 | relativeUrls: true, 24 | logLevel : 2 25 | } 26 | }); -------------------------------------------------------------------------------- /test/specs/models/ListColumnModel.js: -------------------------------------------------------------------------------- 1 | define([ 2 | "src/models/ListColumnModel" 3 | ], 4 | function( 5 | ListColumnModel 6 | ){ 7 | 8 | var xmlField = (function(){ 9 | var 10 | parser = new DOMParser(), 11 | xml = parser.parseFromString( 12 | '' + 13 | '' + 14 | '(1) High' + 15 | '(2) Normal' + 16 | '(3) Low' + 17 | '' + 18 | '' + 19 | '(1) High' + 20 | '(2) Normal' + 21 | '(3) Low' + 22 | '' + 23 | '(2) Normal' + 24 | '', "text/xml"); 25 | return xml; 26 | }()); 27 | 28 | describe("ListColumnModel", function(){ 29 | 30 | beforeEach(function(){ 31 | beforeEach(function(){ 32 | this.col = ListColumnModel.create( 33 | { 34 | ID: "{a8eb573e-9e11-481a-a8c9-1104a54b2fbd}", 35 | Type: "Choice", 36 | Name: "Priority", 37 | DisplayName: "Priority", 38 | SourceID: "http://schemas.microsoft.com/sharepoint/v3", 39 | StaticName: "Priority", 40 | ColName: "nvarchar3", 41 | MyBoolean: "True" 42 | }, 43 | { 44 | source: xmlField 45 | } 46 | ); 47 | }); 48 | }); 49 | 50 | it("exposes defaults", function(){ 51 | expect(ListColumnModel.defaults).toBeDefined(); 52 | }); 53 | 54 | it("Returns a ListcolumnModel instance", function(){ 55 | expect(ListColumnModel.isInstanceOf(this.col)).toBe(true); 56 | }); 57 | 58 | it("has object propertis for the column", function(){ 59 | expect(this.col.Type).toMatch("Choice"); 60 | expect(this.col.DisplayName).toMatch("Priority"); 61 | expect(this.col.MyBoolean).toMatch("True"); 62 | }); 63 | 64 | it("has .getColumnValues() method", function(){ 65 | expect(this.col.getColumnValues).toBeDefined(); 66 | }); 67 | 68 | it(".getColumnValues() returns Promise", function(){ 69 | expect(this.col.getColumnValues().then).toBeDefined(); 70 | }); 71 | 72 | it(".getColumnValues() Promise resolves to array of values for CHOICE column", function(done){ 73 | this.col.getColumnValues().then(function (values) { 74 | expect(values.length).toBe(3); 75 | done(); 76 | }); 77 | }); 78 | 79 | // FYI: the actual checking that this method returns values is done in getListColumn() 80 | 81 | }); 82 | 83 | }); 84 | -------------------------------------------------------------------------------- /test/specs/models/ListItemModel.js: -------------------------------------------------------------------------------- 1 | define([ 2 | "src/models/ListItemModel" 3 | ], function(ListItemModel){ 4 | 5 | describe("ListItemModel", function(){ 6 | 7 | describe("Instances", function(){ 8 | beforeEach(function(){ 9 | this.itemObj = { 10 | Name: "Test item", 11 | ID: "123" 12 | }; 13 | this.listItem = ListItemModel.create(this.itemObj); 14 | }); 15 | 16 | it("has properties defined on input", function(){ 17 | expect(this.listItem).toEqual(this.itemObj); 18 | }); 19 | }); 20 | 21 | }); 22 | 23 | }); 24 | -------------------------------------------------------------------------------- /test/specs/models/ListModel.js: -------------------------------------------------------------------------------- 1 | define([ 2 | "src/models/ListModel", 3 | "test/server/mock.soap.GetList" 4 | ], function( 5 | ListModel, 6 | mockSoapGetList 7 | ){ 8 | 9 | var xmlDoc = (function(){ 10 | var 11 | parser = new DOMParser(), 12 | xmlDoc = parser.parseFromString(mockSoapGetList.msgSuccess, "text/xml"); 13 | return xmlDoc; 14 | }()); 15 | 16 | describe("ListModel", function(){ 17 | 18 | describe("General validations", function(){ 19 | it("exposes defaults options", function(){ 20 | expect(ListModel.defaults).toBeDefined(); 21 | }); 22 | 23 | it("instanciates", function(){ 24 | var list = ListModel.create(xmlDoc); 25 | expect(ListModel.isInstanceOf(list)).toBe(true); 26 | }); 27 | }); 28 | 29 | describe("Create from XML", function(){ 30 | beforeEach(function(){ 31 | this.list = ListModel.create(xmlDoc, { 32 | webURL: "https://test.com/sites/test" 33 | }); 34 | }); 35 | 36 | it("responds to getListUrl()", function(){ 37 | expect("function" === typeof this.list.getListUrl).toBe(true); 38 | }); 39 | 40 | it("contains list properties", function(){ 41 | expect(this.list.Title).toBe("Tasks"); 42 | expect(this.list.MaxItemsPerThrottledOperation).toBe("5000"); 43 | }); 44 | 45 | it("converts true/false strings to Boolean types", function(){ 46 | expect(this.list.Followable).toBe(false); 47 | expect(this.list.ShowUser).toBe(true); 48 | }); 49 | 50 | it("getSource() returns original data source", function(){ 51 | expect(this.list.getSource()).toBe(xmlDoc); 52 | }); 53 | }); 54 | 55 | describe("Method", function(){ 56 | beforeEach(function(){ 57 | this.list = ListModel.create(xmlDoc, { 58 | webURL: "https://test.com/sites/test" 59 | }); 60 | }); 61 | 62 | describe("getListUrl()", function(){ 63 | it("returns full URL of list when webURL is known", function(){ 64 | expect(this.list.getListUrl()).toBe("https://test.com/sites/test/Lists/Tasks"); 65 | }); 66 | it("returns list root URL when webURL is NOT known", function(){ 67 | this.list = ListModel.create(xmlDoc); 68 | expect(this.list.getListUrl()).toBe("/sites/test/Lists/Tasks"); 69 | }); 70 | }); 71 | 72 | }); 73 | }); 74 | 75 | }); 76 | 77 | -------------------------------------------------------------------------------- /test/specs/spapi/getList.js: -------------------------------------------------------------------------------- 1 | define([ 2 | "src/spapi/getList", 3 | "../../server/mock.soap.GetList", 4 | "../../server/mock.soap.WebUrlFromPageUrl" 5 | ], function( 6 | getList, 7 | mockSoapGetList, 8 | mockSoapWebUrlFromPageUrl 9 | ){ 10 | 11 | describe("getList SP API", function(){ 12 | 13 | beforeEach(function(){ 14 | jasmine.Ajax.install(); 15 | mockSoapWebUrlFromPageUrl.install(); 16 | mockSoapGetList.install(); 17 | }); 18 | 19 | afterEach(function(){ 20 | jasmine.Ajax.uninstall(); 21 | }); 22 | 23 | describe("Interface", function() { 24 | 25 | it("return a promise", function(){ 26 | var req = getList({listName: "auto_respond"}); 27 | expect(req).toBeDefined(); 28 | expect(req.then).toBeDefined(); 29 | }); 30 | 31 | it("exposes defaults", function(){ 32 | expect(getList.defaults).toBeDefined(); 33 | }); 34 | 35 | }); 36 | 37 | describe("Data retrieval", function(){ 38 | 39 | it("returns an object that is an instance of ListModel", function(done){ 40 | getList({listName: "auto_respond"}).then(function(list){ 41 | expect(getList.defaults.ListModel.isInstanceOf(list)).toBe(true); 42 | done(); 43 | }); 44 | }); 45 | 46 | it("ListModel instance contains list definition attributes", function(done){ 47 | getList({listName: "auto_respond"}).then(function(list){ 48 | expect(list.ID).toMatch("{7EE477D9-D257-47F5-A25D-A882D882E51F}"); 49 | expect(list.Title).toMatch("Tasks"); 50 | expect(list.HasUniqueScopes).toBe(false); 51 | expect(list.ShowUser).toBe(true); 52 | done(); 53 | }); 54 | }); 55 | 56 | it("caches data by default", function(done){ 57 | getList({listName: "auto_respond"}).then(function(list1){ 58 | getList({listName: "auto_respond"}).then(function(list2){ 59 | expect(list1 === list2).toBe(true); 60 | done(); 61 | }); 62 | }); 63 | }); 64 | 65 | }); 66 | 67 | }); 68 | 69 | }); 70 | -------------------------------------------------------------------------------- /test/specs/spapi/getListFormCollection.js: -------------------------------------------------------------------------------- 1 | define([ 2 | "../../server/mock.soap.GetListFormCollection", 3 | "../../server/mock.soap.WebUrlFromPageUrl", 4 | "src/spapi/getListFormCollection" 5 | ], function( 6 | mockSoapGetListFormCollection, 7 | mockSoapWebUrlFromPageUrl, 8 | getListFormCollection 9 | ){ 10 | 11 | describe("getListFormCollection SP API", function(){ 12 | 13 | beforeEach(function(){ 14 | jasmine.Ajax.install(); 15 | mockSoapWebUrlFromPageUrl.install(); 16 | mockSoapGetListFormCollection.install(); 17 | }); 18 | 19 | afterEach(function(){ 20 | jasmine.Ajax.uninstall(); 21 | }); 22 | 23 | describe("Interface", function() { 24 | it("exposes defaults", function(){ 25 | expect(getListFormCollection.defaults).toBeDefined(); 26 | }); 27 | 28 | it("return a promise", function(done){ 29 | var req = getListFormCollection({listName: "auto_respond"}); 30 | expect(req).toBeDefined(); 31 | expect(req.then).toBeDefined(); 32 | 33 | req 34 | .then(function(){ 35 | done(); 36 | }) 37 | .catch(function(e){ 38 | console.log("Request failed: " + e); 39 | }); 40 | 41 | }); 42 | }); 43 | 44 | describe("Data retrieval", function(){ 45 | 46 | it("resolves to an array", function(done){ 47 | getListFormCollection({listName: "auto_respond"}) 48 | .then(function(forms){ 49 | expect(Array.isArray(forms)).toBe(true); 50 | done(); 51 | }) 52 | .catch(function (err) { 53 | console.log("----: ERROR :-----"); 54 | console.log(err); 55 | }); 56 | }); 57 | 58 | it("Array contains objects with forms", function(done){ 59 | getListFormCollection({listName: "auto_respond"}).then(function(forms){ 60 | expect(typeof forms[0]).toBe("object"); 61 | done(); 62 | }); 63 | }); 64 | 65 | it("Array objects contains type and url attributes", function(done){ 66 | getListFormCollection({listName: "auto_respond"}).then(function(forms){ 67 | expect(forms[0].url).toBeDefined(); 68 | expect(forms[0].type).toBeDefined(); 69 | done(); 70 | }); 71 | }); 72 | 73 | it("Form url attribute starts with http", function(done){ 74 | getListFormCollection({listName: "auto_respond"}).then(function(forms){ 75 | expect(forms[0].url.toLowerCase().indexOf('http') === 0).toBe(true); 76 | done(); 77 | }); 78 | }); 79 | 80 | }); 81 | }); 82 | }); 83 | -------------------------------------------------------------------------------- /test/specs/spapi/searchPrincipals.js: -------------------------------------------------------------------------------- 1 | define([ 2 | "src/spapi/searchPrincipals", 3 | "test/server/mock.soap.webUrlFromPageUrl", 4 | "../../server/mock.soap.SearchPrincipals" 5 | ], function( 6 | searchPrincipals, 7 | mockSoapWebUrlFromPageUrl, 8 | mockSoapSearchPrincipals 9 | ){ 10 | 11 | describe("SearchPrincipals SP API", function(){ 12 | 13 | beforeEach(function(){ 14 | jasmine.Ajax.install(); 15 | mockSoapWebUrlFromPageUrl.install(); 16 | mockSoapSearchPrincipals.install(); 17 | }); 18 | 19 | afterEach(function(){ 20 | jasmine.Ajax.uninstall(); 21 | }); 22 | 23 | describe("Method API", function(){ 24 | it("is a function", function(){ 25 | expect(typeof searchPrincipals).toMatch("function"); 26 | }); 27 | 28 | it("exposes defaults", function(){ 29 | expect(searchPrincipals.defaults).toBeDefined(); 30 | }); 31 | 32 | it("has defaults.searchText", function(){ 33 | expect(searchPrincipals.defaults.searchText).toBeDefined(); 34 | }); 35 | 36 | it("has defaults.maxResults", function(){ 37 | expect(searchPrincipals.defaults.maxResults).toBeDefined(); 38 | }); 39 | 40 | it("defaults.maxResults is a Number", function(){ 41 | expect(typeof searchPrincipals.defaults.maxResults).toMatch("number"); 42 | }); 43 | 44 | it("has defaults.principalType", function(){ 45 | expect(searchPrincipals.defaults.principalType).toBeDefined(); 46 | }); 47 | 48 | it("has defaults.webURL", function(){ 49 | expect(searchPrincipals.defaults.webURL).toBeDefined(); 50 | }); 51 | 52 | it("has defaults.cache", function(){ 53 | expect(searchPrincipals.defaults.cache).toBeDefined(); 54 | }); 55 | 56 | it("defaults.cache is set to false", function(){ 57 | expect(searchPrincipals.defaults.cache).toBe(true); 58 | }); 59 | 60 | it("has defaults.UserProfileModel", function(){ 61 | expect(searchPrincipals.defaults.UserProfileModel).toBeDefined(); 62 | }); 63 | 64 | }); 65 | 66 | describe("DATA Retrieval", function(){ 67 | 68 | beforeEach(function(){ 69 | this.searchReq = searchPrincipals({ 70 | searchText: "auto_respond" 71 | }); 72 | }); 73 | 74 | it("returns a promise", function(done){ 75 | expect(this.searchReq.then).toBeDefined(); 76 | this.searchReq.then(function(){ 77 | done(); 78 | }); 79 | }); 80 | 81 | it("Promise resolved with Array", function(done){ 82 | this.searchReq.then(function(results){ 83 | expect(results).toBeDefined(); 84 | expect(Array.isArray(results)).toBe(true); 85 | done(); 86 | }); 87 | }); 88 | }); 89 | }); 90 | }); 91 | -------------------------------------------------------------------------------- /test/specs/sputils/doesMsgHaveError.js: -------------------------------------------------------------------------------- 1 | define([ 2 | "src/sputils/doesMsgHaveError", 3 | "vendor/jsutils/parseXML", 4 | "text!../../server/soapMsgs/login.operation.response.noError.xml", 5 | "text!../../server/soapMsgs/error.faultcode.xml", 6 | "text!../../server/soapMsgs/error.ErrorCode.good.xml", 7 | "text!../../server/soapMsgs/error.ErrorCode.bad.xml", 8 | "text!../../server/soapMsgs/error.copyResult.xml" 9 | ], function( 10 | doesMsgHaveError, 11 | parseXML, 12 | msgNoError, 13 | msgFaultcode, 14 | msgErrorCodeGood, 15 | msgErrorCodeBad, 16 | msgCopyResult 17 | ){ 18 | 19 | describe("doesMsgHaveError", function(){ 20 | 21 | it("is a function", function(){ 22 | expect(typeof doesMsgHaveError).toMatch("function"); 23 | }); 24 | 25 | it("finds bad ErrorCode", function(){ 26 | var xml = parseXML(msgErrorCodeBad); 27 | expect(doesMsgHaveError(xml)).toBe(true); 28 | }); 29 | 30 | it("Ignores good ErrorCode", function(){ 31 | var xml = parseXML(msgErrorCodeGood); 32 | expect(doesMsgHaveError(xml)).toBe(false); 33 | }); 34 | 35 | it("finds faultcode", function(){ 36 | var xml = parseXML(msgFaultcode); 37 | expect(doesMsgHaveError(xml)).toBe(true); 38 | }); 39 | 40 | it("finds CopyResult ErrorMessage", function(){ 41 | var xml = parseXML(msgCopyResult); 42 | expect(doesMsgHaveError(xml)).toBe(true); 43 | }); 44 | }); 45 | 46 | }); -------------------------------------------------------------------------------- /test/specs/sputils/getMsgError.js: -------------------------------------------------------------------------------- 1 | define([ 2 | "src/sputils/getMsgError", 3 | "vendor/jsutils/parseXML", 4 | "text!../../server/soapMsgs/login.operation.response.noError.xml", 5 | "text!../../server/soapMsgs/error.faultcode.xml", 6 | "text!../../server/soapMsgs/error.ErrorCode.good.xml", 7 | "text!../../server/soapMsgs/error.ErrorCode.bad.xml", 8 | "text!../../server/soapMsgs/error.copyResult.xml" 9 | ], function( 10 | getMsgError, 11 | parseXML, 12 | msgNoError, 13 | msgFaultcode, 14 | msgErrorCodeGood, 15 | msgErrorCodeBad, 16 | msgCopyResult 17 | ){ 18 | 19 | describe("getMsgError", function(){ 20 | 21 | it("is a function", function(){ 22 | expect(typeof getMsgError).toMatch("function"); 23 | }); 24 | 25 | it("finds ErrorCode", function(){ 26 | var xml = parseXML(msgErrorCodeBad); 27 | expect(getMsgError(xml)).toMatch(/Some error message/); 28 | }); 29 | 30 | it("finds faultcode", function(){ 31 | var xml = parseXML(msgFaultcode); 32 | expect(getMsgError(xml)).toMatch(/Exception of type/); 33 | expect(getMsgError(xml)).toMatch(/0x81020067/); 34 | }); 35 | 36 | it("finds CopyResult ErrorMessage", function(){ 37 | var xml = parseXML(msgCopyResult); 38 | expect(getMsgError(xml)).toMatch(/Some Message here/); 39 | expect(getMsgError(xml)).toMatch(/err101/); 40 | }); 41 | }); 42 | 43 | }); -------------------------------------------------------------------------------- /test/suite.js: -------------------------------------------------------------------------------- 1 | define([ 2 | // All test cases should resize in the test/specs folder and be 3 | // referenced below as a dependency. 4 | "./specs/jquery.SPWidgets", 5 | "./specs/jsutils/Compose", 6 | "./specs/models/ListItemModel", 7 | "./specs/models/ListModel", 8 | "./specs/models/ListColumnModel", 9 | "./specs/spapi/getList", 10 | "./specs/spapi/getListColumns", 11 | "./specs/spapi/getListFormCollection", 12 | "./specs/spapi/getListItems", 13 | "./specs/spapi/searchPrincipals", 14 | "./specs/sputils/doesMsgHaveError", 15 | "./specs/sputils/getMsgError" 16 | 17 | ], function(){}); 18 | -------------------------------------------------------------------------------- /test/test.SPWidgets.aspx: -------------------------------------------------------------------------------- 1 | <%-- SPWIDGETS DEV PAGE--%> 2 | <%@ Page language="C#" MasterPageFile="~masterurl/default.master" Inherits="Microsoft.SharePoint.WebPartPages.WebPartPage,Microsoft.SharePoint,Version=12.0.0.0,Culture=neutral,PublicKeyToken=71e9bce111e9429c" %> 3 | <%@ Register Tagprefix="SharePoint" Namespace="Microsoft.SharePoint.WebControls" Assembly="Microsoft.SharePoint, Version=12.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" %> 4 | <%@ Register Tagprefix="Utilities" Namespace="Microsoft.SharePoint.Utilities" Assembly="Microsoft.SharePoint, Version=12.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" %> 5 | <%@ Import Namespace="Microsoft.SharePoint" %> 6 | <%@ Register Tagprefix="WebPartPages" Namespace="Microsoft.SharePoint.WebPartPages" Assembly="Microsoft.SharePoint, Version=12.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" %> 7 | 8 | SPWidgets Test Page 9 | 10 | 11 |

    SPWidgets Test Page

    12 |
    13 | 14 | 15 | 16 | 17 | 18 | 22 | 23 | 26 | 27 | 35 | 36 | 37 | 38 | 39 | 40 | SPWidgets - Widgets for building custom UIs 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 |
    49 | 50 | 51 | 52 | 53 | 54 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 86 |
    87 | 88 | 89 | -------------------------------------------------------------------------------- /tools/copy.process.minifyHtml.js: -------------------------------------------------------------------------------- 1 | (function(){ 2 | 3 | var htmlMinifier= require('html-minifier').minify, 4 | grunt = require("grunt"), 5 | isHtmlFile = /\.html|htm$/i; 6 | 7 | /** 8 | * Grunt copy task processor that returns minified HTML markup. Meant to be 9 | * used with the processContent/process option of the copy task. 10 | * 11 | * @param {String} fileContent 12 | * @param {String} filePath 13 | * @return {String} file content 14 | * 15 | * @see https://www.npmjs.com/package/html-minifier 16 | * 17 | * @example 18 | * 19 | * copy: { 20 | * build: { 21 | * src: '', 22 | * dest: '', 23 | * expand: true, 24 | * options: { 25 | * processConent: minifyHtml 26 | * } 27 | * } 28 | * } 29 | * 30 | */ 31 | exports.minifyHtml = function(fileContent, filePath){ 32 | 33 | if (isHtmlFile.test(filePath)) { 34 | 35 | grunt.verbose.writeln("minifyHtml: minifying: " + filePath); 36 | 37 | return htmlMinifier(fileContent, { 38 | removeComments : true, 39 | collapseWhitespace : true, 40 | conservativeCollapse : true, 41 | collapseBooleanAttributes : true, 42 | removeEmptyAttributes : true, 43 | caseSensitive : true, 44 | ignoreCustomComments : [ 45 | /^\s+ko/, 46 | /\/ko\s+$/ 47 | ] 48 | }); 49 | 50 | } 51 | return fileContent; 52 | }; 53 | 54 | }()); 55 | -------------------------------------------------------------------------------- /tools/server.js: -------------------------------------------------------------------------------- 1 | var connect = require('../node_modules/grunt-contrib-connect/node_modules/connect'); 2 | var serveStatic = require('serve-static'); 3 | var path = require("path"); 4 | var open = require("open"); 5 | connect().use( 6 | serveStatic(path.join(__dirname, "..")) 7 | ) 8 | .listen(8080); 9 | console.log("Started on port 8080"); 10 | open("http://127.0.0.1:8080/test/index.html"); 11 | --------------------------------------------------------------------------------