Diffstat (limited to 'frontend/delta/js/Clipperz/PM/UI/Components') (more/less context) (ignore whitespace changes)
6 files changed, 218 insertions, 25 deletions
diff --git a/frontend/delta/js/Clipperz/PM/UI/Components/CardDetail.js b/frontend/delta/js/Clipperz/PM/UI/Components/CardDetail.js index df514a2..12ddce3 100644 --- a/frontend/delta/js/Clipperz/PM/UI/Components/CardDetail.js +++ b/frontend/delta/js/Clipperz/PM/UI/Components/CardDetail.js @@ -1,142 +1,195 @@ /* Copyright 2008-2013 Clipperz Srl This file is part of Clipperz, the online password manager. For further information about its features and functionalities please refer to http://www.clipperz.com. * Clipperz is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. * Clipperz is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. * You should have received a copy of the GNU Affero General Public License along with Clipperz. If not, see http://www.gnu.org/licenses/. */ Clipperz.PM.UI.Components.CardDetail = React.createClass({ getDefaultProps: function () { return { // searchDelay: 0.3 } }, propTypes: { card: React.PropTypes.object.isRequired }, getInitialState: function () { return { // showSearch: false, // searchTimer: null, + unmaskedFields: new Clipperz.Set(), starred: false }; }, handleDirectLoginClick: function (aDirectLoginReference, anEvent) { MochiKit.Signal.signal(Clipperz.Signal.NotificationCenter, 'runDirectLogin', {record:this.props.card['reference'], directLogin:aDirectLoginReference}); }, + toggleFieldVisibility: function (aField, anEvent) { + var unmaskedFields; + var fieldReference; + + unmaskedFields = this.state['unmaskedFields']; + fieldReference = aField['reference'] + if (unmaskedFields.contains(fieldReference)) { + unmaskedFields.remove(fieldReference) + } else { + unmaskedFields.add(fieldReference) + } + + this.setState({'unmaskedFields': unmaskedFields}); + }, + + handleGoAction: function (aField, anEvent) { + var newWindow; + + newWindow = MochiKit.DOM.currentWindow().open(aField['value'], '_blank'); + newWindow.focus(); + }, + + handleEmailAction: function (aField, anEvent) { + MochiKit.DOM.currentWindow().location = 'mailto:' + aField['value']; + }, + //========================================================================= normalizeFieldValue: function (aValue) { var result = []; var rows = aValue.split('\n'); for (var i = 0; i < rows.length; i++) { if (i > 0) { result.push(React.DOM.br()); } result.push(rows[i].replace(/[\s]/g, '\u00A0')); } return result; }, - renderField: function (aField) { -//console.log("FIELD", aField); - var actionLabel; + renderFieldActionButton: function (aField) { +// var actionLabel; + var result; if (aField['actionType'] == 'URL') { - actionLabel = "go"; + result = React.DOM.div({className:'actionWrapper', onClick:MochiKit.Base.method(this, 'handleGoAction', aField)}, [ + React.DOM.a({className:aField['actionType']}, "go") + ]); } else if (aField['actionType'] == 'PASSWORD') { - actionLabel = "locked"; + var icon; + + if (this.state['unmaskedFields'].contains(aField['reference'])) { + icon = "unlocked"; + } else { + icon = "locked"; + } + result = React.DOM.div({className:'actionWrapper', onClick:MochiKit.Base.method(this, 'toggleFieldVisibility', aField)}, [ + React.DOM.a({className:aField['actionType']}, icon) + ]); } else if (aField['actionType'] == 'EMAIL') { - actionLabel = "email"; + result = React.DOM.div({className:'actionWrapper', onClick:MochiKit.Base.method(this, 'handleEmailAction', aField)}, [ + React.DOM.a({className:aField['actionType']}, "email") + ]); } else { - actionLabel = ""; + result = null; + } + + return result; + }, + + renderField: function (aField) { +//console.log("FIELD", aField); + var fieldExtraClass; + + fieldExtraClass = aField['actionType']; + if (this.state['unmaskedFields'].contains(aField['reference'])) { + fieldExtraClass = fieldExtraClass + ' unlocked'; } - return React.DOM.div({className:'listItem ' + aField['actionType']}, [ + return React.DOM.div({className:'listItem ' + fieldExtraClass, key:aField['reference']}, [ React.DOM.div({className:'fieldWrapper'}, [ React.DOM.div({className:'fieldInnerWrapper'}, [ React.DOM.div({className:'labelWrapper'}, React.DOM.span({className:'label'}, aField['label'])), - React.DOM.div({className:'valueWrapper'}, React.DOM.span({className:'value ' + aField['actionType']}, this.normalizeFieldValue(aField['value']))) + React.DOM.div({className:'valueWrapper'}, React.DOM.span({className:'value ' + fieldExtraClass}, this.normalizeFieldValue(aField['value']))) ]) ]), - React.DOM.div({className:'actionWrapper'}, [ - React.DOM.div({className:aField['actionType']}, actionLabel) - ]) + this.renderFieldActionButton(aField) +// React.DOM.div({className:'actionWrapper'}, [ +// React.DOM.div({className:aField['actionType']}, actionLabel) +// ]) ]); }, renderDirectLogin: function (aDirectLogin) { //console.log("DIRECT LOGIN", aDirectLogin); return React.DOM.div({className:'listItem', onClick:MochiKit.Base.method(this, 'handleDirectLoginClick', aDirectLogin['reference'])}, [ React.DOM.div({className:'labelWrapper'}, React.DOM.span({className:'label'}, aDirectLogin['label'])), React.DOM.div({className:'faviconWrapper'}, React.DOM.img({className:'favicon', src:aDirectLogin['favicon']})), React.DOM.div({className:'directLoginLinkWrapper'}, React.DOM.span({className:'directLoginLink'}, "go")) ]); }, handleBackClick: function (anEvent) { - window.history.back(); +// window.history.back(); + MochiKit.Signal.signal(Clipperz.Signal.NotificationCenter, 'goBack'); }, handleStarClick: function (anEvent) { this.setState({starred: !this.state['starred']}); }, //========================================================================= render: function () { var card = this.props.card; - var starredStatus = (this.state['starred'] ? "starred" : "unstarred"); +// var starredStatus = (this.state['starred'] ? "starred" : "unstarred"); if ((typeof(card['fields']) != 'undefined') && (card['notes'] != '')) { card['fields'].push({ 'actionType': 'NOTES', 'isHidden': false, 'label': "notes", 'reference': "notes", 'value': card['notes'] }) } return React.DOM.div({className:'cardDetail'}, [ React.DOM.div({className:'header'}, [ React.DOM.div({className:'titleWrapper'}, React.DOM.div({className:'title'}, card.title)), -// React.DOM.div({className:'titleWrapper'}, React.DOM.div({className:'title'}, card.title + ' ' + card.title + ' ' + card.title + ' ' + card.title)), React.DOM.div({className:'backWrapper'}, React.DOM.a({className:'button back', onClick:this.handleBackClick}, "back")), - React.DOM.div({className:'starWrapper'}, React.DOM.a({className:'star', onClick:this.handleStarClick}, starredStatus)) +// React.DOM.div({className:'starWrapper'}, React.DOM.a({className:'star', onClick:this.handleStarClick}, starredStatus)) ]), React.DOM.div({className:'content'}, [ card.fields ? React.DOM.div({className:'fields'}, MochiKit.Base.map(this.renderField, card.fields)) : null, card.directLogins ? React.DOM.div({className:'directLogins'}, MochiKit.Base.map(this.renderDirectLogin, card.directLogins)): null ]), React.DOM.div({className:'footer'}, [ /* // React.DOM.a({className:'cancel'}, "cancel"), // React.DOM.a({className:'save'}, "save") React.DOM.a({className:'cancel button'}, "failed"), React.DOM.a({className:'save button'}, "done") */ ]) ]); } //========================================================================= }); diff --git a/frontend/delta/js/Clipperz/PM/UI/Components/CardList.js b/frontend/delta/js/Clipperz/PM/UI/Components/CardList.js index 66d20f1..5a44a4a 100644 --- a/frontend/delta/js/Clipperz/PM/UI/Components/CardList.js +++ b/frontend/delta/js/Clipperz/PM/UI/Components/CardList.js @@ -52,110 +52,117 @@ Clipperz.PM.UI.Components.CardList = React.createClass({ showSearchBox = !this.state.showSearch; this.setState({showSearch: showSearchBox}); if (showSearchBox) { MochiKit.Async.callLater(0.1, MochiKit.Base.method(this, 'focusOnSearchField')); } }, updateSearchText: function (anEvent) { var searchText; searchText = anEvent.target.value; //console.log(">>> updateSearchText", searchText); if ((this.state['searchTimer'] != null) && (searchText != this.state['searchText'])) { this.state['searchTimer'].cancel(); } if (searchText != this.state['searchText']) { this.state['searchText'] = searchText; this.state['searchTimer'] = MochiKit.Async.callLater(this.props['searchDelay'], MochiKit.Signal.signal, Clipperz.Signal.NotificationCenter, 'searchCards', searchText); } }, focusOnSearchField: function () { console.log("focusOnSearchField", this.refs['searchField']); this.refs['searchField'].getDOMNode.focus(); }, searchBox: function () { var result; if (this.state.showSearch) { result = React.DOM.div({className:'searchBox'}, [ React.DOM.div(null, [ React.DOM.input({type:'search', placeholder:"search", ref:'searchField', onChange:this.updateSearchText}) ]) ]); } else { result = null; } return result; }, //========================================================================= + showPreferences: function (anEvent) { + MochiKit.Signal.signal(Clipperz.Signal.NotificationCenter, 'showPreferences', anEvent); + }, + + //========================================================================= + cardItem: function (aRecordReference) { var reference = aRecordReference['_reference']; var selectedCard = (reference == this.props.selectedCard); - return React.DOM.div({className:'listItem', onClick:MochiKit.Base.method(this, 'handleClickOnCardDetail', reference)}, [ + // TODO: verify if it is possible to put the onClick handler on the container 'div', instead of adding it to each 'div' item. + return React.DOM.div({className:'listItem', key:reference, onClick:MochiKit.Base.method(this, 'handleClickOnCardDetail', reference)}, [ React.DOM.div({className:'labelWrapper'}, React.DOM.span({className:'label'}, aRecordReference.label)), // React.DOM.div({className:'labelWrapper'}, React.DOM.span({className:'label'}, aRecordReference.label + ' ' + aRecordReference.label + ' ' + aRecordReference.label + ' ' + aRecordReference.label + ' ' + aRecordReference.label)), React.DOM.div({className:'faviconWrapper'}, aRecordReference.favicon ? React.DOM.img({className:'favicon', src:aRecordReference.favicon}) : React.DOM.div({className:'favicon'}, '\u00A0')), React.DOM.div({className:'detailLinkWrapper'}, React.DOM.span({className:'detailLink ' + (selectedCard ? 'icon-spin' : '')}, (selectedCard ? "loading" : "detail"))) ]); }, handleClickOnCardDetail: function (aRecordReference, anEvent) { MochiKit.Signal.signal(Clipperz.Signal.NotificationCenter, 'showRecord', aRecordReference); }, cardListItems: function () { var list; var result; list = this.props['cardList']; if (typeof(list) != 'undefined') { result = MochiKit.Base.map(MochiKit.Base.method(this, 'cardItem'), list); } else { result = null; } return result; }, //========================================================================= handleChange: function (anEvent) { // var refs = this.refs; // var refName = MochiKit.Base.filter(function (aRefName) { return refs[aRefName].getDOMNode() == anEvent.target}, MochiKit.Base.keys(this.refs))[0]; // var newState = {}; // // newState[refName] = event.target.value; // this.setState(newState); }, //========================================================================= render: function() { return React.DOM.div(null, [ React.DOM.div({className:'header'}, [ React.DOM.a({className:'account'}, 'clipperz'), React.DOM.div({className:'features'}, [ - React.DOM.a({className:'addCard'}, 'add'), +// React.DOM.a({className:'addCard'}, 'add'), React.DOM.a({className:'search ' + (this.state.showSearch ? 'selected' : ''), onClick:this.toggleSearch}, 'search'), - React.DOM.a({className:'settings'}, 'settings') + React.DOM.a({className:'settings', onClick:this.showPreferences}, 'settings') ]), // this.searchBox() ]), this.searchBox(), React.DOM.div({className:'content cardList'}, this.cardListItems()), ]); } //========================================================================= }); diff --git a/frontend/delta/js/Clipperz/PM/UI/Components/Checkbox.js b/frontend/delta/js/Clipperz/PM/UI/Components/Checkbox.js new file mode 100644 index 0000000..9538063 --- a/dev/null +++ b/frontend/delta/js/Clipperz/PM/UI/Components/Checkbox.js @@ -0,0 +1,44 @@ +/* + +Copyright 2008-2013 Clipperz Srl + +This file is part of Clipperz, the online password manager. +For further information about its features and functionalities please +refer to http://www.clipperz.com. + +* Clipperz is free software: you can redistribute it and/or modify it + under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + +* Clipperz is distributed in the hope that it will be useful, but + WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + See the GNU Affero General Public License for more details. + +* You should have received a copy of the GNU Affero General Public + License along with Clipperz. If not, see http://www.gnu.org/licenses/. + +*/ + +Clipperz.PM.UI.Components.Checkbox = React.createClass({ +// http://development.tobypitman.com/iphoneCheckboxes/iphoneCheckboxes2.html + + propTypes: { + 'checked': React.PropTypes.bool.isRequired, + 'id': React.PropTypes.string.isRequired, + 'eventHandler': React.PropTypes.func.isRequired + }, + + //========================================================================= + + render: function () { + return React.DOM.div({className:'checkbox', onClick:this.props['eventHandler']}, [ + React.DOM.input({name:this.props['id'], id:this.props['id'], value:this.props['id'], type:'checkbox', checked:this.props['checked']}), + React.DOM.label({className:'check', 'for':this.props['id']}), + React.DOM.label({className:'info', 'for':this.props['id']}, "enable local storage") + ]); + } + + //========================================================================= +}); diff --git a/frontend/delta/js/Clipperz/PM/UI/Components/LoginForm.js b/frontend/delta/js/Clipperz/PM/UI/Components/LoginForm.js index 2b5b4a4..801549f 100644 --- a/frontend/delta/js/Clipperz/PM/UI/Components/LoginForm.js +++ b/frontend/delta/js/Clipperz/PM/UI/Components/LoginForm.js @@ -47,104 +47,104 @@ Clipperz.PM.UI.Components.LoginForm = React.createClass({ }; }, //========================================================================= handleChange: function (anEvent) { var refs = this.refs; var refName = MochiKit.Base.filter(function (aRefName) { return refs[aRefName].getDOMNode() == anEvent.target}, MochiKit.Base.keys(this.refs))[0]; var newState = {}; newState[refName] = event.target.value; this.setState(newState); }, //========================================================================= handleCredentialSubmit: function (event) { event.preventDefault(); this.refs['passphrase'].getDOMNode().blur(); var credentials = { 'username': this.refs['username'].getDOMNode().value, 'passphrase': this.refs['passphrase'].getDOMNode().value } MochiKit.Signal.signal(Clipperz.Signal.NotificationCenter, 'doLogin', credentials); }, handleRegistrationLinkClick: function (event) { event.preventDefault(); MochiKit.Signal.signal(Clipperz.Signal.NotificationCenter, 'showRegistrationForm'); }, //------------------------------------------------------------------------- shouldEnableLoginButton: function () { var result; return ( ((this.state['username'] != '') && (this.state['passphrase'] != '')) || (this.state['pin'] != '') ) && !this.props['disabled']; }, loginForm: function () { registrationLink = React.DOM.div({'className':'registrationLink'}, [ - React.DOM.a({'onClick':this.handleRegistrationLinkClick}, "Need an account") + React.DOM.a({'onClick':this.handleRegistrationLinkClick}, "Sign up") ]); return React.DOM.div({'className':'loginForm credentials'},[ React.DOM.form({onChange: this.handleChange, onSubmit:this.handleCredentialSubmit}, [ React.DOM.div(null,[ - React.DOM.label({'for':'name'}, "username"), + React.DOM.label({'for' :'name'}, "username"), React.DOM.input({'type':'text', 'name':'name', 'ref':'username', 'placeholder':"username", 'key':'username', 'autoCapitalize':'none'}), - React.DOM.label({'for':'passphrase'}, "passphrase"), + React.DOM.label({'for' :'passphrase'}, "passphrase"), React.DOM.input({'type':'password', 'name':'passphrase', 'ref':'passphrase', 'placeholder':"passphrase", 'key':'passphrase'}) ]), React.DOM.button({'type':'submit', 'disabled':!this.shouldEnableLoginButton(), 'className':'button'}, "login") ]), this.props.isNewUserRegistrationAvailable ? registrationLink : null ]); }, handlePINSubmit: function (event) { event.preventDefault(); this.refs['pin'].getDOMNode().blur(); var credentials = { pin: this.refs['pin'].getDOMNode().value } MochiKit.Signal.signal(Clipperz.Signal.NotificationCenter, 'doLogin', credentials); }, pinForm: function () { return React.DOM.div({'className':'loginForm pin'},[ React.DOM.form({onChange: this.handleChange, onSubmit:this.handlePINSubmit}, [ React.DOM.div(null,[ React.DOM.label({'for':'pin'}, "pin"), React.DOM.input({'type':'text', 'name':'pin', 'ref':'pin', placeholder:"PIN", 'key':'pin', 'autocapitalize':'none'}) ]), React.DOM.button({'type':'submit', 'disabled':this.props.disabled, 'className':'button'}, "login") ]) ]); }, setInitialFocus: function () { if (this.props.mode == 'PIN') { this.refs['pin'].getDOMNode().select(); } else { if (this.refs['username'].getDOMNode().value == '') { this.refs['username'].getDOMNode().focus(); } else{ this.refs['passphrase'].getDOMNode().select(); } } }, render: function() { return new this.props.template({'innerComponent': this.props.mode == 'PIN' ? this.pinForm() : this.loginForm()}); } }); diff --git a/frontend/delta/js/Clipperz/PM/UI/Components/Overlay.js b/frontend/delta/js/Clipperz/PM/UI/Components/Overlay.js index cc4a06c..cb5f81a 100644 --- a/frontend/delta/js/Clipperz/PM/UI/Components/Overlay.js +++ b/frontend/delta/js/Clipperz/PM/UI/Components/Overlay.js @@ -49,74 +49,75 @@ Clipperz.Base.extend(Clipperz.PM.UI.Components.Overlay, Object, { 'getElement': function (aClass) { return MochiKit.Selector.findChildElements(this.element(), ['.'+aClass])[0]; }, //------------------------------------------------------------------------- 'show': function (aMessage) { this.resetStatus(); this.setMessage(aMessage); MochiKit.DOM.removeElementClass(this.element(), 'ios-overlay-hide'); MochiKit.DOM.addElementClass(this.element(), 'ios-overlay-show'); }, 'done': function (aMessage, aDelayBeforeHiding) { this.completed(this.showDoneIcon, aMessage, aDelayBeforeHiding); }, 'failed': function (aMessage, aDelayBeforeHiding) { this.completed(this.showFailIcon, aMessage, aDelayBeforeHiding); }, //------------------------------------------------------------------------- 'resetStatus': function () { MochiKit.Style.showElement(this.element()); MochiKit.Style.showElement(this.getElement('spinner')); MochiKit.Style.hideElement(this.getElement('done')); MochiKit.Style.hideElement(this.getElement('failed')); }, 'setMessage': function (aMessage) { if (typeof(aMessage) != 'undefined') { this.getElement('title').innerHTML = aMessage; } }, 'completed': function (aFunctionToShowResult, aMessage, aDelayBeforeHiding) { var delay = aDelayBeforeHiding || this.defaultDelay(); this.hideSpinner(); MochiKit.Base.bind(aFunctionToShowResult, this)(); this.setMessage(aMessage); MochiKit.Async.callLater(delay, MochiKit.Base.bind(this.hide, this)) }, 'hide': function () { - MochiKit.DOM.removeElementClass(this.element(), 'ios-overlay-show'); - MochiKit.DOM.addElementClass(this.element(), 'ios-overlay-hide'); - MochiKit.Async.callLater(1, MochiKit.Style.hideElement, this.element()); + var element = this.element(); + MochiKit.DOM.removeElementClass(element, 'ios-overlay-show'); + MochiKit.DOM.addElementClass(element, 'ios-overlay-hide'); + MochiKit.Async.callLater(1, MochiKit.Style.hideElement, element); }, 'hideSpinner': function () { MochiKit.Style.hideElement(this.getElement('spinner')); }, 'showDoneIcon': function () { MochiKit.Style.showElement(this.getElement('done')); }, 'showFailIcon': function () { MochiKit.Style.showElement(this.getElement('failed')); }, //------------------------------------------------------------------------- 'defaultDelay': function () { return this._defaultDelay; }, //------------------------------------------------------------------------- __syntaxFix__: "syntax fix" }); diff --git a/frontend/delta/js/Clipperz/PM/UI/Components/PreferencePage.js b/frontend/delta/js/Clipperz/PM/UI/Components/PreferencePage.js new file mode 100644 index 0000000..822acc2 --- a/dev/null +++ b/frontend/delta/js/Clipperz/PM/UI/Components/PreferencePage.js @@ -0,0 +1,88 @@ +/* + +Copyright 2008-2013 Clipperz Srl + +This file is part of Clipperz, the online password manager. +For further information about its features and functionalities please +refer to http://www.clipperz.com. + +* Clipperz is free software: you can redistribute it and/or modify it + under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + +* Clipperz is distributed in the hope that it will be useful, but + WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + See the GNU Affero General Public License for more details. + +* You should have received a copy of the GNU Affero General Public + License along with Clipperz. If not, see http://www.gnu.org/licenses/. + +*/ + +Clipperz.PM.UI.Components.PreferencePage = React.createClass({ + + getDefaultProps: function () { + return { + } + }, + + propTypes: { +// card: React.PropTypes.object.isRequired +// checked: React.PropTypes.boolean.isRequired + }, + + getInitialState: function () { +// return { +// shouldStoreDataLocally: false +// }; + }, + + handleBackClick: function (anEvent) { + MochiKit.Signal.signal(Clipperz.Signal.NotificationCenter, 'goBack'); + }, + + toggleShouldStoreDataLocally: function (anEvent) { +// this.setState({shouldStoreDataLocally: !this.state['shouldStoreDataLocally']}); + Clipperz.PM.DataModel.devicePreferences.setShouldStoreDataLocally(!Clipperz.PM.DataModel.devicePreferences.shouldStoreDataLocally()); + this.setState({}); + }, + + shouldStoreDataLocally: function () { + return Clipperz.PM.DataModel.devicePreferences.shouldStoreDataLocally(); + }, + + syncNow: function (anEvent) { + MochiKit.Signal.signal(Clipperz.Signal.NotificationCenter, 'synchronizeLocalData'); + }, + + //========================================================================= + + render: function () { + return React.DOM.div({className:'preferences'}, [ + React.DOM.div({className:'header'}, [ + React.DOM.div({className:'titleWrapper'}, React.DOM.div({className:'title'}, "Preferences")), + React.DOM.div({className:'backWrapper'}, React.DOM.a({className:'button back', onClick:this.handleBackClick}, "back")), + ]), + React.DOM.div({className:'content'}, [ + React.DOM.form(null, [ + React.DOM.div({className:'section'}, [ + React.DOM.h4(null, "Local storage"), + React.DOM.p(null, "Store you account data locally for offline viewing"), + new Clipperz.PM.UI.Components.Checkbox({'id':'shouldStoreLocally_checkbox', 'checked':this.shouldStoreDataLocally(), 'eventHandler':this.toggleShouldStoreDataLocally}), + this.shouldStoreDataLocally() ? React.DOM.div({className:'syncInfo'}, [ +// React.DOM.h5(null, "data were never synchronized before"), + React.DOM.a({className:'button', onClick:this.syncNow}, "Sync now") + ]) : null + ]) + ]) + ]), + React.DOM.div({className:'footer'}, [ + + ]) + ]); + } + + //========================================================================= +}); |