├── .edpproj └── .gitignore ├── .fecsrc ├── .gitignore ├── .gitmodules ├── .jshintrc ├── LICENSE ├── README.md ├── doc ├── conf.json └── meta │ ├── BatchUpdateContext.js │ ├── FieldError.js │ ├── Request.js │ ├── RequestConfig.js │ ├── StatusTransition.js │ ├── extension.js │ └── mvc.js ├── module.conf ├── package.json ├── src ├── common │ └── css │ │ ├── common.less │ │ ├── form.less │ │ ├── layout.less │ │ └── list.less ├── extension │ ├── mvc.js │ └── ui.js ├── main.js ├── mvc │ ├── BaseAction.js │ ├── BaseChildView.js │ ├── BaseModel.js │ ├── BaseView.js │ ├── DetailAction.js │ ├── DetailModel.js │ ├── DetailView.js │ ├── EntityValidator.js │ ├── FormAction.js │ ├── FormModel.js │ ├── FormView.js │ ├── IoCActionFactory.js │ ├── ListAction.js │ ├── ListModel.js │ ├── ListView.js │ ├── ReadAction.js │ ├── ReadModel.js │ ├── ReadView.js │ ├── RequestManager.js │ ├── RequestStrategy.js │ ├── SingleEntityModel.js │ ├── StaticListData.js │ ├── checker │ │ ├── enumChecker.js │ │ ├── maxChecker.js │ │ ├── maxLengthChecker.js │ │ ├── minChecker.js │ │ ├── minLengthChecker.js │ │ ├── patternChecker.js │ │ ├── rangeChecker.js │ │ ├── rangeLengthChecker.js │ │ ├── requiredChecker.js │ │ └── typeChecker.js │ ├── filterHelper.js │ ├── handler │ │ ├── RedirectSubmitHandler.js │ │ ├── SubmitHandler.js │ │ └── ToastSubmitHandler.js │ └── rule.js ├── tpl.js ├── ui │ ├── DrawerActionPanel.js │ ├── PartialForm.js │ ├── Uploader.js │ ├── Warn.js │ ├── css │ │ ├── Button.less │ │ ├── Dialog.less │ │ ├── DrawerActionPanel.less │ │ ├── Pager.less │ │ ├── Panel.less │ │ ├── Uploader.less │ │ ├── Warn.less │ │ ├── esui-flat-theme.less │ │ ├── main.less │ │ ├── mixin.less │ │ └── spriteIconMixin.less │ └── extension │ │ ├── AutoSubmit.js │ │ ├── ExternSearch.js │ │ ├── ExternSelect.js │ │ ├── OverrideDefaults.js │ │ ├── TrimInput.js │ │ └── WordCount.js ├── update.js └── util.js └── test ├── config.js └── spec ├── RequestManager.js ├── update.spec.js └── util.spec.js /.edpproj/.gitignore: -------------------------------------------------------------------------------- 1 | # Please keep this file -------------------------------------------------------------------------------- /.fecsrc: -------------------------------------------------------------------------------- 1 | { 2 | "files": [ 3 | "src/**", 4 | 5 | "!src/**/*.less" 6 | ], 7 | "htmlcs": { 8 | "viewport": false 9 | }, 10 | "htmlhint": { 11 | "style-disabled": false 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | lib-cov 2 | *.seed 3 | *.log 4 | *.csv 5 | *.dat 6 | *.out 7 | *.pid 8 | *.gz 9 | 10 | pids 11 | logs 12 | results 13 | 14 | npm-debug.log 15 | node_modules 16 | 17 | dep 18 | doc/api 19 | .DS_Store 20 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ecomfe/ub-ria/c23b28a56215bf56f2ec157460e2b36c8c59c7eb/.gitmodules -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "immed": true, 3 | "latedef": false, 4 | "newcap": true, 5 | "noarg": true, 6 | "noempty": true, 7 | "nonew": true, 8 | "plusplus": false, 9 | "quotmark": "single", 10 | "regexp": false, 11 | "undef": true, 12 | "unused": "vars", 13 | "strict": false, 14 | "trailing": true, 15 | "maxparams": 20, 16 | "maxdepth": 4, 17 | "maxlen": 120, 18 | 19 | "asi": false, 20 | "boss": false, 21 | "debug": false, 22 | "eqnull": true, 23 | "esnext": false, 24 | "evil": false, 25 | "expr": true, 26 | "funcscope": false, 27 | "globalstrict": false, 28 | "iterator": false, 29 | "lastsemic": false, 30 | "laxbreak": true, 31 | "laxcomma": false, 32 | "loopfunc": false, 33 | "multistr": false, 34 | "onecase": false, 35 | "proto": false, 36 | "regexdash": false, 37 | "scripturl": false, 38 | "smarttabs": false, 39 | "shadow": true, 40 | "sub": false, 41 | "supernew": false, 42 | "validthis": true, 43 | 44 | "browser": true, 45 | "couch": false, 46 | "devel": true, 47 | "dojo": false, 48 | "jquery": true, 49 | "mootools": false, 50 | "node": false, 51 | "nonstandard": false, 52 | "prototypejs": false, 53 | "rhino": false, 54 | "wsh": false, 55 | 56 | "nomen": true, 57 | "onevar": false, 58 | "passfail": false, 59 | "white": false, 60 | 61 | "predef": ["define", "URL"] 62 | } 63 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ub-ria 2 | ====== 3 | 4 | RIA base for union business 5 | 6 | ## 生成文档 7 | 8 | ```shell 9 | npm i -g jsdoc 10 | cd {ub-ria} 11 | jsdoc -c doc/conf.json 12 | open doc/api/index.html 13 | ``` 14 | -------------------------------------------------------------------------------- /doc/conf.json: -------------------------------------------------------------------------------- 1 | { 2 | "tags": { 3 | "allowUnknownTags": false 4 | }, 5 | "source": { 6 | "include": ["src", "doc/meta", "README.md"], 7 | "includePattern": ".+\\.(js|md)$" 8 | }, 9 | "plugins": ["plugins/markdown"], 10 | "templates": { 11 | "cleverLinks": false, 12 | "monospaceLinks": false, 13 | "systemName": "emc", 14 | "footer": "{string}", 15 | "copyright": "Baidu Inc. All rights reserved.", 16 | "navType": "vertical", 17 | "theme": "flatly", 18 | "linenums": true, 19 | "collapseSymbols": true 20 | }, 21 | "opts": { 22 | "destination": "doc/api/", 23 | "encoding": "utf-8", 24 | "recurse": true 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /doc/meta/BatchUpdateContext.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @class meta.BatchUpdateContext 3 | * 4 | * 列表批量更新上下文对象 5 | */ 6 | function BatchUpdateContext() { 7 | /** 8 | * @property {Object[]} items 9 | * 10 | * 待更新的实体对象列表 11 | */ 12 | this.items; 13 | 14 | /** 15 | * @property {string[] | number[]} ids 16 | * 17 | * 待更新的实体的id列表 18 | */ 19 | this.ids; 20 | 21 | /** 22 | * @property {number} status 23 | * 24 | * 更新的目标状态数字量,如“删除”操作是将实体标为“已删除”状态,对应值为`0` 25 | */ 26 | this.status; 27 | 28 | /** 29 | * @property {string} statusName 30 | * 31 | * 更新的目标状态的英文表示,如“删除”操作表示为`"remove"` 32 | */ 33 | this.statusName; 34 | 35 | /** 36 | * @property {string} command 37 | * 38 | * 更新操作的操作名称,如“删除”操作即为字符串`"删除"` 39 | */ 40 | this.command; 41 | } 42 | -------------------------------------------------------------------------------- /doc/meta/FieldError.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @class meta.FieldError 3 | * 4 | * 表示一个字段的验证错误信息 5 | */ 6 | function FieldError() { 7 | /** 8 | * @property {string} field 9 | * 10 | * 字段名,通常对应输入控件的`name`属性 11 | */ 12 | this.field; 13 | 14 | /** 15 | * @property {string} message 16 | * 17 | * 错误信息 18 | */ 19 | this.message; 20 | } 21 | -------------------------------------------------------------------------------- /doc/meta/Request.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @class meta.Request 3 | * 4 | * AJAX请求对象 5 | */ 6 | function Request() { 7 | /** 8 | * @property {string} name 9 | * 10 | * 请求的名称 11 | */ 12 | this.name; 13 | 14 | /** 15 | * @property {er.meta.AjaxConfig} options 16 | * 17 | * 请求相关的配置 18 | */ 19 | this.options; 20 | 21 | /** 22 | * @property {meta.RequestConfig} config 23 | * 24 | * 预先配置的请求冲突策略等内容 25 | */ 26 | this.config; 27 | } 28 | -------------------------------------------------------------------------------- /doc/meta/RequestConfig.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @class meta.RequestConfig 3 | * 4 | * AJAX请求配置项 5 | */ 6 | function RequestConfig() { 7 | /** 8 | * @property {string} name 9 | * 10 | * 配置名称 11 | */ 12 | this.name; 13 | 14 | /** 15 | * @property {string} scope 16 | * 17 | * 请求配置的作用范围,可以为: 18 | * 19 | * - `"instance"`:仅在一个{@link RequestManager}实例生命周期内有效 20 | * - `"global"`:跨多个{@link RequestManager}实例共享,全局生效 21 | */ 22 | this.scope; 23 | 24 | /** 25 | * @property {string} policy 26 | * 27 | * 当多个同名请求发起时的处理策略,支持: 28 | * 29 | * - `"abort"`:第二个请求发起时,会自动中断第一个请求,常用于更新等操作 30 | * - `"reuse"`:第二个请求发起时,如第一个请求正在进行, 31 | * 则直接使用第一个请求,常用于读取操作 32 | * - `"parallel"`:多个请求并行,互不干扰,如同普通的AJAX请求,常用于日志发送 33 | * - `"auto"`:自动选择,当2个请求的配置和发送数据完全相同时,使用`reuse`策略, 34 | * 否则当`GET`或`PUT`请求时使用`parallel`,其它情况使用`abort` 35 | */ 36 | this.policy; 37 | 38 | /** 39 | * @property {Object} [options] 40 | * 41 | * 请求的配置项,发起请求时与实时传入的配置进行合并,具体参考{er.meta.AjaxConfig}类 42 | */ 43 | this.options; 44 | } 45 | -------------------------------------------------------------------------------- /doc/meta/StatusTransition.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 实体状态转换对象,用于表达实体在一定状态下可以或不可以进行的状态迁移,以及该状态的相关信息 3 | * 4 | * @class meta.StatusTransition 5 | */ 6 | function StatusTransition() { 7 | /** 8 | * 目标当前状态 9 | * 10 | * @member meta.StatusTransition#status 11 | * @type {numer} 12 | */ 13 | this.status; 14 | 15 | /** 16 | * 不能从{@link meta.StatusTransition#status}迁移的对应状态 17 | * 18 | * @member meta.StatusTransition#deny 19 | * @type {number[]} 20 | */ 21 | this.deny; 22 | 23 | /** 24 | * 可以从{@link meta.StatusTransition#status}迁移的对应状态 25 | * 26 | * @member meta.StatusTransition#accept 27 | * @type {number[]} 28 | */ 29 | this.accept; 30 | 31 | /** 32 | * 当前状态对应的操作名,是一个`camelCase`的格式 33 | * 34 | * @member meta.StatusTransition#statusName 35 | * @type {string} 36 | */ 37 | this.statusName; 38 | 39 | /** 40 | * 当前对应的中文描述 41 | * 42 | * @member meta.StatusTransition#command 43 | * @type {string} 44 | */ 45 | this.command; 46 | } 47 | -------------------------------------------------------------------------------- /doc/meta/extension.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @namespace extension 3 | */ 4 | var extension = {}; 5 | -------------------------------------------------------------------------------- /doc/meta/mvc.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @namespace mvc 3 | */ 4 | var mvc = {}; 5 | -------------------------------------------------------------------------------- /module.conf: -------------------------------------------------------------------------------- 1 | { 2 | "baseUrl": "src", 3 | "paths": {}, 4 | "packages": [ 5 | { 6 | "name": "ef", 7 | "location": "../dep/ef/src", 8 | "main": "main" 9 | }, 10 | { 11 | "name": "eicons", 12 | "location": "../dep/eicons/src", 13 | "main": "main.less" 14 | }, 15 | { 16 | "name": "eoo", 17 | "location": "../dep/eoo/src", 18 | "main": "main" 19 | }, 20 | { 21 | "name": "er", 22 | "location": "../dep/er/src", 23 | "main": "main" 24 | }, 25 | { 26 | "name": "esf", 27 | "location": "../dep/esf/src" 28 | }, 29 | { 30 | "name": "est", 31 | "location": "../dep/est/src" 32 | }, 33 | { 34 | "name": "esui", 35 | "location": "../dep/esui/src", 36 | "main": "main" 37 | }, 38 | { 39 | "name": "etpl", 40 | "location": "../dep/etpl/src", 41 | "main": "main" 42 | }, 43 | { 44 | "name": "mini-event", 45 | "location": "../dep/mini-event/src", 46 | "main": "main" 47 | }, 48 | { 49 | "name": "moment", 50 | "location": "../dep/moment/src", 51 | "main": "moment" 52 | }, 53 | { 54 | "name": "promise", 55 | "location": "../dep/promise/1.0.2/src", 56 | "main": "main" 57 | }, 58 | { 59 | "name": "uioc", 60 | "location": "../dep/uioc/0.3.1/src", 61 | "main": "main" 62 | }, 63 | { 64 | "name": "underscore", 65 | "location": "../dep/underscore/src", 66 | "main": "underscore" 67 | } 68 | ] 69 | } 70 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ub-ria", 3 | "version": "2.0.0", 4 | "maintainers": [ 5 | { 6 | "name": "otakustay", 7 | "email": "otakustay@gmail.com" 8 | } 9 | ], 10 | "contributors": [ 11 | { 12 | "name": "DDDBear" 13 | }, 14 | { 15 | "name": "wangyaqiong" 16 | } 17 | ], 18 | "main": "main", 19 | "description": "联盟前端产品RIA基础库", 20 | "homepage": "https://github.com/ecomfe/ub-ria", 21 | "edp": { 22 | "wwwroot": "/", 23 | "depDir": "dep", 24 | "srcDir": "src", 25 | "loaderAutoConfig": "js,htm,html,tpl,vm,phtml", 26 | "loaderUrl": "http://s1.bdstatic.com/r/www/cache/ecom/esl/1-8-2/esl.js", 27 | "dependencies": { 28 | "er": ">=3.1.x", 29 | "esui": ">=3.1.x", 30 | "ef": ">=3.1.x", 31 | "underscore": "1.5.x", 32 | "mini-event": "1.x", 33 | "etpl": "2.x", 34 | "moment": "2.x", 35 | "uioc": "0.x", 36 | "eoo": "0.x", 37 | "promise": "1.x", 38 | "eicons": "1.x", 39 | "esf": "~1.0.0-beta.1", 40 | "est": "~1.3.0" 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/common/css/common.less: -------------------------------------------------------------------------------- 1 | .hidden { 2 | display: none; 3 | } 4 | 5 | // 并列richSelector布局 6 | .rich-selector-group { 7 | overflow: hidden; 8 | .ui-richselector { 9 | float: left; 10 | margin-right: 10px; 11 | } 12 | } 13 | 14 | .ui-select, 15 | .ui-button { 16 | display: inline-block; 17 | } 18 | 19 | 20 | .button-like-select { 21 | display: inline-block; 22 | background-color: #ebebeb; 23 | width: 240px; 24 | padding: 9px 0; 25 | text-align: center; 26 | } 27 | 28 | 29 | .text-line-layout { 30 | .ui-textline-num-line { 31 | background: #f7f7f7; 32 | } 33 | padding: 20px 10px 10px; 34 | background: #ebebeb; 35 | width: 580px; 36 | .setting-tip { 37 | color: #555555; 38 | padding-bottom: 10px; 39 | display: block; 40 | } 41 | .ui-textline { 42 | background: #f7f7f7; 43 | } 44 | } 45 | 46 | .skin-fake-select-layer { 47 | display: none; 48 | } 49 | 50 | // 进度加载页面 51 | @-webkit-keyframes bouncedelay { 52 | 0%, 80%, 100% { -webkit-transform: scale(0.0) } 53 | 40% { -webkit-transform: scale(1.0) } 54 | } 55 | 56 | @keyframes bouncedelay { 57 | 0%, 80%, 100% { 58 | transform: scale(0.0); 59 | -webkit-transform: scale(0.0); 60 | } 40% { 61 | transform: scale(1.0); 62 | -webkit-transform: scale(1.0); 63 | } 64 | } -------------------------------------------------------------------------------- /src/common/css/form.less: -------------------------------------------------------------------------------- 1 | // 放一些可能需要复用的变量 2 | @form-field-line-height: 30px; 3 | @form-field-required-star-width: 12px; 4 | @form-field-label-width: 104px; 5 | 6 | .form-view .content-main { 7 | background: none repeat scroll 0 0 #fff; 8 | padding: 25px 50px; 9 | color: #656565; 10 | } 11 | 12 | .form-section { 13 | margin: 0 0 60px; 14 | } 15 | 16 | .form-toggle-section { 17 | .ui-togglepanel-title { 18 | // TODO: panel宽度应根据文字自适应。 19 | width: 130px; 20 | } 21 | } 22 | 23 | .form-section-title { 24 | font-size: 14px; 25 | font-weight: bold; 26 | margin-bottom: 30px; 27 | } 28 | 29 | .form-section-title-hint { 30 | color: #999; 31 | font-size: 12px; 32 | padding: 0 8px; 33 | font-weight: normal; 34 | } 35 | 36 | 37 | .form-field, 38 | .form-field-required { 39 | .clearfix(); 40 | font-size: 14px; 41 | margin: 30px 0; 42 | } 43 | 44 | .form-field-label { 45 | float: left; 46 | zoom: 1; 47 | width: @form-field-label-width; 48 | line-height: @form-field-line-height; 49 | text-align: left; 50 | vertical-align: top; 51 | } 52 | 53 | .form-field-value { 54 | line-height: @form-field-line-height; 55 | float: left; 56 | padding-left: @form-field-required-star-width; 57 | 58 | .ui-textbox { 59 | input, textarea { 60 | font-size: 14px; 61 | } 62 | } 63 | } 64 | 65 | .form-field-required .form-field-value { 66 | padding-left: 0px; 67 | } 68 | 69 | .form-field-value-required-star { 70 | float: left; 71 | color: red; 72 | width: @form-field-required-star-width; 73 | text-align: left; 74 | } 75 | 76 | // 放在field中的控件,其实我忘了为啥有这个样式了,先放着吧。。。 77 | .form-field-control { 78 | float: left; 79 | } 80 | 81 | // 问号注释 82 | .form-field-tip { 83 | margin: 0 0 0 10px; 84 | } 85 | 86 | .form-submit-section { 87 | border-top: 1px solid #eaeaea; 88 | margin: 0 0 0 30px; 89 | padding-top: 20px; 90 | 91 | .ui-button { 92 | padding: 0 50px; 93 | } 94 | } 95 | 96 | .form-notice-field { 97 | margin-top: 30px; 98 | } 99 | -------------------------------------------------------------------------------- /src/common/css/layout.less: -------------------------------------------------------------------------------- 1 | // 产品线统一 2 | @header-color: #fff; 3 | @header-height: 60px; 4 | @header-link-color: #fff; 5 | 6 | @sidebar-width: 168px; 7 | @sidebar-collapse-width: 55px; 8 | @sidebar-menu-item-height:55px; 9 | @sidebar-bg: #efefef; 10 | @sidebar-menu-item-hover-bg: #e0e0e0; 11 | @sidebar-menu-item-selected-bg: #c8caca; 12 | 13 | 14 | // 系统自定义 15 | @loading-mask-background-color: #c0ede8; 16 | @header-bg: #0fb4a0; 17 | @sidebar-inverse-bg: #1da290; 18 | 19 | @logo-image-url: none; 20 | @logo-image-width: 0; 21 | @logo-image-height: 0; 22 | 23 | 24 | // 始终显示纵向滚动条,以免页面高度导致内容横移 25 | body { 26 | overflow-y: scroll; 27 | width: 100%; 28 | } 29 | 30 | // 进度加载遮挡 31 | .global-loading { 32 | .loading-mask { 33 | background: @loading-mask-background-color; 34 | bottom: 0; 35 | left: 0; 36 | opacity: 0.9; 37 | position: fixed; 38 | right: 0; 39 | top: 0; 40 | z-index: 5000; 41 | } 42 | 43 | .loading-bounce { 44 | text-align: center; 45 | position: fixed; 46 | width: 120px; 47 | top: 360px; 48 | left: 46%; 49 | height: 60px; 50 | z-index: 6000; 51 | } 52 | 53 | .loading-bounce-point { 54 | background-color: #fff; 55 | border-radius: 100%; 56 | display: inline-block; 57 | height: 18px; 58 | width: 18px; 59 | -webkit-animation: bouncedelay 1.4s infinite ease-in-out; 60 | animation: bouncedelay 1.4s infinite ease-in-out; 61 | /* Prevent first frame from flickering when animation starts */ 62 | -webkit-animation-fill-mode: both; 63 | animation-fill-mode: both; 64 | } 65 | 66 | .loading-bounce-one { 67 | -webkit-animation-delay: -0.32s; 68 | animation-delay: -0.32s; 69 | } 70 | .loading-bounce-two { 71 | -webkit-animation-delay: -0.16s; 72 | animation-delay: -0.16s; 73 | } 74 | 75 | .loading-bounce-three { 76 | } 77 | } 78 | 79 | header { 80 | background: @header-bg; 81 | height: @header-height; 82 | width: 100%; 83 | z-index: 100; 84 | color: @header-color; 85 | a { 86 | color: @header-link-color; 87 | } 88 | transition: opacity 500ms; 89 | position: relative; 90 | } 91 | 92 | .header-fixed { 93 | position: fixed; 94 | top: 0; 95 | left: 0; 96 | } 97 | 98 | .header-logo { 99 | margin: 10px 30px 0 83px; 100 | .inline-block; 101 | a { 102 | display: block; 103 | .normal-icon(@logo-image-url, @logo-image-width, @logo-image-height); 104 | text-indent: -99999px; 105 | } 106 | } 107 | 108 | .nav { 109 | position: absolute; 110 | right: 142px; 111 | top: 0; 112 | } 113 | 114 | .nav-main { 115 | overflow: hidden; 116 | margin-top: 21px; 117 | li { 118 | padding-left: 0; 119 | list-style: none; 120 | a { 121 | width: 56px; 122 | text-indent: -99999px; 123 | display: block; 124 | width: 17px; 125 | height: 17px; 126 | } 127 | } 128 | } 129 | 130 | .toolbar { 131 | position: absolute; 132 | top: 0; 133 | right: 0; 134 | .toolbar-account { 135 | padding: 27px 30px 0 0; 136 | height: @header-height - 27; 137 | cursor: pointer; 138 | .ui-dropdown-icon { 139 | top: 30px; 140 | } 141 | } 142 | .toolbar-tools { 143 | padding-bottom: 10px; 144 | background: #f6f6f6; 145 | li { 146 | margin-top: 10px; 147 | } 148 | a { 149 | display: block; 150 | background: #fffaee; 151 | padding: 10px; 152 | color: #666; 153 | } 154 | .top-pointer(#dfdfdf, 0, 30px); 155 | } 156 | } 157 | 158 | .page-container {} 159 | 160 | .page-sidebar {} 161 | 162 | .inverse-sidebar { 163 | position: fixed; 164 | top: 0; 165 | left: 0; 166 | z-index: 101; 167 | background: @sidebar-inverse-bg; 168 | height: @header-height; 169 | width: @sidebar-collapse-width; 170 | text-align: center; 171 | cursor: pointer; 172 | i { 173 | } 174 | &:hover { 175 | i { 176 | } 177 | } 178 | } 179 | 180 | .inverse-sidebar-collapse { 181 | i { 182 | } 183 | &:hover { 184 | i { 185 | } 186 | } 187 | } 188 | 189 | .ui-sidebar-menu-icon { 190 | } 191 | 192 | // sidebar 旁边的页面 193 | .page-content { 194 | min-width: 960px; 195 | margin-left: @sidebar-width; 196 | } 197 | 198 | .skin-action-dialog { 199 | .content-main { 200 | padding:25px 50px; 201 | } 202 | } 203 | 204 | -------------------------------------------------------------------------------- /src/common/css/list.less: -------------------------------------------------------------------------------- 1 | @table-operation-field-text-color: #67c1a5; 2 | @table-operation-layer-max-width: 100%; 3 | @list-meta-height: 35px; 4 | @list-action-button-bacth-gap: 10px; 5 | @list-action-button-width: 155px; 6 | 7 | .list-view { 8 | position: relative; 9 | background: none repeat scroll 0 0 #fff; 10 | padding: 15px 30px; 11 | 12 | .ui-pager { 13 | .ui-pager-select-wrapper { 14 | margin: 0; 15 | margin-right: 18px; 16 | } 17 | 18 | .ui-select { 19 | padding-left: 0; 20 | } 21 | } 22 | } 23 | 24 | .list-summary { 25 | line-height: 16px; 26 | } 27 | 28 | .list-meta { 29 | overflow: hidden; 30 | position: relative; 31 | height: @list-meta-height; 32 | margin-bottom: 10px; 33 | .clearfix(); 34 | } 35 | 36 | .list-action { 37 | height: @list-meta-height; 38 | float: left; 39 | z-index: 1; 40 | width: @list-action-button-width; 41 | } 42 | 43 | .list-filter { 44 | height: @list-meta-height; 45 | .clearfix(); 46 | background: #efefef; 47 | margin-left: @list-action-button-width + @list-action-button-bacth-gap; 48 | .ui-searchbox { 49 | float: right; 50 | margin: 5px 10px; 51 | } 52 | .ui-select { 53 | // TODO 54 | } 55 | .list-filter-fields { 56 | float: left; 57 | } 58 | .list-batch-operation { 59 | float: left; 60 | } 61 | } 62 | 63 | .list-content-table { 64 | overflow: hidden; 65 | } 66 | 67 | .table-operation, 68 | .table-operation-trigger { 69 | padding: 0 10px; 70 | cursor: pointer; 71 | } 72 | 73 | .table-operation-layer { 74 | position: absolute; 75 | top: 0; 76 | bottom: 0; 77 | right: 0; 78 | visibility: hidden; 79 | padding-left: 15px; 80 | background: #fff; 81 | 82 | .translate(100%); 83 | transition: -webkit-transform .5s; 84 | transition: -ms-transform .5s; 85 | transition: transform .5s; 86 | 87 | .table-operation { 88 | height: 16px; 89 | line-height: 16px; 90 | padding: 0; 91 | margin: 0 10px; 92 | vertical-align: middle; 93 | } 94 | 95 | &:before { 96 | content: ''; 97 | display: inline-block; 98 | height: 100%; 99 | vertical-align: middle; 100 | } 101 | } 102 | 103 | // 鼠标悬浮最后一列的时候,触发弹层展现 104 | .ui-table-cell-text-last:hover .table-operation-layer { 105 | visibility: visible; 106 | .translate(0); 107 | } 108 | 109 | .ui-table-cell-field-operation { 110 | color: @table-operation-field-text-color; 111 | } 112 | -------------------------------------------------------------------------------- /src/extension/mvc.js: -------------------------------------------------------------------------------- 1 | /** 2 | * UB RIA Base 3 | * Copyright 2014 Baidu Inc. All rights reserved. 4 | * 5 | * @file MVC体系扩展 6 | * @author otakustay 7 | */ 8 | define( 9 | function (require) { 10 | var u = require('../util'); 11 | var util = require('er/util'); 12 | var events = require('er/events'); 13 | 14 | function addPageClassName() { 15 | var add = function (e) { 16 | if (!e.target.getPageCategories) { 17 | return; 18 | } 19 | 20 | var element = util.getElement(e.target.context.container); 21 | 22 | if (!element) { 23 | return; 24 | } 25 | 26 | var pageClasses = e.target.getPageCategories(); 27 | 28 | // `addClass`的简单实现 29 | if (element.classList) { 30 | u.each( 31 | pageClasses, 32 | function (className) { 33 | element.classList.add(className); 34 | } 35 | ); 36 | } 37 | else { 38 | var classes = element.className 39 | ? element.className.split(/\s+/) 40 | : []; 41 | classes = u.union(classes, pageClasses); 42 | element.className = classes.join(' '); 43 | } 44 | }; 45 | 46 | var remove = function (e) { 47 | if (!e.action || !e.action.getPageCategories) { 48 | return; 49 | } 50 | 51 | // `enteractionfail`可以直接取到`e.container`,`leaveaction`则要从`action`上取 52 | var container = e.actionContext 53 | ? e.container 54 | : (e.action.context ? e.action.context.container : null); // 非常极端的情况会导致`null` 55 | var element = util.getElement(container); 56 | 57 | if (!element) { 58 | return; 59 | } 60 | 61 | var pageClasses = e.action.getPageCategories(); 62 | 63 | // `removeClass`的简单实现 64 | if (element.classList) { 65 | u.each( 66 | pageClasses, 67 | function (className) { 68 | element.classList.remove(className); 69 | } 70 | ); 71 | } 72 | else { 73 | var classes = element.className 74 | ? element.className.split(/\s+/) 75 | : []; 76 | var newClasses = u.difference(classes, pageClasses); 77 | if (newClasses.length !== classes.length) { 78 | element.className = newClasses.join(' '); 79 | } 80 | } 81 | }; 82 | 83 | events.on( 84 | 'enteraction', 85 | function (e) { 86 | e.action.on('enter', add); 87 | } 88 | ); 89 | events.on('leaveaction', remove); 90 | events.on('enteractionfail', remove); 91 | } 92 | 93 | function enable() { 94 | addPageClassName(); 95 | } 96 | 97 | /** 98 | * MVC体系扩展 99 | * 100 | * @namespace extension.mvc 101 | * @memberof extension 102 | */ 103 | return { 104 | /** 105 | * 启动扩展 106 | * 107 | * @method extension.mvc.enable 108 | */ 109 | enable: u.once(enable) 110 | }; 111 | } 112 | ); 113 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | /** 2 | * UB RIA Base 3 | * Copyright 2013 Baidu Inc. All rights reserved. 4 | * 5 | * @file 入口模块 6 | * @author otakustay 7 | */ 8 | define( 9 | function (require) { 10 | /** 11 | * 模块入口 12 | * 13 | * @namespace main 14 | */ 15 | var main = { 16 | /** 17 | * @property {string} 版本号 18 | * @readonly 19 | */ 20 | version: '2.0.0-beta.6', 21 | 22 | /** 23 | * 启动所有扩展 24 | * 25 | * @method main.enableExtensions 26 | */ 27 | enableExtensions: function () { 28 | // 加载扩展 29 | require('./extension/mvc').enable(); 30 | require('./extension/ui').enable(); 31 | }, 32 | 33 | /** 34 | * 启动MVC程序 35 | * 36 | * @method main.start 37 | */ 38 | start: function () { 39 | main.enableExtensions(); 40 | 41 | // 启动ER 42 | require('er').start(); 43 | } 44 | }; 45 | 46 | return main; 47 | } 48 | ); 49 | -------------------------------------------------------------------------------- /src/mvc/BaseAction.js: -------------------------------------------------------------------------------- 1 | /** 2 | * UB RIA Base 3 | * Copyright 2013 Baidu Inc. All rights reserved. 4 | * 5 | * @file Action基类 6 | * @author otakustay 7 | */ 8 | define( 9 | function (require) { 10 | var u = require('../util'); 11 | 12 | /** 13 | * Action基类,提供实体名称和实体描述的维护 14 | * 15 | * @class mvc.BaseAction 16 | * @extends er.Action 17 | */ 18 | var exports = {}; 19 | 20 | /** 21 | * @constructs mvc.BaseAction 22 | * @param {string} [entityName] 负责的实体名称 23 | * @override 24 | */ 25 | exports.constructor = function (entityName) { 26 | this.$super(arguments); 27 | 28 | this.entityName = entityName; 29 | }; 30 | 31 | /** 32 | * 获取当前Action所处理的实体名称 33 | * 34 | * @method mvc.BaseAction#getEntityName 35 | * @return {string} 36 | */ 37 | exports.getEntityName = function () { 38 | if (!this.entityName) { 39 | // 如果在`enteractionfail`之类的事件中用到,此时是没有`context`的 40 | if (this.context) { 41 | // 几乎所有的URL都是`/{entityName}/list|update|create|view`结构 42 | var path = this.context.url.getPath(); 43 | this.entityName = path.split('/')[1]; 44 | } 45 | 46 | return ''; 47 | } 48 | 49 | return this.entityName; 50 | }; 51 | 52 | /** 53 | * 获取当前Action的实体简介名称 54 | * 55 | * @protected 56 | * @member mvc.BaseAction#entityDescription 57 | * @type {string} 58 | */ 59 | exports.entityDescription = ''; 60 | 61 | /** 62 | * 获取实体的简介名称 63 | * 64 | * @method mvc.BaseAction#getEntityDescription 65 | * @return {string} 66 | */ 67 | exports.getEntityDescription = function () { 68 | return this.entityDescription || ''; 69 | }; 70 | 71 | /** 72 | * 当前Action的所属分组名称,通常用于控制导航条的选中状态 73 | * 74 | * @protected 75 | * @member mvc.BaseAction#group 76 | * @type {string} 77 | */ 78 | exports.group = ''; 79 | 80 | /** 81 | * 获取当前Action的所属分组名称 82 | * 83 | * @method mvc.BaseAction#getGroup 84 | * @return {string} 85 | */ 86 | exports.getGroup = function () { 87 | return this.group; 88 | }; 89 | 90 | /** 91 | * 当前页面的分类,如列表为`"list"` 92 | * 93 | * @protected 94 | * @member mvc.BaseAction#category 95 | * @type {string} 96 | */ 97 | exports.category = ''; 98 | 99 | /** 100 | * 获取当前页面的分类 101 | * 102 | * @method mvc.BaseAction#getCategory 103 | * @return {string} 104 | */ 105 | exports.getCategory = function () { 106 | return this.category || ''; 107 | }; 108 | 109 | /** 110 | * 获取当前页面的分类 111 | * 112 | * 默认返回以下内容: 113 | * 114 | * - `{category}-page` 115 | * - `{entityName}-page` 116 | * - `{entityName}-{category}-page` 117 | * - `{packageName}-package` 118 | * - `{packageName}-pacakge-{category}` 119 | * 120 | * @method mvc.BaseAction#getPageCategories 121 | * @return {string[]} 122 | */ 123 | exports.getPageCategories = function () { 124 | var categories = []; 125 | var category = u.dasherize(this.getCategory()); 126 | var entityName = u.dasherize(this.getEntityName()); 127 | var packageName = u.dasherize(this.getPackageName()); 128 | 129 | if (category) { 130 | categories.push(category + '-page'); 131 | } 132 | if (entityName) { 133 | categories.push(entityName + '-page'); 134 | } 135 | if (category && entityName) { 136 | categories.push(entityName + '-' + this.category + '-page'); 137 | } 138 | if (packageName) { 139 | categories.push(packageName + '-package'); 140 | } 141 | if (packageName && category) { 142 | categories.push(packageName + '-package-' + category); 143 | } 144 | 145 | return categories; 146 | }; 147 | 148 | /** 149 | * @override 150 | */ 151 | exports.createModel = function (args) { 152 | args.entityDescription = this.getEntityDescription(); 153 | 154 | var model = this.$super(arguments); 155 | 156 | // Action基类的默认返回值是一个空对象`{}`, 157 | // 但是普通的`Model`对象因为方法和属性全在`prototype`上,也会被判断为空 158 | var Model = require('er/Model'); 159 | if (!(model instanceof Model) && u.isEmpty(model)) { 160 | var BaseModel = require('./BaseModel'); 161 | var entityName = this.getEntityName(); 162 | model = new BaseModel(entityName, args); 163 | } 164 | 165 | return model; 166 | }; 167 | 168 | /** 169 | * 设置数据模型对象,会给`model`增加`entityDescription`字段 170 | * 可选。一般由IoC统一配置。 171 | * 172 | * @method mvc.BaseAction#setModel 173 | * @param {er.Model} model 数据模型 174 | */ 175 | exports.setModel = function (model) { 176 | model.set('entityName', this.getEntityName()); 177 | model.set('entityDescription', this.getEntityDescription()); 178 | this.model = model; 179 | }; 180 | 181 | var oo = require('eoo'); 182 | 183 | oo.defineAccessor(exports, 'packageName'); 184 | 185 | var Action = require('er/Action'); 186 | var BaseAction = oo.create(Action, exports); 187 | 188 | return BaseAction; 189 | } 190 | ); 191 | -------------------------------------------------------------------------------- /src/mvc/BaseChildView.js: -------------------------------------------------------------------------------- 1 | /** 2 | * UB RIA Base 3 | * Copyright 2014 Baidu Inc. All rights reserved. 4 | * 5 | * @file 子视图基类 6 | * @author liyidong 7 | */ 8 | define( 9 | function (require) { 10 | var u = require('../util'); 11 | 12 | /** 13 | * @class mvc.BaseChildView 14 | * @extends ef.UIView 15 | */ 16 | var exports = {}; 17 | 18 | /** 19 | * 获取view数据的接口 20 | * 21 | * @method mvc.BaseChildView#getViewData 22 | * @return {Object} 23 | */ 24 | exports.getViewData = function () { 25 | // 默认实现为返回所有类型为InputControll控件的RawValue值,key为控件的name 26 | var store = {}; 27 | var inputs = this.getAllInputControls(); 28 | 29 | for (var i = 0, length = inputs.length; i < length; i++) { 30 | var control = inputs[i]; 31 | 32 | // 排除未选中的选择框控件 33 | if (control.getCategory() === 'check' && !control.isChecked()) { 34 | continue; 35 | } 36 | 37 | // 不需要禁用了的控件的值 38 | if (control.isDisabled()) { 39 | continue; 40 | } 41 | 42 | var name = control.get('name'); 43 | var value = control.getRawValue(); 44 | if (store.hasOwnProperty(name)) { 45 | store[name] = [].concat(store[name], value); 46 | } 47 | else { 48 | store[name] = value; 49 | } 50 | } 51 | 52 | return store; 53 | }; 54 | 55 | /** 56 | * 获取view数据的接口 57 | * 58 | * @method mvc.BaseChildView#setViewData 59 | * @param {Object} values 控件name与value组成的对象 60 | */ 61 | exports.setViewData = function (values) { 62 | // 默认实现 63 | u.each( 64 | values, 65 | function (value, key) { 66 | key = u.dasherize(key); 67 | this.get(key).set('rawValue', value); 68 | }, 69 | this 70 | ); 71 | }; 72 | 73 | /** 74 | * disable当前View下所有控件 75 | * 76 | * @method mvc.BaseChildView#disableInputControls 77 | */ 78 | exports.disableInputControls = function () { 79 | // 默认实现为将所有类型为InputControll控件设置为disable状态 80 | var inputs = this.getAllInputControls(); 81 | 82 | u.each( 83 | inputs, 84 | function (control) { 85 | if (u.isFunction(control.disable)) { 86 | control.disable(); 87 | } 88 | } 89 | ); 90 | }; 91 | 92 | /** 93 | * enable当前View下所有控件 94 | * 95 | * @method mvc.BaseChildView#enableInputControls 96 | */ 97 | exports.enableInputControls = function () { 98 | // 默认实现为将所有类型为InputControll控件设置为enable状态 99 | var inputs = this.getAllInputControls(); 100 | 101 | u.each( 102 | inputs, 103 | function (control) { 104 | if (u.isFunction(control.enable)) { 105 | control.enable(); 106 | } 107 | } 108 | ); 109 | }; 110 | 111 | /** 112 | * setReadOnly当前View下所有控件 113 | * 114 | * @method mvc.BaseChildView#setReadOnly 115 | * @param {boolean} status 设置的readOnly状态 116 | */ 117 | exports.setReadOnly = function (status) { 118 | // 默认实现为将所有类型为InputControll控件设置为status所规定的readOnly状态 119 | var inputs = this.getAllInputControls(); 120 | 121 | u.each( 122 | inputs, 123 | function (control) { 124 | if (u.isFunction(control.setReadOnly)) { 125 | control.setReadOnly(status); 126 | } 127 | } 128 | ); 129 | }; 130 | 131 | /** 132 | * 向用户通知提交错误信息 133 | * 134 | * @method mvc.BaseChildView#notifyErrors 135 | * @param {Array.} errors 错误信息数组 136 | */ 137 | exports.notifyErrors = function (errors) { 138 | var Validity = require('esui/validator/Validity'); 139 | var ValidityState = require('esui/validator/ValidityState'); 140 | 141 | for (var i = 0; i < errors.length; i++) { 142 | var fail = errors[i]; 143 | var state = new ValidityState(false, fail.message); 144 | var validity = new Validity(); 145 | validity.addState('server', state); 146 | 147 | var inputId = u.dasherize(fail.field); 148 | var input = this.get(inputId); 149 | 150 | if (input) { 151 | input.showValidity(validity); 152 | } 153 | } 154 | }; 155 | 156 | /** 157 | * 触发当前View下所有控件的自身校验 158 | * 159 | * @method mvc.BaseChildView#validate 160 | * @return {boolean} 控件的验证状态 161 | */ 162 | exports.validate = function () { 163 | var inputs = this.getAllInputControls(); 164 | var result = true; 165 | 166 | for (var i = 0; i < inputs.length; i++) { 167 | var control = inputs[i]; 168 | // 不对disabled的控件进行验证 169 | if (control.isDisabled()) { 170 | continue; 171 | } 172 | 173 | result &= control.validate(); 174 | } 175 | 176 | return !!result; 177 | }; 178 | 179 | /** 180 | * 获取当前视图下所有的InputControl控件 181 | * 182 | * @method mvc.BaseChildView#getAllInputControls 183 | * @return {Array.} 所有InputControl控件 184 | */ 185 | exports.getAllInputControls = function () { 186 | var controls = this.viewContext.getControls(); 187 | var inputs = []; 188 | 189 | u.each( 190 | controls, 191 | function (control) { 192 | if (isInputControl(control)) { 193 | inputs.push(control); 194 | } 195 | } 196 | ); 197 | 198 | return inputs; 199 | }; 200 | 201 | /** 202 | * 判断是否为输入控件 203 | * 204 | * @param {Control} control 控件 205 | * @return {boolean} 206 | */ 207 | function isInputControl(control) { 208 | var category = control.getCategory(); 209 | return category === 'input' || category === 'check'; 210 | } 211 | 212 | var BaseView = require('./BaseView'); 213 | var BaseChildView = require('eoo').create(BaseView, exports); 214 | 215 | return BaseChildView; 216 | } 217 | ); 218 | -------------------------------------------------------------------------------- /src/mvc/BaseModel.js: -------------------------------------------------------------------------------- 1 | /** 2 | * UB RIA Base 3 | * Copyright 2013 Baidu Inc. All rights reserved. 4 | * 5 | * @file 数据模型基类 6 | * @author otakustay 7 | */ 8 | define( 9 | function (require) { 10 | var u = require('../util'); 11 | var eoo = require('eoo'); 12 | 13 | /** 14 | * @class mvc.BaseModel 15 | * @extends ef.UIModel 16 | */ 17 | var exports = {}; 18 | 19 | /** 20 | * 添加一个数据对象,以便当前数据模型对象可以进行管理 21 | * 22 | * @protected 23 | * @method mvc.BaseModel#addData 24 | * @param {string} [name="default"] 数据对象的名称,没有则使用 25 | * @param {mvc.RequestManager} instance 一个数据对象 26 | */ 27 | exports.addData = function (name, instance) { 28 | if (!this.dataPool) { 29 | this.dataPool = {}; 30 | } 31 | 32 | if (arguments.length < 2) { 33 | instance = name; 34 | name = 'default'; 35 | } 36 | if (!name) { 37 | name = 'default'; 38 | } 39 | 40 | if (!this.dataPool[name]) { 41 | this.dataPool[name] = instance; 42 | } 43 | }; 44 | 45 | /** 46 | * 设置当前所属模块的默认`Data`实现 47 | * 可选。一般由IoC统一配置。 48 | * 49 | * @method mvc.BaseModel#setData 50 | * @param {mvc.RequestManager} instance 一个数据对象 51 | */ 52 | exports.setData = function (instance) { 53 | this.addData(instance); 54 | }; 55 | 56 | /** 57 | * 获取关联在当前Model上的数据对象 58 | * 59 | * @protected 60 | * @method mvc.BaseModel#data 61 | * @param {string} [name] 需要的数据对象的名称,不提供则返回默认的数据对象 62 | * @return {mvc.RequestManager} 63 | */ 64 | exports.data = function (name) { 65 | if (!name) { 66 | name = 'default'; 67 | } 68 | return this.dataPool[name] || null; 69 | }; 70 | 71 | /** 72 | * 设置globalData方法 73 | * 可选。一般由IoC统一配置。 74 | * 75 | * @public 76 | * @method mvc.BaseModel#setGlobalData 77 | * @param {mvc.RequestManager} data 全局数据对象 78 | */ 79 | exports.setGlobalData = function (data) { 80 | this.addData('global', data); 81 | }; 82 | 83 | /** 84 | * 添加一个数据源 85 | * 86 | * @protected 87 | * @method mvc.BaseModel#putDatasource 88 | * @param {Object} item 数据源配置,参考ER框架的说明 89 | * @param {number} [index] 数据源放置的位置,如果不提供则放在最后,提供则和那个位置的并行 90 | */ 91 | exports.putDatasource = function (item, index) { 92 | // 先复制一份,避免合并时相互污染 93 | item = u.clone(item); 94 | 95 | if (!this.datasource) { 96 | this.datasource = []; 97 | } 98 | else if (!u.isArray(this.datasource)) { 99 | this.datasource = [this.datasource]; 100 | } 101 | 102 | if (index === undefined) { 103 | this.datasource.push(item); 104 | } 105 | else { 106 | var originalItem = this.datasource[index] || {}; 107 | // 如果是数组就加到最后,是对象就混一起了,但我们并不希望这里是数组 108 | if (u.isArray(originalItem)) { 109 | originalItem.push(item); 110 | } 111 | else { 112 | u.extend(originalItem, item); 113 | } 114 | this.datasource[index] = originalItem; 115 | } 116 | }; 117 | 118 | /** 119 | * 判断是否有给定的权限 120 | * 121 | * @public 122 | * @method mvc.BaseModel#checkPermission 123 | * @param {string} permissionName 需要判断的权限名称 124 | * @return {boolean} 125 | * @throws {Error} 没有关联的`permission`对象 126 | * @throws {Error} 关联的`permission`对象不提供`permissionName`对应的权限的判断 127 | */ 128 | exports.checkPermission = function (permissionName) { 129 | var permission = this.getPermission(); 130 | 131 | if (!permission) { 132 | throw new Error('No attached permission object'); 133 | } 134 | 135 | var method = permission[permissionName]; 136 | 137 | if (!method) { 138 | throw new Error('No "' + method + '" method on permission object'); 139 | } 140 | 141 | return method.call(permission); 142 | }; 143 | 144 | /** 145 | * @override 146 | */ 147 | exports.dispose = function () { 148 | this.$super(arguments); 149 | 150 | u.each( 151 | this.dataPool, 152 | function (data) { 153 | data.dispose(); 154 | } 155 | ); 156 | this.dataPool = null; 157 | }; 158 | 159 | /** 160 | * 获取权限对象 161 | * 162 | * @method mvc.BaseModel#getPermission 163 | * @return {Object} 权限对象,其中不同权限对应不同方法,由实际需要的模块定义接口 164 | */ 165 | 166 | /** 167 | * 设置权限对象 168 | * 169 | * @method mvc.BaseModel#setPermission 170 | * @param {Object} permission 权限对象,其中不同权限对应不同方法,由实际需要的模块定义接口 171 | */ 172 | eoo.defineAccessor(exports, 'permission'); 173 | 174 | var UIModel = require('ef/UIModel'); 175 | var BaseModel = eoo.create(UIModel, exports); 176 | 177 | return BaseModel; 178 | } 179 | ); 180 | -------------------------------------------------------------------------------- /src/mvc/BaseView.js: -------------------------------------------------------------------------------- 1 | /** 2 | * UB RIA Base 3 | * Copyright 2013 Baidu Inc. All rights reserved. 4 | * 5 | * @file 视图基类 6 | * @author otakustay 7 | */ 8 | define( 9 | function (require) { 10 | var u = require('../util'); 11 | var Promise = require('promise'); 12 | 13 | /** 14 | * 视图基类 15 | * 16 | * @class mvc.BaseView 17 | * @extends ef.UIView 18 | */ 19 | var exports = {}; 20 | 21 | /** 22 | * 添加控件事件的配置 23 | * 24 | * @protected 25 | * @method mvc.BaseView#addUIEvents 26 | * @param {Object} uiEvents 控件绑定的事件 27 | */ 28 | exports.addUIEvents = function (uiEvents) { 29 | // 对传入的控件事件参数进行格式变换 30 | var extendedUIEvents = [uiEvents]; 31 | u.each( 32 | uiEvents, 33 | function (events, key) { 34 | // `uiEvents`对象支持两种方式的事件绑定 35 | // 36 | // { 'controlId:eventType': functionName } 37 | // { 'controlId:eventType': [functionNameA, functionNameB] } 38 | // 39 | // 绑定的事件函数是数组类型时,组装为多个`uiEvents`对象形式传入 40 | if (u.isArray(events)) { 41 | while (events.length > 1) { 42 | // 从`events`中拆出来的新`uiEvents` 43 | var newUIEvents = {}; 44 | newUIEvents[key] = events.splice(-1)[0]; 45 | // 插入到`extendedUIEvents`中 46 | extendedUIEvents.push(newUIEvents); 47 | } 48 | 49 | // 当数组只剩一个元素时,修正为具体的元素类型 50 | if (events.length) { 51 | uiEvents[key] = events.splice(-1)[0]; 52 | } 53 | } 54 | } 55 | ); 56 | 57 | var thisEvents = this.uiEvents; 58 | // `this.uiEvents`可能会以`null`/`Object`/`Array`三种类型出现 59 | // 这边统一为数组类型 60 | this.uiEvents = (thisEvents && [].concat(thisEvents)) || []; 61 | // 将`extendedUIEvents`拼接到`this.uiEvents`后面 62 | this.uiEvents = this.uiEvents.concat(extendedUIEvents); 63 | }; 64 | 65 | /** 66 | * 获取控件事件配置的数组形式 67 | * 68 | * @private 69 | * @method mvc.BaseView#getUIEventsCollection 70 | * @return {Array} 控件事件 71 | */ 72 | exports.getUIEventsCollection = function () { 73 | var events = this.uiEvents; 74 | 75 | // 重写父类实现 76 | // 将`this.uiEvents`包装为数组返回 77 | return (events && [].concat(events)) || []; 78 | }; 79 | 80 | /** 81 | * @override 82 | */ 83 | exports.bindEvents = function () { 84 | // 扩展后`uiEvents`可以是个数组,每一项和以前的`uiEvents`格式是一样的,一一注册就行。 85 | // 两层`each`,第一层分解数组,第二层和基类的`bindEvents`一样就是绑事件 86 | u.each( 87 | this.getUIEventsCollection(), 88 | function (events) { 89 | u.each( 90 | events, 91 | function (handler, key) { 92 | this.bindUIEvent(key, handler); 93 | }, 94 | this 95 | ); 96 | }, 97 | this 98 | ); 99 | }; 100 | 101 | /** 102 | * 添加控件的额外属性 103 | * 104 | * @protected 105 | * @method mvc.BaseView#addUIProperties 106 | * @param {Object} newUIProperties 控件的额外属性 107 | */ 108 | exports.addUIProperties = function (newUIProperties) { 109 | // `this.uiProperties`可能以`null`/`Object`两种类型出现 110 | // 统一为对象类型 111 | this.uiProperties = this.uiProperties || {}; 112 | 113 | var uiProperties = this.uiProperties; 114 | u.each( 115 | newUIProperties, 116 | function (properties, controlId) { 117 | // 子类设置的控件属性在父类设置中已有同名 118 | if (uiProperties.hasOwnProperty(controlId)) { 119 | // 新增属性,则扩展控件设置 120 | // 已有属性,则重写对应属性值 121 | u.extend(uiProperties[controlId], properties); 122 | } 123 | // 子类设置新控件的属性 124 | else { 125 | uiProperties[controlId] = properties; 126 | } 127 | } 128 | ); 129 | }; 130 | 131 | /** 132 | * @override 133 | */ 134 | exports.getUIProperties = function () { 135 | // 重写父类实现 136 | // 获取 直接重写`uiProperties` 及 调用`addUIProperties`接口 设置的控件额外属性 137 | return this.uiProperties || {}; 138 | }; 139 | 140 | /** 141 | * 获取对应模板名称 142 | * 143 | * 当一个视图被作为子Action使用时,需要在其视图模板名后加上`"Main"`以进行区分, 144 | * 根据此设计,可以将视图切分为“完整页面”和“仅用于嵌套”两部分,根据约定命名 145 | * 146 | * @protected 147 | * @method mvc.BaseView#getTemplateName 148 | * @return {string} 149 | * @override 150 | */ 151 | exports.getTemplateName = function () { 152 | var templateName = this.$super(arguments); 153 | 154 | // 作为子Action嵌入页面时,模板使用`xxxMain`这个target 155 | if (this.model && this.model.get('isChildAction') && !this.model.get('isInDrawerPanel')) { 156 | templateName += 'Main'; 157 | } 158 | 159 | return templateName; 160 | }; 161 | 162 | /** 163 | * 等待用户的选择 164 | * 165 | * 参数同`ef.UIView.prototype.confirm`,但返回一个`Promise`对象 166 | * 167 | * @method mvc.BaseView#waitDecision 168 | * @return {Promise} 一个`Promise`对象,进入`resolved`状态时提供用户选择的按钮名称,默认有`"ok"`和`"cancel"`可选 169 | */ 170 | exports.waitDecision = function () { 171 | var dialog = this.confirm.apply(this, arguments); 172 | 173 | var executor = function (resolve, reject) { 174 | dialog.on('ok', u.partial(resolve, 'ok')); 175 | dialog.on('cancel', u.partial(resolve, 'cancel')); 176 | }; 177 | return new Promise(executor); 178 | }; 179 | 180 | /** 181 | * 等待用户确认 182 | * 183 | * 参数同`ef.UIView.prototype.confirm`,但返回一个`Promise`对象 184 | * 185 | * 当用户选择“确认”后,`Promise`对象进行`resolved`状态,用户选择取消则没有任何效果 186 | * 187 | * 如果需要知道用户选择“取消”,则应当使用{@link mvc.BaseView#waitDecision|waitDecision方法} 188 | * 189 | * @method mvc.BaseView#waitConfirm 190 | * @return {Promise} 一个`Promise`对象,用户确认则进入`resolved`状态,用户取消则进入`rejected`状态 191 | */ 192 | exports.waitConfirm = function () { 193 | var waiting = this.waitDecision.apply(this, arguments); 194 | var executor = function (resolve) { 195 | var receiveOK = function (result) { 196 | if (result === 'ok') { 197 | resolve(); 198 | } 199 | }; 200 | waiting.then(receiveOK); 201 | }; 202 | return new Promise(executor); 203 | }; 204 | 205 | /** 206 | * 等待一个`DialogAction`加载完成 207 | * 208 | * @method mvc.BaseView#waitActionDialog 209 | * @return {Promise} 一个`Promise`对象,对应的Action加载完成时进入`resolved`状态,如Action加载失败则进入`rejected`状态 210 | */ 211 | exports.waitActionDialog = function () { 212 | var dialog = this.popActionDialog.apply(this, arguments); 213 | 214 | var executor = function (resolve, reject) { 215 | dialog.on('actionloaded', resolve); 216 | dialog.on('actionloadfail', reject); 217 | dialog.on('actionloadabort', reject); 218 | }; 219 | return new Promise(executor); 220 | }; 221 | 222 | /** 223 | * 获取规则值 224 | * 225 | * @protected 226 | * @method mvc.BaseView#getRuleValue 227 | * @param {string} path 相对规则`rule`对象的路径 228 | * @return {*} 规则对应的值 229 | */ 230 | exports.getRuleValue = function (path) { 231 | path = path.split('.'); 232 | 233 | var value = this.model.get('rule') || this.getRule(); 234 | for (var i = 0; i < path.length; i++) { 235 | value = value[path[i]]; 236 | } 237 | 238 | return value; 239 | 240 | }; 241 | 242 | /** 243 | * @override 244 | */ 245 | exports.replaceValue = function (value) { 246 | if (typeof value !== 'string') { 247 | return value; 248 | } 249 | 250 | if (value.indexOf('@rule.') === 0) { 251 | return this.getRuleValue(value.substring(6)); 252 | } 253 | 254 | return this.$super(arguments); 255 | }; 256 | 257 | /** 258 | * @override 259 | */ 260 | exports.getTemplateData = function () { 261 | var templateData = this.$super(arguments); 262 | var getProperty = templateData.get; 263 | var model = this.model; 264 | var view = this; 265 | 266 | templateData.get = function (path) { 267 | // 访问`rule`的会做一次拦截,但如果`model`中正好也有`rule`,以`model`的优先 268 | if (path.indexOf('rule.') === 0) { 269 | return view.getRuleValue(path.substring(5)); 270 | } 271 | 272 | // 以`?`结尾的是权限判断,如`${canModify?}` 273 | if (path.charAt(path.length - 1) === '?') { 274 | var permissionName = path.slice(0, -1); 275 | return model.checkPermission(permissionName); 276 | } 277 | 278 | return getProperty(path); 279 | }; 280 | 281 | return templateData; 282 | }; 283 | 284 | /** 285 | * 通过`DrawerActionPanel`控件加载指定的Action 286 | * 287 | * @protected 288 | * @method mvc.BaseView#popDrawerAction 289 | * @param {Object} options 控件配置项,参考`DrawerActionPanel`控件的说明 290 | * @return {ub-ria-ui.DrawerActionPanel} 291 | */ 292 | exports.popDrawerAction = function (options) { 293 | options.id = options.id || 'drawer-action'; 294 | var drawerActionPanel = this.get(options.id); 295 | 296 | if (!drawerActionPanel) { 297 | drawerActionPanel = this.create('DrawerActionPanel', options); 298 | drawerActionPanel.render(); 299 | } 300 | else { 301 | drawerActionPanel.setProperties(options); 302 | } 303 | return drawerActionPanel; 304 | }; 305 | 306 | var oo = require('eoo'); 307 | 308 | /** 309 | * 获取对应的规则对象 310 | * 311 | * @method mvc.BaseView#getRule 312 | * @return {Object} 313 | */ 314 | 315 | /** 316 | * 设置对应的规则对象 317 | * 318 | * @method mvc.BaseView#setRule 319 | * @param {Object} rule 对应的规则对象 320 | */ 321 | oo.defineAccessor(exports, 'rule'); 322 | 323 | var UIView = require('ef/UIView'); 324 | var BaseView = oo.create(UIView, exports); 325 | return BaseView; 326 | } 327 | ); 328 | -------------------------------------------------------------------------------- /src/mvc/DetailAction.js: -------------------------------------------------------------------------------- 1 | /** 2 | * UB RIA Base 3 | * Copyright 2014 Baidu Inc. All rights reserved. 4 | * 5 | * @file 详情页Action基类 6 | * @author otakustay 7 | */ 8 | define( 9 | function (require) { 10 | var u = require('../util'); 11 | 12 | /** 13 | * @class mvc.DetailAction 14 | * @extends mvc.BaseAction 15 | */ 16 | var exports = {}; 17 | 18 | /** 19 | * 当前页面的分类,始终为`"detail"` 20 | * 21 | * @type {string} 22 | * @readonly 23 | * @override 24 | */ 25 | exports.category = 'detail'; 26 | 27 | /** 28 | * 获取指定的跳转URL 29 | * 30 | * @param {Object} args 新的请求对象 31 | * @return {string} 32 | */ 33 | function getURLForQuery(args) { 34 | var url = this.context.url; 35 | var path = url.getPath(); 36 | 37 | args = u.purify(args); 38 | 39 | return require('er/URL').withQuery(path, args).toString(); 40 | } 41 | 42 | /** 43 | * 根据请求重新跳转 44 | * 45 | * @protected 46 | * @method mvc.DetailAction#reloadWithQueryUpdate 47 | * @param {Object} args 新的请求参数对象 48 | */ 49 | exports.reloadWithQueryUpdate = function (args) { 50 | var url = getURLForQuery.call(this, args); 51 | this.redirect(url, {force: true}); 52 | }; 53 | 54 | /** 55 | * 列表搜索 56 | * 57 | * @param {mini-event.Event} e 事件对象 58 | * @param {boolean} withPage 列表是否用自己的page 59 | */ 60 | function refreshList(e, withPage) { 61 | // 防止子Action自己跳转 62 | e.preventDefault(); 63 | var args = { 64 | id: this.model.get('id') 65 | }; 66 | 67 | var query = this.view.getListQuery(); 68 | 69 | // 当为切换页数的操作,query能自己拿到正确的页数。 70 | // 否则回到第一页。 71 | if (!withPage) { 72 | query.page = 1; 73 | } 74 | 75 | // 所有列表参数加上`list.`前缀 76 | u.each( 77 | query, 78 | function (value, key) { 79 | args['list.' + key] = value; 80 | } 81 | ); 82 | this.reloadWithQueryUpdate(args); 83 | } 84 | 85 | /** 86 | * 切换页数引起的search 87 | * 88 | * @event 89 | * @param {mini-event.Event} e 事件对象 90 | */ 91 | function changePage(e) { 92 | refreshList.call(this, e, true); 93 | } 94 | 95 | /** 96 | * 初始化交互行为 97 | * 98 | * @override 99 | */ 100 | exports.initBehavior = function () { 101 | this.$super(arguments); 102 | this.view.on('listrefresh', refreshList, this); 103 | this.view.on('pagechange', changePage, this); 104 | }; 105 | 106 | 107 | var BaseAction = require('./BaseAction'); 108 | var DetailAction = require('eoo').create(BaseAction, exports); 109 | return DetailAction; 110 | } 111 | ); 112 | -------------------------------------------------------------------------------- /src/mvc/DetailModel.js: -------------------------------------------------------------------------------- 1 | /** 2 | * UB RIA Base 3 | * Copyright 2014 Baidu Inc. All rights reserved. 4 | * 5 | * @file 详情页Model基类 6 | * @author otakustay 7 | */ 8 | define( 9 | function (require) { 10 | var u = require('../util'); 11 | 12 | /** 13 | * @class mvc.DetailModel 14 | * @extends mvc.SingleEntityModel 15 | */ 16 | var exports = {}; 17 | 18 | /** 19 | * 获取列表子Action的URL 20 | * 21 | * @protected 22 | * @method mvc.DetailModel#getListActionURL 23 | * @return {string} 24 | */ 25 | exports.getListActionURL = function () { 26 | var query = this.get('url').getQuery(); 27 | 28 | // 所有列表参数都拥有`list.`前缀 29 | var args = {}; 30 | u.each( 31 | query, 32 | function (value, key) { 33 | if (key.indexOf('list.') === 0) { 34 | args[key.substring(5)] = value; 35 | } 36 | } 37 | ); 38 | 39 | if (query.id) { 40 | args[this.entityName + 'Id'] = query.id; 41 | } 42 | 43 | // 扩展加载列表时的额外参数 44 | u.extend(args, this.getListExtraArgs()); 45 | 46 | var actionURL = require('er/URL').withQuery('/' + this.getListActionName() + '/list', args); 47 | return actionURL + ''; 48 | }; 49 | 50 | /** 51 | * 获取列表子Action的实体名称 52 | * 53 | * @protected 54 | * @method mvc.DetailModel#getListActionName 55 | * @return {string} 56 | */ 57 | exports.getListActionName = function () { 58 | return this.entityName; 59 | }; 60 | 61 | /** 62 | * 获取列表子Action的额外参数 63 | * 64 | * @protected 65 | * @method mvc.DetailModel#getListExtraArgs 66 | * @return {Object} 67 | */ 68 | exports.getListExtraArgs = function () { 69 | return {}; 70 | }; 71 | 72 | /** 73 | * 获取当前详情页对应树节点的的实体名称 74 | * 75 | * 默认使用`entityName`,但并不一定会相同,通过重写此方法覆盖 76 | * 77 | * @protected 78 | * @method mvc.DetailModel#getTreeNodeEntityName 79 | * @return {string} 80 | */ 81 | exports.getTreeNodeEntityName = function () { 82 | return this.entityName; 83 | }; 84 | 85 | /** 86 | * 设置列表子Action的URL 87 | */ 88 | function setListActionURL() { 89 | var url = this.getListActionURL(); 90 | this.set('listActionURL', url); 91 | } 92 | 93 | /** 94 | * @override 95 | */ 96 | exports.load = function () { 97 | var loading = this.$super(arguments); 98 | return loading.then(u.bind(setListActionURL, this)); 99 | }; 100 | 101 | var SingleEntityModel = require('./SingleEntityModel'); 102 | var DetailModel = require('eoo').create(SingleEntityModel, exports); 103 | 104 | return DetailModel; 105 | } 106 | ); 107 | -------------------------------------------------------------------------------- /src/mvc/DetailView.js: -------------------------------------------------------------------------------- 1 | /** 2 | * UB RIA Base 3 | * Copyright 2014 Baidu Inc. All rights reserved. 4 | * 5 | * @file 详情页视图基类 6 | * @author otakustay 7 | */ 8 | define( 9 | function (require) { 10 | require('../ui/DrawerActionPanel'); 11 | 12 | /** 13 | * @class mvc.DetailView 14 | * @extends mvc.BaseView 15 | */ 16 | var exports = {}; 17 | 18 | exports.constructor = function () { 19 | this.$super(arguments); 20 | 21 | var uiEvents = { 22 | 'create:click': popDrawerActionPanel, 23 | 'modify:click': popDrawerActionPanel 24 | }; 25 | this.addUIEvents(uiEvents); 26 | }; 27 | 28 | /** 29 | * 弹出drawerActionPanel 30 | * 31 | * @event 32 | * @param {mini-event.Event} e 事件参数 33 | */ 34 | function popDrawerActionPanel(e) { 35 | e.preventDefault(); 36 | var url = e.target.get('href') + ''; 37 | 38 | // 传给 ActionPanel 的 url 是不能带 hash 符号的。。 39 | if (url.charAt(0) === '#') { 40 | url = url.slice(1); 41 | } 42 | this.popDrawerAction({url: url}).show(); 43 | } 44 | 45 | /** 46 | * @override 47 | */ 48 | exports.popDrawerAction = function (options) { 49 | var drawerActionPanel = this.$super(arguments); 50 | 51 | drawerActionPanel.on('action@submitcancel', cancel); 52 | drawerActionPanel.on('action@back', back); 53 | 54 | return drawerActionPanel; 55 | }; 56 | 57 | /** 58 | * 取消 59 | * 60 | * @event 61 | * @param {mini-event.Event} e 事件参数 62 | */ 63 | function cancel(e) { 64 | e.preventDefault(); 65 | this.dispose(); 66 | } 67 | 68 | /** 69 | * 返回 70 | * 71 | * @event 72 | * @param {mini-event.Event} e 事件参数 73 | */ 74 | function back(e) { 75 | e.stopPropagation(); 76 | e.preventDefault(); 77 | this.hide(); 78 | } 79 | 80 | /** 81 | * 绑定控件事件 82 | * 83 | * @override 84 | */ 85 | exports.bindEvents = function () { 86 | var delegate = require('mini-event').delegate; 87 | 88 | // 子Action提交查询请求转发出去 89 | var listActionPanel = this.getSafely('detail-list'); 90 | delegate( 91 | listActionPanel, 'action@search', 92 | this, 'listrefresh', 93 | {preserveData: true, syncState: true} 94 | ); 95 | // 其它操作都需要把页码置为1 96 | delegate( 97 | listActionPanel, 'action@pagechange', 98 | this, 'pagechange', 99 | {preserveData: true, syncState: true} 100 | ); 101 | delegate( 102 | listActionPanel, 'action@statusupdate', 103 | this, 'listrefresh', 104 | {preserveData: true, syncState: true} 105 | ); 106 | delegate( 107 | listActionPanel, 'action@pagesizechange', 108 | this, 'listrefresh', 109 | {preserveData: true, syncState: true} 110 | ); 111 | delegate( 112 | listActionPanel, 'action@tablesort', 113 | this, 'listrefresh', 114 | {preserveData: true, syncState: true} 115 | ); 116 | this.$super(arguments); 117 | }; 118 | 119 | /** 120 | * 获取列表子Action的查询条件 121 | * 122 | * @public 123 | * @method mvc.DetailView#getListQuery 124 | * @return {Object} 查询条件 125 | */ 126 | exports.getListQuery = function () { 127 | var listAction = this.getSafely('detail-list').get('action'); 128 | if (listAction) { 129 | return listAction.getSearchQuery(); 130 | } 131 | 132 | return {}; 133 | }; 134 | 135 | var BaseView = require('./BaseView'); 136 | var DetailView = require('eoo').create(BaseView, exports); 137 | 138 | return DetailView; 139 | } 140 | ); 141 | -------------------------------------------------------------------------------- /src/mvc/FormAction.js: -------------------------------------------------------------------------------- 1 | /** 2 | * UB RIA Base 3 | * Copyright 2013 Baidu Inc. All rights reserved. 4 | * 5 | * @file 表单Action基类 6 | * @author otakustay 7 | */ 8 | define( 9 | function (require) { 10 | var u = require('../util'); 11 | var Promise = require('promise'); 12 | 13 | /** 14 | * 表单Action基类 15 | * 16 | * @class mvc.FormAction 17 | * @extends mvc.BaseAction 18 | */ 19 | var exports = {}; 20 | 21 | /** 22 | * 当前页面的分类,始终为`"form"` 23 | * 24 | * @member mvc.FormAction#category 25 | * @type {string} 26 | * @readonly 27 | * @override 28 | */ 29 | exports.category = 'form'; 30 | 31 | /** 32 | * 处理提交数据时发生的错误,默认无行为,如验证信息显示等需要实现此方法 33 | * 34 | * @protected 35 | * @method mvc.FormAction#handleSubmitError 36 | * @param {er.meta.FakeXHR | meta.FieldError[]} errors `XMLHttpRequest`对象,或者model校验的错误结果集 37 | * @return {boolean} 返回`true`表示错误已经处理完毕 38 | */ 39 | exports.handleSubmitError = function (errors) { 40 | // 处理409的验证失败 41 | if (errors.status === 409) { 42 | errors = require('er/util').parseJSON(errors.responseText); 43 | } 44 | // 处理全局错误 45 | if (errors.message) { 46 | this.view.notifyGlobalError(errors.message); 47 | } 48 | // 处理model校验产生的错误信息,或者后端校验返回的错误信息 49 | if (errors.fields) { 50 | this.view.notifyErrors(errors); 51 | } 52 | 53 | return errors.message || errors.fields; 54 | }; 55 | 56 | /** 57 | * 处理提交数据成功后的返回,流程如下: 58 | * 59 | * - 触发`entitysave` 60 | * - 若`entitysave`未被阻止,调用`submitHanlder` 61 | * - 触发`handlefinish` 62 | * 63 | * @protected 64 | * @method mvc.FormAction#handleSubmitResult 65 | * @param {Object} entity 提交成功后返回的实体 66 | * @fires mvc.FormAction#handlefinish 67 | */ 68 | exports.handleSubmitResult = function (entity) { 69 | var entitySaveEvent = this.fire('entitysave', {entity: entity}); 70 | if (!entitySaveEvent.isDefaultPrevented()) { 71 | var submitHandler = this.getSubmitHandler(); 72 | if (submitHandler) { 73 | submitHandler.handle(entity, this); 74 | } 75 | } 76 | this.fire('handlefinish'); 77 | }; 78 | 79 | /** 80 | * 获取处理组件 81 | * 82 | * @protected 83 | * @method mvc.FormAction#getSubmitHandler 84 | * @return {mvc.handler.SubmitHandler} 85 | */ 86 | exports.getSubmitHandler = function () { 87 | return this.submitHandler; 88 | }; 89 | 90 | /** 91 | * 设置处理组件 92 | * 可选。默认值为空。 93 | * 94 | * @protected 95 | * @method mvc.FormAction#setSubmitHandler 96 | * @param {mvc.handler.SubmitHandler} handler 提交成功处理组件 97 | */ 98 | exports.setSubmitHandler = function (handler) { 99 | this.submitHandler = handler; 100 | }; 101 | 102 | /** 103 | * 处理提交错误 104 | * 105 | * @param {er.meta.FakeXHR | meta.FieldError[]} errors `XMLHttpRequest`对象,或者model校验的错误信息集 106 | */ 107 | function handleError(errors) { 108 | var handled = this.handleSubmitError(errors); 109 | if (!handled) { 110 | require('er/events').notifyError(errors.responseText); 111 | } 112 | } 113 | 114 | /** 115 | * 根据FormType获取Model提交接口的方法名 116 | * 117 | * @protected 118 | * @method mvc.FormAction#getSubmitMethod 119 | * @param {string} formType 表单类型 120 | * @return {string} 121 | */ 122 | exports.getSubmitMethod = function (formType) { 123 | var methodMap = { 124 | create: 'save', 125 | update: 'update', 126 | copy: 'save' 127 | }; 128 | 129 | return methodMap[formType] || null; 130 | }; 131 | 132 | /** 133 | * 提交实体(新建或更新) 134 | * 135 | * @protected 136 | * @method mvc.FormAction#submitEntity 137 | * @param {Object} entity 实体数据 138 | * @return {Promise} 139 | */ 140 | exports.submitEntity = function (entity) { 141 | var method = this.getSubmitMethod(this.context.formType); 142 | 143 | try { 144 | if (method) { 145 | return this.model[method](entity) 146 | .then(u.bind(this.handleSubmitResult, this)) 147 | .fail(u.bind(handleError, this)); 148 | } 149 | 150 | throw new Error('Cannot find formType in methodMap'); 151 | 152 | } 153 | catch (ex) { 154 | return Promise.reject(ex); 155 | } 156 | }; 157 | 158 | /** 159 | * 设置取消编辑时的提示信息标题 160 | * 161 | * @protected 162 | * @member mvc.FormAction#cancelConfirmTitle 163 | * @type {string} 164 | */ 165 | exports.cancelConfirmTitle = '确认取消编辑'; 166 | 167 | /** 168 | * 获取取消编辑时的提示信息标题 169 | * 170 | * @protected 171 | * @method mvc.FormAction#getCancelConfirmTitle 172 | * @return {string} 173 | */ 174 | exports.getCancelConfirmTitle = function () { 175 | var formType = this.model.get('formType'); 176 | if (formType === 'create') { 177 | return '新建'; 178 | } 179 | 180 | return '编辑'; 181 | }; 182 | 183 | /** 184 | * 设置取消编辑时的提示信息内容 185 | * 186 | * @protected 187 | * @member mvc.FormAction#cancelConfirmMessage 188 | * @type {string} 189 | */ 190 | exports.cancelConfirmMessage = '取消编辑将不保留已经填写的数据,确定继续吗?'; 191 | 192 | /** 193 | * 获取取消编辑时的提示信息内容 194 | * 195 | * @protected 196 | * @method mvc.FormAction#getCancelConfirmMessage 197 | * @return {string} 198 | */ 199 | exports.getCancelConfirmMessage = function () { 200 | return this.cancelConfirmMessage; 201 | }; 202 | 203 | function cancel() { 204 | var submitCancelEvent = this.fire('submitcancel'); 205 | var handleFinishEvent = this.fire('handlefinish'); 206 | if (!submitCancelEvent.isDefaultPrevented() 207 | && !handleFinishEvent.isDefaultPrevented() 208 | ) { 209 | this.redirectAfterCancel(); 210 | } 211 | } 212 | 213 | /** 214 | * 取消编辑 215 | * 216 | * @protected 217 | * @method mvc.FormAction#cancelEdit 218 | */ 219 | exports.cancelEdit = function () { 220 | // 从model中拿出表单最初数据,判断是否被更改 221 | var initialFormData = this.model.get('initialFormData'); 222 | 223 | if (this.isFormDataChanged(initialFormData)) { 224 | var options = { 225 | title: this.getCancelConfirmTitle(), 226 | content: this.getCancelConfirmMessage() 227 | }; 228 | this.view.waitCancelConfirm(options) 229 | .then(u.bind(cancel, this)); 230 | } 231 | else { 232 | cancel.call(this); 233 | } 234 | }; 235 | 236 | /** 237 | * 在取消编辑后重定向 238 | * 239 | * @protected 240 | * @method mvc.FormAction#redirectAfterCancel 241 | */ 242 | exports.redirectAfterCancel = function () { 243 | // 默认返回列表页 244 | this.back('/' + this.getEntityName() + '/list'); 245 | }; 246 | 247 | /** 248 | * 判断表单信息是否被更改,默认返回false 249 | * 250 | * @protected 251 | * @method mvc.FormAction#isFormDataChanged 252 | * @param {Object} initialFormData 进入页面时的表单初始数据 253 | * @return {boolean} 254 | */ 255 | exports.isFormDataChanged = function (initialFormData) { 256 | return true; 257 | }; 258 | 259 | /** 260 | * 设置修改提交时的提示信息内容 261 | * 262 | * @protected 263 | * @member mvc.FormAction#submitConfirmMessage 264 | * @type {string} 265 | */ 266 | exports.submitConfirmMessage = '确认提交修改?'; 267 | 268 | /** 269 | * 获取修改提交时的提示信息内容 270 | * 271 | * @protected 272 | * @method mvc.FormAction#getUpdateConfirmMessage 273 | * @return {string} 274 | */ 275 | exports.getSubmitConfirmMessage = function () { 276 | return this.submitConfirmMessage; 277 | }; 278 | 279 | function submit() { 280 | this.view.clearGlobalError(); 281 | var entity = this.view.getEntity(); 282 | 283 | var options = { 284 | content: this.getSubmitConfirmMessage() 285 | }; 286 | 287 | this.view.waitSubmitConfirm(options) 288 | .then(u.bind(this.view.disableSubmit, this.view)) 289 | .then(u.bind(this.submitEntity, this, entity)) 290 | .ensure(u.bind(this.view.enableSubmit, this.view)); 291 | } 292 | 293 | /** 294 | * @override 295 | */ 296 | exports.initBehavior = function () { 297 | this.$super(arguments); 298 | 299 | // 保存一份最初的form表单内容到model,用于判断表单内容是否被更改 300 | var initialFormData = this.view.getFormData(); 301 | this.model.set('initialFormData', initialFormData, {silent: true}); 302 | 303 | this.view.on('submit', submit, this); 304 | this.view.on('cancel', this.cancelEdit, this); 305 | }; 306 | 307 | /** 308 | * 判断表单是否作为其它表单的子表单存在 309 | * 310 | * @method mvc.FormAction#isChildForm 311 | * @return {boolean} 312 | */ 313 | exports.isChildForm = function () { 314 | // 如果表单进入时带来returnUrl参数,则认为该表单为一个ChildForm 315 | return !!this.model.get('returnUrl'); 316 | }; 317 | 318 | var BaseAction = require('./BaseAction'); 319 | var FormAction = require('eoo').create(BaseAction, exports); 320 | 321 | return FormAction; 322 | } 323 | ); 324 | -------------------------------------------------------------------------------- /src/mvc/FormModel.js: -------------------------------------------------------------------------------- 1 | /** 2 | * UB RIA Base 3 | * Copyright 2013 Baidu Inc. All rights reserved. 4 | * 5 | * @file 表单数据模型基类 6 | * @author otakustay 7 | */ 8 | define( 9 | function (require) { 10 | var Promise = require('promise'); 11 | 12 | /** 13 | * 表单数据模型基类 14 | * 15 | * @class mvc.FormModel 16 | * @extends mvc.SingleEntityModel 17 | */ 18 | var exports = {}; 19 | 20 | /** 21 | * 检查实体数据完整性,可在此补充一些视图无法提供的属性 22 | * 23 | * @protected 24 | * @method mvc.FormModel#fillEntity 25 | * @param {Object} entity 实体数据 26 | * @return {Object} 补充完整的实体数据 27 | */ 28 | exports.fillEntity = function (entity) { 29 | return entity; 30 | }; 31 | 32 | /** 33 | * 设置当前对象关联的{@link mvc.EntityValidator}实例 34 | * 可选。 35 | * 参数类型 ub-ria.mvc.EntityValidator 36 | * 37 | * @method mvc.FormModel#setValidator 38 | * @param {mvc.EntityValidator} validator 关联的实例 39 | */ 40 | exports.setValidator = function (validator) { 41 | if (validator && !validator.getRule()) { 42 | validator.setRule(this.get('rule')); 43 | } 44 | this.validator = validator; 45 | }; 46 | 47 | /** 48 | * 获取当前对象关联的{@link mvc.EntityValidator}实例 49 | * 50 | * @method mvc.FormModel#getValidator 51 | * @return {mvc.EntityValidator} 52 | */ 53 | exports.getValidator = function () { 54 | return this.validator; 55 | }; 56 | 57 | /** 58 | * 校验实体 59 | * 60 | * @method mvc.FormModel#validateEntity 61 | * @param {Object} entity 需要校验的实体 62 | * @return {Object[]} 63 | */ 64 | exports.validateEntity = function (entity) { 65 | var validator = this.getValidator(); 66 | if (!validator) { 67 | throw new Error('No validator object attached to this Model'); 68 | } 69 | 70 | return validator.validate(entity); 71 | }; 72 | 73 | /** 74 | * 保存新建的实体 75 | * 76 | * @method mvc.FormModel#save 77 | * @param {Object} entity 新建的实体对象 78 | * @return {Promise} 79 | */ 80 | exports.save = function (entity) { 81 | entity = this.fillEntity(entity); 82 | 83 | var validationResult = this.validateEntity(entity); 84 | 85 | if (validationResult.length > 0) { 86 | return Promise.reject({fields: validationResult}); 87 | } 88 | 89 | return this.saveEntity(entity); 90 | }; 91 | 92 | /** 93 | * 更新已有的实体 94 | * 95 | * @method mvc.FormModel#update 96 | * @param {Object} entity 待更新的实体对象 97 | * @return {Promise} 98 | */ 99 | exports.update = function (entity) { 100 | entity = this.fillEntity(entity); 101 | 102 | // 更新默认加上id 103 | entity.id = this.get('id'); 104 | 105 | var validationResult = this.validateEntity(entity); 106 | 107 | if (validationResult.length > 0) { 108 | return Promise.reject({fields: validationResult}); 109 | } 110 | 111 | return this.updateEntity(entity); 112 | }; 113 | 114 | /** 115 | * 完成实体的保存操作 116 | * 117 | * @protected 118 | * @method mvc.FormModel#saveEntity 119 | * @param {Object} entity 已经补充完整并且验证通过的实体 120 | * @return {Promise} 121 | */ 122 | exports.saveEntity = function (entity) { 123 | var data = this.data(); 124 | if (!data) { 125 | throw new Error('No default data object attached to this Model'); 126 | } 127 | if (typeof data.save !== 'function') { 128 | throw new Error('No save method implemented on default data object'); 129 | } 130 | 131 | return data.save(entity); 132 | }; 133 | 134 | /** 135 | * 完成实体的更新操作 136 | * 137 | * @protected 138 | * @method mvc.FormModel#updateEntity 139 | * @param {Object} entity 已经补充完整并且验证通过的实体 140 | * @return {Promise} 141 | */ 142 | exports.updateEntity = function (entity) { 143 | var data = this.data(); 144 | if (!data) { 145 | throw new Error('No default data object attached to this Model'); 146 | } 147 | if (typeof data.update !== 'function') { 148 | throw new Error('No update method implemented on default data object'); 149 | } 150 | 151 | return data.update(entity); 152 | }; 153 | 154 | var SingleEntityModel = require('./SingleEntityModel'); 155 | var FormModel = require('eoo').create(SingleEntityModel, exports); 156 | 157 | return FormModel; 158 | } 159 | ); 160 | -------------------------------------------------------------------------------- /src/mvc/IoCActionFactory.js: -------------------------------------------------------------------------------- 1 | /** 2 | * UB RIA Base 3 | * Copyright 2014 Baidu Inc. All rights reserved. 4 | * 5 | * @file IoCAction工厂 6 | * @author shenbin(bobshenbin@gmail.com) 7 | */ 8 | define( 9 | function (require) { 10 | var u = require('../util'); 11 | var eoo = require('eoo'); 12 | 13 | /** 14 | * 使用IoC创建Action的工厂 15 | * 16 | * @class mvc.IoCActionFactory 17 | */ 18 | var exports = {}; 19 | 20 | /** 21 | * 默认构造函数 22 | * 23 | * @param {string} actionComponent action组件名 24 | * @param {Object} options 相关配置 25 | * @param {boolean} [options.noSchema] 模块没有`schema`信息,通常只有列表的模块会这样 26 | */ 27 | exports.constructor = function (actionComponent, options) { 28 | options = options || {}; 29 | this.actionComponent = actionComponent; 30 | this.noSchema = options.noSchema || false; 31 | }; 32 | 33 | /** 34 | * 创建一个Action实例 35 | * 36 | * @method mvc.IoCActionFactory#createRuntimeAction 37 | * @param {er.meta.ActionContext} actionContext Action的执行上下文 38 | * @return {Promise} 39 | */ 40 | exports.createRuntimeAction = function (actionContext) { 41 | var Promise = require('promise'); 42 | var ioc = this.getIocContainer(); 43 | return new Promise(u.bind(ioc.getComponent, ioc, this.actionComponent)) 44 | .then(u.bind(this.buildAction, this, actionContext)); 45 | }; 46 | 47 | /** 48 | * 获取视图名称 49 | * 50 | * @param {er.meta.ActionContext} actionContext Action的执行上下文 51 | * @return {string} 52 | */ 53 | function getViewName(actionContext) { 54 | var parts = u.compact(actionContext.url.getPath().split('/')); 55 | 56 | var pageType = parts[parts.length - 1]; 57 | if (pageType === 'create' || pageType === 'update') { 58 | parts[parts.length - 1] = 'form'; 59 | } 60 | 61 | return u.map(parts, u.dasherize).join('-'); 62 | } 63 | 64 | /** 65 | * 组装Action 66 | * 67 | * @protected 68 | * @method mvc.IoCActionFactory#buildAction 69 | * @param {er.meta.ActionContext} actionContext Action的执行上下文 70 | * @param {er.Action} action 待组装的`Action`实例 71 | * @return {er.Action} 72 | */ 73 | exports.buildAction = function (actionContext, action) { 74 | action.view.name = getViewName(actionContext); 75 | 76 | return action; 77 | }; 78 | 79 | eoo.defineAccessor(exports, 'iocContainer'); 80 | 81 | var IoCActionFactory = eoo.create(exports); 82 | 83 | return IoCActionFactory; 84 | } 85 | ); 86 | -------------------------------------------------------------------------------- /src/mvc/ListAction.js: -------------------------------------------------------------------------------- 1 | /** 2 | * UB RIA Base 3 | * Copyright 2013 Baidu Inc. All rights reserved. 4 | * 5 | * @file 列表Action基类 6 | * @author otakustay 7 | */ 8 | define( 9 | function (require) { 10 | var eoo = require('eoo'); 11 | var URL = require('er/URL'); 12 | var u = require('../util'); 13 | 14 | /** 15 | * 列表Action基类 16 | * 17 | * @class mvc.ListAction 18 | * @extends mvc.BaseAction 19 | */ 20 | var exports = {}; 21 | 22 | /** 23 | * 当前页面的分类,始终为`"list"` 24 | * 25 | * @member mvc.ListAction#category 26 | * @type {string} 27 | * @readonly 28 | * @override 29 | */ 30 | exports.category = 'list'; 31 | 32 | /** 33 | * @override 34 | */ 35 | exports.initBehavior = function () { 36 | this.$super(arguments); 37 | 38 | this.view.on('search', search, this); 39 | this.view.on('pagesizechange', updatePageSize, this); 40 | this.view.on('pagechange', updatePage, this); 41 | this.view.on('batchmodify', batchModifyStatus, this); 42 | this.view.on('tablesort', updateTableSort, this); 43 | this.view.on('modifystatus', modifyStatus, this); 44 | this.getLayoutChangeNotifier().on('layoutchanged', this.adjustLayout, this); 45 | }; 46 | 47 | /** 48 | * 查询的事件处理函数 49 | * 50 | * @event 51 | */ 52 | function search() { 53 | this.performSearch(); 54 | } 55 | 56 | /** 57 | * 进行查询 58 | * 59 | * @protected 60 | * @method mvc.ListAction#performSearch 61 | */ 62 | exports.performSearch = function () { 63 | var event = this.fire('search'); 64 | if (!event.isDefaultPrevented()) { 65 | var query = this.getSearchQuery(); 66 | query.page = 1; 67 | this.reloadWithQueryUpdate(query); 68 | } 69 | }; 70 | 71 | /** 72 | * 获取查询条件 73 | * 74 | * @protected 75 | * @method mvc.ListAction#getSearchQuery 76 | * @return {Object} 查询条件 77 | */ 78 | exports.getSearchQuery = function () { 79 | var query = this.view.getSearchArgs(); 80 | query.page = this.view.getPageIndex(); 81 | 82 | return query; 83 | }; 84 | 85 | /** 86 | * 页码更新后重新加载操作 87 | * 88 | * @protected 89 | * @method mvc.ListAction#reloadWithQueryUpdate 90 | * @param {Object} args 新的请求参数对象 91 | */ 92 | exports.reloadWithQueryUpdate = function (args) { 93 | var url = getURLForQuery.call(this, args); 94 | this.redirect(url, {force: true}); 95 | }; 96 | 97 | /** 98 | * 更新每页显示条数 99 | * 100 | * @event 101 | * @param {mini-event.Event} e 事件对象 102 | * @param {number} e.pageSize 每页显示条目数 103 | */ 104 | function updatePageSize(e) { 105 | // 先请求后端更新每页显示条数,然后直接刷新当前页 106 | this.model.updatePageSize(e.pageSize) 107 | .then(u.bind(afterPageSizeUpdate, this, e.pageSize)); 108 | } 109 | 110 | /** 111 | * 每页大小更新后重新加载操作 112 | */ 113 | function afterPageSizeUpdate() { 114 | var event = this.fire('pagesizechange'); 115 | if (!event.isDefaultPrevented()) { 116 | var query = this.getSearchQuery(); 117 | query.page = 1; 118 | this.reloadWithQueryUpdate(query); 119 | } 120 | } 121 | 122 | /** 123 | * 更新页码 124 | * 125 | * @event 126 | */ 127 | function updatePage() { 128 | var event = this.fire('pagechange'); 129 | if (!event.isDefaultPrevented()) { 130 | var query = this.getSearchQuery(); 131 | this.reloadWithQueryUpdate(query); 132 | } 133 | } 134 | 135 | /** 136 | * 批量修改事件处理 137 | * 138 | * @event 139 | * @param {mini-event.Event} e 事件对象 140 | */ 141 | function batchModifyStatus(e) { 142 | var items = this.view.getSelectedItems(); 143 | this.modifyStatus(items, e.status); 144 | } 145 | 146 | /** 147 | * 修改实体状态 148 | * 149 | * @protected 150 | * @method mvc.ListAction#modifyStatus 151 | * @param {Object[]} items 待修改状态的实体数组 152 | * @param {number} status 修改后实体的状态值 153 | */ 154 | exports.modifyStatus = function (items, status) { 155 | var ids = u.pluck(items, 'id'); 156 | var transitionItem = u.findWhere( 157 | this.model.getStatusTransitions(), 158 | {status: status} 159 | ); 160 | var context = { 161 | ids: ids, 162 | items: items, 163 | status: status, 164 | statusName: transitionItem.statusName, 165 | command: transitionItem.command, 166 | reload: transitionItem.reload 167 | }; 168 | 169 | if (this.requireAdviceFor(context)) { 170 | // 需要后端提示消息的,再额外加入用户确认的过程 171 | this.model.getAdvice(status, ids) 172 | .then(u.bind(waitConfirmForAdvice, this, context)) 173 | .then(u.bind(updateEntities, this, context)); 174 | } 175 | else { 176 | updateEntities.call(this, context); 177 | } 178 | }; 179 | 180 | /** 181 | * 检查指定操作是否需要后端提示消息,默认删除操作时要求提示用户确认 182 | * 183 | * @protected 184 | * @method mvc.ListAction#requireAdviceFor 185 | * @param {meta.UpdateContext} context 操作的上下文对象 186 | * @return {boolean} 返回`true`表示需要提示用户 187 | */ 188 | exports.requireAdviceFor = function (context) { 189 | return context.statusName === 'remove'; 190 | }; 191 | 192 | /** 193 | * 根据删除前确认 194 | * 195 | * @param {meta.UpdateContext} context 操作的上下文对象 196 | * @param {Object} advice 提示对象 197 | * @return {Promise} 198 | */ 199 | function waitConfirmForAdvice(context, advice) { 200 | return this.view.waitModifyStatusConfirm(context, advice); 201 | } 202 | 203 | /** 204 | * 更新实体状态 205 | * 206 | * @param {meta.UpdateContext} context 操作的上下文对象 207 | */ 208 | function updateEntities(context) { 209 | this.model[context.statusName](context.ids) 210 | .then(u.bind(updateListStatus, this, context)) 211 | .fail(u.bind(this.notifyModifyFail, this, context)); 212 | } 213 | 214 | /** 215 | * 根据删除、启用的状态更新当前Action,默认行为为直接刷新当前的Action 216 | * 217 | * @param {meta.UpdateContext} context 操作的上下文对象 218 | */ 219 | function updateListStatus(context) { 220 | this.notifyModifySuccess(context); 221 | 222 | var event = this.fire('statusupdate', context); 223 | if (context.reload === false) { 224 | this.updateItems(context); 225 | } 226 | else if (!event.isDefaultPrevented()) { 227 | this.reload(); 228 | } 229 | } 230 | 231 | /** 232 | * 通知修改状态操作成功 233 | * 234 | * @protected 235 | * @method mvc.ListAction#notifyModifySuccess 236 | * @param {meta.UpdateContext} context 批量操作的上下文对象 237 | */ 238 | exports.notifyModifySuccess = function (context) {}; 239 | 240 | /** 241 | * 通知修改状态操作失败 242 | * 243 | * 默认提示用户“无法[操作名]部分或全部[实体名]”或“无法[操作名]该[实体名]” 244 | * 245 | * @protected 246 | * @method mvc.ListAction#notifyModifyFail 247 | * @param {meta.UpdateContext} context 批量操作的上下文对象 248 | */ 249 | exports.notifyModifyFail = function (context) { 250 | var entityDescription = this.getEntityDescription(); 251 | if (context.ids.length > 1) { 252 | this.view.alert( 253 | '无法' + context.command + '部分或全部' + entityDescription, 254 | context.command + entityDescription 255 | ); 256 | } 257 | else { 258 | this.view.alert( 259 | '无法' + context.command + '该' + entityDescription, 260 | context.command + entityDescription 261 | ); 262 | } 263 | }; 264 | 265 | /** 266 | * 更新列表中的实体的状态 267 | * 268 | * @protected 269 | * @method mvc.ListAction#updateItems 270 | * @param {meta.UpdateContext} context 操作的上下文对象 271 | */ 272 | exports.updateItems = function (context) { 273 | var ids = context.ids; 274 | var targetStatus = context.status; 275 | var items = []; 276 | u.each( 277 | ids, 278 | function (id) { 279 | var item = this.model.getItemById(id); 280 | if (item) { 281 | item.status = targetStatus; 282 | items.push(item); 283 | } 284 | }, 285 | this 286 | ); 287 | this.view.updateItems(items); 288 | }; 289 | 290 | /** 291 | * 根据删除、启用的状态更新当前Action,默认行为为直接刷新当前的Action 292 | * 293 | * @event 294 | * @param {mini-event.Event} e 事件对象 295 | * @param {number} e.tableProperties 表格排序信息 296 | */ 297 | function updateTableSort(e) { 298 | var event = this.fire('tablesort'); 299 | if (!event.isDefaultPrevented()) { 300 | var query = this.getSearchQuery(); 301 | query.page = 1; 302 | this.reloadWithQueryUpdate(query); 303 | } 304 | } 305 | 306 | /** 307 | * 处理状态修改 308 | * 309 | * @param {Object} e 包含待修改实体的id,以及更新到哪个状态 310 | */ 311 | function modifyStatus(e) { 312 | var item = this.model.getItemById(e.id); 313 | this.modifyStatus([item], e.status); 314 | } 315 | 316 | /** 317 | * 进行查询引起的重定向操作 318 | * 319 | * @protected 320 | * @method mvc.ListAction#redirectForSearch 321 | * @param {Object} args 查询参数 322 | */ 323 | exports.redirectForSearch = function (args) { 324 | var path = this.model.get('url').getPath(); 325 | var url = URL.withQuery(path, args); 326 | this.redirect(url, {force: true}); 327 | }; 328 | 329 | /** 330 | * 获取指定排序的跳转URL 331 | * 332 | * @param {Object} args 新的请求对象 333 | * @return {string} 334 | */ 335 | function getURLForQuery(args) { 336 | var url = this.context.url; 337 | var path = url.getPath(); 338 | 339 | // 如果跟默认的参数相同,去掉默认字段 340 | var defaultArgs = this.model.getDefaultArgs(); 341 | args = u.purify(args, defaultArgs); 342 | 343 | return URL.withQuery(path, args).toString(); 344 | } 345 | 346 | /** 347 | * 根据布局变化重新调整自身布局 348 | * 349 | * @protected 350 | * @method mvc.ListAction#adjustLayout 351 | */ 352 | exports.adjustLayout = function () { 353 | this.view.adjustLayout(); 354 | }; 355 | 356 | /** 357 | * @override 358 | */ 359 | exports.leave = function () { 360 | this.getLayoutChangeNotifier().un('layoutchanged', this.adjustLayout, this); 361 | 362 | this.$super(arguments); 363 | }; 364 | 365 | /** 366 | * 获取table已经选择的列的数据 367 | * 368 | * @protected 369 | * @method mvc.ListAction#getSelectedItems 370 | * @return {Object[]} 当前table的已选择列对应的数据 371 | */ 372 | exports.getSelectItems = function () { 373 | return this.view.getSelectedItems(); 374 | }; 375 | 376 | eoo.defineAccessor(exports, 'layoutChangeNotifier'); 377 | 378 | var BaseAction = require('./BaseAction'); 379 | var ListAction = eoo.create(BaseAction, exports); 380 | 381 | return ListAction; 382 | } 383 | ); 384 | -------------------------------------------------------------------------------- /src/mvc/ReadAction.js: -------------------------------------------------------------------------------- 1 | /** 2 | * UB RIA Base 3 | * Copyright 2013 Baidu Inc. All rights reserved. 4 | * 5 | * @file 只读页Action基类 6 | * @author lixiang(lixiang05@baidu.com) 7 | */ 8 | define( 9 | function (require) { 10 | /** 11 | * 只读Action基类 12 | * 13 | * @class mvc.ReadAction 14 | * @extends mvc.BaseAction 15 | */ 16 | var exports = {}; 17 | 18 | /** 19 | * 当前页面的分类,始终为`"read"` 20 | * 21 | * @member mvc.ReadAction#category 22 | * @type {string} 23 | * @readonly 24 | * @override 25 | */ 26 | exports.category = 'read'; 27 | 28 | /** 29 | * 点击返回后的处理 30 | * 31 | * @protected 32 | * @method mvc.ReadAction#returnBack 33 | */ 34 | exports.returnBack = function () { 35 | // 默认返回列表页 36 | this.fire('back'); 37 | }; 38 | 39 | /** 40 | * @override 41 | */ 42 | exports.initBehavior = function () { 43 | this.view.on('return', this.returnBack, this); 44 | }; 45 | 46 | var BaseAction = require('./BaseAction'); 47 | var ReadAction = require('eoo').create(BaseAction, exports); 48 | 49 | return ReadAction; 50 | } 51 | ); 52 | -------------------------------------------------------------------------------- /src/mvc/ReadModel.js: -------------------------------------------------------------------------------- 1 | /** 2 | * UB RIA Base 3 | * Copyright 2013 Baidu Inc. All rights reserved. 4 | * 5 | * @file 只读页数据模型基类 6 | * @author otakustay 7 | */ 8 | define( 9 | function (require) { 10 | /** 11 | * 只读页数据模型基类 12 | * 13 | * @class mvc.ReadModel 14 | * @extends mvc.SingleEntityModel 15 | */ 16 | var exports = {}; 17 | 18 | /** 19 | * 字段无值时的默认显示文本,默认为`"--"` 20 | * 21 | * @member mvc.ReadModel#defaultDisplayText 22 | * @type {string} 23 | */ 24 | exports.defaultDisplayText = '--'; 25 | 26 | // 全局所有Model都可能有的属性名,这些属性不需要被自动转为`'--'` 27 | var GLOBAL_MODEL_PROPERTIES = { 28 | url: true, 29 | referrer: true, 30 | isChildAction: true, 31 | container: true, 32 | entity: true 33 | }; 34 | 35 | /** 36 | * 获取属性值 37 | * 38 | * @param {string} name 属性名称 39 | * @return {*} 对应属性的值,如果不存在属性则返回{@link ReadModel#defaultDisplayText} 40 | * @override 41 | */ 42 | exports.get = function (name) { 43 | var value = this.$super(arguments); 44 | 45 | if (GLOBAL_MODEL_PROPERTIES.hasOwnProperty(name)) { 46 | return value; 47 | } 48 | 49 | return this.hasReadableValue(name) ? value : this.defaultDisplayText; 50 | }; 51 | 52 | var SingleEntityModel = require('./SingleEntityModel'); 53 | var ReadModel = require('eoo').create(SingleEntityModel, exports); 54 | 55 | return ReadModel; 56 | } 57 | ); 58 | -------------------------------------------------------------------------------- /src/mvc/ReadView.js: -------------------------------------------------------------------------------- 1 | /** 2 | * UB RIA Base 3 | * Copyright 2013 Baidu Inc. All rights reserved. 4 | * 5 | * @file 只读页视图基类 6 | * @author otakustay 7 | */ 8 | define( 9 | function (require) { 10 | /** 11 | * 只读页视图基类 12 | * 13 | * @class mvc.ReadView 14 | * @extends mvc.BaseView 15 | */ 16 | var exports = {}; 17 | 18 | /** 19 | * @override 20 | */ 21 | exports.bindEvents = function () { 22 | this.$super(arguments); 23 | 24 | var returnButton = this.get('return'); 25 | if (returnButton) { 26 | var delegate = require('mini-event').delegate; 27 | delegate(returnButton, 'click', this, 'return'); 28 | } 29 | }; 30 | 31 | var BaseView = require('./BaseView'); 32 | var ReadView = require('eoo').create(BaseView, exports); 33 | 34 | return ReadView; 35 | } 36 | ); 37 | -------------------------------------------------------------------------------- /src/mvc/RequestStrategy.js: -------------------------------------------------------------------------------- 1 | /** 2 | * UB RIA Base 3 | * Copyright 2013 Baidu Inc. All rights reserved. 4 | * 5 | * @file 请求处理策略类 6 | * @author otakustay 7 | */ 8 | define( 9 | function (require) { 10 | /** 11 | * 请求处理策略类 12 | * 13 | * 该类用于按一定规则处理请求时的URL、请求名称、请求参数等,通常每个项目会根据自身的前后端接口约定有一个通用的实现 14 | * 15 | * 默认实现是不对输入进行任何处理,添加`"json"`作为默认响应格式 16 | * 17 | * @class mvc.RequestStrategy 18 | */ 19 | var exports = {}; 20 | 21 | /** 22 | * 处理请求名称,具体业务可以使用此方法对请求名称进行一些替换操作,如可以根据当前对象的`entityName`属性为请求名称加上前缀等 23 | * 24 | * @protected 25 | * @method mvc.RequestStrategy#formatName 26 | * @param {string} name 当前请求的名称 27 | * @param {Object} options 请求的配置,此配置为调用{@link mvc.RequestManager#request}时提供的初始配置 28 | * @return {string} 29 | */ 30 | exports.formatName = function (name, options) { 31 | return name; 32 | }; 33 | 34 | /** 35 | * 处理请求的URL,具体业务可以使用此方法对请求的URL进行一些替换操作, 36 | * 如可以根据当前对象的`entityName`来生成通用的URL等 37 | * 38 | * @protected 39 | * @method mvc.RequestStrategy#formatURL 40 | * @param {string} url 当前请求的URL 41 | * @param {Object} options 请求的配置,此配置为已经被处理过的完整的配置 42 | * @return {string} 43 | */ 44 | exports.formatURL = function (url, options) { 45 | return url; 46 | }; 47 | 48 | /** 49 | * 处理请求参数 50 | * 51 | * @protected 52 | * @method mvc.RequestStrategy#formatOptions 53 | * @param {Object} options 请求的参数 54 | * @return {Object} 55 | */ 56 | exports.formatOptions = function (options) { 57 | // 默认使用JSON作为响应格式 58 | if (!options.dataType) { 59 | options.dataType = 'json'; 60 | } 61 | 62 | return options; 63 | }; 64 | 65 | var RequestStrategy = require('eoo').create(exports); 66 | 67 | return RequestStrategy; 68 | } 69 | ); 70 | -------------------------------------------------------------------------------- /src/mvc/SingleEntityModel.js: -------------------------------------------------------------------------------- 1 | /** 2 | * UB RIA Base 3 | * Copyright 2013 Baidu Inc. All rights reserved. 4 | * 5 | * @file 以单个实体为主要数据源的页面的数据模型基类 6 | * @author otakustay 7 | */ 8 | define( 9 | function (require) { 10 | var u = require('../util'); 11 | 12 | /** 13 | * 把实体信息展开到`Model`自身上,以便直接访问到某些属性 14 | * 15 | * @param {Object} entity 加载来的实体信息 16 | * @return {Object} 返回`entity`自身 17 | */ 18 | function fillEntityToModel(entity) { 19 | // 而这个实体信息本身还要单独以`entity`为键保存一份,当取消编辑时用作比对 20 | this.fill(entity); 21 | return entity; 22 | } 23 | 24 | var ENTITY_DATASOURCE = { 25 | entity: function (model) { 26 | // 如新建页之类的是不需要这个实体的,因此通过是否有固定的`id`字段来判断 27 | var id = model.get('id'); 28 | 29 | if (id) { 30 | // 可能作为子Action的时候,从外面传了进来一个实体, 31 | // 这个时候就不用自己加载了,直接展开用就行了 32 | var entity = model.get('entity'); 33 | if (entity) { 34 | return fillEntityToModel.call(model, entity); 35 | } 36 | 37 | return model.findById(id).then(u.bind(fillEntityToModel, model)); 38 | } 39 | 40 | return {}; 41 | } 42 | }; 43 | 44 | /** 45 | * 以单个实体为主要数据源的页面的数据模型基类 46 | * 47 | * @class mvc.SingleEntityModel 48 | * @extends mvc.BaseModel 49 | */ 50 | var exports = {}; 51 | 52 | /** 53 | * @constructs mvc.SingleEntityModel 54 | * @override 55 | */ 56 | exports.constructor = function () { 57 | this.$super(arguments); 58 | 59 | this.putDatasource(ENTITY_DATASOURCE); 60 | }; 61 | 62 | /** 63 | * 根据id获取实体 64 | * 65 | * @param {string | number} id 实体的id 66 | * @return {Promise} 67 | */ 68 | exports.findById = function (id) { 69 | var data = this.data(); 70 | if (!data) { 71 | throw new Error('No default data object attached to this Model'); 72 | } 73 | if (typeof data.findById !== 'function') { 74 | throw new Error('No findById method implemented on default data object'); 75 | } 76 | 77 | return data.findById(id); 78 | }; 79 | 80 | var BaseModel = require('./BaseModel'); 81 | var SingleEntityModel = require('eoo').create(BaseModel, exports); 82 | 83 | return SingleEntityModel; 84 | } 85 | ); 86 | -------------------------------------------------------------------------------- /src/mvc/StaticListData.js: -------------------------------------------------------------------------------- 1 | /** 2 | * UB RIA Base 3 | * Copyright 2014 Baidu Inc. All rights reserved. 4 | * 5 | * @file 静态排序数据类 6 | * @author lixiang, shenbnin(bobshenbin@gmail.com) 7 | */ 8 | define( 9 | function (require) { 10 | var u = require('../util'); 11 | 12 | /** 13 | * 静态搜索相关搜索参数 14 | * 15 | * @const 16 | * @type {Array} 17 | */ 18 | var STATIC_KEYS = ['order', 'orderBy', 'pageNo', 'pageSize']; 19 | 20 | /** 21 | * 静态排序数据类 22 | * 23 | * @class mvc.StaticListData 24 | * @extends mvc.RequestManager 25 | */ 26 | var exports = {}; 27 | 28 | /** 29 | * 获取一个实体列表(不分页) 30 | * 31 | * @method mvc.StaticListData#list 32 | * @param {Object} query 查询参数 33 | * @return {er.meta.FakeXHR} 34 | */ 35 | exports.list = function (query) { 36 | return this.request( 37 | '$entity/list', 38 | query, 39 | { 40 | method: 'GET', 41 | url: '/$entity' 42 | } 43 | ); 44 | }; 45 | 46 | /** 47 | * 过滤数据 48 | * 49 | * @protected 50 | * @method mvc.StaticListData#filterData 51 | * @param {Object} query 查询条件 52 | * @return {Object} 53 | */ 54 | exports.filterData = function (query) { 55 | var sortData = u.clone(this.cacheList); 56 | var results = sortData.results; 57 | // 补上totalCount 58 | sortData.totalCount = results.length; 59 | 60 | // 先排序 61 | if (query.orderBy) { 62 | this.sort(results, query.order, query.orderBy); 63 | } 64 | 65 | // 再截断 66 | if (query.pageNo) { 67 | // 0代表起始 68 | var start = (query.pageNo - 1) * query.pageSize; 69 | var end = Math.min(start + query.pageSize, results.length); 70 | results = results.slice(start, end); 71 | // 深克隆一下,免得外部对数据的修改影响到cache 72 | sortData.results = u.deepClone(results); 73 | } 74 | 75 | return sortData; 76 | }; 77 | 78 | /** 79 | * 检索一个实体列表,返回一个结果集 80 | * 81 | * @method mvc.StaticListData#search 82 | * @param {Object} query 查询参数 83 | * @return {er.meta.FakeXHR} 84 | */ 85 | exports.search = function (query) { 86 | var isStaticKeyChanged = this.checkStaticKeyChanged(query); 87 | if (isStaticKeyChanged) { 88 | u.extend(this, u.pick(query, STATIC_KEYS)); 89 | } 90 | if (!this.cacheList || !isStaticKeyChanged) { 91 | var cache = function (data) { 92 | return this.doCache(data, query); 93 | }; 94 | return this.list(query).then(u.bind(cache, this)); 95 | } 96 | return require('promise').resolve(this.filterData(query)); 97 | }; 98 | 99 | /** 100 | * 数据缓存方法 101 | * 102 | * @protected 103 | * @param {Object} data 要做cache的数据 104 | * @param {Object} query 过滤参数 105 | * @return {Object} 过滤后数据 106 | */ 107 | exports.doCache = function (data, query) { 108 | this.cacheList = data; 109 | return this.filterData(query); 110 | }; 111 | 112 | /** 113 | * 判断静态搜索相关的字段是否变化 114 | * 115 | * @method mvc.StaticListData#checkStaticKeyChanged 116 | * @param {Object} query 搜索参数 117 | * @return {boolean} 118 | */ 119 | exports.checkStaticKeyChanged = function (query) { 120 | return u.some( 121 | STATIC_KEYS, 122 | function (key) { 123 | return this[key] !== query[key]; 124 | }, 125 | this 126 | ); 127 | }; 128 | 129 | /** 130 | * 返回全集 131 | * 132 | * @method mvc.StaticListData#getCacheList 133 | * @return {Array} 134 | */ 135 | exports.getCacheList = function () { 136 | return this.cacheList; 137 | }; 138 | 139 | /** 140 | * 比较算法 141 | * 142 | * @param {string | number} a 原数据源第j个数据 143 | * @param {string | number} b 原数据源第j-1个数据 144 | * @param {string} order desc | asc 145 | * @return {number} 146 | */ 147 | function compare(a, b, order) { 148 | var symbol = 1; 149 | 150 | if (order === 'asc') { 151 | symbol = -1; 152 | } 153 | 154 | // 相等,返回0 155 | if (a === b) { 156 | return 0; 157 | } 158 | 159 | if (a == null && b == null) { 160 | return 0; 161 | } 162 | 163 | // b是null,desc时排在最后 164 | if (b == null) { 165 | return symbol * 1; 166 | } 167 | else if (a == null) { 168 | return symbol * (-1); 169 | } 170 | 171 | var aIsNumber = !isNaN(a); 172 | var bIsNumber = !isNaN(b); 173 | 174 | // a, b 都是数字 175 | if (aIsNumber && bIsNumber) { 176 | return symbol * (parseFloat(a) - parseFloat(b)); 177 | } 178 | 179 | // a, b 如果有一个能转成数字 180 | // 能转成数字的永远大。 181 | if (aIsNumber || bIsNumber) { 182 | return aIsNumber ? (symbol * 1) : (symbol * -1); 183 | } 184 | 185 | // 否则就是文字对比 186 | return symbol * (a + '').localeCompare(b); 187 | } 188 | 189 | /** 190 | * 排序js原生的sort方法在不同浏览器上表现不同(稳定或不稳定),因此自己写一个稳定排序, 冒泡排序 191 | * 192 | * @method mvc.StaticListData#sort 193 | * @param {Array} array 待排序数组 194 | * @param {string} order desc | asc 195 | * @param {string} orderBy 排序字段 196 | */ 197 | exports.sort = function (array, order, orderBy) { 198 | var length = array.length; 199 | for (var i = 0; i <= length - 2; i++) { 200 | for (var j = length - 1; j >= 1; j--) { 201 | // 对两个元素进行交换 202 | compare('', 0, 'desc'); 203 | var compareResult = compare( 204 | array[j][orderBy], 205 | array[j - 1][orderBy], 206 | order 207 | ); 208 | 209 | if (compareResult > 0) { 210 | var temp = array[j]; 211 | array[j] = array[j - 1]; 212 | array[j - 1] = temp; 213 | } 214 | } 215 | } 216 | }; 217 | 218 | var RequestManager = require('./RequestManager'); 219 | var StaticListData = require('eoo').create(RequestManager, exports); 220 | 221 | return StaticListData; 222 | } 223 | ); 224 | -------------------------------------------------------------------------------- /src/mvc/checker/enumChecker.js: -------------------------------------------------------------------------------- 1 | /** 2 | * UB RIA Base 3 | * Copyright 2014 Baidu Inc. All rights reserved. 4 | * 5 | * @file 枚举字段校验器 6 | * @author yanghuabei(yanghuabei@baidu.com) 7 | */ 8 | define( 9 | function (require) { 10 | var checker = { 11 | name: 'enum', 12 | errorMessage: '${title}的值不合法', 13 | priority: 20, 14 | check: check 15 | }; 16 | 17 | /** 18 | * 枚举类型字段值检验器,如果传入的value是undefined或null,返回true 19 | * 20 | * @param {number | undefined | null} value 要检验的数字 21 | * @param {Array} schema 字段的定义、约束, 长度为3的数组 22 | * @return {boolean} 检验成功返回true,失败返回false 23 | */ 24 | function check(value, schema) { 25 | // 如果value为null、undefined, 不做检查 26 | if (!value && value !== 0) { 27 | return true; 28 | } 29 | 30 | var enumObject = schema[2].datasource; 31 | var item = enumObject.fromValue(value); 32 | 33 | return !!item; 34 | } 35 | 36 | return checker; 37 | } 38 | ); 39 | -------------------------------------------------------------------------------- /src/mvc/checker/maxChecker.js: -------------------------------------------------------------------------------- 1 | /** 2 | * UB RIA Base 3 | * Copyright 2014 Baidu Inc. All rights reserved. 4 | * 5 | * @file 最大值校验器 6 | * @author yanghuabei(yanghuabei@baidu.com) 7 | */ 8 | define( 9 | function (require) { 10 | var checker = { 11 | name: 'max', 12 | errorMessage: '${title}不能大于${max}', 13 | priority: 20, 14 | check: check 15 | }; 16 | 17 | /** 18 | * 数字上界检验器,如果value为undefined、null,返回true 19 | * 20 | * @param {number | undefined | null} value 待检验的数值 21 | * @param {Array} schema 字段的定义、约束, 长度为3的数组 22 | * @return {boolean} 检验成功返回true,失败返回false 23 | */ 24 | function check(value, schema) { 25 | // 如果value为null、undefined, 不做检查 26 | if (!value && value !== 0) { 27 | return true; 28 | } 29 | 30 | var max = schema[2].max; 31 | 32 | return value <= max; 33 | } 34 | 35 | return checker; 36 | } 37 | ); 38 | -------------------------------------------------------------------------------- /src/mvc/checker/maxLengthChecker.js: -------------------------------------------------------------------------------- 1 | /** 2 | * UB RIA Base 3 | * Copyright 2014 Baidu Inc. All rights reserved. 4 | * 5 | * @file 最大长度校验器 6 | * @author yanghuabei(yanghuabei@baidu.com) 7 | */ 8 | define( 9 | function (require) { 10 | var checker = { 11 | name: 'maxLength', 12 | errorMessage: { 13 | array: '${title}不能超过${maxLength}个', 14 | string: '${title}不能超过${maxLength}个字符' 15 | }, 16 | priority: 20, 17 | check: check 18 | }; 19 | 20 | /** 21 | * 字符串、数组最大长度检验器,value为undefind、null时返回true 22 | * 23 | * @param {string | Object | undefined | null} value 待校验的值 24 | * @param {Object[]} schema 字段的定义、约束, 长度为3的数组 25 | * @return {boolean} 检验成功返回true,失败返回false 26 | */ 27 | function check(value, schema) { 28 | var maxLength = schema[2].maxLength; 29 | 30 | return !(value && value.length !== 0 && value.length > maxLength); 31 | } 32 | 33 | return checker; 34 | } 35 | ); 36 | -------------------------------------------------------------------------------- /src/mvc/checker/minChecker.js: -------------------------------------------------------------------------------- 1 | /** 2 | * UB RIA Base 3 | * Copyright 2014 Baidu Inc. All rights reserved. 4 | * 5 | * @file 最小值校验器 6 | * @author yanghuabei(yanghuabei@baidu.com) 7 | */ 8 | define( 9 | function (require) { 10 | var checker = { 11 | name: 'min', 12 | errorMessage: '${title}不能小于${min}', 13 | priority: 20, 14 | check: check 15 | }; 16 | 17 | /** 18 | * 数字下界检验器,如果value为undefined、null,返回true 19 | * 20 | * @param {number | undefined | null} value 待检验的数值 21 | * @param {Array} schema 字段的定义、约束, 长度为3的数组 22 | * @return {boolean} 检验成功返回true,失败返回false 23 | */ 24 | function check(value, schema) { 25 | // 如果value为null、undefined, 不做检查 26 | if (!value && value !== 0) { 27 | return true; 28 | } 29 | 30 | var min = schema[2].min; 31 | 32 | return value >= min; 33 | } 34 | 35 | return checker; 36 | } 37 | ); 38 | -------------------------------------------------------------------------------- /src/mvc/checker/minLengthChecker.js: -------------------------------------------------------------------------------- 1 | /** 2 | * UB RIA Base 3 | * Copyright 2014 Baidu Inc. All rights reserved. 4 | * 5 | * @file 最短长度校验器 6 | * @author yanghuabei(yanghuabei@baidu.com) 7 | */ 8 | define( 9 | function (require) { 10 | var checker = { 11 | name: 'minLength', 12 | errorMessage: { 13 | array: '${title}不能小于${minLength}个', 14 | string: '${title}不能小于${minLength}个字符' 15 | }, 16 | priority: 20, 17 | check: check 18 | }; 19 | 20 | /** 21 | * 字符串、数组最小长度检验器,value为undefind、null时返回true 22 | * 23 | * @param {string | Object[] | undefined | null} value 待校验的值 24 | * @param {Object[]} schema 字段的定义、约束, 长度为3的数组 25 | * @return {boolean} 检验成功返回true,失败返回false 26 | */ 27 | function check(value, schema) { 28 | var minLength = schema[2].minLength; 29 | 30 | return !(value && value.length !== 0 && value.length < minLength); 31 | } 32 | 33 | return checker; 34 | } 35 | ); 36 | -------------------------------------------------------------------------------- /src/mvc/checker/patternChecker.js: -------------------------------------------------------------------------------- 1 | /** 2 | * UB RIA Base 3 | * Copyright 2014 Baidu Inc. All rights reserved. 4 | * 5 | * @file 正则校验器 6 | * @author yanghuabei(yanghuabei@baidu.com) 7 | */ 8 | define( 9 | function (require) { 10 | var checker = { 11 | name: 'pattern', 12 | errorMessage: '${title}格式不符合要求', 13 | priority: 30, 14 | check: check 15 | }; 16 | 17 | /** 18 | * 正则检验器,value为null、undefined、''时,返回true 19 | * 20 | * @param {string | number} value 待检验的值 21 | * @param {Array} schema 字段的定义、约束, 长度为3的数组 22 | * @return {boolean} 检验成功返回true,失败返回false 23 | */ 24 | function check(value, schema) { 25 | // 如果value为null, undefined, '', 不做检查 26 | if (!value && value !== 0) { 27 | return true; 28 | } 29 | 30 | var regex = new RegExp(schema[2].pattern); 31 | 32 | return regex.test(value); 33 | } 34 | 35 | return checker; 36 | } 37 | ); 38 | -------------------------------------------------------------------------------- /src/mvc/checker/rangeChecker.js: -------------------------------------------------------------------------------- 1 | /** 2 | * UB RIA Base 3 | * Copyright 2014 Baidu Inc. All rights reserved. 4 | * 5 | * @file 数字范围校验器 6 | * @author yanghuabei(yanghuabei@baidu.com) 7 | */ 8 | define( 9 | function (require) { 10 | var checker = { 11 | name: 'range', 12 | errorMessage: '${title}必须是≥${min}且≤${max}的数字', 13 | priority: 20, 14 | check: check 15 | }; 16 | 17 | /** 18 | * 数字上下界检验器,如果value为undefined、null,返回true 19 | * 20 | * @param {number | undefined | null} value 待检验的数值 21 | * @param {Array} schema 字段的定义、约束, 长度为3的数组 22 | * @return {boolean} 检验成功返回true,失败返回false 23 | */ 24 | function check(value, schema) { 25 | // 如果value为null、undefined, 不做检查 26 | if (!value && value !== 0) { 27 | return true; 28 | } 29 | 30 | var min = schema[2].min; 31 | var max = schema[2].max; 32 | 33 | if (max < min) { 34 | var temp = max; 35 | max = min; 36 | min = temp; 37 | } 38 | 39 | return value >= min && value <= max; 40 | } 41 | 42 | return checker; 43 | } 44 | ); 45 | -------------------------------------------------------------------------------- /src/mvc/checker/rangeLengthChecker.js: -------------------------------------------------------------------------------- 1 | /** 2 | * UB RIA Base 3 | * Copyright 2014 Baidu Inc. All rights reserved. 4 | * 5 | * @file 字段长度范围校验器 6 | * @author yanghuabei(yanghuabei@baidu.com) 7 | */ 8 | define( 9 | function (require) { 10 | var checker = { 11 | name: 'rangeLength', 12 | errorMessage: { 13 | array: '${title}不能小于${minLength}个,且不能超过${maxLength}个', 14 | string: '${title}不能小于${minLength}个字符,且不能超过${maxLength}个字符' 15 | }, 16 | priority: 20, 17 | check: check 18 | }; 19 | 20 | /** 21 | * 字符串、数组最小最大长度检验器,value为undefind、null、[]、''时返回true 22 | * 23 | * @param {string | Object | undefined | null} value 待校验的值 24 | * @param {Object[]} schema 字段的定义、约束, 长度为3的数组 25 | * @return {boolean} 检验成功返回true,失败返回false 26 | */ 27 | function check(value, schema) { 28 | var minLength = schema[2].minLength; 29 | var maxLength = schema[2].maxLength; 30 | 31 | if (maxLength < minLength) { 32 | var temp = maxLength; 33 | maxLength = minLength; 34 | minLength = temp; 35 | } 36 | 37 | return !value || value.length === 0 || (value.length >= minLength && value.length <= maxLength); 38 | } 39 | 40 | return checker; 41 | } 42 | ); 43 | -------------------------------------------------------------------------------- /src/mvc/checker/requiredChecker.js: -------------------------------------------------------------------------------- 1 | /** 2 | * UB RIA Base 3 | * Copyright 2014 Baidu Inc. All rights reserved. 4 | * 5 | * @file 必填字段校验器 6 | * @author yanghuabei(yanghuabei@baidu.com) 7 | */ 8 | define( 9 | function (require) { 10 | var u = require('../../util'); 11 | var checker = { 12 | name: 'required', 13 | errorMessage: '${title}不能为空', 14 | priority: 1, 15 | check: check 16 | }; 17 | 18 | /** 19 | * required检验器 20 | * 检验逻辑:undefined, null, {}, [], ''均无法通过校验 21 | * 22 | * @param {string | boolean | number | Object | Array | undefined} value 输入的值 23 | * @param {Array} schema 字段的定义、约束, 长度为3的数组 24 | * @return {boolean} 检验成功返回true,失败返回false 25 | */ 26 | function check(value, schema) { 27 | return !u.isEmpty(value) || u.isNumber(value) || u.isBoolean(value); 28 | } 29 | 30 | return checker; 31 | } 32 | ); 33 | -------------------------------------------------------------------------------- /src/mvc/checker/typeChecker.js: -------------------------------------------------------------------------------- 1 | /** 2 | * UB RIA Base 3 | * Copyright 2014 Baidu Inc. All rights reserved. 4 | * 5 | * @file 类型校验对象 6 | * @author yanghuabei(yanghuabei@baidu.com) 7 | */ 8 | define( 9 | function (require) { 10 | var u = require('../../util'); 11 | var checker = { 12 | name: 'type', 13 | errorMessage: '${title}的类型不符合要求', 14 | priority: 10, 15 | check: check 16 | }; 17 | 18 | 19 | /** 20 | * 类型检验器,value值为undefined、null时,不做检查; 21 | * enum、number类型字段值为number时通过检查; 22 | * 23 | * @param {string | boolean | number | Object | Array | undefined} value 待检验的值 24 | * @param {Array} schema 字段的定义、约束, 长度为3或2的数组 25 | * @return {boolean} 检验成功返回true,失败返回false 26 | */ 27 | function check(value, schema) { 28 | var expectedType = schema[0]; 29 | // typeMapping的key为值类型,value为与key匹配的定义中的类型数组 30 | var typeMapping = { 31 | Undefined: true, 32 | Null: true, 33 | Array: ['array'], 34 | String: ['string'], 35 | Number: ['number', 'enum'], 36 | Boolean: ['bool'], 37 | Object: ['object'] 38 | }; 39 | var key = ''; 40 | 41 | // ie8下Object.prototype.toString.call(null/undefined)返回的是[Object Object] 42 | // 所以这里单独处理null和undefined 43 | if (value === null) { 44 | key = 'Null'; 45 | } 46 | else if (value === undefined) { 47 | key = 'Undefined'; 48 | } 49 | else { 50 | key = Object.prototype.toString.call(value); 51 | key = key.substring(8, key.length - 1); 52 | } 53 | 54 | return typeMapping[key] === true || u.indexOf(typeMapping[key], expectedType) >= 0; 55 | } 56 | 57 | return checker; 58 | } 59 | ); 60 | -------------------------------------------------------------------------------- /src/mvc/filterHelper.js: -------------------------------------------------------------------------------- 1 | /** 2 | * UB RIA Base 3 | * Copyright 2013 Baidu Inc. All rights reserved. 4 | * 5 | * @file 筛选控件辅助函数 6 | * @author Exodia 7 | */ 8 | define( 9 | function (require) { 10 | var u = require('../util'); 11 | 12 | /** 13 | * 列表筛选控件辅助函数 14 | * 15 | * @namespace mvc.filterHelper 16 | */ 17 | var helper = {}; 18 | 19 | /** 20 | * 下拉单选辅助函数 21 | * 22 | * @namespace mvc.filterHelper.select 23 | */ 24 | var select = helper.select = {}; 25 | 26 | /** 27 | * 获取筛选条件文本 28 | * 29 | * @method mvc.filterHelper.select.getText 30 | * @param {esui.Select} filter 对应的控件实例 31 | * @return {string} 32 | */ 33 | select.getText = function (filter) { 34 | var item = u.find( 35 | filter.datasource, 36 | function (item) { 37 | /* eslint-disable eqeqeq */ 38 | return item.value == filter.value; 39 | /* eslint-enable eqeqeq */ 40 | } 41 | ); 42 | return item && item.text || ''; 43 | }; 44 | 45 | /** 46 | * 多选辅助函数 47 | * 48 | * @namespace mvc.filterHelper.toggleSelector 49 | */ 50 | var toggleSelector = helper.toggleSelector = {}; 51 | 52 | /** 53 | * 获取筛选条件文本 54 | * 55 | * @method mvc.filterHelper.toggleSelector.getText 56 | * @param {ub-ria-ui.ToggleSelector} filter 对应的控件实例 57 | * @return {string} 58 | */ 59 | toggleSelector.getText = function (filter) { 60 | var item = u.find( 61 | filter.datasource, 62 | function (item) { 63 | /* eslint-disable eqeqeq */ 64 | return item.id == filter.value; 65 | /* eslint-enable eqeqeq */ 66 | } 67 | ); 68 | return item && item.name || ''; 69 | }; 70 | 71 | return helper; 72 | } 73 | ); 74 | -------------------------------------------------------------------------------- /src/mvc/handler/RedirectSubmitHandler.js: -------------------------------------------------------------------------------- 1 | /** 2 | * UB RIA Base 3 | * Copyright 2014 Baidu Inc. All rights reserved. 4 | * 5 | * @file 表单提交成功后的跳转组件 6 | * @author yanghuabei(yanghuabei@baidu.com) 7 | */ 8 | define( 9 | function (require) { 10 | var u = require('../../util'); 11 | var SubmitHandler = require('./SubmitHandler'); 12 | 13 | /** 14 | * 表单提交成功后的跳转组件 15 | * 16 | * @class mvc.handler.RedirectSubmitHandler 17 | * @extends mvc.handler.SubmitHandler 18 | */ 19 | var exports = {}; 20 | 21 | /** 22 | * 跳转url模版 23 | * 24 | * @member mvc.handler.RedirectSubmitHandler#template 25 | * @type {string} 26 | */ 27 | exports.template = '/${entityName}/list'; 28 | 29 | /** 30 | * 跳转参数 31 | * 32 | * @member mvc.handler.RedirectSubmitHandler#redirectOptions 33 | * @type {string} 34 | */ 35 | exports.redirectOptions = null; 36 | 37 | /** 38 | * 设置组件的url模版 39 | * 可选。默认值为'/${entityName}/list'。 40 | * 修改后可指定跳转url模板。 41 | * 42 | * @method mvc.handler.RedirectSubmitHandler#setTemplate 43 | * @param {string} template 跳转url模版 44 | */ 45 | exports.setTemplate = function (template) { 46 | this.template = template; 47 | }; 48 | 49 | /** 50 | * 获取模版 51 | * 52 | * @method mvc.handler.RedirectSubmitHandler#getTemplate 53 | * @return {string} 54 | */ 55 | exports.getTemplate = function () { 56 | return this.template; 57 | }; 58 | 59 | /** 60 | * 设置跳转参数 61 | * 可选。默认值为空。 62 | * 修改后可指定跳转配置。 63 | * 64 | * @method mvc.handler.RedirectSubmitHandler#setRedirectOptions 65 | * @param {Object} options 跳转参数 66 | */ 67 | exports.setRedirectOptions = function (options) { 68 | this.redirectOptions = options; 69 | }; 70 | 71 | /** 72 | * 获取跳转参数 73 | * 74 | * @method mvc.handler.RedirectSubmitHandler#getRedirectOptions 75 | * @return {Object} 76 | */ 77 | exports.getRedirectOptions = function () { 78 | return this.redirectOptions; 79 | }; 80 | 81 | /** 82 | * @override 83 | */ 84 | exports.handle = function (entity, action) { 85 | var data = this.getData(entity, action); 86 | var url = u.template(this.getTemplate(), data); 87 | this.redirect(action, url, this.getRedirectOptions()); 88 | 89 | this.next(entity, action); 90 | }; 91 | 92 | /** 93 | * 跳转的方法 94 | * 95 | * @protected 96 | * @method mvc.handler.RedirectSubmitHandler#redirect 97 | * @param {er.Action} action 表单Action实例 98 | * @param {string} url 跳转目的url 99 | * @param {Object} options 跳转参数 100 | */ 101 | exports.redirect = function (action, url, options) { 102 | action.redirect(url, options); 103 | }; 104 | 105 | /** 106 | * 获取url模版的数据 107 | * 108 | * @protected 109 | * @method mvc.handler.RedirectSubmitHandler#getData 110 | * @param {Object} entity 提交后服务器端返回的实体信息 111 | * @param {er.Action} action 表单Action实例 112 | * @return {Object} 113 | */ 114 | exports.getData = function (entity, action) { 115 | return {entityName: action.getEntityName()}; 116 | }; 117 | 118 | var RedirectSubmitHandler = require('eoo').create(SubmitHandler, exports); 119 | 120 | return RedirectSubmitHandler; 121 | } 122 | ); 123 | -------------------------------------------------------------------------------- /src/mvc/handler/SubmitHandler.js: -------------------------------------------------------------------------------- 1 | /** 2 | * UB RIA Base 3 | * Copyright 2014 Baidu Inc. All rights reserved. 4 | * 5 | * @file 表单提交成功后跳转处理组件基类 6 | * @author yanghuabei(yanghuabei@baidu.com) 7 | */ 8 | define( 9 | function (require) { 10 | /** 11 | * 表单提交成功后跳转处理组件基类 12 | * 13 | * @class mvc.handler.SubmitHandler 14 | */ 15 | var exports = {}; 16 | 17 | /** 18 | * 下一个处理组件 19 | * 20 | * @member mvc.handler.SubmitHandler#nextSubmitHandler 21 | * @type {mvc.handler.SubmitHandler} 22 | */ 23 | exports.nextSubmitHandler = null; 24 | 25 | /** 26 | * 设置下一个组件 27 | * 可选。默认值为空。 28 | * 修改后可指下一个Handler(如toast后刷新)。 29 | * 30 | * @method mvc.handler.SubmitHandler#setNextSubmitHandler 31 | * @param {mvc.handler.SubmitHandler} handler 下一个组件 32 | */ 33 | exports.setNextSubmitHandler = function (handler) { 34 | this.nextSubmitHandler = handler; 35 | }; 36 | 37 | /** 38 | * 获取下一个组件 39 | * 40 | * @method mvc.handler.SubmitHandler#getNextSubmitHandler 41 | * @return {SubmitHandler} 42 | */ 43 | exports.getNextSubmitHandler = function () { 44 | return this.nextSubmitHandler; 45 | }; 46 | 47 | /** 48 | * 提交成功处理函数 49 | * 50 | * @method mvc.handler.SubmitHandler#handle 51 | * @param {Object} entity 提交后服务器端返回的实体信息 52 | * @param {er.Action} action 表单Action实例 53 | */ 54 | exports.handle = function (entity, action) { 55 | this.next(entity, action); 56 | }; 57 | 58 | /** 59 | * 调用下一个handler 60 | * 61 | * @method mvc.handler.SubmitHandler#next 62 | * @param {Object} entity 提交后服务器端返回的实体信息 63 | * @param {er.Action} action 表单Action实例 64 | */ 65 | exports.next = function (entity, action) { 66 | var nextSubmitHandler = this.getNextSubmitHandler(); 67 | if (nextSubmitHandler) { 68 | nextSubmitHandler.handle(entity, action); 69 | } 70 | }; 71 | 72 | var SubmitHandler = require('eoo').create(exports); 73 | 74 | return SubmitHandler; 75 | } 76 | ); 77 | -------------------------------------------------------------------------------- /src/mvc/handler/ToastSubmitHandler.js: -------------------------------------------------------------------------------- 1 | /** 2 | * UB RIA Base 3 | * Copyright 2014 Baidu Inc. All rights reserved. 4 | * 5 | * @file 表单提交成功后的toast提醒组件 6 | * @author yanghuabei(yanghuabei@baidu.com) 7 | */ 8 | define( 9 | function (require) { 10 | var u = require('../../util'); 11 | var Toast = require('esui/Toast'); 12 | var SubmitHandler = require('./SubmitHandler'); 13 | 14 | /** 15 | * 表单提交成功后的toast提醒组件 16 | * 17 | * @class mvc.handler.ToastSubmitHandler 18 | * @extends mvc.hadnler.SubmitHandler 19 | */ 20 | var exports = {}; 21 | 22 | /** 23 | * toast消息模版 24 | * 25 | * @member mvc.handler.ToastSubmitHandler#template 26 | * @type {string} 27 | */ 28 | exports.template = ''; 29 | 30 | /** 31 | * 设置下一个组件 32 | * 可选。默认值为空。 33 | * 修改后可指定toast模板。 34 | * 35 | * @method mvc.handler.ToastSubmitHandler#setTemplate 36 | * @param {string} template toast消息模版 37 | */ 38 | exports.setTemplate = function (template) { 39 | this.template = template; 40 | }; 41 | 42 | /** 43 | * 获取模版 44 | * 45 | * @method mvc.handler.ToastSubmitHandler#getTemplate 46 | * @return {string} 47 | */ 48 | exports.getTemplate = function () { 49 | return this.template; 50 | }; 51 | 52 | /** 53 | * @override 54 | */ 55 | exports.handle = function (entity, action) { 56 | var message = this.getToastMessage(entity, action); 57 | if (message) { 58 | var toast = Toast.success(message); 59 | toast.show(); 60 | } 61 | 62 | this.next(entity, action); 63 | }; 64 | 65 | /** 66 | * 获取表单提交成功后显示的信息 67 | * 68 | * 默认提示信息为“您[创建|修改]的{实体名称}{name}已经成功保存” 69 | * 70 | * @protected 71 | * @method mvc.handler.ToastSubmitHandler#getToastMessage 72 | * @param {Object} entity 提交后服务器端返回的实体信息 73 | * @param {er.Action} action 表单Action实例 74 | * @return {string} 75 | */ 76 | exports.getToastMessage = function (entity, action) { 77 | var template = this.getTemplate(); 78 | if (template == null) { 79 | return ''; 80 | } 81 | 82 | if (template) { 83 | return u.template(template, entity || {}); 84 | } 85 | 86 | var actionType = action.context.formType === 'update' 87 | ? '修改' 88 | : '创建'; 89 | return '您' + actionType + '的' 90 | + action.getEntityDescription() 91 | + '[' + u.escape(entity.name) + ']' 92 | + '已经成功保存'; 93 | }; 94 | 95 | var ToastSubmitHandler = require('eoo').create(SubmitHandler, exports); 96 | 97 | return ToastSubmitHandler; 98 | } 99 | ); 100 | -------------------------------------------------------------------------------- /src/mvc/rule.js: -------------------------------------------------------------------------------- 1 | /** 2 | * UB RIA Base 3 | * Copyright 2013 Baidu Inc. All rights reserved. 4 | * 5 | * @file 常用校验规则 6 | * @author otakustay 7 | */ 8 | define( 9 | function (require) { 10 | /** 11 | * 规则常量 12 | * 13 | * @namespace mvc.rule 14 | * @deprecated 使用具体系统中的`rule`对象 15 | */ 16 | return { 17 | maxLength: 100, 18 | 19 | mail: { 20 | /** 21 | * 默认的mail字段最大长度 22 | */ 23 | maxLength: 64, 24 | 25 | /** 26 | * 电子邮件地址正则 27 | */ 28 | pattern: /^\w+([-+.]\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*$/ 29 | }, 30 | 31 | description: { 32 | /** 33 | * 默认的description说明字段最大长度 34 | */ 35 | maxLength: 4000 36 | }, 37 | 38 | phone: { 39 | /** 40 | * 电话号码,可为空,区号和分机号可选 41 | * 格式{3到4位区号}-{7到8位号码}-{3到5位分机号} 42 | */ 43 | pattern: /^((0\d{2,3})-)(\d{7,8})(-(\d{3,}))?$/ 44 | }, 45 | 46 | mobile: { 47 | /** 48 | * 手机号码,以13、14、15、18开头的11位数字 49 | */ 50 | pattern: /^(1(3|4|5|8)\d{9})?$/ 51 | }, 52 | 53 | url: { 54 | /** 55 | * url网址最大长度 56 | */ 57 | maxLength: 1000, 58 | 59 | /** 60 | * url网址正则 61 | */ 62 | /* jshint maxlen: 120 */ 63 | pattern: /^(?:https?|ftp|wap):\/\/.+$|^(?!(?:https?|ftp|wap):\/\/).+$/ 64 | }, 65 | 66 | positiveInteger: { 67 | /** 68 | * 正整数正则 69 | */ 70 | pattern: /^\d+$/ 71 | }, 72 | 73 | money: { 74 | /** 75 | * 价格类数字正则 76 | */ 77 | pattern: /^\d+(\.\d{1,2})?$/ 78 | } 79 | }; 80 | } 81 | ); 82 | -------------------------------------------------------------------------------- /src/tpl.js: -------------------------------------------------------------------------------- 1 | /** 2 | * UB RIA Base 3 | * Copyright 2013 Baidu Inc. All rights reserved. 4 | * 5 | * @file tpl加载插件 6 | * @author otakustay 7 | */ 8 | define( 9 | function (require) { 10 | var u = require('./util'); 11 | var ajax = require('er/ajax'); 12 | var etpl = require('etpl'); 13 | var template = etpl; 14 | 15 | // 添加一堆`filter`用用 16 | var util = require('./util'); 17 | template.addFilter('trim', util.trim); 18 | template.addFilter('pascalize', util.pascalize); 19 | template.addFilter('camelize', util.camelize); 20 | template.addFilter('dasherize', util.dasherize); 21 | template.addFilter('constlize', util.constlize); 22 | template.addFilter('pluralize', util.pluralize); 23 | 24 | var controlModulePrefix = { 25 | // Sidebar不使用esui的,那个不大符合要求 26 | BoxGroup: 'esui', 27 | Button: 'esui', 28 | Calendar: 'esui', 29 | CheckBox: 'esui', 30 | CommandMenu: 'esui', 31 | Crumb: 'esui', 32 | Dialog: 'esui', 33 | Form: 'esui', 34 | Frame: 'esui', 35 | Label: 'esui', 36 | Link: 'esui', 37 | MonthView: 'esui', 38 | Pager: 'esui', 39 | Panel: 'esui', 40 | RangeCalendar: 'esui', 41 | Region: 'esui', 42 | RichCalendar: 'esui', 43 | Schedule: 'esui', 44 | SearchBox: 'esui', 45 | Select: 'esui', 46 | Tab: 'esui', 47 | Table: 'esui', 48 | TextBox: 'esui', 49 | TextLine: 'esui', 50 | Tip: 'esui', 51 | TipLayer: 'esui', 52 | Tree: 'esui', 53 | Validity: 'esui', 54 | Wizard: 'esui', 55 | ActionPanel: 'ef', 56 | ActionDialog: 'ef', 57 | ViewPanel: 'ef', 58 | DrawerActionPanel: 'ub-ria', 59 | RichSelector: 'ub-ria-ui', 60 | TableRichSelector: 'ub-ria-ui', 61 | TogglePanel: 'ub-ria-ui', 62 | ToggleSelector: 'ub-ria-ui', 63 | TreeRichSelector: 'ub-ria-ui', 64 | PartialForm: 'ub-ria/ui', 65 | Uploader: 'ub-ria/ui', 66 | Warn: 'ub-ria/ui' 67 | }; 68 | 69 | var extensionModulePrefix = { 70 | AutoSort: 'esui/extension', 71 | Command: 'esui/extension', 72 | CustomData: 'esui/extension', 73 | TableEdit: 'esui/extension', 74 | TableSubrow: 'esui/extension', 75 | AutoSubmit: 'ub-ria/ui/extension', 76 | ExternSearch: 'ub-ria/ui/extension', 77 | ExternSelect: 'ub-ria/ui/extension', 78 | TrimInput: 'ub-ria/ui/extension', 79 | WordCount: 'ub-ria/ui/extension' 80 | }; 81 | 82 | var CONTROL_CUSTOM_ELEMENT_PREFIX = 'esui-'; 83 | 84 | var enableCustomElementShim = function (type) { 85 | var customElementName = CONTROL_CUSTOM_ELEMENT_PREFIX + u.dasherize(type); 86 | document.createElement(customElementName); 87 | }; 88 | enableCustomElementShim = u.memoize(enableCustomElementShim); 89 | 90 | /** 91 | * 获取控件依赖关系 92 | * 93 | * @param {string} text 模板内容 94 | * @return {string[]} 依赖的控件列表 95 | */ 96 | function getControlDependencies(text) { 97 | var dependencies = []; 98 | var defined = {}; 99 | 100 | var regex = /<\s*esui-([\w-]+)[^>]*>|data-ui-type="(\w+)"/g; 101 | var match = regex.exec(text); 102 | while (match) { 103 | var type = match[1] && util.pascalize(match[1]) || match[2]; 104 | if (!defined[type]) { 105 | defined[type] = true; 106 | 107 | var prefix = (controlModulePrefix[type] || 'ui') + '/'; 108 | dependencies.push(prefix + type); 109 | enableCustomElementShim(type); 110 | } 111 | 112 | match = regex.exec(text); 113 | } 114 | 115 | return dependencies; 116 | } 117 | 118 | /** 119 | * 获取扩展依赖关系 120 | * 121 | * @param {string} text 模板内容 122 | * @return {string[]} 依赖的扩展列表 123 | */ 124 | function getExtensionDependencies(text) { 125 | var dependencies = []; 126 | var defined = {}; 127 | 128 | var regex = /data-ui-extension-[^-]+-type="(\w+)"/g; 129 | var match = regex.exec(text); 130 | while (match) { 131 | var type = match[1]; 132 | if (!defined[type]) { 133 | defined[type] = true; 134 | 135 | var prefix = (extensionModulePrefix[type] || 'ui/extension') + '/'; 136 | dependencies.push(prefix + type); 137 | } 138 | 139 | match = regex.exec(text); 140 | } 141 | 142 | return dependencies; 143 | } 144 | 145 | /** 146 | * 模板加载插件,类似[etpl](https://github.com/ecomfe/etpl)的AMD插件, 147 | * 但此插件会分析模板的源码,当模板按标准书写时,可自动分析控件的依赖 148 | * 149 | * 使用此插件的自动控件依赖分析功能,模板必须满足以下条件: 150 | * 151 | * - 控件的HTML必须写`data-ui-type="SomeControl"`这一格式,即*不能*有`data-ui="type: SomeControl"`这样的写法 152 | * - 对于非ESUI、EF框架,且不在`src/ui`文件夹下的控件,必须通过{@link tpl.registerControl}方法注册模块前缀 153 | * - 对于ESUI扩展,必须写`data-ui-extension-xxx-type="Xxx"`的形式 154 | * - 业务ESUI扩展必须放置在`src/ui/extension`文件夹下 155 | * 156 | * @namespace tpl 157 | */ 158 | var plugin = { 159 | 160 | /** 161 | * 设置模板引擎实例,可通过此方法来使用非默认引擎实例 162 | * 163 | * @method tpl.setupTemplateEngine 164 | * @param {etpl.Engine} engine 引擎的实例 165 | */ 166 | setupTemplateEngine: function (engine) { 167 | template = engine || etpl; 168 | }, 169 | 170 | /** 171 | * 加载模板,AMD插件对象暴露的方法 172 | * 173 | * @method tpl.load 174 | * @param {string} resourceId 模板资源id 175 | * @param {Function} parentRequire 父级`require`函数 176 | * @param {Function} load 加载完成后调用 177 | */ 178 | load: function (resourceId, parentRequire, load) { 179 | function addTemplate(text) { 180 | template.parse(text); 181 | 182 | var controls = getControlDependencies(text); 183 | var extensions = getExtensionDependencies(text); 184 | var dependencies = controls.concat(extensions); 185 | 186 | window.require( 187 | dependencies, 188 | function () { 189 | load(text); 190 | } 191 | ); 192 | } 193 | 194 | if (resourceId.indexOf('.tpl.html') >= 0) { 195 | var options = { 196 | method: 'GET', 197 | url: parentRequire.toUrl(resourceId), 198 | cache: true, 199 | dataType: 'text' 200 | }; 201 | ajax.request(options).then(addTemplate); 202 | } 203 | else { 204 | parentRequire([resourceId], addTemplate); 205 | } 206 | }, 207 | 208 | /** 209 | * 注册业务控件的模块 210 | * 211 | * @method tpl.registerControl 212 | * @param {string} moduleId 业务控件对应的模块id,必须为顶级id 213 | */ 214 | registerControl: function (moduleId) { 215 | var lastIndexOfSlash = moduleId.lastIndexOf('/'); 216 | var prefix = moduleId.substring(0, lastIndexOfSlash); 217 | var type = moduleId.substring(lastIndexOfSlash + 1); 218 | controlModulePrefix[type] = prefix; 219 | }, 220 | 221 | /** 222 | * 注册业务控件扩展的模块 223 | * 224 | * @method tpl.registerExtension 225 | * @param {string} moduleId 业务控件对应的模块id,必须为顶级id 226 | */ 227 | registerExtension: function (moduleId) { 228 | var lastIndexOfSlash = moduleId.lastIndexOf('/'); 229 | var prefix = moduleId.substring(0, lastIndexOfSlash); 230 | var type = moduleId.substring(lastIndexOfSlash + 1); 231 | extensionModulePrefix[type] = prefix; 232 | } 233 | }; 234 | 235 | return plugin; 236 | } 237 | ); 238 | -------------------------------------------------------------------------------- /src/ui/DrawerActionPanel.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file DrawerActionPanel.js 抽屉控件 3 | * @class ub-ria.DrawerActionPanel 4 | * @extends ef.ActionPanel 5 | * @author exodia(dengxinxin@baidu.com) 6 | */ 7 | define( 8 | function (require) { 9 | var lib = require('esui/lib'); 10 | var ActionPanel = require('ef/ActionPanel'); 11 | 12 | function close(e) { 13 | if (this.helper.isPart(e.target, 'close-btn')) { 14 | this.hide(); 15 | this.fire('close'); 16 | } 17 | } 18 | 19 | var exports = {}; 20 | 21 | exports.type = 'DrawerActionPanel'; 22 | 23 | exports.initStructure = function () { 24 | this.$super(arguments); 25 | // 先创建一个,万一加载 action 挂掉了,这个关闭按钮还是可以保证存在的 26 | createCloseBtn.call(this); 27 | document.body.appendChild(this.main); 28 | this.addState('hidden'); 29 | }; 30 | 31 | exports.initEvents = function () { 32 | this.$super(arguments); 33 | this.helper.addDOMEvent(this.main, 'click', close); 34 | // action 加载好后会把 main 清空, 再创建次 35 | this.on('actionloaded', createCloseBtn); 36 | }; 37 | 38 | exports.enterAction = function () { 39 | this.action.context.args.isInDrawerPanel = true; 40 | this.$super(arguments); 41 | }; 42 | 43 | exports.show = function () { 44 | getMask(this).style.display = 'block'; 45 | document.body.style.overflowY = 'hidden'; 46 | this.$super(arguments); 47 | }; 48 | 49 | exports.hide = function () { 50 | getMask(this).style.display = 'none'; 51 | document.body.style.overflowY = ''; 52 | this.$super(arguments); 53 | }; 54 | 55 | exports.dispose = function () { 56 | this.hide(); 57 | lib.removeNode(this.helper.getId('mask')); 58 | lib.removeNode(this.main.id); 59 | this.$super(arguments); 60 | }; 61 | 62 | function getMask(panel) { 63 | return panel.helper.getPart('mask') || document.body.appendChild(panel.helper.createPart('mask')); 64 | } 65 | 66 | function createCloseBtn() { 67 | if (!this.helper.getPart('close-btn')) { 68 | var el = this.main.appendChild(this.helper.createPart('close-btn'), 'span'); 69 | el.title = '关闭'; 70 | el.className += ' ui-icon ui-icon-close'; 71 | } 72 | } 73 | 74 | var DrawerActionPanel = require('eoo').create(ActionPanel, exports); 75 | require('esui').register(DrawerActionPanel); 76 | 77 | return DrawerActionPanel; 78 | } 79 | ); 80 | -------------------------------------------------------------------------------- /src/ui/PartialForm.js: -------------------------------------------------------------------------------- 1 | /** 2 | * ADM 2.0 3 | * Copyright 2014 Baidu Inc. All rights reserved. 4 | * 5 | * @file PartialForm控件 6 | * @author zhanglili(otakustay@gmail.com) 7 | */ 8 | define( 9 | function (require) { 10 | var lib = require('esui/lib'); 11 | 12 | /** 13 | * 局部表单控件 14 | * 15 | * 这控件本质上是个`ActionPanel`,但它表现得像是`InputControl`,因此可以用作表单的一部分,从而细粒度地切割表单的组成 16 | * 17 | * @class ui.PartialForm 18 | * @extends ef.ActionPanel 19 | */ 20 | var exports = {}; 21 | 22 | /** 23 | * 控件类型,始终为`"PartialForm"` 24 | * 25 | * @member ui.PartialForm#type 26 | * @type {string} 27 | * @readonly 28 | * @override 29 | */ 30 | exports.type = 'PartialForm'; 31 | 32 | function getHelperForm(action) { 33 | var Form = require('esui/Form'); 34 | var properties = { 35 | main: lib.g(action.view.container), 36 | viewContext: action.view.viewContext 37 | }; 38 | return new Form(properties); 39 | } 40 | 41 | /** 42 | * 进行验证 43 | * 44 | * @method ui.PartialForm#validate 45 | * @return {boolean} 46 | */ 47 | exports.validate = function () { 48 | var action = this.get('action'); 49 | 50 | if (!action) { 51 | return true; 52 | } 53 | 54 | if (typeof action.validate === 'function') { 55 | return action.validate(); 56 | } 57 | 58 | // 标准的验证必须有`ViewContext`的协助 59 | var viewContext = action.view && action.view.viewContext; 60 | if (!viewContext) { 61 | return true; 62 | } 63 | 64 | var Validity = require('esui/validator/Validity'); 65 | var validity = new Validity(); 66 | var event = { 67 | validity: validity 68 | }; 69 | this.fire('beforevalidate', event); 70 | 71 | // 拿子Action中的输入控件,看上去是一个hack,但工作的不错, 72 | // 就当是`Form`的特殊功能,把控件作为一种helper来用 73 | // TODO: 后续想想能不能整得更合理些 74 | var helperForm = getHelperForm(action); 75 | var inputs = helperForm.getInputControls(); 76 | 77 | var isValid = true; 78 | for (var i = 0; i < inputs.length; i++) { 79 | var control = inputs[i]; 80 | // 不对disabled的控件进行验证 81 | if (control.isDisabled()) { 82 | continue; 83 | } 84 | 85 | isValid &= control.validate(); 86 | } 87 | 88 | if (!isValid) { 89 | this.fire('invalid', event); 90 | } 91 | 92 | this.fire('aftervalidate', event); 93 | 94 | return isValid; 95 | }; 96 | 97 | /** 98 | * 向用户通知提交错误信息,默认根据`field`字段查找对应`name`的控件并显示错误信息 99 | * 100 | * @method ui.PartialForm#notifyErrors 101 | * @param {Object} errors 错误信息 102 | * @param {meta.FieldError[]} errors.fields 出现错误的字段集合 103 | * @param {string} [errors.message] 全局错误信息 104 | */ 105 | exports.notifyErrors = function (errors) { 106 | var Validity = require('esui/validator/Validity'); 107 | var ValidityState = require('esui/validator/ValidityState'); 108 | var action = this.get('action'); 109 | var form = getHelperForm(action); 110 | 111 | var inputs = form.getInputControls(); 112 | var fields = errors.fields; 113 | for (var i = 0; i < inputs.length && fields.length > 0; i++) { 114 | var input = inputs[i]; 115 | if (typeof input.notifyErrors === 'function') { 116 | fields = input.notifyErrors(errors) || fields; 117 | continue; 118 | } 119 | for (var j = 0; j < fields.length; j++) { 120 | var fail = fields[j]; 121 | if (input.name === fail.field) { 122 | var state = new ValidityState(false, fail.message); 123 | var validity = new Validity(); 124 | validity.addState('server', state); 125 | 126 | if (typeof input.showValidity === 'function') { 127 | input.showValidity(validity); 128 | } 129 | } 130 | } 131 | } 132 | }; 133 | 134 | /** 135 | * 获取值 136 | * 137 | * @method ui.PartialForm#getRawValue 138 | * @return {*} 139 | */ 140 | exports.getRawValue = function () { 141 | var action = this.get('action'); 142 | if (!action) { 143 | return null; 144 | } 145 | 146 | if (typeof action.getRawValue === 'function') { 147 | return action.getRawValue(); 148 | } 149 | 150 | if (typeof action.getValue === 'function') { 151 | return action.getValue(); 152 | } 153 | }; 154 | 155 | /** 156 | * 获取控件分类,伪装为表单控件 157 | * 158 | * @method ui.PartialForm#getCategory 159 | * @return {string} 始终返回`"input"` 160 | */ 161 | exports.getCategory = function () { 162 | return 'input'; 163 | }; 164 | 165 | var ActionPanel = require('ef/ActionPanel'); 166 | var PartialForm = require('eoo').create(ActionPanel, exports); 167 | 168 | require('esui').register(PartialForm); 169 | 170 | return PartialForm; 171 | } 172 | ); 173 | -------------------------------------------------------------------------------- /src/ui/Warn.js: -------------------------------------------------------------------------------- 1 | /** 2 | * UB RIA Base 3 | * Copyright 2014 Baidu Inc. All rights reserved. 4 | * 5 | * @file 简易信息提示控件 6 | * @author lixiang 7 | */ 8 | define( 9 | function (require) { 10 | var lib = require('esui/lib'); 11 | var Control = require('esui/Control'); 12 | 13 | /** 14 | * Warn控件 15 | * 16 | * @class ui.Warn 17 | * @extends esui.Control 18 | */ 19 | var exports = {}; 20 | 21 | /** 22 | * 控件类型,始终为`"Warn"` 23 | * 24 | * @member ui.Warn#type 25 | * @type {string} 26 | * @readonly 27 | * @override 28 | */ 29 | exports.type = 'Warn'; 30 | 31 | /** 32 | * @override 33 | */ 34 | exports.initOptions = function (options) { 35 | var properties = {}; 36 | lib.extend(properties, this.$self.defaultProperties, options); 37 | if (properties.content == null) { 38 | properties.content = this.main.innerHTML; 39 | } 40 | 41 | this.setProperties(properties); 42 | }; 43 | 44 | var tempalte = '' 45 | + '
' 46 | + '
' 47 | + ' ${okLabel}' 48 | + ' ' 49 | + ' ${cancelLabel}' 50 | + '
'; 51 | 52 | 53 | function btnClickHandler(warn, type) { 54 | // 如果在参数里设置了处理函数,会在fire时执行 55 | warn.fire(type); 56 | if (type === 'ok') { 57 | warn.dispose(); 58 | } 59 | else { 60 | warn.hide(); 61 | } 62 | } 63 | 64 | /** 65 | * @override 66 | */ 67 | exports.initStructure = function () { 68 | this.main.innerHTML = lib.format( 69 | tempalte, 70 | { 71 | iconClass: this.helper.getPartClassName('icon'), 72 | contentId: this.helper.getId('content'), 73 | contentClass: this.helper.getPartClassName('content'), 74 | okBtnClass: this.helper.getPartClassName('ok-btn'), 75 | cancelBtnClass: this.helper.getPartClassName('cancel-btn'), 76 | okLabel: this.okLabel, 77 | cancelLabel: this.cancelLabel, 78 | operationFieldClass: this.helper.getPartClassName('operation-field') 79 | } 80 | ); 81 | 82 | this.initChildren(); 83 | 84 | this.getChild('btnOk').on( 85 | 'click', 86 | lib.curry(btnClickHandler, this, 'ok') 87 | ); 88 | this.getChild('btnCancel').on( 89 | 'click', 90 | lib.curry(btnClickHandler, this, 'cancel') 91 | ); 92 | 93 | }; 94 | 95 | /** 96 | * @override 97 | */ 98 | exports.repaint = require('esui/painters').createRepaint( 99 | Control.prototype.repaint, 100 | { 101 | name: 'content', 102 | paint: function (control, content) { 103 | var container = control.helper.getPart('content'); 104 | container.innerHTML = content; 105 | } 106 | } 107 | ); 108 | 109 | /** 110 | * @override 111 | */ 112 | exports.hide = function () { 113 | this.fire('hide'); 114 | this.dispose(); 115 | }; 116 | 117 | /** 118 | * @override 119 | */ 120 | exports.dispose = function () { 121 | if (this.helper.isInStage('DISPOSED')) { 122 | return; 123 | } 124 | 125 | this.$super(arguments); 126 | 127 | lib.removeNode(this.main); 128 | }; 129 | 130 | var Warn = require('eoo').create(Control, exports); 131 | 132 | /** 133 | * 快捷显示信息的方法 134 | * 135 | * @method ui.Warn.show 136 | * @param {Object} options 其它配置项 137 | * @return {ui.Warn} 138 | */ 139 | Warn.show = function (options) { 140 | var warn = new Warn(options); 141 | warn.appendTo(options.wrapper || document.body); 142 | return warn; 143 | }; 144 | 145 | Warn.defaultProperties = { 146 | okLabel: '取消新建', 147 | cancelLabel: '继续新建' 148 | }; 149 | 150 | require('esui').register(Warn); 151 | return Warn; 152 | } 153 | ); 154 | -------------------------------------------------------------------------------- /src/ui/css/Button.less: -------------------------------------------------------------------------------- 1 | /** 提供一些产品线内通用的按钮皮肤 **/ 2 | @link-button-color: #66c2a4; 3 | @link-button-color-hover: #999; 4 | @link-button-color-active: #999; 5 | 6 | /** 文字按钮 */ 7 | .skin-link-button { 8 | background: none; 9 | border: 0 none; 10 | height: @button-height; 11 | line-height: @button-height; 12 | color: @link-button-color; 13 | &:active, 14 | &:hover { 15 | background: none; 16 | border: 0 none; 17 | height: @button-height; 18 | line-height: @button-height; 19 | overflow: hidden; 20 | color: @link-button-color-hover; 21 | } 22 | 23 | &:active { 24 | color: @link-button-color-active; 25 | } 26 | } 27 | 28 | button.skin-link-button { 29 | *height: @button-height + 2px; 30 | } 31 | button.skin-link-button:active, 32 | button.skin-link-button:hover { 33 | height: @button-height; 34 | } 35 | 36 | 37 | /** 添加按钮 */ 38 | .skin-add-button, 39 | .skin-add-button:hover, 40 | .skin-add-button:active { 41 | padding-left: 40px; 42 | height: 33px; 43 | line-height: 33px; 44 | font-size: 15px; 45 | font-weight: bold; 46 | .pseudo-icon(@eicons-var-plus, #fff, 11px, left, 15px, top); 47 | } 48 | 49 | a.skin-add-button { 50 | text-decoration: none; 51 | } 52 | -------------------------------------------------------------------------------- /src/ui/css/Dialog.less: -------------------------------------------------------------------------------- 1 | /** 警告框,确认框 */ 2 | .skin-alert-dialog, 3 | .skin-confirm-dialog { 4 | .ui-dialog-body { 5 | padding: 5px 40px; 6 | } 7 | 8 | .ui-dialog-icon-warning, .ui-dialog-icon-confirm { 9 | display: none; 10 | } 11 | 12 | .ui-dialog-cancel-btn { 13 | .skin-link-button(); 14 | width: 30px; 15 | height: @dialog-foot-button-height; 16 | line-height: @dialog-foot-button-height; 17 | } 18 | 19 | .ui-dialog-text { 20 | margin-top: 0; 21 | line-height: 18px; 22 | margin-right: 0; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/ui/css/DrawerActionPanel.less: -------------------------------------------------------------------------------- 1 | @import "./mixin.less"; 2 | 3 | @ui-class-prefix: ui; 4 | @drawer-action-panel-offset: 220px; 5 | 6 | .@{ui-class-prefix}-draweractionpanel { 7 | position: fixed; 8 | left: 0; 9 | right: 0; 10 | top: 0; 11 | bottom: 0; 12 | margin-left: @drawer-action-panel-offset; 13 | overflow: auto; 14 | z-index: 1000; 15 | .transition(all .5s); 16 | 17 | &-hidden { 18 | display: block !important; 19 | visibility: hidden; 20 | .translate(100% 0); 21 | .translate(100% 0 0); 22 | } 23 | } 24 | 25 | .@{ui-class-prefix}-draweractionpanel-mask { 26 | display: none; 27 | position: fixed; 28 | top: 0; 29 | left: 0; 30 | right: 0; 31 | bottom: 0; 32 | background: #333; 33 | opacity: .2; 34 | filter: alpha(opacity=20); 35 | z-index: 999; 36 | } 37 | 38 | .@{ui-class-prefix}-draweractionpanel-close-btn { 39 | position: absolute; 40 | right: 20px; 41 | top: 20px; 42 | width: 16px; 43 | height: 16px; 44 | font-size: 16px; 45 | color: #bbb; 46 | cursor: pointer; 47 | } 48 | 49 | esui-drawer-action-panel { 50 | display: block; 51 | } -------------------------------------------------------------------------------- /src/ui/css/Pager.less: -------------------------------------------------------------------------------- 1 | .skin-frontPager-pager { 2 | .ui-pager-select-wrapper, 3 | .ui-pager-item, 4 | .ui-pager-item-omit { 5 | display: none; 6 | } 7 | 8 | .ui-pager-item-current { 9 | display: inline-block; 10 | } 11 | } -------------------------------------------------------------------------------- /src/ui/css/Panel.less: -------------------------------------------------------------------------------- 1 | .skin-pointer-panel { 2 | .top-pointer(#dfdfdf, 0, 0); 3 | } -------------------------------------------------------------------------------- /src/ui/css/Uploader.less: -------------------------------------------------------------------------------- 1 | esui-uploader { 2 | display: block; 3 | } 4 | 5 | .fill-all () { 6 | // 占满空间 7 | position: absolute; 8 | top: 0; 9 | bottom: 0; 10 | left: 0; 11 | right: 0; 12 | } 13 | 14 | .ui-uploader { 15 | position: relative; 16 | display: inline-block; 17 | *display: inline; 18 | zoom: 1; 19 | margin: 0; // 重置IE6-7 20 | font-size: 12px; 21 | overflow: hidden; 22 | 23 | .ui-uploader-button { 24 | .fill-all (); 25 | background-image: -webkit-linear-gradient(top, #fff, #f6f6f6); 26 | background-image: -moz-linear-gradient(top, #fff, #f6f6f6); 27 | background-image: -ms-linear-gradient(top, #fff, #f6f6f6); 28 | background-image: -o-linear-gradient(top, #fff, #f6f6f6); 29 | background-image: linear-gradient(top, #fff, #f6f6f6); 30 | background-color: #fff; 31 | border: 1px solid #CCC; 32 | border-radius: 3px; 33 | vertical-align: top; 34 | text-align: center; 35 | display: block; 36 | } 37 | 38 | input { 39 | opacity: 0; 40 | filter: alpha(opacity=0); 41 | .fill-all (); 42 | z-index: 1; 43 | border: 0; 44 | padding: 0; 45 | margin: 0; 46 | width: 100px; 47 | font-size: 50px; 48 | } 49 | 50 | iframe { 51 | display: none; 52 | } 53 | 54 | .ui-uploader-indicator { 55 | // 这个要用来显示进度,不能用left + right的形式撑满 56 | position: absolute; 57 | top: 0; 58 | left: 0; 59 | width: 100%; 60 | height: 100%; 61 | display: none; 62 | text-align: center; 63 | } 64 | } 65 | 66 | .ui-uploader-busy { 67 | input, 68 | .ui-uploader-button { 69 | display: none; 70 | } 71 | 72 | .ui-uploader-indicator { 73 | display: block; 74 | 75 | span { 76 | width: 100%; 77 | } 78 | } 79 | } 80 | 81 | .ui-uploader-complete { 82 | .ui-uploader-indicator { 83 | display: block; 84 | background-color: #3892e3; 85 | } 86 | } 87 | 88 | // 装了`ModernUpload`扩展以后的效果 89 | .ui-uploader-droppable, 90 | .ui-uploader-accepting { 91 | 92 | .ui-uploader-button { 93 | display: none; 94 | } 95 | 96 | .ui-uploader-indicator { 97 | display: table; 98 | 99 | span { 100 | display: table-cell; 101 | vertical-align: middle; 102 | line-height: normal !important; 103 | border: 2px dashed green; 104 | border-radius: 6px; 105 | } 106 | } 107 | } 108 | 109 | .ui-uploader-progress { 110 | .ui-uploader-indicator { 111 | background-image: none; 112 | background-color: #3892e3; 113 | 114 | span { 115 | display: none; 116 | } 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /src/ui/css/Warn.less: -------------------------------------------------------------------------------- 1 | @warn-height: 95px; 2 | @warn-button-height: 35px; 3 | 4 | .ui-warn { 5 | position: absolute; 6 | left: 0; 7 | right: 0; 8 | bottom: 0; 9 | height: @warn-height; 10 | 11 | z-index: 3000; 12 | background: #fff5d9; 13 | .opacity(90); 14 | border-top: 1px solid #ececec; 15 | font-size: 14px; 16 | 17 | padding: 0 29px; 18 | 19 | .clearfix(); 20 | 21 | .ui-warn-icon, 22 | .ui-warn-content, 23 | .ui-warn-operation-field { 24 | float: left; 25 | } 26 | 27 | .ui-warn-icon { 28 | margin-right: 8px; 29 | line-height: @warn-height; 30 | font-size: 23px; 31 | color: #f8df92; 32 | } 33 | 34 | .ui-warn-content { 35 | line-height: @warn-height; 36 | margin-right: 60px; 37 | color: #333; 38 | } 39 | 40 | .ui-warn-operation-field { 41 | margin-top: (@warn-height - @warn-button-height)/2; 42 | 43 | .ui-button { 44 | height: @warn-button-height; 45 | line-height: @warn-button-height; 46 | padding: 0 19px; 47 | } 48 | 49 | .ui-warn-ok-btn { 50 | margin-right: 30px; 51 | } 52 | } 53 | } 54 | 55 | .ui-warn { 56 | bottom: 0; 57 | } 58 | -------------------------------------------------------------------------------- /src/ui/css/main.less: -------------------------------------------------------------------------------- 1 | @import "mixin"; 2 | @import "spriteIconMixin"; 3 | 4 | // 对esui-flat-theme的产品线样式定义 5 | @import "esui-flat-theme"; 6 | 7 | // 产品线通用的esui控件皮肤 8 | @import "Button"; 9 | @import "Dialog"; 10 | @import "Pager"; 11 | @import "Panel"; 12 | 13 | // 联盟产品线通用的自定义控件样式 14 | @import "Uploader"; 15 | @import "Warn"; 16 | 17 | @import "DrawerActionPanel"; -------------------------------------------------------------------------------- /src/ui/css/mixin.less: -------------------------------------------------------------------------------- 1 | // 浮动 2 | .fl (@type:left) { 3 | float: @type; 4 | } 5 | 6 | // 清除浮动 7 | .clear-float () { 8 | display: block; 9 | &:after { 10 | content: "."; 11 | display: block; 12 | height: 0; 13 | clear: both; 14 | visibility: hidden; 15 | } 16 | } 17 | 18 | // 背景函数 19 | .background (@image-url, @repeat-type:no-repeat, @s-x:0, @s-y:0, @b-color:transparent) { 20 | @url: ~"@{image-url}"; 21 | @is-url-exp: ~`/^url\([^()]+\)$/i.test("@{url}") ? 'true' : 'false'`; 22 | 23 | .background-image(@is-url-exp) when not (@is-url-exp) { 24 | background-image: url(@url); 25 | } 26 | .background-image(@is-url-exp) when (@is-url-exp) { 27 | background-image: @url; 28 | } 29 | .background-image(@is-url-exp); 30 | 31 | background-repeat: @repeat-type; 32 | background-position: @s-x @s-y; 33 | background-color: @b-color; 34 | } 35 | 36 | 37 | // CSS3 相关 38 | 39 | 40 | // 阴影 41 | // @x 横轴偏移 42 | // @y 纵轴偏移 43 | // @blur 阴影偏移 44 | // @color 阴影颜色 45 | .box-shadow (...) { 46 | -moz-box-shadow: @arguments; 47 | -webkit-box-shadow: @arguments; 48 | box-shadow: @arguments; 49 | } 50 | 51 | 52 | 53 | // 盒模型计算模式 54 | // @type border-box | content-box 55 | .box-sizing (@type:border-box) { 56 | -webkit-box-sizing: @type; 57 | -moz-box-sizing: @type; 58 | -ms-box-sizing: @type; 59 | box-sizing: @type; 60 | } 61 | 62 | 63 | // 圆角 64 | .border-radius (...) { 65 | -moz-border-radius: @arguments; 66 | -webkit-border-radius: @arguments; 67 | border-radius: @arguments; 68 | } 69 | 70 | 71 | 72 | // 变换 73 | .transition (...) { 74 | -moz-transition: @arguments; 75 | -webkit-transition: @arguments; 76 | transition: @arguments; 77 | } 78 | 79 | // 解决input输入框在IE7下 80 | // 显示height(width)= 设置height(width) + 上下(左右)padding和 + 上下(左右)border和 81 | // 的不一致 82 | 83 | 84 | 85 | // 设置尺寸为整体尺寸 86 | // @height 设置高度 87 | // @width 设置宽度 88 | // @tb-padding 上下padding和 89 | // @lr-padding 左右padding和 90 | // @tb-border 上下border和 91 | // @lr-border 左右border和 92 | // @TODO 是否将line-height置为可配置? 93 | .input-outter-size (@height, @width, @tb-padding, @lr-padding, @tb-border, @lr-border) { 94 | height: @height; 95 | line-height: 1; 96 | width: @width; 97 | *height: (@height - @tb-padding - @tb-border); 98 | *line-height: (@height - @tb-padding - @tb-border); 99 | *width: (@width - @lr-padding - @lr-border); 100 | .box-sizing(border-box); 101 | } 102 | 103 | .input-outter-size-height (@height, @tb-padding, @tb-border) { 104 | height: @height; 105 | line-height: 1; 106 | *height: (@height - @tb-padding - @tb-border); 107 | *line-height: (@height - @tb-padding - @tb-border); 108 | .box-sizing(border-box); 109 | } 110 | 111 | 112 | // 设置尺寸为内部显示尺寸 113 | // @height 设置高度 114 | // @width 设置宽度 115 | .input-inner-size (@height, @width) { 116 | height: @height; 117 | line-height: @height; 118 | width: @width; 119 | .box-sizing(content-box); 120 | } 121 | 122 | 123 | // 提供inline-block的兼容显示效果 124 | .inline-block () { 125 | display: inline-block; 126 | *display: inline; 127 | zoom: 1; 128 | } 129 | 130 | // 隐藏元素内文字 131 | .indent-text () { 132 | text-indent: -500%; 133 | // IE6,7 下使用text-indent会导致内容消失,因此做下hack 134 | *text-indent: 0; 135 | *font-size: 0; 136 | *line-height: 0; 137 | } 138 | 139 | .interactive-cursor () { 140 | cursor: pointer; 141 | } 142 | 143 | .ellipse-overflow () { 144 | overflow: hidden; 145 | text-overflow: ellipsis; 146 | white-space: nowrap; 147 | word-wrap: normal; 148 | } 149 | 150 | // 重设列表的样式 151 | .reset-list () { 152 | padding: 0; 153 | margin: 0; 154 | list-style: none; 155 | } 156 | 157 | // Opacity 158 | .opacity (@opacity) { 159 | opacity: @opacity / 100; 160 | filter: ~"alpha(opacity=@{opacity})"; 161 | } 162 | 163 | // 文字选择 164 | .user-select (@type:none) { 165 | // 火狐 166 | -moz-user-select: @type; 167 | // webkit浏览器 168 | -webkit-user-select: @type; 169 | // IE10 170 | -ms-user-select: @type; 171 | // 早期浏览器 172 | -khtml-user-select: @type; 173 | user-select: @type; 174 | } 175 | 176 | 177 | // 提示层气泡尖角 178 | .top-pointer(@color, @left, @right) { 179 | border-top: 3px solid @color; 180 | position: relative; 181 | &:before { 182 | border-style: solid; 183 | border-width: 0 5px 5px; 184 | border-color: transparent transparent @color transparent; 185 | content: ""; 186 | position: absolute; 187 | top: 0; 188 | margin-top: -8px; 189 | } 190 | } 191 | 192 | .top-pointer(@color, @left, @right) when (@left > 0) { 193 | &:before { 194 | left: @left; 195 | } 196 | } 197 | 198 | .top-pointer(@color, @left, @right) when (@right > 0) { 199 | &:before { 200 | right: @right; 201 | } 202 | } 203 | 204 | // 复杂控件的错误提示 205 | .complex-component-validity-invalid() { 206 | border: 1px solid red; 207 | } 208 | 209 | .complex-component-validity-label() { 210 | display:block; 211 | margin-top: 5px; 212 | } 213 | 214 | .complex-component-validity-label-invalid() { 215 | background: #FEDBDC; 216 | border: 1px solid #F0CCCC; 217 | color: #dd6767; 218 | padding: 0 5px; 219 | } -------------------------------------------------------------------------------- /src/ui/css/spriteIconMixin.less: -------------------------------------------------------------------------------- 1 | // 图片图标 2 | .sprite-icon(none) { 3 | background: none; 4 | } 5 | 6 | .sprite-icon(@img) { 7 | @url: ~"@{img}"; 8 | background: @url scroll 0 0 no-repeat transparent; 9 | } 10 | 11 | // 字体图标 12 | @font-icon-family: FontAwesome; 13 | .font-icon(@font-content, @size, @color) { 14 | font-family: @font-icon-family; 15 | font-size: @size; 16 | color: @color; 17 | -webkit-font-smoothing: antialiased; 18 | -moz-osx-font-smoothing: grayscale; 19 | content: "@{img}"; 20 | // 重置indent,抵消外部可能的覆盖 21 | text-indent: 0; 22 | } 23 | 24 | // repeat背景 25 | .repeat-x(@img, @bg-color:transparent) { 26 | background: @img scroll 0 0 repeat-x @bg-color; 27 | } 28 | 29 | // 一般元素型icon 30 | .normal-icon (@img, @width:0, @height:0) { 31 | .sprite-icon(@img); 32 | width: @width; 33 | height: @height; 34 | } 35 | 36 | .pseudo-icon() { 37 | &:after { 38 | .sprite-icon(none); 39 | } 40 | } 41 | // 参数多态,主要通过第二个参数判断 42 | // 1. 如果第二个参数是颜色值,则代表是字体图标,此时: 43 | // 第二个参数是字体背景;第三个参数代表字体宽度; 44 | // 2. 如果第二个参数不是颜色值,则代表是图片图标,此时 45 | // 第二个参数是图片宽度;第三个参数代表图片高度 46 | // 47 | .pseudo-icon (@img, @width:0, @height:0, @horizontal:left, @hDuration:0, @vertical:center, @vDuration:0, @relativePos:relative) { 48 | .relative(@relativePos) when not (@relativePos = none) { 49 | position: @relativePos; 50 | } 51 | .relative(@relativePos); 52 | &:after { 53 | position: absolute; 54 | // 判断是否是字体图标 55 | @is-font-icon: ~`/^#[A-Za-z0-9]+$/i.test("@{width}") ? 'true' : 'false'`; 56 | 57 | .icon(@img) when (@is-font-icon) { 58 | @size: @height; 59 | @color: @width; 60 | .font-icon(@img, @size, @color); 61 | } 62 | .icon(@img) when not (@is-font-icon) { 63 | width: @width; 64 | height: @height; 65 | content: '~"@{img}"'; 66 | white-space: nowrap; 67 | text-indent: -9999px; 68 | overflow: hidden; 69 | .sprite-icon(@img); 70 | } 71 | 72 | .icon(@img); 73 | 74 | .position-vertical(@vertical) when (@vertical = center), (@is-font-icon = 'true') { 75 | top: 50%; 76 | margin-top: -(@height/2); 77 | line-height: @height; 78 | } 79 | 80 | .position-vertical(@vertical) when (@vertical = center), (@is-font-icon = 'false') { 81 | top: 50%; 82 | margin-top: -(@height/2); 83 | } 84 | 85 | .position-vertical(@vertical) when (@vertical = line-height) { 86 | line-height: inherit; 87 | } 88 | 89 | .position-vertical(@vertical) when not (@vertical = center) and not (@vertical = line-height) { 90 | @{vertical}: @vDuration; 91 | line-height: @height; 92 | } 93 | 94 | .position-horizontal(@horizontal) when (@horizontal = center), (@is-font-icon = 'true') { 95 | left: 50%; 96 | margin-left: -(@height/2); 97 | } 98 | 99 | .position-horizontal(@horizontal) when (@horizontal = center), (@is-font-icon = 'false') { 100 | left: 50%; 101 | margin-left: -(@width/2); 102 | } 103 | 104 | .position-horizontal(@horizontal) when not (@horizontal = center) { 105 | @{horizontal}: @hDuration; 106 | } 107 | 108 | .position-vertical(@vertical); 109 | .position-horizontal(@horizontal); 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /src/ui/extension/AutoSubmit.js: -------------------------------------------------------------------------------- 1 | /** 2 | * UB RIA Base 3 | * Copyright 2013 Baidu Inc. All rights reserved. 4 | * 5 | * @file 让输入控件在特定事件下自动提交表单的扩展 6 | * @author otakustay 7 | */ 8 | define( 9 | function (require) { 10 | var u = require('../../util'); 11 | var lib = require('esui/lib'); 12 | var ui = require('esui'); 13 | var Form = require('esui/Form'); 14 | 15 | var store = {}; 16 | 17 | /** 18 | * 让输入控件在特定事件下自动提交表单的扩展 19 | * 20 | * @class ui.extension.AutoSubmit 21 | * @extends esui.Extension 22 | */ 23 | var exports = {}; 24 | 25 | /** 26 | * @constructs ui.extension.AutoSubmit 27 | * @param {Object} [options] 配置项 28 | * @override 29 | */ 30 | exports.constructor = function (options) { 31 | options = options || {}; 32 | if (typeof options.events === 'string') { 33 | options.events = u.map( 34 | lib.splitTokenList(options.events), 35 | lib.trim 36 | ); 37 | } 38 | 39 | this.$super(arguments); 40 | }; 41 | 42 | /** 43 | * 扩展的类型,始终为`"AutoSubmit"` 44 | * 45 | * @member ui.extension.AutoSubmit#type 46 | * @type {string} 47 | * @readonly 48 | * @override 49 | */ 50 | exports.type = 'AutoSubmit'; 51 | 52 | /** 53 | * 指定对应的表单的id,不指定的话会进行自动查找,使用包含当前控件的main元素的`Form`控件 54 | * 55 | * @member ui.extension.AutoSubmit#form 56 | * @type {string | null} 57 | */ 58 | exports.form = null; 59 | 60 | /** 61 | * 指定用于提交表单的事件名称,默认为`click`、`change`和`search`事件 62 | * 63 | * @member ui.extension.AutoSubmit#events 64 | * @type {string[]} 65 | */ 66 | exports.events = ['click', 'change', 'search']; 67 | 68 | /** 69 | * 找到控件对应的`Form`控件 70 | * 71 | * @protected 72 | * @method ui.extension.AutoSubmit#resolveForm 73 | * @return {esui.Form} 74 | */ 75 | exports.resolveForm = function () { 76 | if (this.form) { 77 | return this.target.viewContext.get(this.form); 78 | } 79 | 80 | // 如果没指定表单,就沿DOM结构向上找一个表单控件 81 | var element = this.target && this.target.main && this.target.main.parentNode; 82 | while (element) { 83 | var control = ui.getControlByDOM(element); 84 | if (control && control instanceof Form) { 85 | return control; 86 | } 87 | element = element.parentNode; 88 | } 89 | 90 | return null; 91 | }; 92 | 93 | /** 94 | * 提交表单 95 | * 96 | * @param {esui.Control} this 触发事件的控件 97 | */ 98 | function submit() { 99 | var form = this.resolveForm(); 100 | if (form) { 101 | form.validateAndSubmit(); 102 | } 103 | } 104 | 105 | /** 106 | * @override 107 | */ 108 | exports.activate = function () { 109 | var handler = submit; 110 | // 对指定的 form 进行 debounce 限制 111 | if (this.form && this.debounce) { 112 | handler = store[this.form] || u.debounce(submit, 0); 113 | store[this.form] = handler; 114 | } 115 | u.each( 116 | this.events, 117 | function (eventName) { 118 | this.target.on(eventName, handler, this); 119 | }, 120 | this 121 | ); 122 | 123 | this.$super(arguments); 124 | }; 125 | 126 | /** 127 | * @override 128 | */ 129 | exports.inactivate = function () { 130 | var handler = this.form && this.debounce ? store[this.form] : submit; 131 | u.each( 132 | this.events, 133 | function (eventName) { 134 | this.target.un(eventName, handler, this); 135 | }, 136 | this 137 | ); 138 | 139 | this.$super(arguments); 140 | }; 141 | 142 | var Extension = require('esui/Extension'); 143 | var AutoSubmit = require('eoo').create(Extension, exports); 144 | 145 | require('esui').registerExtension(AutoSubmit); 146 | 147 | return AutoSubmit; 148 | } 149 | ); 150 | -------------------------------------------------------------------------------- /src/ui/extension/ExternSearch.js: -------------------------------------------------------------------------------- 1 | /** 2 | * UB RIA Base 3 | * Copyright 2013 Baidu Inc. All rights reserved. 4 | * 5 | * @file 使用外部搜索控件代理控件的搜索的扩展 6 | * @author lixiang 7 | */ 8 | define( 9 | function (require) { 10 | var lib = require('esui/lib'); 11 | 12 | /** 13 | * 使用外部搜索控件代理控件的搜索 14 | * 15 | * @class ui.extension.ExternSearch 16 | * @extends esui.Extension 17 | */ 18 | var exports = {}; 19 | 20 | /** 21 | * @constructs ui.extends.ExternSearch 22 | * @param {Object} [options] 配置项 23 | * @override 24 | */ 25 | exports.constructor = function (options) { 26 | options = options || {}; 27 | 28 | this.$super(arguments); 29 | }; 30 | 31 | /** 32 | * 扩展的类型,始终为`"ExternSearch"` 33 | * 34 | * @member ui.extension.ExternSearch#type 35 | * @type {string} 36 | * @readonly 37 | * @override 38 | */ 39 | exports.type = 'ExternSearch'; 40 | 41 | /** 42 | * 指定对应的searchBox的id 43 | * 44 | * @member ui.extension.ExternSearch#searchBox 45 | * @type {string | null} 46 | */ 47 | exports.searchBox = null; 48 | 49 | /** 50 | * 找到控件对应的搜索类控件 51 | * 52 | * @protected 53 | * @method ui.extension.ExternSearch#resolveControl 54 | * @return {esui.SearchBox} 55 | */ 56 | exports.resolveControl = function () { 57 | var searchBox; 58 | // 这个searchbox为了向前兼容。。。 59 | if (!this.searchBox && this.searchbox) { 60 | this.searchBox = this.searchbox; 61 | } 62 | 63 | if (this.searchBox) { 64 | searchBox = this.target.viewContext.get(this.searchBox); 65 | // 只有扩展处于激活状态才抛异常 66 | if (!searchBox && this.active) { 67 | throw new Error('Cannot find related searchBox "#' + searchBox + '" in view context'); 68 | } 69 | } 70 | else { 71 | throw new Error('searchBox cannot be null'); 72 | } 73 | 74 | return searchBox; 75 | }; 76 | 77 | /** 78 | * @override 79 | */ 80 | exports.activate = function () { 81 | var searchBox = this.resolveControl(); 82 | searchBox.on('search', search, this); 83 | 84 | // 接收控件内清空搜索操作 85 | this.target.on('clearquery', clearQuery, this); 86 | // 接收控件的search事件 87 | this.target.on('search', doSearch, this); 88 | 89 | this.$super(arguments); 90 | }; 91 | 92 | function search(e) { 93 | this.target.search(); 94 | } 95 | 96 | function doSearch(e) { 97 | var searchBox = this.resolveControl(); 98 | var filter = {value: searchBox.getValue()}; 99 | // 外部searchbox是不有配搜索包含关键字段 100 | if (searchBox.dataKeys) { 101 | filter.keys = lib.splitTokenList(searchBox.dataKeys); 102 | } 103 | e.filterData.push(filter); 104 | e.preventDefault(); 105 | } 106 | 107 | function clearQuery() { 108 | var searchBox = this.resolveControl(); 109 | searchBox.set('text', ''); 110 | } 111 | 112 | /** 113 | * @override 114 | */ 115 | exports.inactivate = function () { 116 | this.$super(arguments); 117 | 118 | var searchBox = this.resolveControl(); 119 | if (searchBox) { 120 | searchBox.un('search', search, this); 121 | } 122 | 123 | this.target.un('clearquery', clearQuery, this); 124 | this.target.un('search', doSearch, this); 125 | }; 126 | 127 | var Extension = require('esui/Extension'); 128 | var ExternSearch = require('eoo').create(Extension, exports); 129 | 130 | require('esui').registerExtension(ExternSearch); 131 | 132 | return ExternSearch; 133 | } 134 | ); 135 | -------------------------------------------------------------------------------- /src/ui/extension/ExternSelect.js: -------------------------------------------------------------------------------- 1 | /** 2 | * UB RIA Base 3 | * Copyright 2013 Baidu Inc. All rights reserved. 4 | * 5 | * @file 使用外部下拉选择控件代理控件的搜索 6 | * @author lixiang 7 | */ 8 | define( 9 | function (require) { 10 | var u = require('../../util'); 11 | var lib = require('esui/lib'); 12 | 13 | /** 14 | * 使用外部下拉选择控件代理控件的搜索 15 | * 16 | * @class ui.extension.ExternSelect 17 | * @extends esui.Extension 18 | */ 19 | var exports = {}; 20 | 21 | /** 22 | * @constructs ui.extends.ExternSelect 23 | * @param {Object} [options] 配置项 24 | * @override 25 | */ 26 | exports.constructor = function (options) { 27 | options = options || {}; 28 | 29 | this.$super(arguments); 30 | }; 31 | 32 | /** 33 | * 扩展的类型,始终为`"ExternSelect"` 34 | * 35 | * @member ui.extension.ExternSelect#type 36 | * @type {string} 37 | * @readonly 38 | * @override 39 | */ 40 | exports.type = 'ExternSelect'; 41 | 42 | /** 43 | * 指定对应的一组select的id, 逗号或空格分隔,必须指定 44 | * 45 | * @member ui.extension.ExternSelect#selects 46 | * @type {string | null} 47 | */ 48 | exports.selects = null; 49 | 50 | /** 51 | * 找到代理控件 52 | * 53 | * @protected 54 | * @method ui.extension.ExternSelect#resolveControls 55 | * @return {esui.SearchBox} 56 | */ 57 | exports.resolveControls = function () { 58 | var controls = []; 59 | if (this.selects) { 60 | var selects; 61 | if (u.isString(this.selects)) { 62 | selects = lib.splitTokenList(this.selects); 63 | } 64 | else { 65 | selects = this.selects; 66 | } 67 | if (u.isArray(selects)) { 68 | u.each( 69 | selects, 70 | function (select, index) { 71 | select = this.target.viewContext.get(select); 72 | if (select) { 73 | controls.push(select); 74 | } 75 | // 只有扩展处于激活状态才抛异常 76 | else if (this.active) { 77 | throw new Error('Cannot find related select "#' + select + '" in view context'); 78 | } 79 | }, 80 | this 81 | ); 82 | } 83 | else { 84 | throw new Error('selects can only be Array or String'); 85 | } 86 | } 87 | else { 88 | throw new Error('selects cannot be null'); 89 | } 90 | 91 | return controls; 92 | }; 93 | 94 | /** 95 | * @override 96 | */ 97 | exports.activate = function () { 98 | this.handleControls( 99 | function (select, index) { 100 | select.on('change', search, this); 101 | } 102 | ); 103 | 104 | // 接收控件内清空搜索操作 105 | this.target.on('clearquery', clearQuery, this); 106 | // 接收控件的search事件 107 | this.target.on('search', doSearch, this); 108 | 109 | this.$super(arguments); 110 | }; 111 | 112 | function search(e) { 113 | this.target.search(); 114 | } 115 | 116 | function doSearch(e) { 117 | this.handleControls( 118 | function (select, index) { 119 | var item = select.getSelectedItem(); 120 | if (item.value !== '' && select.dataKey) { 121 | e.filterData.push({keys: [select.dataKey], value: item.value}); 122 | } 123 | } 124 | ); 125 | 126 | e.preventDefault(); 127 | } 128 | 129 | function clearQuery() { 130 | this.handleControls( 131 | function (select) { 132 | select.un('change', search, this); 133 | select.setProperties({selectedIndex: 0}); 134 | select.on('change', search, this); 135 | } 136 | ); 137 | } 138 | 139 | /** 140 | * @override 141 | */ 142 | exports.inactivate = function () { 143 | this.$super(arguments); 144 | 145 | this.handleControls( 146 | function (select) { 147 | select.un('change', search, this); 148 | }, 149 | true 150 | ); 151 | 152 | this.target.un('clearquery', clearQuery, this); 153 | this.target.un('search', doSearch, this); 154 | }; 155 | 156 | /** 157 | * 搜索控件的处理函数 158 | * 159 | * @param {Function} handler 处理句柄 160 | */ 161 | exports.handleControls = function (handler) { 162 | var controls = this.resolveControls(); 163 | if (controls.length) { 164 | u.each(controls, handler, this); 165 | } 166 | }; 167 | 168 | var Extension = require('esui/Extension'); 169 | var ExternSelect = require('eoo').create(Extension, exports); 170 | 171 | require('esui').registerExtension(ExternSelect); 172 | 173 | return ExternSelect; 174 | } 175 | ); 176 | -------------------------------------------------------------------------------- /src/ui/extension/OverrideDefaults.js: -------------------------------------------------------------------------------- 1 | /** 2 | * UB RIA Base 3 | * Copyright 2013 Baidu Inc. All rights reserved. 4 | * 5 | * @file 重写控件默认配置用的扩展 6 | * @author otakustay 7 | */ 8 | define( 9 | function (require) { 10 | var u = require('../../util'); 11 | 12 | /** 13 | * 重写控件默认配置用的扩展 14 | * 15 | * @class ui.extension.OverrideDefaults 16 | * @extends esui.Extension 17 | */ 18 | var exports = {}; 19 | 20 | /** 21 | * 扩展的类型,始终为`"OverrideDefaults"` 22 | * 23 | * @member ui.extension.OverrideDefaults#type 24 | * @type {string} 25 | * @readonly 26 | * @override 27 | */ 28 | exports.type = 'OverrideDefaults'; 29 | 30 | /** 31 | * @override 32 | */ 33 | exports.activate = function () { 34 | this.target.on('init', onInit, this); 35 | 36 | this.$super(arguments); 37 | }; 38 | 39 | /** 40 | * @override 41 | */ 42 | exports.inactivate = function () { 43 | this.target.un('init', onInit, this); 44 | 45 | this.$super(arguments); 46 | }; 47 | 48 | function onInit(e) { 49 | this.overrideDefaults(e.options); 50 | } 51 | 52 | /** 53 | * 重写默认属性 54 | * 55 | * @protected 56 | * @method ui.extension.OverrideDefaults#overrideDefaults 57 | * @param {Object} [rawOptions] 初始化控件时传入的参数 58 | */ 59 | exports.overrideDefaults = function (rawOptions) { 60 | // 只有初始化时没有显式指定的才覆盖 61 | var overrides = u.omit(this.overrides[this.target.type], u.keys(rawOptions || {})); 62 | if (overrides) { 63 | this.target.setProperties(overrides); 64 | } 65 | }; 66 | 67 | var Extension = require('esui/Extension'); 68 | var OverrideDefaults = require('eoo').create(Extension, exports); 69 | 70 | require('esui').registerExtension(OverrideDefaults); 71 | 72 | return OverrideDefaults; 73 | } 74 | ); 75 | -------------------------------------------------------------------------------- /src/ui/extension/TrimInput.js: -------------------------------------------------------------------------------- 1 | /** 2 | * UB RIA Base 3 | * Copyright 2013 Baidu Inc. All rights reserved. 4 | * 5 | * @file 文本框控件自动去除首尾空格 6 | * @author lixiang(lixiang05@baidu.com) 7 | */ 8 | define( 9 | function (require) { 10 | var lib = require('esui/lib'); 11 | var InputControl = require('esui/InputControl'); 12 | var main = require('esui/main'); 13 | 14 | /** 15 | * 文本框自动去除首尾空格扩展 16 | * 17 | * @class ui.extension.TrimInput 18 | * @extends esui.Extension 19 | */ 20 | var exports = {}; 21 | 22 | /** 23 | * 扩展的类型,始终为`"TrimInput"` 24 | * 25 | * @member ui.extension.TrimInput#type 26 | * @type {string} 27 | * @readonly 28 | * @override 29 | */ 30 | exports.type = 'TrimInput'; 31 | 32 | /** 33 | * @override 34 | */ 35 | exports.activate = function () { 36 | var target = this.target; 37 | // 暂时只对`InputControl`控件生效 38 | if (!(target instanceof InputControl)) { 39 | return; 40 | } 41 | 42 | target.on('afterrender', trim, this); 43 | target.on('change', trim, this); 44 | 45 | this.$super(arguments); 46 | }; 47 | 48 | /** 49 | * @override 50 | */ 51 | exports.inactivate = function () { 52 | var target = this.target; 53 | // 只对`InputControl`控件生效 54 | if (!(target instanceof InputControl)) { 55 | return; 56 | } 57 | 58 | target.un('afterrender', trim, this); 59 | target.un('change', trim, this); 60 | 61 | this.$super(arguments); 62 | }; 63 | 64 | function trim() { 65 | var trimedValue = lib.trim(this.target.getValue()); 66 | this.target.setValue(trimedValue); 67 | } 68 | 69 | var Extension = require('esui/Extension'); 70 | var TrimInput = require('eoo').create(Extension, exports); 71 | 72 | main.registerExtension(TrimInput); 73 | 74 | return TrimInput; 75 | } 76 | ); 77 | -------------------------------------------------------------------------------- /src/ui/extension/WordCount.js: -------------------------------------------------------------------------------- 1 | /** 2 | * UB RIA Base 3 | * Copyright 2013 Baidu Inc. All rights reserved. 4 | * 5 | * @file 计算文本框可输入字符的扩展 6 | * @author otakustay 7 | */ 8 | define( 9 | function (require) { 10 | var lib = require('esui/lib'); 11 | var Validity = require('esui/validator/Validity'); 12 | 13 | /** 14 | * 计算文本框可输入字符的扩展 15 | * 16 | * @class ui.extension.WordCount 17 | * @extends esui.Extension 18 | */ 19 | var exports = {}; 20 | 21 | /** 22 | * 扩展的类型,始终为`"WordCount"` 23 | * 24 | * @member ui.extension.WordCount#type 25 | * @type {string} 26 | * @readonly 27 | * @override 28 | */ 29 | exports.type = 'WordCount'; 30 | 31 | /** 32 | * 设置未输入字符时的提示信息模板,可用以下占位符: 33 | * 34 | * - `${available}`:表示可输入字符个数 35 | * - `${current}`:表示已输入的字符个数 36 | * - `${max}`:表示最大可输入字符个数 37 | * 38 | * @member ui.extension.WordCount#initialTemplate 39 | * @type {string} 40 | */ 41 | exports.initialTemplate = '最多可输入${available}个字符'; 42 | 43 | /** 44 | * 设置还可以输入字符时的提示信息模板,可用以下占位符: 45 | * 46 | * - `${available}`:表示剩余字符个数 47 | * - `${current}`:表示已输入的字符个数 48 | * - `${max}`:表示最大可输入字符个数 49 | * 50 | * @member ui.extension.WordCount#remainingTemplate 51 | * @type {string} 52 | */ 53 | exports.remainingTemplate = '还可输入${available}个字符'; 54 | 55 | /** 56 | * 设置已超出可输入字符限制时的提示信息模板,可用以下占位符: 57 | * 58 | * - `${available}`:表示超出的字符数 59 | * - `${current}`:表示已输入的字符个数 60 | * - `${max}`:表示最大可输入字符个数 61 | * 62 | * @member ui.extension.WordCount#exceededTemplate 63 | * @type {string} 64 | */ 65 | exports.exceededTemplate = '已超出${available}个字符'; 66 | 67 | /** 68 | * 获取提示信息 69 | * 70 | * @protected 71 | * @method ui.extension.WordCount#getHintMessage 72 | * @param {Object} data 长度计算的相关数据 73 | * @param {number} data.available 还可输入的字符个数,已超出时为负数 74 | * @param {number} data.current 已经输入的字符个数 75 | * @param {number} data.max 最大可输入的字符个数 76 | * @return {string} 77 | */ 78 | exports.getHintMessage = function (data) { 79 | var template; 80 | if (!data.current) { 81 | template = this.initialTemplate; 82 | } 83 | else if (data.available >= 0) { 84 | template = this.remainingTemplate; 85 | } 86 | else { 87 | template = this.exceededTemplate; 88 | data.available = -data.available; 89 | } 90 | 91 | return lib.format(template, data); 92 | }; 93 | 94 | /** 95 | * 获取最大可输入字符数 96 | * 97 | * @protected 98 | * @method ui.extension.WordCount#getMaxLength 99 | * @return {number} 100 | */ 101 | exports.getMaxLength = function () { 102 | if (+this.target.get('maxLength') === -1) { 103 | return this.target.get('length'); 104 | } 105 | return this.target.get('maxLength'); 106 | }; 107 | 108 | /** 109 | * 检查长度并显示提示信息 110 | */ 111 | function checkLength() { 112 | var maxLength = this.getMaxLength(); 113 | var currentLength = this.target.getValue().length; 114 | 115 | var data = { 116 | max: maxLength, 117 | current: currentLength, 118 | available: maxLength - currentLength 119 | }; 120 | 121 | var validState = data.available < 0 ? 'error' : 'hint'; 122 | var message = this.getHintMessage(data); 123 | 124 | var validity = new Validity(); 125 | validity.setCustomValidState(validState); 126 | validity.setCustomMessage(message); 127 | 128 | this.target.showValidity(validity); 129 | } 130 | 131 | /** 132 | * @override 133 | */ 134 | exports.activate = function () { 135 | var maxLength = this.getMaxLength(); 136 | 137 | if (maxLength) { 138 | this.target.on('input', checkLength, this); 139 | this.target.on('afterrender', checkLength, this); 140 | } 141 | 142 | this.$super(arguments); 143 | }; 144 | 145 | /** 146 | * @override 147 | */ 148 | exports.inactivate = function () { 149 | this.target.un('input', checkLength, this); 150 | this.target.un('afterrender', checkLength, this); 151 | 152 | this.$super(arguments); 153 | }; 154 | 155 | var Extension = require('esui/Extension'); 156 | var WordCount = require('eoo').create(Extension, exports); 157 | 158 | require('esui').registerExtension(WordCount); 159 | 160 | return WordCount; 161 | } 162 | ); 163 | -------------------------------------------------------------------------------- /src/update.js: -------------------------------------------------------------------------------- 1 | /** 2 | * UB RIA Base 3 | * Copyright 2015 Baidu Inc. All rights reserved. 4 | * 5 | * @file 对象更新辅助方法 6 | * @author otakustay 7 | */ 8 | define( 9 | function (require) { 10 | var u = require('./util'); 11 | 12 | var AVAILABLE_COMMANDS = { 13 | $set: function (oldValue, newValue) { 14 | return newValue; 15 | }, 16 | 17 | $push: function (oldValue, newValue) { 18 | var result = oldValue.slice(); 19 | result.push(newValue); 20 | return result; 21 | }, 22 | 23 | $unshift: function (oldValue, newValue) { 24 | var result = oldValue.slice(); 25 | result.unshift(newValue); 26 | return result; 27 | }, 28 | 29 | $merge: function (oldValue, newValue) { 30 | return u.extend({}, oldValue, newValue); 31 | }, 32 | 33 | $defaults: function (oldValue, newValue) { 34 | return u.defaults(u.clone(oldValue), newValue); 35 | }, 36 | 37 | $invoke: function (oldValue, factory) { 38 | return factory(oldValue); 39 | } 40 | }; 41 | 42 | /** 43 | * 不可变数据的更新辅助函数 44 | * 45 | * 用此模块的函数可以在不修改一个对象的前提下对其进行更新,并获得更新后的新对象 46 | * 47 | * @namespace update 48 | */ 49 | var exports = {}; 50 | 51 | /** 52 | * 根据给定的指令更新一个对象的属性,并返回更新后的新对象,原对象不会被修改 53 | * 54 | * 指令支持以下几种: 55 | * 56 | * - `$set`用于更新属性的值 57 | * - `$push`用于向类型为数组的属性最后位置添加值 58 | * - `$unshift`用于向类型为数组的属性最前位置添加值 59 | * - `$merge`用于在原对象上合并新属性 60 | * - `$invoke`用于执行一个函数获取新的属性值,该函数接收旧的属性值作为唯一的参数 61 | * 62 | * 可以一次使用多个指令更新对象: 63 | * 64 | * ```javascript 65 | * var newObject = update.run( 66 | * source, 67 | * { 68 | * foo: {bar: {$set: 1}}, 69 | * alice: {$push: 1}, 70 | * tom: {jack: {$set: {x: 1}} 71 | * } 72 | * ); 73 | * ``` 74 | * 75 | * @param {Object} source 需要更新的原对象 76 | * @param {Object} commands 更新的指令 77 | * @return {Object} 更新了属性的新对象 78 | */ 79 | exports.run = function (source, commands) { 80 | // 可能是第一层的指令,直接对原数据进行处理,不访问任何属性 81 | var possibleFirstLevelCommand = u.find( 82 | Object.keys(AVAILABLE_COMMANDS), 83 | function (command) { 84 | return commands.hasOwnProperty(command); 85 | } 86 | ); 87 | if (possibleFirstLevelCommand) { 88 | return AVAILABLE_COMMANDS[possibleFirstLevelCommand](source, commands[possibleFirstLevelCommand]); 89 | } 90 | 91 | var result = Object.keys(commands).reduce( 92 | function (result, key) { 93 | var propertyCommand = commands[key]; 94 | // 如果有我们支持的指令,则是针对这一个属性的指令,直接操作 95 | var isCommand = u.any( 96 | AVAILABLE_COMMANDS, 97 | function (execute, command) { 98 | if (propertyCommand.hasOwnProperty(command)) { 99 | result[key] = execute(result[key], propertyCommand[command]); 100 | return true; 101 | } 102 | return false; 103 | } 104 | ); 105 | // 如果没有任何指令,说明是多层的,所以递归 106 | if (!isCommand) { 107 | result[key] = exports.run(result[key] || {}, propertyCommand); 108 | } 109 | 110 | return result; 111 | }, 112 | u.clone(source) 113 | ); 114 | 115 | return result; 116 | }; 117 | 118 | function buildPathObject(path, value) { 119 | if (!path) { 120 | return value; 121 | } 122 | 123 | if (typeof path === 'string') { 124 | path = [path]; 125 | } 126 | 127 | var result = {}; 128 | var current = result; 129 | for (var i = 0; i < path.length - 1; i++) { 130 | current = current[path[i]] = {}; 131 | } 132 | current[path[path.length - 1]] = value; 133 | return result; 134 | } 135 | 136 | /** 137 | * 快捷更新属性的方法,效果相当于使用`update`方法传递`$set`指令 138 | * 139 | * @param {Object} source 待更新的原对象 140 | * @param {string?|Array.} path 属性路径,当路径深度大于1时使用数组,为空或非值则直接对`source`对象操作 141 | * @param {*} value 更新的值 142 | * @return {Object} 更新后的新对象 143 | */ 144 | exports.set = function (source, path, value) { 145 | return exports.run(source, buildPathObject(path, {$set: value})); 146 | }; 147 | 148 | /** 149 | * 快捷更新属性的方法,效果相当于使用`update`方法传递`$push`指令 150 | * 151 | * @param {Object} source 待更新的原对象 152 | * @param {string?|Array.} path 属性路径,当路径深度大于1时使用数组,为空或非值则直接对`source`对象操作 153 | * @param {*} value 更新的值 154 | * @return {Object} 更新后的新对象 155 | */ 156 | exports.push = function (source, path, value) { 157 | return exports.run(source, buildPathObject(path, {$push: value})); 158 | }; 159 | 160 | /** 161 | * 快捷更新属性的方法,效果相当于使用`update`方法传递`$unshift`指令 162 | * 163 | * @param {Object} source 待更新的原对象 164 | * @param {string?|Array.} path 属性路径,当路径深度大于1时使用数组,为空或非值则直接对`source`对象操作 165 | * @param {*} value 更新的值 166 | * @return {Object} 更新后的新对象 167 | */ 168 | exports.unshift = function (source, path, value) { 169 | return exports.run(source, buildPathObject(path, {$unshift: value})); 170 | }; 171 | 172 | /** 173 | * 快捷更新属性的方法,效果相当于使用`update`方法传递`$merge`指令 174 | * 175 | * @param {Object} source 待更新的原对象 176 | * @param {string?|Array.} path 属性路径,当路径深度大于1时使用数组,为空或非值则直接对`source`对象操作 177 | * @param {Object} value 更新的值 178 | * @return {Object} 更新后的新对象 179 | */ 180 | exports.merge = function (source, path, value) { 181 | return exports.run(source, buildPathObject(path, {$merge: value})); 182 | }; 183 | 184 | /** 185 | * 快捷更新属性的方法,效果相当于使用`update`方法传递`$defaults`指令 186 | * 187 | * @param {Object} source 待更新的原对象 188 | * @param {string?|Array.} path 属性路径,当路径深度大于1时使用数组,为空或非值则直接对`source`对象操作 189 | * @param {Object} value 更新的值 190 | * @return {Object} 更新后的新对象 191 | */ 192 | exports.defaults = function (source, path, value) { 193 | return exports.run(source, buildPathObject(path, {$defaults: value})); 194 | }; 195 | 196 | /** 197 | * 快捷更新属性的方法,效果相当于使用`update`方法传递`$invoke`指令 198 | * 199 | * @param {Object} source 待更新的原对象 200 | * @param {string?|Array.} path 属性路径,当路径深度大于1时使用数组,为空或非值则直接对`source`对象操作 201 | * @param {Function} factory 产生新属性值的工厂函数,接受旧属性值为参数 202 | * @return {Object} 更新后的新对象 203 | */ 204 | exports.invoke = function (source, path, factory) { 205 | return exports.run(source, buildPathObject(path, {$invoke: factory})); 206 | }; 207 | 208 | return exports; 209 | } 210 | ); 211 | -------------------------------------------------------------------------------- /test/config.js: -------------------------------------------------------------------------------- 1 | // Test configuration for edp-test 2 | // Generated on Tue Jul 21 2015 14:47:10 GMT+0800 (CST) 3 | module.exports = { 4 | 5 | // base path, that will be used to resolve files and exclude 6 | basePath: '../', 7 | 8 | 9 | // frameworks to use 10 | frameworks: ['jasmine', 'esl'], 11 | 12 | 13 | // list of files / patterns to load in the browser 14 | files: [ 15 | 'test/**/*.spec.js' 16 | ], 17 | 18 | 19 | // list of files to exclude 20 | exclude: [ 21 | 22 | ], 23 | 24 | // optionally, configure the reporter 25 | coverageReporter: { 26 | // text-summary | text | html | json | teamcity | cobertura | lcov 27 | // lcovonly | none | teamcity 28 | type : 'text|html', 29 | dir : 'test/coverage/' 30 | }, 31 | 32 | // web server port 33 | port: 8120, 34 | 35 | 36 | // enable / disable watching file and executing tests whenever any file changes 37 | watch: true, 38 | 39 | 40 | // Start these browsers, currently available: 41 | // - Chrome 42 | // - Firefox 43 | // - Opera 44 | // - Safari 45 | // - PhantomJS 46 | // - IE (only Windows) 47 | browsers: [ 48 | // 'Chrome', 49 | // 'Firefox', 50 | // 'Safari', 51 | 'PhantomJS' 52 | ], 53 | 54 | 55 | // Continuous Integration mode 56 | // if true, it capture browsers, run tests and exit 57 | singleRun: true, 58 | 59 | 60 | // Custom HTML templates 61 | // context | debug | runner 62 | templates: { 63 | // context: 'context.html' 64 | } 65 | }; 66 | -------------------------------------------------------------------------------- /test/spec/update.spec.js: -------------------------------------------------------------------------------- 1 | define( 2 | function (require) { 3 | var update = require('update'); 4 | 5 | function createSourceObject() { 6 | return { 7 | x: { 8 | y: { 9 | z: [1, 2, 3] 10 | } 11 | }, 12 | foo: [1, 2, 3], 13 | alice: 1, 14 | bob: 2, 15 | tom: { 16 | jack: 1 17 | } 18 | }; 19 | } 20 | 21 | describe('update method', function () { 22 | it('should update a single property value', function () { 23 | var source = createSourceObject(); 24 | var result = update.run(source, {alice: {$set: 2}}); 25 | expect(result.alice).toBe(2); 26 | expect(source).toEqual(createSourceObject()); 27 | result.alice = 1; 28 | expect(result).toEqual(source); 29 | }); 30 | 31 | it('shoud update a nested property value', function () { 32 | var source = createSourceObject(); 33 | var result = update.run(source, {tom: {jack: {$set: 2}}}); 34 | expect(result.tom.jack).toBe(2); 35 | expect(source).toEqual(createSourceObject()); 36 | result.tom.jack = 1; 37 | expect(result).toEqual(source); 38 | }); 39 | 40 | it('should create nested property if not exist', function () { 41 | var source = createSourceObject(); 42 | var result = update.run(source, {a: {b: {$set: 2}}}); 43 | expect(result.a.b).toBe(2); 44 | expect(source).toEqual(createSourceObject()); 45 | delete result.a; 46 | expect(result).toEqual(source); 47 | }); 48 | 49 | it('should recognize push command', function () { 50 | var source = createSourceObject(); 51 | var result = update.run(source, {x: {y: {z: {$push: 4}}}}); 52 | expect(result.x.y.z).toEqual([1, 2, 3, 4]); 53 | expect(source).toEqual(createSourceObject()); 54 | result.x.y.z.pop(); 55 | expect(result).toEqual(source); 56 | }); 57 | 58 | it('should recognize unshift command', function () { 59 | var source = createSourceObject(); 60 | var result = update.run(source, {x: {y: {z: {$unshift: 0}}}}); 61 | expect(result.x.y.z).toEqual([0, 1, 2, 3]); 62 | expect(source).toEqual(createSourceObject()); 63 | result.x.y.z.shift(); 64 | expect(result).toEqual(source); 65 | }); 66 | 67 | it('should recognize merge command', function () { 68 | var source = createSourceObject(); 69 | var result = update.run(source, {x: {y: {$merge: {a: 1, b: 2, z: 3}}}}); 70 | expect(result.x.y).toEqual({a: 1, b: 2, z: 3}); 71 | expect(source).toEqual(createSourceObject()); 72 | }); 73 | 74 | it('should recognize defaults command', function () { 75 | var source = createSourceObject(); 76 | var result = update.run(source, {x: {y: {$defaults: {a: 1, b: 2, z: 3}}}}); 77 | expect(result.x.y).toEqual({a: 1, b: 2, z: [1, 2, 3]}); 78 | expect(source).toEqual(createSourceObject()); 79 | }); 80 | 81 | it('should recognize invoke command', function () { 82 | var source = createSourceObject(); 83 | var result = update.run(source, {tom: {jack: {$invoke: function(x) { return x * 2; }}}}); 84 | expect(result.tom.jack).toBe(2); 85 | expect(source).toEqual(createSourceObject()); 86 | }); 87 | 88 | it('should expose set function', function () { 89 | var source = createSourceObject(); 90 | var result = update.set(source, ['tom', 'jack'], 2); 91 | expect(result.tom.jack).toBe(2); 92 | expect(source).toEqual(createSourceObject()); 93 | result.tom.jack = 1; 94 | expect(result).toEqual(source); 95 | }); 96 | 97 | it('should expose push function', function () { 98 | var source = createSourceObject(); 99 | var result = update.push(source, ['x', 'y', 'z'], 4); 100 | expect(result.x.y.z).toEqual([1, 2, 3, 4]); 101 | expect(source).toEqual(createSourceObject()); 102 | result.x.y.z.pop(); 103 | expect(result).toEqual(source); 104 | }); 105 | 106 | it('should expose unshift function', function () { 107 | var source = createSourceObject(); 108 | var result = update.unshift(source, ['x', 'y', 'z'], 0); 109 | expect(result.x.y.z).toEqual([0, 1, 2, 3]); 110 | expect(source).toEqual(createSourceObject()); 111 | result.x.y.z.shift(); 112 | expect(result).toEqual(source); 113 | }); 114 | 115 | it('should expose merge function', function () { 116 | var source = createSourceObject(); 117 | var result = update.merge(source, ['x', 'y'], {a: 1, b: 2, z: 3}); 118 | expect(result.x.y).toEqual({a: 1, b: 2, z: 3}); 119 | expect(source).toEqual(createSourceObject()); 120 | }); 121 | 122 | it('should expose defaults function', function () { 123 | var source = createSourceObject(); 124 | var result = update.defaults(source, ['x', 'y'], {a: 1, b: 2, z: 3}); 125 | expect(result.x.y).toEqual({a: 1, b: 2, z: [1, 2, 3]}); 126 | expect(source).toEqual(createSourceObject()); 127 | }); 128 | 129 | it('should expose invoke function', function () { 130 | var source = createSourceObject(); 131 | var result = update.invoke(source, ['tom', 'jack'], function(x) { return x * 2; }); 132 | expect(result.tom.jack).toBe(2); 133 | expect(source).toEqual(createSourceObject()); 134 | }); 135 | 136 | describe('run with first level command', function () { 137 | it('should work with $set', function () { 138 | var source = {}; 139 | var result = update.run(source, {$set: 1}); 140 | expect(result).toBe(1); 141 | expect(source).toEqual({}); 142 | }); 143 | 144 | it('should work with $push', function () { 145 | var source = [1, 2, 3]; 146 | var result = update.run(source, {$push: 4}); 147 | expect(result).toEqual([1, 2, 3, 4]); 148 | expect(source).toEqual([1, 2, 3]); 149 | }); 150 | 151 | it('should work with $unshift', function () { 152 | var source = [1, 2, 3]; 153 | var result = update.run(source, {$unshift: 0}); 154 | expect(result).toEqual([0, 1, 2, 3]); 155 | expect(source).toEqual([1, 2, 3]); 156 | }); 157 | 158 | it('should work with $merge', function () { 159 | var source = {foo: 1}; 160 | var result = update.run(source, {$merge: {bar: 2}}); 161 | expect(result).toEqual({foo: 1, bar: 2}); 162 | expect(source).toEqual({foo: 1}); 163 | }); 164 | 165 | it('should work with $defaults', function () { 166 | var source = {foo: 1}; 167 | var result = update.run(source, {$defaults: {foo: 2, bar: 2}}); 168 | expect(result).toEqual({foo: 1, bar: 2}); 169 | expect(source).toEqual({foo: 1}); 170 | }); 171 | 172 | it('should work with $invoke', function () { 173 | var source = 1; 174 | var result = update.run(source, {$invoke: function (x) { return x * 2; }}); 175 | expect(result).toEqual(2); 176 | expect(source).toEqual(1); 177 | }); 178 | }); 179 | 180 | describe('shortcut function with first level command', function () { 181 | it('should work with $set', function () { 182 | var source = {}; 183 | var result = update.set(source, null, 1); 184 | expect(result).toBe(1); 185 | expect(source).toEqual({}); 186 | }); 187 | 188 | it('should work with $push', function () { 189 | var source = [1, 2, 3]; 190 | var result = update.push(source, null, 4); 191 | expect(result).toEqual([1, 2, 3, 4]); 192 | expect(source).toEqual([1, 2, 3]); 193 | }); 194 | 195 | it('should work with $unshift', function () { 196 | var source = [1, 2, 3]; 197 | var result = update.unshift(source, null, 0); 198 | expect(result).toEqual([0, 1, 2, 3]); 199 | expect(source).toEqual([1, 2, 3]); 200 | }); 201 | 202 | it('should work with $merge', function () { 203 | var source = {foo: 1}; 204 | var result = update.merge(source, null, {bar: 2}) 205 | expect(result).toEqual({foo: 1, bar: 2}); 206 | expect(source).toEqual({foo: 1}); 207 | }); 208 | 209 | it('should work with $defaults', function () { 210 | var source = {foo: 1}; 211 | var result = update.defaults(source, null, {foo: 2, bar: 2}); 212 | expect(result).toEqual({foo: 1, bar: 2}); 213 | expect(source).toEqual({foo: 1}); 214 | }); 215 | 216 | it('should work with $invoke', function () { 217 | var source = 1; 218 | var result = update.invoke(source, null, function (x) { return x * 2; }); 219 | expect(result).toEqual(2); 220 | expect(source).toEqual(1); 221 | }); 222 | }); 223 | }); 224 | } 225 | ); 226 | -------------------------------------------------------------------------------- /test/spec/util.spec.js: -------------------------------------------------------------------------------- 1 | define( 2 | function (require) { 3 | var util = require('util'); 4 | 5 | describe('transformPlainObjectToStructured method', function () { 6 | it('should clone a real-plain object', function () { 7 | var o = { 8 | x: 1, 9 | y: 2 10 | }; 11 | var result = util.transformPlainObjectToStructured(o); 12 | expect(result).toEqual(o); 13 | }); 14 | 15 | it('should create deep property', function () { 16 | var o = { 17 | 'foo.bar': 1, 18 | 'x.y.z': 2, 19 | m: 3 20 | }; 21 | var result = util.transformPlainObjectToStructured(o); 22 | expect(result).toEqual({ 23 | foo: { 24 | bar: 1 25 | }, 26 | x: { 27 | y: { 28 | z: 2 29 | } 30 | }, 31 | m: 3 32 | }); 33 | }) 34 | }); 35 | } 36 | ); 37 | --------------------------------------------------------------------------------