├── 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 |
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 = '';
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 |
--------------------------------------------------------------------------------