├── polyfills ├── demo │ ├── partials │ │ ├── home.html │ │ ├── index1.css │ │ ├── index2.html │ │ ├── index1.html │ │ ├── index1.js │ │ └── index2.js │ └── index.html ├── README.md ├── ui-router-require-polyfill.js └── ng-resource-polyfill.js ├── .gitignore ├── db.json ├── readme.md ├── components ├── bind-html │ └── bind-html.js ├── validator │ ├── tooltips.css │ └── validate.js ├── dynamic-attr │ └── dynamic-attr.js └── Sortable │ ├── ng-sortable.js │ └── Sortable.js ├── index.html ├── package.json ├── services ├── locale_zh-cn.js ├── recursion-helper.js ├── base-services.js └── http-handler.js ├── utils └── base-utils.js └── test └── base-service-test.html /polyfills/demo/partials/home.html: -------------------------------------------------------------------------------- 1 |
2 | home 3 |
-------------------------------------------------------------------------------- /polyfills/demo/partials/index1.css: -------------------------------------------------------------------------------- 1 | body { 2 | background: pink; 3 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | .idea 3 | node_modules -------------------------------------------------------------------------------- /db.json: -------------------------------------------------------------------------------- 1 | { 2 | "from": [ 3 | { 4 | "id": 123, 5 | "name": "ll" 6 | } 7 | ] 8 | } -------------------------------------------------------------------------------- /polyfills/demo/partials/index2.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 产品B 4 |
5 | 10 | 11 | 12 | 13 | 产品B成员管理菜单 14 | 15 | -------------------------------------------------------------------------------- /polyfills/demo/partials/index1.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 产品A 4 | 5 |
6 | 7 |
8 | 9 | 产品A成员管理菜单 10 | 11 | 12 | 13 |
14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | useful angular utils 2 | pure and powerful 3 | 4 | ### how to use 5 | npm 6 | ``` 7 | npm install kuitos-angular-utils 8 | ``` 9 | bower(Deprecated) 10 | ```bash 11 | bower install kuitos-angular-utils 12 | ``` 13 | 14 | 15 | ## todo list 16 | * dynamicAttr:support dynamic attr add or remove,even though it is a directive 17 | * click-delegate:delegate click event in a table list,like jQuery.delegate() -------------------------------------------------------------------------------- /polyfills/demo/partials/index1.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @author Kuitos 3 | * @homepage https://github.com/kuitos/ 4 | * @since 2015-12-14 5 | */ 6 | angular.module('test1', ['test2']) 7 | 8 | .config(function ($stateProvider) { 9 | 10 | $stateProvider 11 | .state('index1.entry', { 12 | url : '/entry', 13 | template: '卡卡西 我爱罗' 14 | }); 15 | 16 | }) 17 | 18 | .run(function () { 19 | 20 | console.log('test1 loaded'); 21 | 22 | }); -------------------------------------------------------------------------------- /polyfills/demo/partials/index2.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @author Kuitos 3 | * @homepage https://github.com/kuitos/ 4 | * @since 2015-12-15 5 | */ 6 | ;(function () { 7 | 8 | 'use strict'; 9 | 10 | angular 11 | .module('test2', []) 12 | 13 | .config(function ($stateProvider) { 14 | 15 | $stateProvider 16 | .state('index2.entry', { 17 | 18 | url: '/entry', 19 | template: '第一王权者 第三王权者' 20 | 21 | }); 22 | 23 | }) 24 | 25 | .run(function () { 26 | console.log('test2'); 27 | }); 28 | 29 | })(); -------------------------------------------------------------------------------- /polyfills/README.md: -------------------------------------------------------------------------------- 1 | ### polyfills 2 | 3 | 1. ng-resource-polyfill 4 | 针对angular-resource不兼容返回结果为基本数据类型(string,number,boolean)的情况做的垫片代码 5 | 2. ui-router-require-polyfill 6 | angular & ui-router 框架下的按需加载解决方案,可达到不修改一行js代码即可实现模版依赖按需加载的需求.依赖于[oclazyload](https://github.com/ocombe/ocLazyLoad) 7 | 通过往模版script标签中加入seq属性来控制加载顺序 8 | 9 | ```html 10 | 11 | 12 | 13 | 14 | ``` 15 | 加载顺序为: index1.js --> index2.js & index4.js --> index3.js 16 | 17 | [demo](http://159.203.248.99/angular-utils/polyfills/demo/index.html) 18 | -------------------------------------------------------------------------------- /components/bind-html/bind-html.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @author kui.liu 3 | * @since 2014/12/26 下午5:15 4 | * 类似ng-bind-html,只是这里嵌入的html支持angular指令(有一个compile过程)而不是纯静态html 5 | */ 6 | ; 7 | (function (angular, undefined) { 8 | "use strict"; 9 | 10 | angular.module("ngUtils.components.bindHtml", []) 11 | 12 | .directive("bindHtml", ["$compile", function ($compile) { 13 | 14 | return { 15 | restrict: "A", 16 | link : function (scope, element, attr) { 17 | 18 | scope.$watch(function (scope) { 19 | return scope.$eval(attr.bindHtml); 20 | }, function (newTpl) { 21 | 22 | element.html(newTpl); 23 | $compile(element.contents())(scope); 24 | 25 | }); 26 | } 27 | }; 28 | 29 | }]); 30 | })(window.angular); -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 |
14 | 15 | 16 | 17 | 18 | 19 | 20 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "kuitos-angular-utils", 3 | "version": "1.3.10", 4 | "description": "a useful & powerful angular utils collections, without dependencies", 5 | "main": "index.html", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/kuitos/angular-utils.git" 12 | }, 13 | "keywords": [ 14 | "angular", 15 | "utils", 16 | "useful" 17 | ], 18 | "author": "Kuitos", 19 | "license": "MIT", 20 | "bugs": { 21 | "url": "https://github.com/kuitos/angular-utils/issues" 22 | }, 23 | "homepage": "https://github.com/kuitos/angular-utils", 24 | "dependencies": { 25 | "angular": "^1.4.8", 26 | "angular-resource": "^1.4.8", 27 | "oclazyload": "^1.0.9", 28 | "angular-ui-router": "^0.2.18" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /services/locale_zh-cn.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 3 | 'use strict'; 4 | 5 | angular.module('ngUtils.services.localeValidation', ["ngLocale"]). 6 | run(['$locale', 7 | function ($locale) { 8 | angular.extend($locale, { 9 | VALIDATE: { 10 | required : '必填!', 11 | number : "必须为数字!", 12 | minlength: '太短!', 13 | maxlength: '太长!', 14 | min : '太小!', 15 | max : '太大!', 16 | more : '太多!', 17 | email : 'Email无效!', 18 | username : '有效字符为汉字、字母、数字、下划线,以汉字或小写字母开头!', 19 | minname : '长度应大于5字节,一个汉字3字节!', 20 | maxname : '长度应小于15字节,一个汉字3字节!', 21 | repasswd : '密码不一致!', 22 | url : 'URL无效!', 23 | tag : '标签错误,不能包含“,”、“,”和“、”' 24 | } 25 | }); 26 | } 27 | ]); 28 | })(); 29 | 30 | -------------------------------------------------------------------------------- /polyfills/demo/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | demo 6 | 7 | 8 | 9 | 产品a 10 | 产品b 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 50 | 51 | -------------------------------------------------------------------------------- /utils/base-utils.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @author Kuitos 3 | * @homepage https://github.com/kuitos/ 4 | * @since 2015-08-21 5 | */ 6 | ; 7 | (function (angular, undefined) { 8 | 9 | 'use strict'; 10 | 11 | var _services = {}; 12 | 13 | // 性能工具 14 | var PerformanceUtils = { 15 | 16 | /** 17 | * 函数只会在确定时间延时后才会被触发(只会执行间隔时间内的最后一次调用) 18 | */ 19 | debounce: function (func, delay, scope, invokeApply) { 20 | var $timeout = _services.$timeout, 21 | timer; 22 | 23 | return function debounced() { 24 | var context = scope, 25 | args = arguments; 26 | 27 | $timeout.cancel(timer); 28 | timer = $timeout(function () { 29 | 30 | timer = undefined; 31 | func.apply(context, args); 32 | 33 | }, delay || 300, invokeApply); 34 | }; 35 | }, 36 | 37 | /** 38 | * 函数节流。使一个持续性触发的函数执行间隔大于指定时间才会有效(只会执行间隔时间内的第一次调用) 39 | */ 40 | throttle: function throttle(func, delay, context) { 41 | var recent; 42 | return function throttled() { 43 | var now = Date.now(); 44 | context = context || null; 45 | 46 | if (!recent || (now - recent > (delay || 10))) { 47 | func.apply(context, arguments); 48 | recent = now; 49 | } 50 | }; 51 | } 52 | }; 53 | 54 | angular 55 | .module('ngUtils.utils.baseUtils', []) 56 | .constant('baseUtils', { 57 | PerformanceUtils: PerformanceUtils 58 | }) 59 | .run(['$timeout', function ($timeout) { 60 | _services.$timeout = $timeout; 61 | }]); 62 | 63 | })(window.angular); 64 | 65 | -------------------------------------------------------------------------------- /test/base-service-test.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | home 11 | 12 | wtf 13 | 14 | 15 |
16 | 17 | 22 | 23 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 66 | 67 | -------------------------------------------------------------------------------- /services/recursion-helper.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @author Kuitos 3 | * @homepage https://github.com/kuitos 4 | * @since 7/29/15. 11:42 5 | */ 6 | ; 7 | (function (angular, undefined) { 8 | "use strict"; 9 | 10 | angular 11 | .module("ticket.services.recursionHelper", []) 12 | .factory("recursionHelper", RecursionHelper); 13 | 14 | /** 15 | * 递归指令服务 16 | * thanks for http://stackoverflow.com/questions/14430655/recursion-in-angular-directives 17 | */ 18 | RecursionHelper.$inject = ["$compile"]; 19 | function RecursionHelper($compile) { 20 | 21 | return { 22 | /** 23 | * Manually compiles the element, fixing the recursion loop. 24 | * @param element 25 | * @param [link] A post-link function, or an object with function(s) registered via pre and post properties. 26 | * @returns {pre: *, post: Function} object containing the linking functions. 27 | */ 28 | compile: function (element, link) { 29 | // Normalize the link parameter 30 | if (angular.isFunction(link)) { 31 | link = {post: link}; 32 | } 33 | 34 | // Break the recursion loop by removing the contents 35 | var contents = element.contents().remove(); 36 | var compiledContents; 37 | 38 | return { 39 | 40 | pre : (link && link.pre) ? link.pre : null, 41 | /** 42 | * Compiles and re-adds the contents 43 | */ 44 | post: function (scope, element) { 45 | 46 | // Compile the contents 47 | if (!compiledContents) { 48 | compiledContents = $compile(contents); 49 | } 50 | 51 | // Re-add the compiled contents to the element 52 | compiledContents(scope, function (clone) { 53 | element.append(clone); 54 | }); 55 | 56 | // Call the post-linking function, if any 57 | if (link && link.post) { 58 | link.post.apply(null, arguments); 59 | } 60 | } 61 | }; 62 | } 63 | }; 64 | } 65 | 66 | })(window.angular); -------------------------------------------------------------------------------- /components/validator/tooltips.css: -------------------------------------------------------------------------------- 1 | .tooltip { 2 | position: absolute; 3 | z-index: 1070; 4 | display: block; 5 | font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; 6 | font-size: 12px; 7 | font-style: normal; 8 | font-weight: normal; 9 | line-height: 1.42857143; 10 | text-align: left; 11 | text-align: start; 12 | text-decoration: none; 13 | text-shadow: none; 14 | text-transform: none; 15 | letter-spacing: normal; 16 | word-break: normal; 17 | word-spacing: normal; 18 | word-wrap: normal; 19 | white-space: normal; 20 | filter: alpha(opacity=0); 21 | opacity: 0; 22 | 23 | line-break: auto; 24 | } 25 | 26 | .tooltip.in { 27 | filter: alpha(opacity=90); 28 | opacity: .9; 29 | } 30 | 31 | .tooltip.top { 32 | padding: 5px 0; 33 | margin-top: -3px; 34 | } 35 | 36 | .tooltip.right { 37 | padding: 0 5px; 38 | margin-left: 3px; 39 | } 40 | 41 | .tooltip.bottom { 42 | padding: 5px 0; 43 | margin-top: 3px; 44 | } 45 | 46 | .tooltip.left { 47 | padding: 0 5px; 48 | margin-left: -3px; 49 | } 50 | 51 | .tooltip-inner { 52 | min-width: 40px; 53 | max-width: 200px; 54 | padding: 3px 8px; 55 | color: #fff; 56 | text-align: center; 57 | border-radius: 4px; 58 | background-color: #d9534f; 59 | white-space: nowrap; 60 | } 61 | 62 | .tooltip-arrow { 63 | position: absolute; 64 | width: 0; 65 | height: 0; 66 | 67 | border: solid transparent; 68 | border-right-color: #d9534f; 69 | } 70 | 71 | .tooltip.top .tooltip-arrow { 72 | bottom: 0; 73 | left: 50%; 74 | margin-left: -5px; 75 | border-width: 5px 5px 0; 76 | border-top-color: #000; 77 | } 78 | 79 | .tooltip.top-left .tooltip-arrow { 80 | right: 5px; 81 | bottom: 0; 82 | margin-bottom: -5px; 83 | border-width: 5px 5px 0; 84 | border-top-color: #000; 85 | } 86 | 87 | .tooltip.top-right .tooltip-arrow { 88 | bottom: 0; 89 | left: 5px; 90 | margin-bottom: -5px; 91 | border-width: 5px 5px 0; 92 | border-top-color: #000; 93 | } 94 | 95 | .tooltip.right .tooltip-arrow { 96 | top: 50%; 97 | left: 0; 98 | margin-top: -5px; 99 | border-width: 5px 5px 5px 0; 100 | border-right-color: #d9534f; 101 | } 102 | 103 | .tooltip.left .tooltip-arrow { 104 | top: 50%; 105 | right: 0; 106 | margin-top: -5px; 107 | border-width: 5px 0 5px 5px; 108 | border-left-color: #000; 109 | } 110 | 111 | .tooltip.bottom .tooltip-arrow { 112 | top: 0; 113 | left: 50%; 114 | margin-left: -5px; 115 | border-width: 0 5px 5px; 116 | border-bottom-color: #000; 117 | } 118 | 119 | .tooltip.bottom-left .tooltip-arrow { 120 | top: 0; 121 | right: 5px; 122 | margin-top: -5px; 123 | border-width: 0 5px 5px; 124 | border-bottom-color: #000; 125 | } 126 | 127 | .tooltip.bottom-right .tooltip-arrow { 128 | top: 0; 129 | left: 5px; 130 | margin-top: -5px; 131 | border-width: 0 5px 5px; 132 | border-bottom-color: #000; 133 | } 134 | 135 | .validate-failed { 136 | border-color: red !important; 137 | } 138 | -------------------------------------------------------------------------------- /components/dynamic-attr/dynamic-attr.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @author Kuitos 3 | * @since 15/5/14. 4 | * @version 1.0.4 5 | * 动态 添加/删除 元素属性,如果是angular支持的事件(如ngClick)则同时对元素做 绑定/解绑 事件切换 6 | */ 7 | ; 8 | (function (angular, undefined) { 9 | "use strict"; 10 | 11 | angular.module("ngUtils.components.dynamicAttr", []) 12 | 13 | // 支持的angular事件集合 14 | .constant("SUPPORTED_NG_EVENTS", "click dblclick mousedown mouseup mouseover mouseout mousemove mouseenter mouseleave keydown keyup keypress submit focus blur copy cut paste".split(" ")) 15 | 16 | .directive("dynamicAttr", ["$parse", "SUPPORTED_NG_EVENTS", function ($parse, SUPPORTED_NG_EVENTS) { 17 | 18 | return { 19 | restrict: "A", 20 | priority: 1, 21 | compile: function (element) { 22 | 23 | // 从元素上收集到的事件属性 24 | var collectedNgEventMapper = {}; 25 | 26 | // 收集当前元素的原始事件(ng-click等) 27 | SUPPORTED_NG_EVENTS.forEach(function (eventName) { 28 | 29 | var ngEventAttr = "ng-" + eventName, 30 | ngEventAttrValue = element.attr(ngEventAttr); 31 | 32 | // 如果绑定存在 33 | if (ngEventAttrValue && (ngEventAttrValue = ngEventAttrValue.trim())) { 34 | 35 | collectedNgEventMapper[ngEventAttr] = { 36 | eventName: eventName, 37 | expression: ngEventAttrValue, 38 | fn: $parse(ngEventAttrValue, null, true) 39 | }; 40 | } 41 | 42 | }); 43 | 44 | return function postLink(scope, element, attr) { 45 | // 因为监听的是一个对象类型,所以这里watch的时候必须是true(调用angular.equals()对比而不是简单的===,简单的===可能会引发TTL负载异常) 46 | scope.$watch(attr.dynamicAttr, function dynamicAttrAction(attributes) { 47 | 48 | if (attributes !== undefined) { 49 | 50 | angular.forEach(attributes, function (attrAvailable, attribute) { 51 | 52 | var originalAttrInfo = collectedNgEventMapper[attribute] || element.attr(attribute) || true; 53 | 54 | // 如果属性为已收集到的angular事件类型 55 | if (originalAttrInfo && originalAttrInfo.eventName) { 56 | 57 | if (attrAvailable) { 58 | 59 | // 如果当前元素上不存在该事件属性但是其原始事件属性存在(表明元素之前做过disable切换),则重新绑定事件回调 60 | if (!element.attr(attribute) && originalAttrInfo) { 61 | 62 | element.removeClass(attribute + "-disabled").attr(attribute, originalAttrInfo.expression); 63 | 64 | /** 65 | * rebind event callback 66 | * @see ngClick 67 | */ 68 | element.bind(originalAttrInfo.eventName, function (event) { 69 | scope.$apply(function () { 70 | originalAttrInfo.fn(scope, {$event: event}); 71 | }); 72 | }); 73 | } 74 | 75 | } else { 76 | 77 | // 状态为false时加入样式并移除对应事件回调 78 | element.addClass(attribute + "-disabled").removeAttr(attribute).unbind(originalAttrInfo.eventName); 79 | } 80 | 81 | } else { 82 | 83 | // TODO 当属性不可用时应该移除绑定在元素上相关的逻辑,而可用时则应加上相关逻辑,如何实现这种动态编译某一指令?? 84 | element[attrAvailable ? "attr" : "removeAttr"](attribute, originalAttrInfo); 85 | } 86 | 87 | }); 88 | } 89 | }, true); 90 | 91 | // unbind events for performance 92 | scope.$on("$destroy", function unbindEvents() { 93 | angular.forEach(collectedNgEventMapper, function (eventInfo) { 94 | element.unbind(eventInfo.eventName); 95 | }); 96 | 97 | }); 98 | } 99 | } 100 | }; 101 | 102 | }]); 103 | 104 | })(window.angular); -------------------------------------------------------------------------------- /polyfills/ui-router-require-polyfill.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @author Kuitos 3 | * @homepage https://github.com/kuitos/ 4 | * @since 2015-12-11 5 | * 基于ui-router & oclazeload实现的按需加载方案,可在不修改一行js代码的情况下实现angular框架的按需加载 6 | * 依赖于 oclazeload 7 | */ 8 | ;(function (angular, undefined) { 9 | 10 | 'use strict'; 11 | 12 | angular 13 | .module('ui.router.requirePolyfill', ['ng', 'ui.router', 'oc.lazyLoad']) 14 | .decorator('uiViewDirective', DecoratorConstructor); 15 | 16 | /** 17 | * 装饰uiView指令,给其加入按需加载的能力 18 | */ 19 | DecoratorConstructor.$inject = ['$delegate', '$log', '$q', '$compile', '$controller', '$interpolate', '$state', '$ocLazyLoad']; 20 | function DecoratorConstructor($delegate, $log, $q, $compile, $controller, $interpolate, $state, $ocLazyLoad) { 21 | 22 | // 移除原始指令逻辑 23 | $delegate.pop(); 24 | // 在原始ui-router的模版加载逻辑中加入脚本请求代码,实现按需加载需求 25 | $delegate.push({ 26 | 27 | restrict: 'ECA', 28 | priority: -400, 29 | compile: function (tElement) { 30 | var initial = tElement.html(); 31 | return function (scope, $element, attrs) { 32 | 33 | var current = $state.$current, 34 | name = getUiViewName(scope, attrs, $element, $interpolate), 35 | locals = current && current.locals[name]; 36 | 37 | if (!locals) { 38 | return; 39 | } 40 | 41 | $element.data('$uiView', {name: name, state: locals.$$state}); 42 | 43 | var template = locals.$template ? locals.$template : initial, 44 | processResult = processTpl(template); 45 | 46 | var compileTemplate = function () { 47 | $element.html(processResult.tpl); 48 | 49 | var link = $compile($element.contents()); 50 | 51 | if (locals.$$controller) { 52 | locals.$scope = scope; 53 | locals.$element = $element; 54 | var controller = $controller(locals.$$controller, locals); 55 | if (locals.$$controllerAs) { 56 | scope[locals.$$controllerAs] = controller; 57 | } 58 | $element.data('$ngControllerController', controller); 59 | $element.children().data('$ngControllerController', controller); 60 | } 61 | 62 | link(scope); 63 | }; 64 | 65 | // 模版中不含脚本则直接编译,否则在获取完脚本之后再做编译 66 | if (processResult.scripts.length) { 67 | loadScripts(processResult.scripts).then(compileTemplate); 68 | } else { 69 | compileTemplate(); 70 | } 71 | 72 | }; 73 | } 74 | 75 | }); 76 | 77 | return $delegate; 78 | 79 | /** 80 | * Shared ui-view code for both directives: 81 | * Given scope, element, and its attributes, return the view's name 82 | */ 83 | function getUiViewName(scope, attrs, element, $interpolate) { 84 | var name = $interpolate(attrs.uiView || attrs.name || '')(scope); 85 | var inherited = element.inheritedData('$uiView'); 86 | return name.indexOf('@') >= 0 ? name : (name + '@' + (inherited ? inherited.state.name : '')); 87 | } 88 | 89 | /** 90 | * 从模版中解析出script外链脚本 91 | * @return tpl:处理后的模版字符串 scripts:提取出来的脚本链接,数组索引对应脚本优先级, 数据结构: [['a.js','b.js'], ['c.js']] 92 | */ 93 | function processTpl(tpl) { 94 | 95 | var SCRIPT_TAG_REGEX = /<(script)\s+((?!type=('|")text\/ng-template\3).)*?>.*?<\/\1>/gi, 96 | SCRIPT_SRC_REGEX = /.*\ssrc=("|')(\S+)\1.*/, 97 | SCRIPT_SEQ_REGEX = /.*\sseq=("|')(\S+)\1.*/, 98 | scripts = []; 99 | 100 | // 处理模版,将script抽取出来 101 | tpl = tpl.replace(SCRIPT_TAG_REGEX, function (match) { 102 | 103 | // 抽取src部分按设置的优先级存入数组,默认优先级为0(最高优先级) 104 | var matchedScriptSeq = match.match(SCRIPT_SEQ_REGEX), 105 | matchedScriptSrc = match.match(SCRIPT_SRC_REGEX); 106 | 107 | var seq = (matchedScriptSeq && matchedScriptSeq[2]) || 0; 108 | scripts[seq] = scripts[seq] || []; 109 | 110 | if (matchedScriptSrc && matchedScriptSrc[2]) { 111 | scripts[seq].push(matchedScriptSrc[2]); 112 | } 113 | 114 | return ''; 115 | }); 116 | 117 | return { 118 | tpl: tpl, 119 | scripts: scripts.filter(function (script) { 120 | // 过滤空的索引 121 | return !!script; 122 | }) 123 | }; 124 | 125 | } 126 | 127 | // 按脚本优先级加载脚本 128 | function loadScripts(scripts) { 129 | 130 | var promise = $ocLazyLoad.load(scripts.shift()), 131 | errorHandle = function (err) { 132 | $log.error(err); 133 | return $q.reject(err); 134 | }, 135 | nextGroup; 136 | 137 | while (scripts.length) { 138 | 139 | nextGroup = scripts.shift(); 140 | 141 | promise = promise.then(function (nextGroup) { 142 | return $ocLazyLoad.load(nextGroup); 143 | }.bind(null, nextGroup)); 144 | } 145 | 146 | return promise.catch(errorHandle); 147 | } 148 | 149 | } 150 | 151 | })(window.angular); 152 | -------------------------------------------------------------------------------- /components/Sortable/ng-sortable.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @author RubaXa 3 | * @licence MIT 4 | */ 5 | (function (factory) { 6 | 'use strict'; 7 | 8 | if (window.angular && window.Sortable) { 9 | factory(angular, Sortable); 10 | } 11 | else if (typeof define === 'function' && define.amd) { 12 | define(['angular', './Sortable'], factory); 13 | } 14 | })(function (angular, Sortable) { 15 | 'use strict'; 16 | 17 | 18 | /** 19 | * @typedef {Object} ngSortEvent 20 | * @property {*} model List item 21 | * @property {Object|Array} models List of items 22 | * @property {number} oldIndex before sort 23 | * @property {number} newIndex after sort 24 | */ 25 | 26 | 27 | angular.module('ng-sortable', []) 28 | .constant('version', '0.3.7') 29 | .directive('ngSortable', ['$parse', function ($parse) { 30 | var removed, 31 | nextSibling; 32 | 33 | function getSource(el) { 34 | var scope = angular.element(el).scope(); 35 | var ngRepeat = [].filter.call(el.childNodes, function (node) { 36 | return ( 37 | (node.nodeType === 8) && 38 | (node.nodeValue.indexOf('ngRepeat:') !== -1) 39 | ); 40 | })[0]; 41 | 42 | if (!ngRepeat) { 43 | // Without ng-repeat 44 | return null; 45 | } 46 | 47 | // tests: http://jsbin.com/kosubutilo/1/edit?js,output 48 | ngRepeat = ngRepeat.nodeValue.match(/ngRepeat:\s*(?:\(.*?,\s*)?([^\s)]+)[\s)]+in\s+([^\s|]+)/); 49 | 50 | var itemExpr = $parse(ngRepeat[1]); 51 | var itemsExpr = $parse(ngRepeat[2]); 52 | 53 | return { 54 | item: function (el) { 55 | return itemExpr(angular.element(el).scope()); 56 | }, 57 | items: function () { 58 | return itemsExpr(scope); 59 | } 60 | }; 61 | } 62 | 63 | 64 | // Export 65 | return { 66 | restrict: 'AC', 67 | link: function (scope, $el, attrs) { 68 | var el = $el[0], 69 | ngSortable = attrs.ngSortable, 70 | options = scope.$eval(ngSortable) || {}, 71 | source = getSource(el), 72 | sortable 73 | ; 74 | 75 | 76 | function _emitEvent(/**Event*/evt, /*Mixed*/item) { 77 | 78 | var eventType = evt.type; 79 | 80 | // Bug: https://bugzilla.mozilla.org/show_bug.cgi?id=1153603 81 | if (eventType === "endEvent") { 82 | eventType = "end"; 83 | } 84 | 85 | var name = 'on' + eventType.charAt(0).toUpperCase() + eventType.substr(1); 86 | 87 | /* jshint expr:true */ 88 | options[name] && options[name]({ 89 | model: item || source && source.item(evt.item), 90 | models: source && source.items(), 91 | oldIndex: evt.oldIndex, 92 | newIndex: evt.newIndex 93 | }); 94 | } 95 | 96 | 97 | function _sync(/**Event*/evt) { 98 | if (!source) { 99 | // Without ng-repeat 100 | return; 101 | } 102 | 103 | var oldIndex = evt.oldIndex, 104 | newIndex = evt.newIndex, 105 | items = source.items(); 106 | 107 | if (el !== evt.from) { 108 | var prevSource = getSource(evt.from), 109 | prevItems = prevSource.items(); 110 | 111 | oldIndex = prevItems.indexOf(prevSource.item(evt.item)); 112 | removed = prevItems[oldIndex]; 113 | 114 | if (evt.clone) { 115 | evt.from.removeChild(evt.clone); 116 | removed = angular.copy(removed); 117 | } 118 | else { 119 | prevItems.splice(oldIndex, 1); 120 | } 121 | 122 | items.splice(newIndex, 0, removed); 123 | 124 | evt.from.insertBefore(evt.item, nextSibling); // revert element 125 | } 126 | else { 127 | items.splice(newIndex, 0, items.splice(oldIndex, 1)[0]); 128 | } 129 | 130 | scope.$apply(); 131 | } 132 | 133 | sortable = Sortable.create(el, Object.keys(options).reduce(function (opts, name) { 134 | opts[name] = opts[name] || options[name]; 135 | return opts; 136 | }, { 137 | onStart: function (/**Event*/evt) { 138 | nextSibling = evt.item.nextSibling; 139 | _emitEvent(evt); 140 | scope.$apply(); 141 | }, 142 | onEnd: function (/**Event*/evt) { 143 | _emitEvent(evt, removed); 144 | scope.$apply(); 145 | }, 146 | onAdd: function (/**Event*/evt) { 147 | _sync(evt); 148 | _emitEvent(evt, removed); 149 | scope.$apply(); 150 | }, 151 | onUpdate: function (/**Event*/evt) { 152 | _sync(evt); 153 | _emitEvent(evt); 154 | }, 155 | onRemove: function (/**Event*/evt) { 156 | _emitEvent(evt, removed); 157 | }, 158 | onDragOutRemove: function (/**Event*/evt) { 159 | _emitEvent(evt, removed); 160 | }, 161 | onSort: function (/**Event*/evt) { 162 | _emitEvent(evt); 163 | } 164 | })); 165 | 166 | $el.on('$destroy', function () { 167 | sortable.destroy(); 168 | sortable = null; 169 | nextSibling = null; 170 | }); 171 | 172 | if (ngSortable && !/{|}/.test(ngSortable)) { // todo: ugly 173 | angular.forEach([ 174 | 'sort', 'disabled', 'draggable', 'handle', 'animation', 175 | 'onStart', 'onEnd', 'onAdd', 'onUpdate', 'onRemove', 'onDragOutRemove', 'onSort' 176 | ], function (name) { 177 | scope.$watch(ngSortable + '.' + name, function (value) { 178 | if (value !== void 0) { 179 | options[name] = value; 180 | 181 | if (!/^on[A-Z]/.test(name)) { 182 | sortable.option(name, value); 183 | } 184 | } 185 | }); 186 | }); 187 | } 188 | } 189 | }; 190 | }]) 191 | 192 | .directive("sortableTrash", function () { 193 | 194 | /** 195 | * dog fucked hack for drag remove feature 196 | */ 197 | return { 198 | restrict: "E", 199 | template: "", 200 | link : function (scope) { 201 | scope.items = []; 202 | scope.$watch("items.length", function (length) { 203 | if (length !== undefined) { 204 | if(length > 10){ 205 | scope.items.length = 0; 206 | } 207 | } 208 | }); 209 | } 210 | }; 211 | }); 212 | }); 213 | -------------------------------------------------------------------------------- /services/base-services.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @author Kuitos 3 | * @since 2014/10/09 上午10:18 4 | */ 5 | ; 6 | (function (angular) { 7 | "use strict"; 8 | 9 | angular.module("ngUtils.services.baseServices", ["ngResource"]) 10 | 11 | /* ******************************** constants & values ******************************** */ 12 | // 定义app 13 | .constant("app", { 14 | /** 15 | * 应用根目录 /src:开发环境 /dist:生产环境 16 | */ 17 | fileRoot: window.ResourceDir, 18 | 19 | /** 20 | * 指令的模板根目录 21 | */ 22 | componentsDir: (window.ResourceDir || "src") + "/common/components/", 23 | 24 | /** 25 | * controller间共享的数据 26 | */ 27 | SharedDataBetweenCtrls: {}, 28 | 29 | /** 30 | * 下面三个方法(genMembers,bindEvents,kickStart)用于划分scope代码块,不同区块代表不同逻辑,从而改善controller的表现形式 31 | */ 32 | // 初始化scope成员 33 | genMembers: function (scope, members) { 34 | 35 | angular.forEach(members, function (value, key) { 36 | scope[key] = value; 37 | }); 38 | 39 | return this; 40 | }, 41 | 42 | // 绑定事件处理器,避免$scope上绑定过多function, events:原始事件处理function集合 43 | bindEvents: function (scope, events) { 44 | 45 | var slice = Array.prototype.slice; 46 | scope.emitEvent = function (eventName) { 47 | return events[eventName].apply(scope, slice.call(arguments, 1)); 48 | }; 49 | 50 | return this; 51 | }, 52 | 53 | // 入口函数 54 | kickStart: function (initFn) { 55 | initFn(); 56 | } 57 | }) 58 | 59 | /* ******************************** providers ******************************** */ 60 | .provider("getFile", ["app", function (app) { 61 | this.html = function (fileName) { 62 | return app.fileRoot + "/app/" + fileName; 63 | }; 64 | this.$get = function () { 65 | return { 66 | html: this.html 67 | }; 68 | }; 69 | }]) 70 | 71 | /* ******************************** factories ******************************** */ 72 | // 中介者模式。用于解决各模块间无法通过 $scope.$emit $scope.$on 等方式实现通信的问题(例如兄弟模块间通信) 73 | .factory("Mediator", ["$log", "$rootScope", function ($log, $rootScope) { 74 | 75 | var topics = {}; 76 | 77 | return { 78 | 79 | /** 80 | * 订阅消息 81 | * @param topic 订阅消息名 82 | * @param listener 消息发布时触发的回调 83 | * @param scope 订阅行为发生所在的scope,用于在scope销毁时作解绑操作 84 | * @returns {Function} 取消订阅的反注册函数 85 | */ 86 | subscribe: function (topic, listener, scope) { 87 | 88 | var topicListeners = topics[topic] = topics[topic] || []; 89 | 90 | // 可清除指定监听器,如果不传则清除对应topic全部监听器 91 | function unSubscribe(listener) { 92 | 93 | var listenerIndex; 94 | 95 | if (!listener) { 96 | // 清空 97 | topicListeners.length = 0; 98 | } else { 99 | 100 | listenerIndex = topicListeners.indexOf(listener); 101 | if (~listenerIndex) { 102 | topicListeners.splice(listenerIndex, 1); 103 | } 104 | } 105 | } 106 | 107 | if (scope && (scope.constructor === $rootScope.constructor)) { 108 | // scope销毁时同步移除对应订阅行为 109 | scope.$on('$destroy', unSubscribe.bind(null, listener)); 110 | } 111 | 112 | topicListeners.push(listener); 113 | 114 | return unSubscribe; 115 | }, 116 | 117 | /** 118 | * 发布消息,支持链式调用 119 | * @param topic 120 | */ 121 | publish: function (topic) { 122 | 123 | var args = arguments, 124 | listeners = topics[topic] || [], 125 | slice = Array.prototype.slice; 126 | 127 | listeners.forEach(function (listener) { 128 | 129 | if (angular.isFunction(listener)) { 130 | listener.apply(null, slice.call(args, 1)); 131 | } else { 132 | $log.error("中介者发布 %s 消息失败,注册的listener不是函数类型!", topic); 133 | } 134 | }); 135 | 136 | return this; 137 | } 138 | }; 139 | }]) 140 | 141 | // rest api相关配置 142 | .constant("restConfig", { 143 | apiPrefix: "" // rest接口前缀 144 | }) 145 | 146 | // rest接口默认cache 147 | .factory("defaultRestCache", ["$cacheFactory", function ($cacheFactory) { 148 | 149 | return $cacheFactory("defaultRestCache", {capacity: 50}); 150 | 151 | }]) 152 | 153 | // 生成resource 154 | .factory("genResource", ["$resource", "$http", "defaultRestCache", "restConfig", function ($resource, $http, defaultRestCache, restConfig) { 155 | 156 | /** 157 | * @url resource url 158 | * @cache 外部cache,默认为$http cache 159 | * @params additionalActions 额外添加的resource action 160 | * @example 使用方式(在自己的app中定义Resources服务) 161 | 162 | .factory("Resources", ["$cacheFactory", "genResource", function ($cacheFactory, genResource) { 163 | return { 164 | // 项目resource 165 | Project: genResource("/projects/:projectId/", $cacheFactory("project", {capacity: 5})) 166 | }; 167 | }]) 168 | 169 | */ 170 | return function (url, cache, params, additionalActions) { 171 | 172 | // 默认cache为defaultRestCache 173 | // 自定义配置(配合$http interceptor) saveStatus:该操作将维护一个保存状态 174 | var restHttpCache = (cache === undefined) ? defaultRestCache : cache, 175 | 176 | DEFAULT_ACTIONS = { 177 | // 查询,结果为对象 178 | "get" : {method: "GET", cache: restHttpCache}, 179 | // 查询,结果为数组 180 | "query" : {method: "GET", isArray: true, cache: restHttpCache}, 181 | // 保存(新增) 182 | "save" : {method: "POST", cache: restHttpCache}, 183 | // 修改(全量) 184 | "update": {method: "PUT", cache: restHttpCache}, 185 | // 修改(部分) 186 | "patch" : {method: "PATCH", cache: restHttpCache}, 187 | // 逻辑删除 188 | "remove": {method: "DELETE", cache: restHttpCache}, 189 | // 物理删除 190 | "delete": {method: "DELETE", cache: restHttpCache} 191 | }; 192 | 193 | return $resource(restConfig.apiPrefix + url, params, angular.extend(DEFAULT_ACTIONS, additionalActions)); 194 | }; 195 | }]) 196 | 197 | /* ******************************** services init ******************************** */ 198 | .run(["genResource", "Mediator", "app", function (genResource, Mediator, app) { 199 | app.genResource = genResource; 200 | app.Mediator = Mediator; 201 | }]); 202 | 203 | })(window.angular); 204 | -------------------------------------------------------------------------------- /services/http-handler.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @author kui.liu 3 | * @since 2014/10/10 下午5:52 4 | * http处理器,用于设定全局http配置,包括loading状态切换,拦截器配置,超时时间配置等 5 | * 基于rest api可以构建超强http缓存中间件 6 | */ 7 | ; 8 | (function (angular, undefined) { 9 | "use strict"; 10 | 11 | // 模拟service的私有服务 12 | var _app = {}; 13 | 14 | angular.module("ngUtils.services.httpHandler", []) 15 | 16 | /* http处理器黑名单列表(该列表中的url不走httpHandler拦截器) */ 17 | .constant('httpHandlerBlacklist', []) 18 | 19 | .config(["$httpProvider", 'httpHandlerBlacklist', function ($httpProvider, httpHandlerBlacklist) { 20 | 21 | var GET = 'GET', 22 | /** http请求相关状态(loading)切换 */ 23 | count = 0, 24 | loading = false, 25 | 26 | stopLoading = function () { 27 | loading = false; 28 | _app.isLoading(false); // end loading 29 | }; 30 | 31 | function isInHttpBlackList(url) { 32 | 33 | return httpHandlerBlacklist.some(function (blackUrl) { 34 | return url.test(blackUrl); 35 | }); 36 | 37 | } 38 | 39 | /*************************** 禁用浏览器缓存 ***************************/ 40 | $httpProvider.defaults.headers.common["Cache-Control"] = "no-cache"; 41 | /* 广告时间哈哈.... */ 42 | $httpProvider.defaults.headers.common["X-Requested-With"] = "https://github.com/kuitos"; 43 | 44 | /******************** http拦截器,用于统一处理错误信息、消息缓存、请求响应状态、响应结果处理等 **********************/ 45 | $httpProvider.interceptors.push(["$q", "$log", "$timeout", "$cacheFactory", '$injector', 46 | function ($q, $log, $timeout, $cacheFactory, $injector) { 47 | 48 | // 清除cache 49 | function clearCache(config, key) { 50 | (angular.isObject(config.cache) ? config.cache : $cacheFactory.get("$http"))[key ? 'remove' : 'removeAll'](key); 51 | } 52 | 53 | return { 54 | 55 | request: function (config) { 56 | 57 | // 不在黑名单中的url才进拦截器 58 | if (!isInHttpBlackList(config.url)) { 59 | 60 | count++; 61 | 62 | if (!loading) { 63 | 64 | // loading start 65 | $timeout(function () { 66 | if (!loading && count > 0) { 67 | loading = true; 68 | _app.isLoading(true); 69 | } 70 | }, 500); // if no response in 500ms, begin loading 71 | } 72 | 73 | } 74 | 75 | /** 76 | * 查询请求中含有私有参数_forceRefresh时也需要强制刷新 77 | */ 78 | if (config.method === GET && config.params && config.params._forceRefresh) { 79 | clearCache(config); 80 | } 81 | 82 | return config; 83 | }, 84 | 85 | requestError: function (rejection) { 86 | $log.error("%s 接口请求失败!", rejection.url); 87 | return $q.reject(rejection); 88 | }, 89 | 90 | response: function (res) { 91 | var config = res.config; 92 | 93 | if (!isInHttpBlackList(config.url)) { 94 | 95 | count--; 96 | 97 | // 响应结束,清除相关状态 98 | if (count === 0) { 99 | if (loading) { 100 | stopLoading(); 101 | } 102 | } 103 | 104 | /** 105 | * 若请求为非查询操作(save,update,delete等更新操作),成功后需要重新刷新cache(清空对应cache)。默认cache为$http 106 | */ 107 | if (config.method !== GET && config.cache) { 108 | clearCache(config); 109 | } 110 | } 111 | 112 | return res; 113 | }, 114 | 115 | responseError: function (rejection) { 116 | 117 | var config = rejection.config; 118 | 119 | if (!isInHttpBlackList(config.url)) { 120 | 121 | count--; 122 | // 响应结束,清除相关状态 123 | if (count === 0) { 124 | if (loading) { 125 | stopLoading(); 126 | } 127 | } 128 | 129 | // 清理相应缓存 130 | if (config.cache) { 131 | clearCache(config, config.url); 132 | } 133 | 134 | // 失败弹出错误提示信息 135 | // 这里通过$injector.get()的方式获取tipsHandler服务,而不是直接注入tipsHandler的方式,是因为tipsHandler实例可能是依赖于$http的服务(如tipsHandler内部会请求模板) 136 | // 如果存在这种循环依赖,angular会抛出cdep(Circular dependency found)异常 137 | $injector.get('tipsHandler').error(rejection.data || "请求错误!"); 138 | $log.error("接口 %s 请求错误! 状态:%s 错误信息:%s", config.url, rejection.status, rejection.statusText); 139 | 140 | } 141 | 142 | return $q.reject(rejection); 143 | } 144 | } 145 | }]); 146 | }]) 147 | 148 | /* 提示信息provider,用于配置错误提示处理器 **/ 149 | .provider("tipsHandler", function () { 150 | 151 | var _tipsHandler = { 152 | error : angular.noop, 153 | warning: angular.noop, 154 | success: angular.noop 155 | }, 156 | 157 | _configuredTipsHandler; 158 | 159 | /** 160 | * 设置具体的tips处理器 161 | * @param tipsHandler {String|Object} String:service Object:handler instance 162 | */ 163 | this.setTipsHandler = function (tipsHandler) { 164 | _configuredTipsHandler = tipsHandler; 165 | }; 166 | 167 | this.$get = ['$injector', '$log', function ($injector, $log) { 168 | 169 | var verifiedTipsHandler; 170 | 171 | if (angular.isString(_configuredTipsHandler)) { 172 | 173 | try { 174 | verifiedTipsHandler = $injector.get(_configuredTipsHandler); 175 | } catch (err) { 176 | $log.error('%s服务未被正常初始化(%s服务get失败)', _configuredTipsHandler, _configuredTipsHandler); 177 | } 178 | 179 | } else if (angular.isFunction(_configuredTipsHandler) || angular.isArray(_configuredTipsHandler)) { 180 | 181 | try { 182 | verifiedTipsHandler = $injector.invoke(_configuredTipsHandler); 183 | } catch (err) { 184 | $log.error('%s服务未被正常初始化(%s服务invoke失败)', _configuredTipsHandler, _configuredTipsHandler); 185 | } 186 | 187 | } else if (angular.isObject(_configuredTipsHandler)) { 188 | verifiedTipsHandler = _configuredTipsHandler; 189 | } 190 | 191 | return angular.extend(_tipsHandler, verifiedTipsHandler); 192 | }]; 193 | 194 | }) 195 | 196 | .run(["$rootScope", function ($rootScope) { 197 | 198 | /** loading状态切换 **/ 199 | _app.isLoading = function (flag) { 200 | $rootScope.loading = flag; 201 | }; 202 | 203 | }]); 204 | 205 | })(window.angular); 206 | -------------------------------------------------------------------------------- /polyfills/ng-resource-polyfill.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @author Kuitos 3 | * @homepage https://github.com/kuitos/ 4 | * @since 2015-09-06 5 | */ 6 | ; 7 | (function (angular, undefined) { 8 | 9 | 'use strict'; 10 | 11 | angular 12 | .module('ngUtils.polyfills.ngResource', ['ng', 'ngResource']) 13 | .config(ngResourcePolyfill); 14 | 15 | var $resourceMinErr = angular.$$minErr('$resource'); 16 | 17 | // Helper functions and regex to lookup a dotted path on an object 18 | // stopping at undefined/null. The path must be composed of ASCII 19 | // identifiers (just like $parse) 20 | var MEMBER_NAME_REGEX = /^(\.[a-zA-Z_$@][0-9a-zA-Z_$@]*)+$/; 21 | 22 | function isValidDottedPath(path) { 23 | return (path != null && path !== '' && path !== 'hasOwnProperty' && 24 | MEMBER_NAME_REGEX.test('.' + path)); 25 | } 26 | 27 | function lookupDottedPath(obj, path) { 28 | if (!isValidDottedPath(path)) { 29 | throw $resourceMinErr('badmember', 'Dotted member path "@{0}" is invalid.', path); 30 | } 31 | var keys = path.split('.'); 32 | for (var i = 0, ii = keys.length; i < ii && obj !== undefined; i++) { 33 | var key = keys[i]; 34 | obj = (obj !== null) ? obj[key] : undefined; 35 | } 36 | return obj; 37 | } 38 | 39 | /** 40 | * Create a shallow copy of an object and clear other fields from the destination 41 | */ 42 | function shallowClearAndCopy(src, dst) { 43 | dst = dst || {}; 44 | 45 | angular.forEach(dst, function (value, key) { 46 | delete dst[key]; 47 | }); 48 | 49 | /** 50 | * fixed: when src is string or number,wrap dst into {_data:src} 51 | * @author Kuitos 52 | * @since 2015-09-06 53 | */ 54 | if (angular.isString(src) || angular.isNumber(src)) { 55 | dst._data = src; 56 | } else { 57 | for (var key in src) { 58 | if (src.hasOwnProperty(key) && !(key.charAt(0) === '$' && key.charAt(1) === '$')) { 59 | dst[key] = src[key]; 60 | } 61 | } 62 | } 63 | 64 | return dst; 65 | } 66 | 67 | /** 68 | * polyfill 69 | */ 70 | ngResourcePolyfill.$inject = ['$provide', '$resourceProvider']; 71 | function ngResourcePolyfill($provide, $resourceProvider) { 72 | 73 | $provide.decorator('$resource', ['$http', '$q', function ($http, $q) { 74 | return genNewNgResource($http, $q); 75 | }]); 76 | 77 | /** 78 | * generate new ngResource 79 | * @param $http 80 | * @param $q 81 | */ 82 | function genNewNgResource($http, $q) { 83 | 84 | var 85 | provider = $resourceProvider, 86 | noop = angular.noop, 87 | forEach = angular.forEach, 88 | extend = angular.extend, 89 | copy = angular.copy, 90 | isFunction = angular.isFunction; 91 | 92 | /** 93 | * We need our custom method because encodeURIComponent is too aggressive and doesn't follow 94 | * http://www.ietf.org/rfc/rfc3986.txt with regards to the character set 95 | * (pchar) allowed in path segments: 96 | * segment = *pchar 97 | * pchar = unreserved / pct-encoded / sub-delims / ":" / "@" 98 | * pct-encoded = "%" HEXDIG HEXDIG 99 | * unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~" 100 | * sub-delims = "!" / "$" / "&" / "'" / "(" / ")" 101 | * / "*" / "+" / "," / ";" / "=" 102 | */ 103 | function encodeUriSegment(val) { 104 | return encodeUriQuery(val, true). 105 | replace(/%26/gi, '&'). 106 | replace(/%3D/gi, '='). 107 | replace(/%2B/gi, '+'); 108 | } 109 | 110 | /** 111 | * This method is intended for encoding *key* or *value* parts of query component. We need a 112 | * custom method because encodeURIComponent is too aggressive and encodes stuff that doesn't 113 | * have to be encoded per http://tools.ietf.org/html/rfc3986: 114 | * query = *( pchar / "/" / "?" ) 115 | * pchar = unreserved / pct-encoded / sub-delims / ":" / "@" 116 | * unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~" 117 | * pct-encoded = "%" HEXDIG HEXDIG 118 | * sub-delims = "!" / "$" / "&" / "'" / "(" / ")" 119 | * / "*" / "+" / "," / ";" / "=" 120 | */ 121 | function encodeUriQuery(val, pctEncodeSpaces) { 122 | return encodeURIComponent(val). 123 | replace(/%40/gi, '@'). 124 | replace(/%3A/gi, ':'). 125 | replace(/%24/g, '$'). 126 | replace(/%2C/gi, ','). 127 | replace(/%20/g, (pctEncodeSpaces ? '%20' : '+')); 128 | } 129 | 130 | function Route(template, defaults) { 131 | this.template = template; 132 | this.defaults = extend({}, provider.defaults, defaults); 133 | this.urlParams = {}; 134 | } 135 | 136 | Route.prototype = { 137 | setUrlParams: function (config, params, actionUrl) { 138 | var self = this, 139 | url = actionUrl || self.template, 140 | val, 141 | encodedVal; 142 | 143 | var urlParams = self.urlParams = {}; 144 | forEach(url.split(/\W/), function (param) { 145 | if (param === 'hasOwnProperty') { 146 | throw $resourceMinErr('badname', "hasOwnProperty is not a valid parameter name."); 147 | } 148 | if (!(new RegExp("^\\d+$").test(param)) && param && 149 | (new RegExp("(^|[^\\\\]):" + param + "(\\W|$)").test(url))) { 150 | urlParams[param] = true; 151 | } 152 | }); 153 | url = url.replace(/\\:/g, ':'); 154 | 155 | params = params || {}; 156 | forEach(self.urlParams, function (_, urlParam) { 157 | val = params.hasOwnProperty(urlParam) ? params[urlParam] : self.defaults[urlParam]; 158 | if (angular.isDefined(val) && val !== null) { 159 | encodedVal = encodeUriSegment(val); 160 | url = url.replace(new RegExp(":" + urlParam + "(\\W|$)", "g"), function (match, p1) { 161 | return encodedVal + p1; 162 | }); 163 | } else { 164 | url = url.replace(new RegExp("(\/?):" + urlParam + "(\\W|$)", "g"), function (match, 165 | leadingSlashes, tail) { 166 | if (tail.charAt(0) == '/') { 167 | return tail; 168 | } else { 169 | return leadingSlashes + tail; 170 | } 171 | }); 172 | } 173 | }); 174 | 175 | // strip trailing slashes and set the url (unless this behavior is specifically disabled) 176 | if (self.defaults.stripTrailingSlashes) { 177 | url = url.replace(/\/+$/, '') || '/'; 178 | } 179 | 180 | // then replace collapse `/.` if found in the last URL path segment before the query 181 | // E.g. `http://url.com/id./format?q=x` becomes `http://url.com/id.format?q=x` 182 | url = url.replace(/\/\.(?=\w+($|\?))/, '.'); 183 | // replace escaped `/\.` with `/.` 184 | config.url = url.replace(/\/\\\./, '/.'); 185 | 186 | // set params - delegate param encoding to $http 187 | forEach(params, function (value, key) { 188 | if (!self.urlParams[key]) { 189 | config.params = config.params || {}; 190 | config.params[key] = value; 191 | } 192 | }); 193 | } 194 | }; 195 | 196 | function resourceFactory(url, paramDefaults, actions, options) { 197 | var route = new Route(url, options); 198 | 199 | actions = extend({}, provider.defaults.actions, actions); 200 | 201 | function extractParams(data, actionParams) { 202 | var ids = {}; 203 | actionParams = extend({}, paramDefaults, actionParams); 204 | forEach(actionParams, function (value, key) { 205 | if (isFunction(value)) { 206 | value = value(); 207 | } 208 | ids[key] = value && value.charAt && value.charAt(0) == '@' ? 209 | lookupDottedPath(data, value.substr(1)) : value; 210 | }); 211 | return ids; 212 | } 213 | 214 | function defaultResponseInterceptor(response) { 215 | return response.resource; 216 | } 217 | 218 | function Resource(value) { 219 | shallowClearAndCopy(value || {}, this); 220 | } 221 | 222 | Resource.prototype.toJSON = function () { 223 | var data = extend({}, this); 224 | delete data.$promise; 225 | delete data.$resolved; 226 | return data; 227 | }; 228 | 229 | forEach(actions, function (action, name) { 230 | var hasBody = /^(POST|PUT|PATCH)$/i.test(action.method); 231 | 232 | Resource[name] = function (a1, a2, a3, a4) { 233 | var params = {}, data, success, error; 234 | 235 | /* jshint -W086 */ 236 | /* (purposefully fall through case statements) */ 237 | switch (arguments.length) { 238 | case 4: 239 | error = a4; 240 | success = a3; 241 | //fallthrough 242 | case 3: 243 | case 2: 244 | if (isFunction(a2)) { 245 | if (isFunction(a1)) { 246 | success = a1; 247 | error = a2; 248 | break; 249 | } 250 | 251 | success = a2; 252 | error = a3; 253 | //fallthrough 254 | } else { 255 | params = a1; 256 | data = a2; 257 | success = a3; 258 | break; 259 | } 260 | case 1: 261 | if (isFunction(a1)) success = a1; 262 | else if (hasBody) data = a1; 263 | else params = a1; 264 | break; 265 | case 0: 266 | break; 267 | default: 268 | throw $resourceMinErr('badargs', 269 | "Expected up to 4 arguments [params, data, success, error], got {0} arguments", 270 | arguments.length); 271 | } 272 | /* jshint +W086 */ 273 | /* (purposefully fall through case statements) */ 274 | 275 | var isInstanceCall = this instanceof Resource; 276 | var value = isInstanceCall ? data : (action.isArray ? [] : new Resource(data)); 277 | var httpConfig = {}; 278 | var responseInterceptor = action.interceptor && action.interceptor.response || 279 | defaultResponseInterceptor; 280 | var responseErrorInterceptor = action.interceptor && action.interceptor.responseError || 281 | undefined; 282 | 283 | forEach(action, function (value, key) { 284 | if (key != 'params' && key != 'isArray' && key != 'interceptor') { 285 | httpConfig[key] = copy(value); 286 | } 287 | }); 288 | 289 | if (hasBody) httpConfig.data = data; 290 | route.setUrlParams(httpConfig, 291 | extend({}, extractParams(data, action.params || {}), params), 292 | action.url); 293 | 294 | var promise = $http(httpConfig).then(function (response) { 295 | var data = response.data, 296 | promise = value.$promise; 297 | 298 | if (data) { 299 | // Need to convert action.isArray to boolean in case it is undefined 300 | // jshint -W018 301 | if (angular.isArray(data) !== (!!action.isArray)) { 302 | throw $resourceMinErr('badcfg', 303 | 'Error in resource configuration for action `{0}`. Expected response to ' + 304 | 'contain an {1} but got an {2} (Request: {3} {4})', name, action.isArray ? 'array' : 'object', 305 | angular.isArray(data) ? 'array' : 'object', httpConfig.method, httpConfig.url); 306 | } 307 | // jshint +W018 308 | if (action.isArray) { 309 | value.length = 0; 310 | forEach(data, function (item) { 311 | if (typeof item === "object") { 312 | value.push(new Resource(item)); 313 | } else { 314 | // Valid JSON values may be string literals, and these should not be converted 315 | // into objects. These items will not have access to the Resource prototype 316 | // methods, but unfortunately there 317 | value.push(item); 318 | } 319 | }); 320 | } else { 321 | shallowClearAndCopy(data, value); 322 | value.$promise = promise; 323 | } 324 | } 325 | 326 | value.$resolved = true; 327 | 328 | response.resource = value; 329 | 330 | return response; 331 | }, function (response) { 332 | value.$resolved = true; 333 | 334 | (error || noop)(response); 335 | 336 | return $q.reject(response); 337 | }); 338 | 339 | promise = promise.then( 340 | function (response) { 341 | var value = responseInterceptor(response); 342 | (success || noop)(value, response.headers); 343 | return value; 344 | }, 345 | responseErrorInterceptor); 346 | 347 | if (!isInstanceCall) { 348 | // we are creating instance / collection 349 | // - set the initial promise 350 | // - return the instance / collection 351 | value.$promise = promise; 352 | value.$resolved = false; 353 | 354 | return value; 355 | } 356 | 357 | // instance call 358 | return promise; 359 | }; 360 | 361 | Resource.prototype['$' + name] = function (params, success, error) { 362 | if (isFunction(params)) { 363 | error = success; 364 | success = params; 365 | params = {}; 366 | } 367 | var result = Resource[name].call(this, params, this, success, error); 368 | return result.$promise || result; 369 | }; 370 | }); 371 | 372 | Resource.bind = function (additionalParamDefaults) { 373 | return resourceFactory(url, extend({}, paramDefaults, additionalParamDefaults), actions); 374 | }; 375 | 376 | return Resource; 377 | } 378 | 379 | return resourceFactory; 380 | } 381 | 382 | } 383 | 384 | })(window.angular); 385 | 386 | -------------------------------------------------------------------------------- /components/validator/validate.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @author Kuitos 3 | * @since 2014/12/04 下午9:39 4 | */ 5 | ; 6 | (function (angular, undefined) { 7 | "use strict"; 8 | 9 | angular.module("ngUtils.components.validate", ["ngUtils.services.localeValidation", "ngUtils.services.baseServices"]) 10 | 11 | .constant("INVALID_CLASS", "validate-failed") 12 | 13 | // 通用校验指令 14 | .directive('uiValidate', function () { 15 | 16 | return { 17 | restrict: 'A', 18 | require : 'ngModel', 19 | link : function (scope, elm, attrs, ctrl) { 20 | var validateFn, validators = {}, 21 | validateExpr = scope.$eval(attrs.uiValidate); 22 | 23 | if (!validateExpr) { 24 | return; 25 | } 26 | 27 | if (angular.isString(validateExpr)) { 28 | validateExpr = {validator: validateExpr}; 29 | } 30 | 31 | angular.forEach(validateExpr, function (exprssn, key) { 32 | validateFn = function (valueToValidate) { 33 | var expression = scope.$eval(exprssn, {'$value': valueToValidate}); 34 | if (angular.isObject(expression) && angular.isFunction(expression.then)) { 35 | // expression is a promise 36 | expression.then(function () { 37 | ctrl.$setValidity(key, true); 38 | }, function () { 39 | ctrl.$setValidity(key, false); 40 | }); 41 | return valueToValidate; 42 | } else if (expression) { 43 | // expression is true 44 | ctrl.$setValidity(key, true); 45 | return valueToValidate; 46 | } else { 47 | // expression is false 48 | ctrl.$setValidity(key, false); 49 | return valueToValidate; 50 | } 51 | }; 52 | validators[key] = validateFn; 53 | ctrl.$formatters.push(validateFn); 54 | ctrl.$parsers.push(validateFn); 55 | }); 56 | 57 | function apply_watch(watch) { 58 | //string - update all validators on expression change 59 | if (angular.isString(watch)) { 60 | scope.$watch(watch, function () { 61 | angular.forEach(validators, function (validatorFn) { 62 | validatorFn(ctrl.$modelValue); 63 | }); 64 | }); 65 | return; 66 | } 67 | 68 | //array - update all validators on change of any expression 69 | if (angular.isArray(watch)) { 70 | angular.forEach(watch, function (expression) { 71 | scope.$watch(expression, function () { 72 | angular.forEach(validators, function (validatorFn) { 73 | validatorFn(ctrl.$modelValue); 74 | }); 75 | }); 76 | }); 77 | return; 78 | } 79 | 80 | //object - update appropriate validator 81 | if (angular.isObject(watch)) { 82 | angular.forEach(watch, function (expression, validatorKey) { 83 | //value is string - look after one expression 84 | if (angular.isString(expression)) { 85 | scope.$watch(expression, function () { 86 | validators[validatorKey](ctrl.$modelValue); 87 | }); 88 | } 89 | 90 | //value is array - look after all expressions in array 91 | if (angular.isArray(expression)) { 92 | angular.forEach(expression, function (intExpression) { 93 | scope.$watch(intExpression, function () { 94 | validators[validatorKey](ctrl.$modelValue); 95 | }); 96 | }); 97 | } 98 | }); 99 | } 100 | } 101 | 102 | // Support for ui-validate-watch 103 | if (attrs.uiValidateWatch) { 104 | apply_watch(scope.$eval(attrs.uiValidateWatch)); 105 | } 106 | } 107 | }; 108 | }) 109 | 110 | // 生成校验提示结果 111 | .directive('genTooltip', ["$timeout", "$compile", "isVisible", "$position", "INVALID_CLASS", function ($timeout, $compile, isVisible, $position, INVALID_CLASS) { 112 | var toolTipTemplate = '
' + 113 | '
' + 114 | '
' + 115 | '
'; 116 | 117 | return { 118 | require: '?ngModel', 119 | link : function (scope, element, attr, ngModel) { 120 | 121 | var enable = false, 122 | options = scope.$eval(attr.genTooltip) || {}, 123 | tooltipElement, 124 | tooltipContent, 125 | popupTimeout; 126 | 127 | attr.msg = scope.$eval(attr.tooltipMsg) || {}; 128 | 129 | // use for AngularJS validation 130 | if (options.validate) { 131 | 132 | if (ngModel) { 133 | ngModel.$formatters.push(validateFn); 134 | ngModel.$parsers.push(validateFn); 135 | } else { 136 | scope.$watch(function () { 137 | return attr.dataOriginalTitle || attr.originalTitle; 138 | }, showTooltip); 139 | } 140 | 141 | element.bind('focus', function () { 142 | // 开启校验 143 | if (!enable) { 144 | enable = true; 145 | } 146 | // 执行校验 147 | validateFn(); 148 | }); 149 | 150 | scope.$on('genTooltipValidate', function (event, collect, turnoff) { 151 | enable = !turnoff; 152 | if (ngModel) { 153 | if (angular.isArray(collect)) { 154 | collect.push(ngModel); 155 | } 156 | invalidMsg(ngModel.$invalid); 157 | } 158 | }); 159 | } 160 | 161 | scope.$on("$destroy", function () { 162 | $timeout.cancel(popupTimeout); 163 | element.unbind("focus"); 164 | }); 165 | 166 | function positionTooltip() { 167 | 168 | var ttPosition = $position.positionElements(element, tooltipElement, "right", false); 169 | ttPosition.top += 'px'; 170 | ttPosition.left += 'px'; 171 | tooltipElement.css(ttPosition); 172 | 173 | popupTimeout && (popupTimeout = null); 174 | } 175 | 176 | function showTooltip(show, title) { 177 | 178 | if (show) { 179 | 180 | if (title) { 181 | 182 | if (!tooltipElement) { 183 | 184 | tooltipElement = angular.element(toolTipTemplate); 185 | tooltipContent = angular.element(tooltipElement.find("div")[1]); 186 | 187 | positionTooltip(); 188 | if (!popupTimeout) { 189 | // to position correctly, we need reposition the tooltip after draw 190 | popupTimeout = $timeout(positionTooltip, 0, false); 191 | } 192 | 193 | element.after(tooltipElement); 194 | } 195 | 196 | tooltipContent.text(title); 197 | } 198 | 199 | element.addClass(INVALID_CLASS); 200 | 201 | } else { 202 | 203 | if (tooltipElement) { 204 | tooltipElement.remove(); 205 | tooltipElement = null; 206 | } 207 | element.removeClass(INVALID_CLASS); 208 | } 209 | } 210 | 211 | function invalidMsg(invalid) { 212 | ngModel.validate = enable && options.validate && isVisible(element); 213 | if (ngModel.validate) { 214 | var title = attr.showTitle && (ngModel.$name && ngModel.$name + ' ') || ''; 215 | var msg = scope.$eval(attr.tooltipMsg) || {}; 216 | if (invalid && options.validateMsg) { 217 | angular.forEach(ngModel.$error, function (value, key) { 218 | if (attr.msg[key]) { 219 | title += (value && msg[key] && msg[key] + ', ') || ''; 220 | } else if (options.validateMsg[key]) { 221 | title += (value && options.validateMsg[key] && options.validateMsg[key] + ', ') || ''; 222 | } 223 | }); 224 | } 225 | title = title.slice(0, -2) || ''; 226 | showTooltip(!!invalid, title); 227 | } else { 228 | showTooltip(false, ''); 229 | } 230 | } 231 | 232 | function validateFn(value) { 233 | $timeout(function () { 234 | invalidMsg(ngModel.$invalid); 235 | }); 236 | return value; 237 | } 238 | 239 | } 240 | } 241 | }]) 242 | 243 | // 元素是否可见 244 | .factory("isVisible", function () { 245 | return function (element) { 246 | var rect = element[0].getBoundingClientRect(); 247 | return Boolean(rect.bottom - rect.top); 248 | }; 249 | }) 250 | 251 | // steal from ui.bootstrap 252 | .factory('$position', ['$document', '$window', function ($document, $window) { 253 | 254 | function getStyle(el, cssprop) { 255 | if (el.currentStyle) { //IE 256 | return el.currentStyle[cssprop]; 257 | } else if ($window.getComputedStyle) { 258 | return $window.getComputedStyle(el)[cssprop]; 259 | } 260 | // finally try and get inline style 261 | return el.style[cssprop]; 262 | } 263 | 264 | /** 265 | * Checks if a given element is statically positioned 266 | * @param element - raw DOM element 267 | */ 268 | function isStaticPositioned(element) { 269 | return (getStyle(element, 'position') || 'static' ) === 'static'; 270 | } 271 | 272 | /** 273 | * returns the closest, non-statically positioned parentOffset of a given element 274 | * @param element 275 | */ 276 | var parentOffsetEl = function (element) { 277 | var docDomEl = $document[0]; 278 | var offsetParent = element.offsetParent || docDomEl; 279 | while (offsetParent && offsetParent !== docDomEl && isStaticPositioned(offsetParent)) { 280 | offsetParent = offsetParent.offsetParent; 281 | } 282 | return offsetParent || docDomEl; 283 | }; 284 | 285 | return { 286 | /** 287 | * Provides read-only equivalent of jQuery's position function: 288 | * http://api.jquery.com/position/ 289 | */ 290 | position: function (element) { 291 | var elBCR = this.offset(element); 292 | var offsetParentBCR = {top: 0, left: 0}; 293 | var offsetParentEl = parentOffsetEl(element[0]); 294 | if (offsetParentEl != $document[0]) { 295 | offsetParentBCR = this.offset(angular.element(offsetParentEl)); 296 | offsetParentBCR.top += offsetParentEl.clientTop - offsetParentEl.scrollTop; 297 | offsetParentBCR.left += offsetParentEl.clientLeft - offsetParentEl.scrollLeft; 298 | } 299 | 300 | var boundingClientRect = element[0].getBoundingClientRect(); 301 | return { 302 | width : boundingClientRect.width || element.prop('offsetWidth'), 303 | height: boundingClientRect.height || element.prop('offsetHeight'), 304 | top : elBCR.top - offsetParentBCR.top, 305 | left : elBCR.left - offsetParentBCR.left 306 | }; 307 | }, 308 | 309 | /** 310 | * Provides read-only equivalent of jQuery's offset function: 311 | * http://api.jquery.com/offset/ 312 | */ 313 | offset: function (element) { 314 | var boundingClientRect = element[0].getBoundingClientRect(); 315 | return { 316 | width : boundingClientRect.width || element.prop('offsetWidth'), 317 | height: boundingClientRect.height || element.prop('offsetHeight'), 318 | top : boundingClientRect.top + ($window.pageYOffset || $document[0].documentElement.scrollTop), 319 | left : boundingClientRect.left + ($window.pageXOffset || $document[0].documentElement.scrollLeft) 320 | }; 321 | }, 322 | 323 | /** 324 | * Provides coordinates for the targetEl in relation to hostEl 325 | */ 326 | positionElements: function (hostEl, targetEl, positionStr, appendToBody) { 327 | 328 | var positionStrParts = positionStr.split('-'); 329 | var pos0 = positionStrParts[0], pos1 = positionStrParts[1] || 'center'; 330 | 331 | var hostElPos, 332 | targetElWidth, 333 | targetElHeight, 334 | targetElPos; 335 | 336 | hostElPos = appendToBody ? this.offset(hostEl) : this.position(hostEl); 337 | 338 | targetElWidth = targetEl.prop('offsetWidth'); 339 | targetElHeight = targetEl.prop('offsetHeight'); 340 | 341 | var shiftWidth = { 342 | center: function () { 343 | return hostElPos.left + hostElPos.width / 2 - targetElWidth / 2; 344 | }, 345 | left : function () { 346 | return hostElPos.left; 347 | }, 348 | right : function () { 349 | return hostElPos.left + hostElPos.width; 350 | } 351 | }; 352 | 353 | var shiftHeight = { 354 | center: function () { 355 | return hostElPos.top + hostElPos.height / 2 - targetElHeight / 2; 356 | }, 357 | top : function () { 358 | return hostElPos.top; 359 | }, 360 | bottom: function () { 361 | return hostElPos.top + hostElPos.height; 362 | } 363 | }; 364 | 365 | switch (pos0) { 366 | case 'right': 367 | targetElPos = { 368 | top : shiftHeight[pos1](), 369 | left: shiftWidth[pos0]() 370 | }; 371 | break; 372 | case 'left': 373 | targetElPos = { 374 | top : shiftHeight[pos1](), 375 | left: hostElPos.left - targetElWidth 376 | }; 377 | break; 378 | case 'bottom': 379 | targetElPos = { 380 | top : shiftHeight[pos0](), 381 | left: shiftWidth[pos1]() 382 | }; 383 | break; 384 | default: 385 | targetElPos = { 386 | top : hostElPos.top - targetElHeight, 387 | left: shiftWidth[pos1]() 388 | }; 389 | break; 390 | } 391 | 392 | return targetElPos; 393 | } 394 | }; 395 | }]) 396 | 397 | /* ******************************** validation init ******************************** */ 398 | .run(["$rootScope", "$locale", "app", function ($rootScope, $locale, app) { 399 | 400 | /** rootScope全局初始化 **/ 401 | $rootScope.validateTooltip = { 402 | validate : true, 403 | validateMsg: $locale.VALIDATE 404 | }; 405 | 406 | /** 表单校验通用方法 **/ 407 | app.validate = function (scope, turnoff) { 408 | var collect = [], 409 | error = []; 410 | scope.$broadcast('genTooltipValidate', collect, turnoff); 411 | 412 | angular.forEach(collect, function (value) { 413 | if (value.validate && value.$invalid) { 414 | error.push(value); 415 | } 416 | }); 417 | 418 | if (error.length === 0) { 419 | app.validate.errorList = null; 420 | scope.$broadcast('genTooltipValidate', collect, true); 421 | } else { 422 | app.validate.errorList = error; 423 | } 424 | return !app.validate.errorList; 425 | }; 426 | 427 | }]); 428 | 429 | })(window.angular); -------------------------------------------------------------------------------- /components/Sortable/Sortable.js: -------------------------------------------------------------------------------- 1 | /**! 2 | * Sortable 3 | * @author RubaXa 4 | * @license MIT 5 | */ 6 | 7 | 8 | (function (factory) { 9 | "use strict"; 10 | 11 | if (typeof define === "function" && define.amd) { 12 | define(factory); 13 | } 14 | else if (typeof module != "undefined" && typeof module.exports != "undefined") { 15 | module.exports = factory(); 16 | } 17 | else if (typeof Package !== "undefined") { 18 | Sortable = factory(); // export for Meteor.js 19 | } 20 | else { 21 | /* jshint sub:true */ 22 | window["Sortable"] = factory(); 23 | } 24 | })(function () { 25 | "use strict"; 26 | 27 | var dragEl, 28 | ghostEl, 29 | cloneEl, 30 | rootEl, 31 | nextEl, 32 | 33 | scrollEl, 34 | scrollParentEl, 35 | 36 | lastEl, 37 | lastCSS, 38 | lastContainer, 39 | 40 | oldIndex, 41 | newIndex, 42 | 43 | activeGroup, 44 | autoScroll = {}, 45 | 46 | /** for dog fucked firefox */ 47 | pointerX = 0, 48 | pointerY = 0, 49 | 50 | bind = false, 51 | 52 | tapEvt, 53 | touchEvt, 54 | 55 | trashElement, 56 | 57 | /** @const */ 58 | TRASH_ELEMENT_NODE_NAME = 'SORTABLE-TRASH', 59 | RSPACE = /\s+/g, 60 | 61 | expando = 'Sortable' + (new Date).getTime(), 62 | 63 | win = window, 64 | document = win.document, 65 | parseInt = win.parseInt, 66 | 67 | isFirefox = window.navigator.userAgent.toLowerCase().indexOf('firefox') > -1, 68 | supportDraggable = !!('draggable' in document.createElement('div')), 69 | 70 | _silent = false, 71 | 72 | _dispatchEvent = function (sortable, rootEl, name, targetEl, fromEl, startIndex, newIndex) { 73 | var evt = document.createEvent('Event'), 74 | options = (sortable || rootEl[expando]).options, 75 | onName = 'on' + name.charAt(0).toUpperCase() + name.substr(1); 76 | 77 | evt.initEvent(name, true, true); 78 | 79 | evt.item = targetEl || rootEl; 80 | evt.from = fromEl || rootEl; 81 | evt.clone = cloneEl; 82 | 83 | evt.oldIndex = startIndex; 84 | evt.newIndex = newIndex; 85 | 86 | if (options[onName]) { 87 | options[onName].call(sortable, evt); 88 | } 89 | 90 | rootEl.dispatchEvent(evt); 91 | }, 92 | 93 | abs = Math.abs, 94 | slice = [].slice, 95 | 96 | touchDragOverListeners = [], 97 | 98 | _autoScroll = _throttle(function (/**Event*/evt, /**Object*/options, /**HTMLElement*/rootEl) { 99 | // Bug: https://bugzilla.mozilla.org/show_bug.cgi?id=505521 100 | if (rootEl && options.scroll) { 101 | var el, 102 | rect, 103 | sens = options.scrollSensitivity, 104 | speed = options.scrollSpeed, 105 | 106 | x = evt.clientX, 107 | y = evt.clientY, 108 | 109 | winWidth = window.innerWidth, 110 | winHeight = window.innerHeight, 111 | 112 | vx, 113 | vy 114 | ; 115 | 116 | // Delect scrollEl 117 | if (scrollParentEl !== rootEl) { 118 | scrollEl = options.scroll; 119 | scrollParentEl = rootEl; 120 | 121 | if (scrollEl === true) { 122 | scrollEl = rootEl; 123 | 124 | do { 125 | if ((scrollEl.offsetWidth < scrollEl.scrollWidth) || 126 | (scrollEl.offsetHeight < scrollEl.scrollHeight) 127 | ) { 128 | break; 129 | } 130 | /* jshint boss:true */ 131 | } while (scrollEl = scrollEl.parentNode); 132 | } 133 | } 134 | 135 | if (scrollEl) { 136 | el = scrollEl; 137 | rect = scrollEl.getBoundingClientRect(); 138 | vx = (abs(rect.right - x) <= sens) - (abs(rect.left - x) <= sens); 139 | vy = (abs(rect.bottom - y) <= sens) - (abs(rect.top - y) <= sens); 140 | } 141 | 142 | 143 | if (!(vx || vy)) { 144 | vx = (winWidth - x <= sens) - (x <= sens); 145 | vy = (winHeight - y <= sens) - (y <= sens); 146 | 147 | /* jshint expr:true */ 148 | (vx || vy) && (el = win); 149 | } 150 | 151 | 152 | if (autoScroll.vx !== vx || autoScroll.vy !== vy || autoScroll.el !== el) { 153 | autoScroll.el = el; 154 | autoScroll.vx = vx; 155 | autoScroll.vy = vy; 156 | 157 | clearInterval(autoScroll.pid); 158 | 159 | if (el) { 160 | autoScroll.pid = setInterval(function () { 161 | if (el === win) { 162 | win.scrollTo(win.pageXOffset + vx * speed, win.pageYOffset + vy * speed); 163 | } else { 164 | vy && (el.scrollTop += vy * speed); 165 | vx && (el.scrollLeft += vx * speed); 166 | } 167 | }, 24); 168 | } 169 | } 170 | } 171 | }, 30) 172 | ; 173 | 174 | 175 | 176 | /** 177 | * @class Sortable 178 | * @param {HTMLElement} el 179 | * @param {Object} [options] 180 | */ 181 | function Sortable(el, options) { 182 | this.el = el; // root element 183 | this.options = options = _extend({}, options); 184 | 185 | 186 | // Export instance 187 | el[expando] = this; 188 | 189 | 190 | // Default options 191 | var defaults = { 192 | group: Math.random(), 193 | sort: true, 194 | disabled: false, 195 | store: null, 196 | handle: null, 197 | scroll: true, 198 | scrollSensitivity: 30, 199 | scrollSpeed: 10, 200 | draggable: /[uo]l/i.test(el.nodeName) ? 'li' : '>*', 201 | ghostClass: 'sortable-ghost', 202 | ignore: 'a, img', 203 | filter: null, 204 | animation: 0, 205 | setData: function (dataTransfer, dragEl) { 206 | dataTransfer.setData('Text', dragEl.textContent); 207 | }, 208 | dropBubble: false, 209 | dragoverBubble: false, 210 | dataIdAttr: 'data-id', 211 | delay: 0, 212 | dragOutRemove: false 213 | }; 214 | 215 | 216 | // Set default options 217 | for (var name in defaults) { 218 | !(name in options) && (options[name] = defaults[name]); 219 | } 220 | 221 | 222 | var group = options.group; 223 | 224 | if (!group || typeof group != 'object') { 225 | group = options.group = { name: group }; 226 | } 227 | 228 | 229 | ['pull', 'put'].forEach(function (key) { 230 | if (!(key in group)) { 231 | group[key] = true; 232 | } 233 | }); 234 | 235 | 236 | options.groups = ' ' + group.name + (group.put.join ? ' ' + group.put.join(' ') : '') + ' '; 237 | 238 | 239 | // Bind all private methods 240 | for (var fn in this) { 241 | if (fn.charAt(0) === '_') { 242 | this[fn] = _bind(this, this[fn]); 243 | } 244 | } 245 | 246 | 247 | // Bind events 248 | _on(el, 'mousedown', this._onTapStart); 249 | _on(el, 'touchstart', this._onTapStart); 250 | 251 | _on(el, 'dragover', this); 252 | _on(el, 'dragenter', this); 253 | 254 | // https://bugzilla.mozilla.org/show_bug.cgi?id=505521 255 | if (isFirefox && !bind) { 256 | _on(document, 'dragover', this._onDocumentDragOver); 257 | bind = true; 258 | } 259 | 260 | touchDragOverListeners.push(this._onDragOver); 261 | 262 | // Restore sorting 263 | options.store && this.sort(options.store.get(this)); 264 | } 265 | 266 | 267 | Sortable.prototype = /** @lends Sortable.prototype */ { 268 | constructor: Sortable, 269 | 270 | _onTapStart: function (/** Event|TouchEvent */evt) { 271 | var _this = this, 272 | el = this.el, 273 | options = this.options, 274 | type = evt.type, 275 | touch = evt.touches && evt.touches[0], 276 | target = (touch || evt).target, 277 | originalTarget = target, 278 | filter = options.filter; 279 | 280 | 281 | if (type === 'mousedown' && evt.button !== 0 || options.disabled) { 282 | return; // only left button or enabled 283 | } 284 | 285 | target = _closest(target, options.draggable, el); 286 | 287 | if (!target) { 288 | return; 289 | } 290 | 291 | // get the index of the dragged element within its parent 292 | oldIndex = _index(target); 293 | 294 | // Check filter 295 | if (typeof filter === 'function') { 296 | if (filter.call(this, evt, target, this)) { 297 | _dispatchEvent(_this, originalTarget, 'filter', target, el, oldIndex); 298 | evt.preventDefault(); 299 | return; // cancel dnd 300 | } 301 | } 302 | else if (filter) { 303 | filter = filter.split(',').some(function (criteria) { 304 | criteria = _closest(originalTarget, criteria.trim(), el); 305 | 306 | if (criteria) { 307 | _dispatchEvent(_this, criteria, 'filter', target, el, oldIndex); 308 | return true; 309 | } 310 | }); 311 | 312 | if (filter) { 313 | evt.preventDefault(); 314 | return; // cancel dnd 315 | } 316 | } 317 | 318 | 319 | if (options.handle && !_closest(originalTarget, options.handle, el)) { 320 | return; 321 | } 322 | 323 | 324 | // Prepare `dragstart` 325 | this._prepareDragStart(evt, touch, target); 326 | }, 327 | 328 | _prepareDragStart: function (/** Event */evt, /** Touch */touch, /** HTMLElement */target) { 329 | var _this = this, 330 | el = _this.el, 331 | options = _this.options, 332 | ownerDocument = el.ownerDocument, 333 | dragStartFn; 334 | 335 | if (target && !dragEl && (target.parentNode === el)) { 336 | tapEvt = evt; 337 | 338 | rootEl = el; 339 | dragEl = target; 340 | nextEl = dragEl.nextSibling; 341 | activeGroup = options.group; 342 | 343 | dragStartFn = function () { 344 | // Delayed drag has been triggered 345 | // we can re-enable the events: touchmove/mousemove 346 | _this._disableDelayedDrag(); 347 | 348 | // Make the element draggable 349 | dragEl.draggable = true; 350 | 351 | // Disable "draggable" 352 | options.ignore.split(',').forEach(function (criteria) { 353 | _find(dragEl, criteria.trim(), _disableDraggable); 354 | }); 355 | 356 | // Bind the events: dragstart/dragend 357 | _this._triggerDragStart(touch); 358 | }; 359 | 360 | _on(ownerDocument, 'mouseup', _this._onDrop); 361 | _on(ownerDocument, 'touchend', _this._onDrop); 362 | _on(ownerDocument, 'touchcancel', _this._onDrop); 363 | 364 | if (options.delay) { 365 | // If the user moves the pointer before the delay has been reached: 366 | // disable the delayed drag 367 | _on(ownerDocument, 'mousemove', _this._disableDelayedDrag); 368 | _on(ownerDocument, 'touchmove', _this._disableDelayedDrag); 369 | 370 | _this._dragStartTimer = setTimeout(dragStartFn, options.delay); 371 | } else { 372 | dragStartFn(); 373 | } 374 | } 375 | }, 376 | 377 | _disableDelayedDrag: function () { 378 | var ownerDocument = this.el.ownerDocument; 379 | 380 | clearTimeout(this._dragStartTimer); 381 | 382 | _off(ownerDocument, 'mousemove', this._disableDelayedDrag); 383 | _off(ownerDocument, 'touchmove', this._disableDelayedDrag); 384 | }, 385 | 386 | _triggerDragStart: function (/** Touch */touch) { 387 | if (touch) { 388 | // Touch device support 389 | tapEvt = { 390 | target: dragEl, 391 | clientX: touch.clientX, 392 | clientY: touch.clientY 393 | }; 394 | 395 | this._onDragStart(tapEvt, 'touch'); 396 | } 397 | else if (!supportDraggable) { 398 | this._onDragStart(tapEvt, true); 399 | } 400 | else { 401 | _on(dragEl, 'dragend', this); 402 | _on(rootEl, 'dragstart', this._onDragStart); 403 | 404 | if (this.options.dragOutRemove) { 405 | _on(dragEl, 'drag', this._onDrag); 406 | } 407 | } 408 | 409 | try { 410 | if (document.selection) { 411 | document.selection.empty(); 412 | } else { 413 | window.getSelection().removeAllRanges(); 414 | } 415 | } catch (err) { 416 | } 417 | }, 418 | 419 | _dragStarted: function () { 420 | if (rootEl && dragEl) { 421 | // Apply effect 422 | _toggleClass(dragEl, this.options.ghostClass, true); 423 | 424 | Sortable.active = this; 425 | 426 | // Drag start event 427 | _dispatchEvent(this, rootEl, 'start', dragEl, rootEl, oldIndex); 428 | } 429 | }, 430 | 431 | _emulateDragOver: function () { 432 | if (touchEvt) { 433 | _css(ghostEl, 'display', 'none'); 434 | 435 | var target = document.elementFromPoint(touchEvt.clientX, touchEvt.clientY), 436 | parent = target, 437 | groupName = ' ' + this.options.group.name + '', 438 | i = touchDragOverListeners.length; 439 | 440 | if (parent) { 441 | do { 442 | if (parent[expando] && parent[expando].options.groups.indexOf(groupName) > -1) { 443 | while (i--) { 444 | touchDragOverListeners[i]({ 445 | clientX: touchEvt.clientX, 446 | clientY: touchEvt.clientY, 447 | target: target, 448 | rootEl: parent 449 | }); 450 | } 451 | 452 | break; 453 | } 454 | 455 | target = parent; // store last element 456 | } 457 | /* jshint boss:true */ 458 | while (parent = parent.parentNode); 459 | } 460 | 461 | _css(ghostEl, 'display', ''); 462 | } 463 | }, 464 | 465 | 466 | _onTouchMove: function (/**TouchEvent*/evt) { 467 | if (tapEvt) { 468 | var touch = evt.touches ? evt.touches[0] : evt, 469 | dx = touch.clientX - tapEvt.clientX, 470 | dy = touch.clientY - tapEvt.clientY, 471 | translate3d = evt.touches ? 'translate3d(' + dx + 'px,' + dy + 'px,0)' : 'translate(' + dx + 'px,' + dy + 'px)'; 472 | 473 | touchEvt = touch; 474 | 475 | _css(ghostEl, 'webkitTransform', translate3d); 476 | _css(ghostEl, 'mozTransform', translate3d); 477 | _css(ghostEl, 'msTransform', translate3d); 478 | _css(ghostEl, 'transform', translate3d); 479 | 480 | if (this.options.dragOutRemove) { 481 | this._onDrag(touchEvt); 482 | } 483 | 484 | evt.preventDefault(); 485 | } 486 | }, 487 | 488 | /** 489 | * @author Kuitos 490 | * @since 2015-06-15 491 | */ 492 | _onDrag: _throttle(function (evt) { 493 | 494 | if(!lastContainer){ 495 | return; 496 | } 497 | 498 | var rect = lastContainer.getBoundingClientRect(), 499 | left = rect.left, 500 | right = rect.right, 501 | top = rect.top, 502 | bottom = rect.bottom, 503 | 504 | x = evt.clientX || pointerX, 505 | y = evt.clientY || pointerY; 506 | 507 | // when drag out from container 508 | if (right < x || x < left || bottom < y || y < top) { 509 | this._onDragOut(lastContainer, dragEl); 510 | } 511 | 512 | }, 120), 513 | 514 | _onDragOut: function (container, dragEl) { 515 | if (container[expando].options.group.pull !== "clone") { 516 | // we can not remove dragEl from container directly because when we remove dragEl on a device which not support draggable api, 517 | // the touchmove/mousemove event will lose the pointer 518 | if (!trashElement) { 519 | trashElement = document.querySelector((TRASH_ELEMENT_NODE_NAME + " ul").toLowerCase()); 520 | } 521 | 522 | if (!trashElement.contains(dragEl)) { 523 | trashElement.appendChild(dragEl); 524 | } 525 | 526 | } 527 | }, 528 | 529 | 530 | _onDragStart: function (/**Event*/evt, /**boolean*/useFallback) { 531 | var dataTransfer = evt.dataTransfer, 532 | options = this.options; 533 | 534 | this._offUpEvents(); 535 | 536 | if (activeGroup.pull == 'clone') { 537 | cloneEl = dragEl.cloneNode(true); 538 | _css(cloneEl, 'display', 'none'); 539 | rootEl.insertBefore(cloneEl, dragEl); 540 | } 541 | 542 | lastContainer = rootEl; 543 | 544 | if (useFallback) { 545 | var rect = dragEl.getBoundingClientRect(), 546 | css = _css(dragEl), 547 | ghostRect; 548 | 549 | ghostEl = dragEl.cloneNode(true); 550 | 551 | _css(ghostEl, 'top', rect.top - parseInt(css.marginTop, 10)); 552 | _css(ghostEl, 'left', rect.left - parseInt(css.marginLeft, 10)); 553 | _css(ghostEl, 'width', rect.width); 554 | _css(ghostEl, 'height', rect.height); 555 | _css(ghostEl, 'opacity', '0.8'); 556 | _css(ghostEl, 'position', 'fixed'); 557 | _css(ghostEl, 'zIndex', '100000'); 558 | 559 | rootEl.appendChild(ghostEl); 560 | 561 | // Fixing dimensions. 562 | ghostRect = ghostEl.getBoundingClientRect(); 563 | _css(ghostEl, 'width', rect.width * 2 - ghostRect.width); 564 | _css(ghostEl, 'height', rect.height * 2 - ghostRect.height); 565 | 566 | if (useFallback === 'touch') { 567 | // Bind touch events 568 | _on(document, 'touchmove', this._onTouchMove); 569 | _on(document, 'touchend', this._onDrop); 570 | _on(document, 'touchcancel', this._onDrop); 571 | } else { 572 | // Old brwoser 573 | _on(document, 'mousemove', this._onTouchMove); 574 | _on(document, 'mouseup', this._onDrop); 575 | } 576 | 577 | this._loopId = setInterval(this._emulateDragOver, 150); 578 | } 579 | else { 580 | if (dataTransfer) { 581 | dataTransfer.effectAllowed = 'move'; 582 | options.setData && options.setData.call(this, dataTransfer, dragEl); 583 | } 584 | 585 | _on(document, 'drop', this); 586 | } 587 | 588 | setTimeout(this._dragStarted, 0); 589 | }, 590 | 591 | _onDragOver: function (/**Event*/evt) { 592 | var el = this.el, 593 | target, 594 | dragRect, 595 | revert, 596 | options = this.options, 597 | group = options.group, 598 | groupPut = group.put, 599 | isOwner = (activeGroup === group), 600 | canSort = options.sort; 601 | 602 | pointerX = evt.clientX; 603 | pointerY = evt.clientY; 604 | 605 | if (evt.preventDefault !== void 0) { 606 | evt.preventDefault(); 607 | !options.dragoverBubble && evt.stopPropagation(); 608 | } 609 | 610 | if (activeGroup && !options.disabled && 611 | (isOwner 612 | ? canSort || (revert = !rootEl.contains(dragEl)) 613 | : activeGroup.pull && groupPut && ( 614 | (activeGroup.name === group.name) || // by Name 615 | (groupPut.indexOf && ~groupPut.indexOf(activeGroup.name)) // by Array 616 | ) 617 | ) && 618 | (evt.rootEl === void 0 || evt.rootEl === this.el) 619 | ) { 620 | // Smart auto-scrolling 621 | _autoScroll(evt, options, this.el); 622 | 623 | if (_silent) { 624 | return; 625 | } 626 | 627 | target = _closest(evt.target, options.draggable, el); 628 | dragRect = dragEl.getBoundingClientRect(); 629 | 630 | if (revert) { 631 | _cloneHide(true); 632 | 633 | lastContainer = rootEl; 634 | 635 | if (cloneEl || nextEl) { 636 | rootEl.insertBefore(dragEl, cloneEl || nextEl); 637 | } 638 | else if (!canSort) { 639 | rootEl.appendChild(dragEl); 640 | } 641 | 642 | return; 643 | } 644 | 645 | 646 | if ((el.children.length === 0) || (el.children[0] === ghostEl) || 647 | (el === evt.target) && (target = _ghostInBottom(el, evt)) 648 | ) { 649 | lastContainer = el; 650 | 651 | if (target) { 652 | if (target.animated) { 653 | return; 654 | } 655 | targetRect = target.getBoundingClientRect(); 656 | } 657 | 658 | _cloneHide(isOwner); 659 | 660 | el.appendChild(dragEl); 661 | this._animate(dragRect, dragEl); 662 | target && this._animate(targetRect, target); 663 | } 664 | else if (target && !target.animated && target !== dragEl && (target.parentNode[expando] !== void 0)) { 665 | if (lastEl !== target) { 666 | lastEl = target; 667 | lastCSS = _css(target); 668 | } 669 | 670 | 671 | var targetRect = target.getBoundingClientRect(), 672 | width = targetRect.right - targetRect.left, 673 | height = targetRect.bottom - targetRect.top, 674 | floating = /left|right|inline/.test(lastCSS.cssFloat + lastCSS.display), 675 | isWide = (target.offsetWidth > dragEl.offsetWidth), 676 | isLong = (target.offsetHeight > dragEl.offsetHeight), 677 | halfway = (floating ? (evt.clientX - targetRect.left) / width : (evt.clientY - targetRect.top) / height) > 0.5, 678 | nextSibling = target.nextElementSibling, 679 | after 680 | ; 681 | 682 | _silent = true; 683 | setTimeout(_unsilent, 30); 684 | 685 | _cloneHide(isOwner); 686 | 687 | if (floating) { 688 | after = (target.previousElementSibling === dragEl) && !isWide || halfway && isWide; 689 | } else { 690 | after = (nextSibling !== dragEl) && !isLong || halfway && isLong; 691 | } 692 | 693 | if (after && !nextSibling) { 694 | lastContainer = el; 695 | el.appendChild(dragEl); 696 | } else { 697 | lastContainer = target.parentNode; 698 | target.parentNode.insertBefore(dragEl, after ? nextSibling : target); 699 | } 700 | 701 | this._animate(dragRect, dragEl); 702 | this._animate(targetRect, target); 703 | } 704 | } 705 | }, 706 | 707 | _onDocumentDragOver: function (evt) { 708 | pointerX = evt.clientX; 709 | pointerY = evt.clientY; 710 | if (evt.preventDefault !== void 0) { 711 | evt.preventDefault(); 712 | !this.options.dragoverBubble && evt.stopPropagation(); 713 | } 714 | }, 715 | 716 | _animate: function (prevRect, target) { 717 | var ms = this.options.animation; 718 | 719 | if (ms) { 720 | var currentRect = target.getBoundingClientRect(); 721 | 722 | _css(target, 'transition', 'none'); 723 | _css(target, 'transform', 'translate3d(' 724 | + (prevRect.left - currentRect.left) + 'px,' 725 | + (prevRect.top - currentRect.top) + 'px,0)' 726 | ); 727 | 728 | target.offsetWidth; // repaint 729 | 730 | _css(target, 'transition', 'all ' + ms + 'ms'); 731 | _css(target, 'transform', 'translate3d(0,0,0)'); 732 | 733 | clearTimeout(target.animated); 734 | target.animated = setTimeout(function () { 735 | _css(target, 'transition', ''); 736 | _css(target, 'transform', ''); 737 | target.animated = false; 738 | }, ms); 739 | } 740 | }, 741 | 742 | _offUpEvents: function () { 743 | var ownerDocument = this.el.ownerDocument; 744 | 745 | _off(document, 'touchmove', this._onTouchMove); 746 | _off(ownerDocument, 'mouseup', this._onDrop); 747 | _off(ownerDocument, 'touchend', this._onDrop); 748 | _off(ownerDocument, 'touchcancel', this._onDrop); 749 | }, 750 | 751 | _onDrop: function (/**Event*/evt) { 752 | var el = this.el, 753 | options = this.options; 754 | 755 | clearInterval(this._loopId); 756 | clearInterval(autoScroll.pid); 757 | 758 | clearTimeout(this._dragStartTimer); 759 | 760 | // Unbind events 761 | _off(document, 'drop', this); 762 | _off(document, 'mousemove', this._onTouchMove); 763 | _off(el, 'dragstart', this._onDragStart); 764 | 765 | this._offUpEvents(); 766 | 767 | if (evt) { 768 | evt.preventDefault(); 769 | !options.dropBubble && evt.stopPropagation(); 770 | 771 | ghostEl && ghostEl.parentNode.removeChild(ghostEl); 772 | 773 | if (dragEl) { 774 | _off(dragEl, 'drag', this._onDrag); 775 | _off(dragEl, 'dragend', this); 776 | 777 | _disableDraggable(dragEl); 778 | _toggleClass(dragEl, this.options.ghostClass, false); 779 | 780 | if (rootEl !== dragEl.parentNode) { 781 | newIndex = _index(dragEl); 782 | 783 | // drag from one list and drop into another 784 | _dispatchEvent(null, dragEl.parentNode, 'sort', dragEl, rootEl, oldIndex, newIndex); 785 | _dispatchEvent(this, rootEl, 'sort', dragEl, rootEl, oldIndex, newIndex); 786 | 787 | // when drag out remove 788 | if (trashElement && trashElement.contains(dragEl.parentNode)) { 789 | _dispatchEvent(null, rootEl, 'dragOutRemove', dragEl, rootEl, oldIndex, newIndex); 790 | } 791 | 792 | // Add event 793 | _dispatchEvent(null, dragEl.parentNode, 'add', dragEl, rootEl, oldIndex, newIndex); 794 | 795 | // Remove event 796 | _dispatchEvent(this, rootEl, 'remove', dragEl, rootEl, oldIndex, newIndex); 797 | } 798 | else { 799 | // Remove clone 800 | cloneEl && cloneEl.parentNode.removeChild(cloneEl); 801 | 802 | if (dragEl.nextSibling !== nextEl) { 803 | // Get the index of the dragged element within its parent 804 | newIndex = _index(dragEl); 805 | 806 | // drag & drop within the same list 807 | _dispatchEvent(this, rootEl, 'update', dragEl, rootEl, oldIndex, newIndex); 808 | _dispatchEvent(this, rootEl, 'sort', dragEl, rootEl, oldIndex, newIndex); 809 | } 810 | } 811 | 812 | // Drag end event 813 | Sortable.active && _dispatchEvent(this, rootEl, 'end', dragEl, rootEl, oldIndex, newIndex); 814 | } 815 | 816 | // Nulling 817 | rootEl = 818 | dragEl = 819 | ghostEl = 820 | nextEl = 821 | cloneEl = 822 | 823 | scrollEl = 824 | scrollParentEl = 825 | 826 | tapEvt = 827 | touchEvt = 828 | 829 | lastEl = 830 | lastCSS = 831 | lastContainer = 832 | 833 | activeGroup = 834 | Sortable.active = null; 835 | 836 | // Save sorting 837 | this.save(); 838 | } 839 | }, 840 | 841 | 842 | handleEvent: function (/**Event*/evt) { 843 | var type = evt.type; 844 | 845 | if (type === 'dragover' || type === 'dragenter') { 846 | if (dragEl) { 847 | this._onDragOver(evt); 848 | _globalDragOver(evt); 849 | } 850 | } 851 | else if (type === 'drop' || type === 'dragend') { 852 | this._onDrop(evt); 853 | } 854 | }, 855 | 856 | 857 | /** 858 | * Serializes the item into an array of string. 859 | * @returns {String[]} 860 | */ 861 | toArray: function () { 862 | var order = [], 863 | el, 864 | children = this.el.children, 865 | i = 0, 866 | n = children.length, 867 | options = this.options; 868 | 869 | for (; i < n; i++) { 870 | el = children[i]; 871 | if (_closest(el, options.draggable, this.el)) { 872 | order.push(el.getAttribute(options.dataIdAttr) || _generateId(el)); 873 | } 874 | } 875 | 876 | return order; 877 | }, 878 | 879 | 880 | /** 881 | * Sorts the elements according to the array. 882 | * @param {String[]} order order of the items 883 | */ 884 | sort: function (order) { 885 | var items = {}, rootEl = this.el; 886 | 887 | this.toArray().forEach(function (id, i) { 888 | var el = rootEl.children[i]; 889 | 890 | if (_closest(el, this.options.draggable, rootEl)) { 891 | items[id] = el; 892 | } 893 | }, this); 894 | 895 | order.forEach(function (id) { 896 | if (items[id]) { 897 | rootEl.removeChild(items[id]); 898 | rootEl.appendChild(items[id]); 899 | } 900 | }); 901 | }, 902 | 903 | 904 | /** 905 | * Save the current sorting 906 | */ 907 | save: function () { 908 | var store = this.options.store; 909 | store && store.set(this); 910 | }, 911 | 912 | 913 | /** 914 | * For each element in the set, get the first element that matches the selector by testing the element itself and traversing up through its ancestors in the DOM tree. 915 | * @param {HTMLElement} el 916 | * @param {String} [selector] default: `options.draggable` 917 | * @returns {HTMLElement|null} 918 | */ 919 | closest: function (el, selector) { 920 | return _closest(el, selector || this.options.draggable, this.el); 921 | }, 922 | 923 | 924 | /** 925 | * Set/get option 926 | * @param {string} name 927 | * @param {*} [value] 928 | * @returns {*} 929 | */ 930 | option: function (name, value) { 931 | var options = this.options; 932 | 933 | if (value === void 0) { 934 | return options[name]; 935 | } else { 936 | options[name] = value; 937 | } 938 | }, 939 | 940 | 941 | /** 942 | * Destroy 943 | */ 944 | destroy: function () { 945 | var el = this.el; 946 | 947 | el[expando] = null; 948 | 949 | _off(el, 'mousedown', this._onTapStart); 950 | _off(el, 'touchstart', this._onTapStart); 951 | 952 | _off(el, 'dragover', this); 953 | _off(el, 'dragenter', this); 954 | _off(document, 'dragover', this._onDocumentDragOver); 955 | 956 | // Remove draggable attributes 957 | Array.prototype.forEach.call(el.querySelectorAll('[draggable]'), function (el) { 958 | el.removeAttribute('draggable'); 959 | }); 960 | 961 | touchDragOverListeners.splice(touchDragOverListeners.indexOf(this._onDragOver), 1); 962 | 963 | this._onDrop(); 964 | 965 | this.el = el = null; 966 | } 967 | }; 968 | 969 | 970 | function _cloneHide(state) { 971 | if (cloneEl && (cloneEl.state !== state)) { 972 | _css(cloneEl, 'display', state ? 'none' : ''); 973 | !state && cloneEl.state && rootEl.insertBefore(cloneEl, dragEl); 974 | cloneEl.state = state; 975 | } 976 | } 977 | 978 | 979 | function _bind(ctx, fn) { 980 | var args = slice.call(arguments, 2); 981 | return fn.bind ? fn.bind.apply(fn, [ctx].concat(args)) : function () { 982 | return fn.apply(ctx, args.concat(slice.call(arguments))); 983 | }; 984 | } 985 | 986 | 987 | function _closest(/**HTMLElement*/el, /**String*/selector, /**HTMLElement*/ctx) { 988 | if (el) { 989 | ctx = ctx || document; 990 | selector = selector.split('.'); 991 | 992 | var tag = selector.shift().toUpperCase(), 993 | re = new RegExp('\\s(' + selector.join('|') + ')\\s', 'g'); 994 | 995 | do { 996 | if ( 997 | (tag === '>*' && el.parentNode === ctx) || ( 998 | (tag === '' || el.nodeName.toUpperCase() == tag) && 999 | (!selector.length || ((' ' + el.className + ' ').match(re) || []).length == selector.length) 1000 | ) 1001 | ) { 1002 | return el; 1003 | } 1004 | } 1005 | while (el !== ctx && (el = el.parentNode)); 1006 | } 1007 | 1008 | return null; 1009 | } 1010 | 1011 | 1012 | function _globalDragOver(/**Event*/evt) { 1013 | evt.dataTransfer.dropEffect = 'move'; 1014 | evt.preventDefault(); 1015 | } 1016 | 1017 | 1018 | function _on(el, event, fn) { 1019 | el.addEventListener(event, fn, false); 1020 | } 1021 | 1022 | 1023 | function _off(el, event, fn) { 1024 | el.removeEventListener(event, fn, false); 1025 | } 1026 | 1027 | 1028 | function _toggleClass(el, name, state) { 1029 | if (el) { 1030 | if (el.classList) { 1031 | el.classList[state ? 'add' : 'remove'](name); 1032 | } 1033 | else { 1034 | var className = (' ' + el.className + ' ').replace(RSPACE, ' ').replace(' ' + name + ' ', ' '); 1035 | el.className = (className + (state ? ' ' + name : '')).replace(RSPACE, ' '); 1036 | } 1037 | } 1038 | } 1039 | 1040 | 1041 | function _css(el, prop, val) { 1042 | var style = el && el.style; 1043 | 1044 | if (style) { 1045 | if (val === void 0) { 1046 | if (document.defaultView && document.defaultView.getComputedStyle) { 1047 | val = document.defaultView.getComputedStyle(el, ''); 1048 | } 1049 | else if (el.currentStyle) { 1050 | val = el.currentStyle; 1051 | } 1052 | 1053 | return prop === void 0 ? val : val[prop]; 1054 | } 1055 | else { 1056 | if (!(prop in style)) { 1057 | prop = '-webkit-' + prop; 1058 | } 1059 | 1060 | style[prop] = val + (typeof val === 'string' ? '' : 'px'); 1061 | } 1062 | } 1063 | } 1064 | 1065 | 1066 | function _find(ctx, tagName, iterator) { 1067 | if (ctx) { 1068 | var list = ctx.getElementsByTagName(tagName), i = 0, n = list.length; 1069 | 1070 | if (iterator) { 1071 | for (; i < n; i++) { 1072 | iterator(list[i], i); 1073 | } 1074 | } 1075 | 1076 | return list; 1077 | } 1078 | 1079 | return []; 1080 | } 1081 | 1082 | 1083 | function _disableDraggable(el) { 1084 | el.draggable = false; 1085 | } 1086 | 1087 | 1088 | function _unsilent() { 1089 | _silent = false; 1090 | } 1091 | 1092 | 1093 | /** @returns {HTMLElement|false} */ 1094 | function _ghostInBottom(el, evt) { 1095 | var lastEl = el.lastElementChild, rect = lastEl.getBoundingClientRect(); 1096 | return (evt.clientY - (rect.top + rect.height) > 5) && lastEl; // min delta 1097 | } 1098 | 1099 | 1100 | /** 1101 | * Generate id 1102 | * @param {HTMLElement} el 1103 | * @returns {String} 1104 | * @private 1105 | */ 1106 | function _generateId(el) { 1107 | var str = el.tagName + el.className + el.src + el.href + el.textContent, 1108 | i = str.length, 1109 | sum = 0; 1110 | 1111 | while (i--) { 1112 | sum += str.charCodeAt(i); 1113 | } 1114 | 1115 | return sum.toString(36); 1116 | } 1117 | 1118 | /** 1119 | * Returns the index of an element within its parent 1120 | * @param el 1121 | * @returns {number} 1122 | * @private 1123 | */ 1124 | function _index(/**HTMLElement*/el) { 1125 | var index = 0; 1126 | while (el && (el = el.previousElementSibling)) { 1127 | if (el.nodeName.toUpperCase() !== 'TEMPLATE') { 1128 | index++; 1129 | } 1130 | } 1131 | return index; 1132 | } 1133 | 1134 | function _throttle(callback, ms) { 1135 | var args, _this; 1136 | 1137 | return function () { 1138 | if (args === void 0) { 1139 | args = arguments; 1140 | _this = this; 1141 | 1142 | setTimeout(function () { 1143 | if (args.length === 1) { 1144 | callback.call(_this, args[0]); 1145 | } else { 1146 | callback.apply(_this, args); 1147 | } 1148 | 1149 | args = void 0; 1150 | }, ms); 1151 | } 1152 | }; 1153 | } 1154 | 1155 | function _extend(dst, src) { 1156 | if (dst && src) { 1157 | for (var key in src) { 1158 | if (src.hasOwnProperty(key)) { 1159 | dst[key] = src[key]; 1160 | } 1161 | } 1162 | } 1163 | 1164 | return dst; 1165 | } 1166 | 1167 | 1168 | // Export utils 1169 | Sortable.utils = { 1170 | on: _on, 1171 | off: _off, 1172 | css: _css, 1173 | find: _find, 1174 | bind: _bind, 1175 | is: function (el, selector) { 1176 | return !!_closest(el, selector, el); 1177 | }, 1178 | extend: _extend, 1179 | throttle: _throttle, 1180 | closest: _closest, 1181 | toggleClass: _toggleClass, 1182 | index: _index 1183 | }; 1184 | 1185 | 1186 | Sortable.version = '1.2.0'; 1187 | 1188 | 1189 | /** 1190 | * Create sortable instance 1191 | * @param {HTMLElement} el 1192 | * @param {Object} [options] 1193 | */ 1194 | Sortable.create = function (el, options) { 1195 | return new Sortable(el, options); 1196 | }; 1197 | 1198 | // Export 1199 | return Sortable; 1200 | }); 1201 | --------------------------------------------------------------------------------