directives.js 37 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027
  1. define(['angular', 'showdown', 'angular-toaster'], function(angular) {
  2. 'use strict';
  3. angular.module('common.directives', ['common.tpls']).directive('loading', function() {
  4. return {
  5. restrict: 'A',
  6. scope: false,
  7. link: function(scope, element, attrs) {
  8. element.addClass('loading-container');
  9. scope.$watch(attrs.loading, function(value) {
  10. element.toggleClass('ng-hide', !value);
  11. });
  12. }
  13. };
  14. }).directive('datetrigger', ['$parse', function($parse) {
  15. return {
  16. restrict: 'EA',
  17. transclude: true,
  18. replace: true,
  19. scope: {
  20. format: '@',
  21. name: '@',
  22. style: '@',
  23. maxDate: '=',
  24. minDate: '='
  25. },
  26. templateUrl: 'template/common/datetrigger.html',
  27. link: function(scope, element, attrs) {
  28. scope.isOpen = false;
  29. scope.open = function($event) {
  30. $event.preventDefault();
  31. $event.stopPropagation();
  32. scope.isOpen = !scope.isOpen;
  33. };
  34. var getModelValue = $parse(attrs.model),
  35. setModelValue = getModelValue.assign;
  36. scope.$parent.$watch(getModelValue, function(val) {
  37. scope.date = val;
  38. });
  39. scope.$watch('date', function(val) {
  40. if (setModelValue) {
  41. setModelValue(scope.$parent, val);
  42. }
  43. });
  44. }
  45. };
  46. }]).directive('ngSearch', ['$parse', function($parse) {
  47. /** Introduction
  48. * 搜索输入框
  49. * eg:<input ng-search="onSearch(keyword)" ng-model="keyword">
  50. */
  51. return {
  52. require: '?ngModel',
  53. restrict: 'A',
  54. link: function(scope, element, attrs, ngModel) {
  55. var searchFn = $parse(attrs.ngSearch);
  56. element.bind('keypress', function(event) {
  57. if (event.keyCode == '13') {
  58. event.preventDefault();
  59. event.stopPropagation();
  60. searchFn(scope, {
  61. $data: ngModel.$modelValue,
  62. $event: event
  63. });
  64. }
  65. });
  66. }
  67. };
  68. }]).directive('scrollTop', ['$document', function($document) { // 向上滚动到屏幕顶部是就固定到屏幕顶部
  69. return {
  70. restrict: 'A',
  71. scope: {
  72. 'scrollTop': '@'
  73. },
  74. link: function(scope, element, attr) {
  75. var scrollTop = scope.scrollTop || 0;
  76. var offsetTop = element[0].offsetTop,
  77. position = element.css('position'),
  78. top = element.css('top');
  79. var height = element[0].offsetHeight,
  80. nextMargin = element.next().css('margin-top');
  81. var m = height + parseInt(nextMargin);
  82. $document.on('scroll', function(e) {
  83. var documentScrollTop = document.documentElement.scrollTop || window.pageYOffset || document.body.scrollTop;
  84. if (documentScrollTop > offsetTop - scrollTop) {
  85. element.css('position', 'fixed').css('top', scrollTop + 'px');
  86. if (position == 'static') { // 给后面兄弟节点加上margin-top,防止Dom结构错乱
  87. element.next().css('margin-top', m + 'px');
  88. }
  89. } else {
  90. element.css('position', position).css('top', top + 'px');
  91. if (position == 'static') {
  92. element.next().css('margin-top', nextMargin);
  93. }
  94. }
  95. });
  96. }
  97. };
  98. }]).directive('margin', ['$document', function($document) { // 向上滚动到屏幕顶部是就固定到屏幕顶部
  99. return {
  100. restrict: 'A',
  101. scope: {
  102. margin: '@',
  103. size: '='
  104. },
  105. link: function(scope, element, attr) {
  106. var margin = scope.margin || 'top';
  107. var size = scope.size || 10;
  108. if (margin == 'top') {
  109. element.css('margin-top', size + 'px');
  110. } else if (margin == 'bottom') {
  111. element.css('margin-bottom', size + 'px');
  112. } else if (margin == 'left') {
  113. element.css('margin-left', size + 'px');
  114. } else if (margin == 'right') {
  115. element.css('margin-right', size + 'px');
  116. }
  117. }
  118. };
  119. }]).directive('lazyLoad', function() {
  120. /**
  121. * 延迟加载:lazy-load="http://www.example.com/1.png"
  122. */
  123. var elements = (function() {
  124. var index = 0;
  125. var _elements = [];
  126. return {
  127. push: function(element) {
  128. _elements[index++] = element;
  129. },
  130. del: function(index) {
  131. _elements.splice(index, 1);
  132. },
  133. get: function() {
  134. return _elements;
  135. },
  136. size: function() {
  137. return _elements.length;
  138. }
  139. }
  140. })();
  141. // 元素是否在可视区域
  142. var isVisible = function(ele) {
  143. var rect = ele[0].getBoundingClientRect();
  144. if (angular.element(window)[0].parent.innerHeight < rect.top) {// 元素位于屏幕下方
  145. return false;
  146. } else {
  147. return true;
  148. }
  149. };
  150. // 加载图片
  151. var load = function(element, index) {
  152. var el = element.el;
  153. el.attr('src', element.src);
  154. el.on('load', function() {
  155. el.css({
  156. 'opacity': '1'
  157. })
  158. });
  159. elements.del(index);
  160. };
  161. // 检查是否可见
  162. var checkElements = function() {
  163. var els = elements.get();
  164. angular.forEach(els, function(v, k) {
  165. isVisible(v.el) ? load(v, k) : false;
  166. });
  167. };
  168. angular.element(window).on('scroll', checkElements);
  169. return {
  170. restrict: 'EA',
  171. replace: false,
  172. link: function(scope, element, attrs) {
  173. var url = attrs.lazyLoad;
  174. if (url) {
  175. if (isVisible(element)) {
  176. element.attr('src', url);
  177. } else {
  178. element.css({
  179. 'background': '#fff',
  180. 'opacity': 0,
  181. 'transition': 'opacity 1s',
  182. '-webkit-transition': 'opacity 1s',
  183. 'animation-duration': '1s'
  184. });
  185. elements.push({
  186. el: element,
  187. src: url
  188. });
  189. }
  190. }
  191. }
  192. };
  193. })
  194. /**
  195. * 分离的下拉框
  196. * <div split-dropdown>
  197. * <div dropdown-scroll>
  198. * <div split-dropdown-trigger="1"></div>
  199. * <div split-dropdown-trigger="2"></div>
  200. * <div split-dropdown-trigger="3"></div>
  201. * </div>
  202. * <div>
  203. * <div split-dropdown-toggle="1"></div>
  204. * <div split-dropdown-toggle="2"></div>
  205. * <div split-dropdown-toggle="3"></div>
  206. * </div>
  207. * </div>
  208. */
  209. .directive('splitDropdownTrigger', [function() {
  210. return {
  211. restrict: 'A',
  212. scope: {
  213. splitDropdownTrigger: '='
  214. },
  215. require: '?splitDropdownToggle',
  216. link: function(scope, element, attr) {
  217. var me = element;
  218. var container = me.parents('[split-dropdown]');
  219. var id = scope.splitDropdownTrigger;
  220. // 获取对应的下拉显示框
  221. // 移动到开关时显示下拉框,移开时隐藏下拉框,其中从开关移动到下拉框时是有两个动作的:1、从开关移出;2、移动下拉框
  222. me.off('mouseenter').bind('mouseenter', function(e) {
  223. var menu = container.find('[split-dropdown-toggle=' + id + ']');
  224. setPosition(menu);
  225. menu.css('display', 'block');
  226. }).bind('mouseleave', function(e) {
  227. var menu = container.find('[split-dropdown-toggle=' + id + ']');
  228. menu.css('display', 'none');
  229. });
  230. function setPosition(menu) {
  231. var top = me.offset().top - container.offset().top;
  232. var left = me.offset().left - container.offset().left;
  233. var width = me.innerWidth(), height = me.innerHeight();
  234. top = top + height;
  235. // 控制下拉显示框的定位
  236. menu.css('top', top + 'px').css('left', left);
  237. }
  238. }
  239. };
  240. }])
  241. .directive('splitDropdownToggle', [function() {
  242. return {
  243. restrict: 'A',
  244. link: function(scope, element, attr) {
  245. // 移动到下拉显示框上不隐藏下拉显示框,移开下拉显示框隐藏下拉显示框
  246. element.off('mouseenter').bind('mouseenter', function(e) {
  247. element.css('display', 'block');
  248. }).bind('mouseleave', function(e) {
  249. element.css('display', 'none');
  250. });
  251. }
  252. }
  253. }])
  254. /**
  255. * 提示框指令,主要用于从overflow popping出来
  256. * 定位的级别是父级的positioned容器/body
  257. */
  258. .directive('callout', [function () {
  259. return {
  260. restrict: 'A',
  261. link: function (scope, element, attrs) {
  262. var left, top;
  263. if(attrs['sticky']) { // 对应data-sticky
  264. // 有问题,无法成功监听DOM改变事件
  265. // var stickySelector = '#' + attrs['sticky'];
  266. // var calloutContainer = '#' + attrs['calloutContainer'];
  267. // left = $(stickySelector).position().left + Number(attrs['leftSticky']);
  268. // top = $(stickySelector).position().top + Number(attrs['topSticky']);
  269. // $(calloutContainer).bind("mouseover",function(){
  270. // var newLeft = $(stickySelector).position().left + Number(attrs['leftSticky']);
  271. // var newTop = $(stickySelector).position().top + Number(attrs['topSticky']);
  272. // if(left != newLeft || top != newTop) {
  273. // element.css({'left': newLeft, 'top' : newTop}); // 便于调试
  274. // }
  275. // });
  276. }else {
  277. left = element.position().left + Number(attrs['left']); // 数据属性获取值时去掉data部分。如获取data-left的值使用attrs[left]
  278. top = element.position().top + Number(attrs['top']);
  279. }
  280. element.css({'left': left, 'top' : top});
  281. }
  282. }
  283. }]).
  284. /**
  285. * 验证所有分段价格的信息
  286. */
  287. directive('validataPrice', ['toaster', function (toaster) {
  288. return {
  289. restrict:'A',
  290. require:'?^ngModel',
  291. link:function(scope, iele, iattr, ctrl){
  292. var validataPrice = function () {
  293. if(ctrl.$viewValue) {
  294. var value = ctrl.$viewValue;
  295. var model = ctrl.$modelValue;
  296. if(!value) {
  297. return ;
  298. }
  299. if(value.indexOf('.') > -1) {
  300. var arr = value.split(".");
  301. if(arr[0].length > 4) {
  302. ctrl.$setViewValue(model);
  303. ctrl.$render();
  304. // toaster.pop('warning', '提示', '单价必须小于10000');
  305. return model;
  306. }
  307. if(arr[1].length > 6) {
  308. ctrl.$setViewValue(model);
  309. ctrl.$render();
  310. // toaster.pop('warning', '提示', '单价只精确到小数点后6位');
  311. return model;
  312. }
  313. }else {
  314. if(value.toString().length > 4) {
  315. ctrl.$setViewValue(model);
  316. ctrl.$render();
  317. // toaster.pop('warning', '提示', '单价必须小于10000');
  318. return model;
  319. }
  320. }
  321. }
  322. return value;
  323. };
  324. ctrl.$parsers.push(validataPrice);
  325. }
  326. }
  327. }]);
  328. angular.module("common.tpls", ["template/common/datetrigger.html"]);
  329. angular.module("template/common/datetrigger.html", []).run(["$templateCache", function($templateCache) {
  330. $templateCache.put("template/common/datetrigger.html",
  331. "<div class=\"input-append\">" +
  332. "<input type=\"text\" name=\"{{name}}\" class=\"input-medium\" " +
  333. "style=\"{{style}}\" "+
  334. "datepicker-popup=\"{{format}}\" ng-model=\"date\" " +
  335. "is-open=\"isOpen\" show-weeks=\"false\"" +
  336. "current-text=\"{{'ui.date.currentText' | i18n}}\"" +
  337. "toggle-weeks-text=\"{{'ui.date.toggleWeeksText' | i18n}}\"" +
  338. "clear-text=\"{{'ui.date.clearText' | i18n}}\"" +
  339. "datepicker-options=\"{formatDayTitle: '{{\'ui.date.formatDayTitle\' | i18n}}', formatMonth: '{{\'ui.date.formatMonth\' | i18n}}', showWeeks: false}\"" +
  340. "readonly=\"readonly\" " +
  341. "max-date=\"{{maxDate}}\"" +
  342. "min-date=\"{{minDate}}\"" +
  343. "close-text=\"{{'ui.date.closeText' | i18n}}\"/>" +
  344. "<button type=\"button\" class=\"btn btn-default\"" +
  345. "ng-click=\"open($event)\">" +
  346. "<i class=\"glyphicon glyphicon-calendar\"></i>" +
  347. "</button>" +
  348. "</div>");
  349. }]);
  350. /**
  351. * 表格相关指令
  352. * @author yangck
  353. */
  354. angular.module('table.directives', [])
  355. /**
  356. * 固定表头
  357. * 部分支持colspan,tbody前一行tr与下一行tr合并的单元格不同的情况还未处理(这种情况较少,暂不处理)
  358. */
  359. .directive('fixedThead', ['$timeout', function ($timeout) {
  360. // 设置宽度,box-sizing=border-box这种
  361. var setTableCss = function (table, state) {
  362. state.thWidths = [], state.tdWidths = [];
  363. table.outerWidth(table.outerWidth(true)); // 设置table宽度,不然可能导致tbody宽度与thead宽度不一致,同时使之在thead position为固定定位后宽度保持不变。给正常定位的tbody或tr设置宽度是不起作用的,他们的宽度总是会与table保持一致,只有给fixed/absolute类似的定位设置宽度才管用
  364. $('thead tr:eq(0) th', table).each(function (i, v) {
  365. state.thWidths.push({width: $(v).outerWidth(true), height: $(v).outerHeight(true)});
  366. });
  367. // 对于fixed的thead,如果给设置给thead设置了宽度,并且设置给内部的th宽度之和大于设置给thead的宽度,则会按照thead宽度重排(因此不要给thead设置宽度)
  368. // $('thead', table).outerWidth($('tbody', table).outerWidth(true));
  369. for (var i = 0; i < state.thWidths.length; i++) {
  370. $('thead th:eq(' + i + ')', table).outerWidth(state.thWidths[i].width);
  371. $('thead th:eq(' + i + ')', table).outerHeight(state.thWidths[i].height);
  372. }
  373. // tbody使用自己的宽度来固定,因为th/td可能会合并单元格,从而设置的宽度不准确
  374. if(isTdValid(table)) {
  375. $('tbody tr:eq(0) td', table).each(function (i, v) {
  376. state.tdWidths.push($(v).outerWidth(true));
  377. });
  378. }
  379. // 为tbody指定宽度是没有意义的,正常表格中设置给tbody与tr的宽度都是无效的,因为他们的宽度始终与table相同,设置在table上才有效
  380. // tbody的td用td自己的宽度来固定,thead的th用th自己的宽度来固定。这样对于th/td合并单元格之类的问题就好处理一点。但tbody前一行tr与下一行tr合并的单元格不同的情况还未处理
  381. for (var i = 0; i < state.tdWidths.length; i++) {
  382. $('tbody tr:eq(0) td:eq('+ i +')', table).outerWidth(state.tdWidths[i]); // 为tbody第一个tr的每个单元格指定宽度,使tbody在position为固定定位后显示效果不变
  383. }
  384. $('tbody tr:eq(0)', table).attr('data-tr-width-marked', "marked"); // 固定tbody表格的样式丢失时(翻页、表格重加载时),这时需要重新设置宽度。而这里的属性就是表示是否设置的样式已经丢失。
  385. table.css({'border-top': 0}); // thead设置为fixed后,table的顶部边框会在tbody的边框之上,应该取消这个边框
  386. $('thead', table).css({position: 'fixed', 'z-index': 100});
  387. state.parent.css({'padding-top': $('thead', table).height()}); // 填充表头的位置
  388. state.thWidthSum = getTrWidth(table, 'thead');
  389. state.needSetCss = false;
  390. };
  391. // 重置表格CSS。表头一行的宽度发生变化时(如:表格列数变化),需要把表头重置为static来重新渲染。不然表头单元格的宽度就和tbody单元格不匹配
  392. var resetTableCss = function (table, state) {
  393. state.needSetCss = true;
  394. // 不要使用removeAttr,因为可能有别人写inline css
  395. $('thead', table).css({
  396. position: 'static',
  397. 'z-index': 'auto',
  398. left: 'auto',
  399. top: 'auto',
  400. width: 'auto'
  401. });
  402. table.css({'width': '', 'border-top': ''});
  403. state.parent.css({'padding-top': 0});
  404. };
  405. // 判断滚动方向
  406. var isScrollUp = function (state) {
  407. if($(window).scrollTop() < state.lastWindowScrollPos) {
  408. return true; // 向上滚动
  409. }else {
  410. return false; // 向下滚动
  411. }
  412. };
  413. // 解析style为json对象
  414. var parseCss = function (style) {
  415. var jsonStyle = {};
  416. if(style) {
  417. var csses = style.split(";");
  418. if(csses[csses.length - 1] == "")
  419. csses.splice(- 1, 1);
  420. for(var i = 0; i < csses.length; i++) {
  421. var css = csses[i].split(":");
  422. var k = $.trim(css[0]);
  423. var v = $.trim(css[1]);
  424. if (k.length > 0 && v.length > 0)
  425. {
  426. jsonStyle[k] = v;
  427. }
  428. }
  429. }
  430. return jsonStyle;
  431. };
  432. // 得到element的style属性里面的某个css property的值。 style ex:"width: 86px; height: 57px;"
  433. var getCssValueInStyle = function (style, property) {
  434. var styleObject = parseCss(style);
  435. var result = styleObject[property];
  436. if(result) {
  437. if(property == 'width') {
  438. var width = styleObject[property];
  439. return Number(width.slice(0, -2)); // px不要包含在内。注:因为我们固定样式用的是px,所以不用考虑em、百分数等单位
  440. }
  441. return result;
  442. }
  443. return null;
  444. };
  445. // 得到tr宽度。type:thead/tbody
  446. var getTrWidth = function (table, type) {
  447. var trWidth = 0; // cell理论宽度之和。对于fixed的thead,如果给设置给thead设置了宽度,并且设置给内部的th宽度之和大于设置给thead的宽度,则会按照thead宽度重排(因此不要给thead设置宽度)。
  448. var cells;
  449. if(type == 'thead') {
  450. cells = $(type + ' tr:eq(0) th', table);
  451. }else {
  452. cells = $(type + ' tr:eq(0) td', table);
  453. }
  454. cells.each(function (i, v) {
  455. // 不能使用"css('width')",因为"css()"返回的是计算的实际宽度,而不是style里面设置的width值
  456. var widthValue = getCssValueInStyle($(v).attr('style'), 'width');
  457. if(widthValue) {
  458. trWidth += widthValue;
  459. }else {
  460. trWidth += $(v).outerWidth(true);
  461. }
  462. });
  463. return trWidth;
  464. };
  465. // 判断单元格宽度是否有效(如设置宽度为90px,实际宽度却是100px)
  466. var isWidthValid = function (table) {
  467. $('thead tr:eq(0) th', table).each(function (i, v) {
  468. if($(v).css('width')) {
  469. var widthValue = getCssValueInStyle($(v).attr('style'), 'width');
  470. var actualWidth = $(v).outerWidth(true);
  471. if(widthValue && widthValue != actualWidth) return false;
  472. }
  473. });
  474. $('tbody tr:eq(0) td', table).each(function (i, v) {
  475. if($(v).css('width')) {
  476. var widthValue = getCssValueInStyle($(v).attr('style'), 'width');
  477. var actualWidth = $(v).outerWidth(true);
  478. if(widthValue && widthValue != actualWidth) return false;
  479. }
  480. });
  481. return true;
  482. };
  483. // 判断td是否有效。暂且认为td数量大于1为有效
  484. var isTdValid = function (table) {
  485. return $('tbody tr:eq(0) td', table).length > 1;
  486. }
  487. // 判断是否th宽度与td对应宽度是否相等
  488. var isThTdWidthEqual = function (state) {
  489. for(var i = 0; i < state.thWidths.length; i++) {
  490. if(state.thWidths[i].width != state.tdWidths[i])
  491. return false;
  492. }
  493. return true;
  494. };
  495. // 所有资源是否加载完毕
  496. var isAllResourceLoadingFinished = function (resourceLoadingRecord) {
  497. for(var resource in resourceLoadingRecord) {
  498. if(resourceLoadingRecord.hasOwnProperty(resource) && resourceLoadingRecord[resource] === false) {
  499. return false;
  500. }
  501. }
  502. return true;
  503. };
  504. // 观察/监听DOM变化。注意MutationObserver只能监听DOM变化,对应的内容变化时监听不到的,如图片加载、CSS、JS文件加载。这些需要自己用事件监听器去获取(使用load事件来完成,注意浏览器缓存带来的事件不触发问题,可使用complete来手动触发)
  505. var observeMutation = function (table, state) {
  506. // 获取图片加载完的通知
  507. var loadImage = function(e) {
  508. var img = e.target,
  509. width = img.clientWidth,
  510. height = img.clientHeight;
  511. img.removeEventListener('load', loadImage, false);
  512. // alert("Image loaded: width: " + width + " height: " + height);
  513. state.resourceLoadingRecord[img.getAttribute('src')] = true; // 通过闭包获取state
  514. };
  515. MutationObserver = window.MutationObserver || window.WebKitMutationObserver || window.MozMutationObserver;
  516. var observer = new MutationObserver(function (mutations, observer) {
  517. // fired when a mutation occurs
  518. mutations.forEach(function (mutation) {
  519. switch (mutation.type) {
  520. case 'childList':
  521. // 可以在这里接收子节点的增加、移除通知。
  522. // 当tbody移除时,取消观察
  523. // observer.disconnect();
  524. break;
  525. case 'characterData':
  526. break;
  527. case 'attributes':
  528. // 可以在这里接收属性变化通知,注意设置了过滤器
  529. // console.log(attributes);
  530. // 图片等资源 加载完成需要手动使用事件监听器来获取通知
  531. if(mutation.target.tagName == 'IMG') { //nodeName
  532. // console.log('IMG');
  533. state.resourceLoadingRecord[mutation.target.getAttribute('src')] = false;
  534. mutation.target.addEventListener('load', loadImage, false);
  535. }
  536. break;
  537. case 'subtree':
  538. break;
  539. default:
  540. }
  541. });
  542. execWhenTableChange(table, state);
  543. });
  544. // configuration of the observer:
  545. var config = {
  546. childList: true,
  547. attributes: true,
  548. characterData: true,
  549. subtree: true,
  550. attributeFilter: ['style', 'src'] // 1) 固定表头主要是通过设置style来完成的。2)图片等资源不属于DOM监听范围,需要另外添加事件监听器来进行通知
  551. };
  552. // pass in the target node, as well as the observer options
  553. var target = $('tbody', table).get(0);
  554. observer.observe(target, config);
  555. };
  556. // tbody的DOM发生变化时,重新设置宽度,因为可能出现某些单元格变宽或者某些单元格在某些特殊条件下不显示的问题。注:不用绑定事件到table,因为thead的位置样式在滚动时始终在变化,因此table在滚动时一直会被触发
  557. var execWhenTableChange = function (table, state) {
  558. // 这里面最好不要修改DOM,不然容易产生无限事件冒泡或非常多的事件冒泡,严重拖慢页面加载速度
  559. if(!state.isExecing) {
  560. state.domUpdateFinished = false; // 仅当延时函数未执行时,触发事件,才认为dom渲染未结束
  561. }
  562. if(state.timeout) {
  563. $timeout.cancel(state.timeout); // 如果dom还未渲染结束,且之前添加过延时函数到延时函数队列,则取消延时函数
  564. }
  565. // 如果0.1s后仍未监听到DOM变化,则认为dom已经渲染完成(这个时间通常小于0.1s。0.1s以上的间隔,认为dom已经渲染/更新结束)。因此0.1s后设置domUpdateFinished为true,如果DOM还未渲染结束,则会在下次事件发生时取消执行延时函数
  566. state.timeout = $timeout(function () {
  567. state.isExecing = true;
  568. state.domLoadingFinished = true; // 执行到这里表示DOM更新完成
  569. if($('thead', table).css('position') == 'fixed' ) {
  570. // 把重置表格CSS放到延时函数里面来执行,这样能保证0.1s内只会执行一次,从而避免最大调用栈溢出(放在外面执行的话,DOM变化事件不断触发容易产生最大调用栈溢出)
  571. if(!isWidthValid(table)) { // 如果某个单元格宽度无效(如设置宽度为90px,实际宽度却是100px),则重置表格样式
  572. state.domUpdateFinished = false;
  573. resetTableCss(table, state);
  574. }else if(getTrWidth(table, 'thead') != state.thWidthSum) { // 判断是否th宽度之和是否发生了变化(表格列数增加或减少)
  575. state.domUpdateFinished = false;
  576. resetTableCss(table, state);
  577. }else if(!isTdValid(table) && !isThTdWidthEqual(state)) { // 判断是否th宽度与td对应宽度是否相等
  578. state.domUpdateFinished = false;
  579. resetTableCss(table, state);
  580. }else if($('tbody tr:eq(0)', table).attr('data-tr-width-marked') !== 'marked') { // 注:不要使用dataset来访问数据属性,因为IE11以前版本不支持这种方式获取值
  581. state.domUpdateFinished = false;
  582. for (var i = 0; i < state.tdWidths.length; i++) {
  583. $('tbody tr:eq(0) td:eq('+ i +')', table).outerWidth(state.tdWidths[i]);
  584. }
  585. $('tbody tr:eq(0)', table).attr('data-tr-width-marked', "marked");
  586. }
  587. }
  588. state.domUpdateFinished = true;
  589. state.timeout = null;
  590. state.isExecing = false;
  591. }, 0.1, false); // false表示跳过 model dirty checking
  592. };
  593. return {
  594. restrict: 'A',
  595. /*scope: {
  596. baseline: '=fixedThead' /!*在页面滚动条滑动时,表头固定时距离顶部的距离*!/
  597. },*/
  598. link: function (scope, element, attrs) {
  599. var table = $(element);
  600. var tbody = $('tbody', table);
  601. var state = {
  602. needSetCss: true, // 是否需要设置宽度
  603. lastWindowScrollPos: $(window).scrollTop(), // 上次window窗口滚动条位置
  604. timeout: null, // $timeout返回的结果,用于保存在0.1s后设置domUpdateFinished为true
  605. parent: table.parent(), // 包裹table的父级容器
  606. thWidthSum: null, // th的宽度之和,在DOM渲染完成后再赋值
  607. thWidths: [], // thead的第一行tr的每个单元格宽度
  608. tdWidths: [], // tbody的第一行tr的每个单元格宽度
  609. domUpdateFinished: false, // dom更新完成后,即表格的样式已经确定后才允许设置表格宽度
  610. domLoadingFinished: false, // DOM是否加载完成
  611. resourceLoadingRecord: {}, // 资源完成记录
  612. allResourceLoadingFinished: false // 所有资源加载是否完成(图片等资源)
  613. };
  614. var parent = state.parent;
  615. parent.scroll(function () {
  616. if(state.domLoadingFinished && !state.allResourceLoadingFinished) {
  617. state.allResourceLoadingFinished = isAllResourceLoadingFinished(state.resourceLoadingRecord);
  618. }
  619. if(state.needSetCss && state.domUpdateFinished && state.allResourceLoadingFinished) {
  620. setTableCss(table, state);
  621. }
  622. // 设置表头位置
  623. if(!state.needSetCss && state.domLoadingFinished && state.allResourceLoadingFinished) {
  624. // 设置表头left
  625. var leftToWindow = $('tbody', table).offset().left - $(window).scrollLeft(); // tbody距离当前窗口左部的距离
  626. $('thead', table).css({left: leftToWindow}); // 保证横向滚动时表头与表格一起滚动
  627. var windowTop = parent.offset().top - $(window).scrollTop(); // 包围表格的容器距离当前窗口顶部的位置
  628. // 设置表头top
  629. $('thead', table).css({top: windowTop});
  630. }
  631. });
  632. $(window).scroll(function () {
  633. if(state.domLoadingFinished && !state.allResourceLoadingFinished) {
  634. state.allResourceLoadingFinished = isAllResourceLoadingFinished(state.resourceLoadingRecord);
  635. }
  636. if(state.needSetCss && state.domUpdateFinished && state.allResourceLoadingFinished) {
  637. setTableCss(table, state);
  638. }
  639. // 设置表头位置
  640. if(!state.needSetCss && state.domLoadingFinished && state.allResourceLoadingFinished) {
  641. // 设置表头left
  642. var leftToWindow = $('tbody', table).offset().left - $(window).scrollLeft(); // tbody距离当前窗口左部的距离
  643. $('thead', table).css({left: leftToWindow}); // 保证横向滚动时表头与表格一起滚动
  644. // 设置表头top
  645. var parentHeight = parent.outerHeight(); // 父级容器高度
  646. var topToWindow = parent.offset().top - $(window).scrollTop(); // 包围表格的容器顶部距离当前窗口顶部的位置
  647. var bottomToWindow = topToWindow + parentHeight; // 包围表格的容器底部距离当前窗口顶部的位置
  648. if(attrs['fixedThead']) { // 指定了基线才相对于整个窗口进行固定。注:属性名在指令里需要使用驼峰表示形式
  649. var theadHeight = $('thead', table).outerHeight(); // thead高度
  650. var baseline = parseInt(attrs['fixedThead']); // 在页面滚动条滑动时,表头固定时距离顶部的距离。属性值都是字符串,需手动解析为整数。
  651. var topToBaseline = topToWindow - baseline; // 包围表格的容器顶部距离基线的距离
  652. var bottomToBaseline = topToBaseline + parentHeight; // 包围表格的容器的底部距离基线的距离
  653. if (topToBaseline >= 0) { // 整个父级容器在基线下方
  654. $('thead', table).css({top: topToWindow}); // 表头位置与父级容器顶部位置一致
  655. } else if (bottomToBaseline >= theadHeight) { // 顶部在基线上方,底部在基线下方,并且距离基线距离大于等于表头高度
  656. $('thead', table).css({top: baseline}); // 固定表头到基线
  657. } else if(bottomToBaseline < theadHeight && bottomToBaseline >= 0) { // 顶部在基线上方,底部在基线下方,并且底部与基线距离小于表头高度
  658. $('thead', table).css({top: bottomToWindow - theadHeight});
  659. } else if(bottomToBaseline < 0) { // 顶部在基线上方,底部在基线上方
  660. $('thead', table).css({top: topToWindow}); // 表头位置与父级容器顶部位置一致
  661. }
  662. }else {
  663. $('thead', table).css({top: topToWindow});
  664. }
  665. }
  666. });
  667. observeMutation(table, state); // DOM变化的监听从DOMSubtreeModified改为MutationObserver
  668. }
  669. };
  670. }]);
  671. /**
  672. * 常用的工具的指令
  673. *
  674. * @author huxz
  675. */
  676. angular.module('tool.directives', ['toaster']).directive('showMarkDown', ['$http', function ($http) {
  677. return {
  678. restrict : 'EA',
  679. replace : true,
  680. template : '<div id="mark-down-content"></div>',
  681. link : function (scope, elem, attrs) {
  682. if (attrs.src) {
  683. // 加载第三方的Markdown转HTML的js库
  684. require(['showdown'], function (showdown) {
  685. var converter = new showdown.Converter();
  686. $http.get(attrs.src).success(function (response) {
  687. var html = converter.makeHtml(response);
  688. elem.html(html);
  689. })
  690. });
  691. }
  692. }
  693. }
  694. }]).directive('imageUpload', ['$http', '$parse', 'BaseService', 'toaster', function ($http, $parse, BaseService, toaster) {
  695. var rootPath = BaseService.getRootPath();
  696. var validateFileType = function (file) {
  697. if (!file || (typeof file != 'object')) return false;
  698. if (!/\/(png|jpeg|pdf|bmp|gif)$/.test(file.type)) {
  699. alert('请上传可支持的格式');
  700. return false;
  701. }
  702. return true;
  703. };
  704. var isOverLimit = function (file) {
  705. if (!file || (typeof file != 'object')) return false;
  706. if (file.size > 3145728) {
  707. alert('请勿超过3M');
  708. //toaster.pop('error', '上传图片的大小不能超过3M');
  709. return true;
  710. }
  711. return false;
  712. };
  713. function isNumber(number) {
  714. if (!number || number === '') return false;
  715. var n = Number(number);
  716. return !isNaN(n);
  717. }
  718. function validateFileSize(file, maxSize, errorSizeMsg) {
  719. if (!file || (typeof file != 'object')) return false;
  720. if (!isNumber(maxSize)) return false;
  721. var fileSize = Number(maxSize);
  722. if (file.size > fileSize) {
  723. alert(errorSizeMsg);
  724. return false;
  725. }
  726. return true;
  727. }
  728. /**
  729. * 上传单个文件操作
  730. *
  731. * @param config 文件上传配置,形如{file: [file], success:[function], error: [function]}
  732. */
  733. var uploadFile = function (config) {
  734. // 验证参数
  735. if (!config) config = {};
  736. if (!config.file || (typeof config.file != 'object')) return;
  737. if (config.success && (typeof config.success != 'function')) return;
  738. if (config.error && (typeof config.error != 'function')) return;
  739. if (!config.maxSize) config.maxSize = 3145728
  740. console.log('upload-file', config.file);
  741. // 检测上传文件的类型
  742. var isImage = true;
  743. if(!/image\/\w+/.test(config.file.type)){
  744. isImage = false;
  745. }
  746. // 限制文件的大小
  747. if (config.file.size > Number(config.maxSize)) {
  748. if (config.maxSize === 3145728) {
  749. alert('上传图片的大小不能超过3M')
  750. } else {
  751. alert('上传pdf的大小不能超过20M')
  752. }
  753. //toaster.pop('error', '上传图片的大小不能超过3M');
  754. return;
  755. }
  756. // 封装表单数据
  757. var data = new FormData();
  758. data.append(isImage ? 'image' : 'file', config.file);
  759. $http({
  760. method : 'POST',
  761. url : rootPath + (isImage ? '/api/images' : '/file'),
  762. data : data,
  763. headers : {'Content-Type' : undefined}
  764. }).success(config.success).error(config.error);
  765. };
  766. /**
  767. * 根据path文件名来判断文件是否是PDF文件
  768. *
  769. * @param path 文件名称
  770. */
  771. function isPdf(path) {
  772. if(path) {
  773. var str = path.slice(path.lastIndexOf('.')).toLowerCase();
  774. return str === '.pdf';
  775. }
  776. return false;
  777. }
  778. /**
  779. * 根据文件的URL生成预览文件URL
  780. *
  781. * @param url 文件URL
  782. */
  783. function createShowUrl(url) {
  784. return isPdf(url) ? 'static/img/vendor/store/timg.png' : url;
  785. }
  786. /**
  787. * tip:
  788. * 上传组件样式通过CSS进行控制
  789. * 上传组件默认是可以进行预览的
  790. */
  791. return {
  792. restrict : 'A',
  793. replace : true,
  794. template : "<div class='preview' style='display: flex;justify-content: center;align-items: center;'></div>",
  795. link : function (scope, element, attr) {
  796. // TODO huxz 样式通过CSS进行控制
  797. // 获取自定义属性
  798. var onSuccess = $parse(attr.onSuccess);
  799. var preview = !attr.nonPreview;
  800. var maxSize = attr.maxSize;
  801. var errorSizeMsg = attr.errorSizeMsg;
  802. var _accept = attr.accept
  803. // 设置图片预览
  804. element.append('<img class=previewImage title=""/>');
  805. var previewImage = $(element).find('.previewImage');
  806. previewImage.attr('src', attr.src);
  807. previewImage.click(function () {
  808. uploadImage.click();
  809. });
  810. // 设置文件上传
  811. if (!_accept) {
  812. element.append('<input type=file class=uploadImage style=display:none; accept=image/jpeg,image/jpg,image/gif,image/bmp,image/png,.pdf />');
  813. } else {
  814. element.append("<input type='file' class='uploadImage' style='display:none;' accept='.pdf' />");
  815. }
  816. var uploadImage = $(element).find('.uploadImage');
  817. uploadImage.change(function () {
  818. var file = $(this)[0];
  819. if (file.files && file.files[0]) {
  820. console.log(file.files[0]);
  821. if (!maxSize) {
  822. if (isOverLimit(file.files[0])) return;
  823. } else {
  824. if (!validateFileSize(file.files[0], maxSize, errorSizeMsg)) return ;
  825. }
  826. if (_accept) {
  827. // 如果有,则先处理pdf
  828. if (!/\/(pdf)$/.test(file.files[0].type)) {
  829. alert('请上传可支持的格式');
  830. return false;
  831. }
  832. }
  833. else {
  834. if (!validateFileType(file.files[0])) return;
  835. }
  836. // 如果不可预览属性设置为false,则显示预览图片
  837. if (preview) {
  838. var reader = new FileReader();
  839. reader.onload = function (evt) {
  840. if (isPdf(file.files[0].name)) {
  841. previewImage.attr('src', createShowUrl(file.files[0].name));
  842. } else {
  843. previewImage.attr('src', evt.target.result);
  844. }
  845. };
  846. reader.readAsDataURL(file.files[0]);
  847. }
  848. uploadFile({
  849. file : file.files[0],
  850. success : function (data) {
  851. onSuccess(scope, {$data: data[0]});
  852. },
  853. error : function (message) {
  854. alert(message);
  855. //toaster.pop('error', message);
  856. },
  857. maxSize: maxSize
  858. });
  859. } else {
  860. // TODO huxz 兼容IE
  861. /*var sFilter='filter:progid:DXImageTransform.Microsoft.AlphaImageLoader(sizingMethod=scale,src="';
  862. file.select();
  863. var src = document.selection.createRange().text;
  864. var img = document.getElementById('imgHead');
  865. img.filters.item('DXImageTransform.Microsoft.AlphaImageLoader').src = src;*/
  866. }
  867. });
  868. }
  869. };
  870. }]);
  871. /*global angular */
  872. /**
  873. * uiTour directive
  874. *
  875. * @example:
  876. * <ul ui-tour="currentStep">
  877. * <li target="#someId">
  878. * First Tooltip
  879. * <a ng-click="currentStep=currentStep+1">Next</a>
  880. * </li>
  881. * <li target=".items:eq(2)" name="two">
  882. * Second Tooltip
  883. * <a ng-click="currentStep=currentStep-1">Prev</a>
  884. * </li>
  885. * <li target=".items:eq(2)">
  886. * Third Tooltip
  887. * <a ng-click="currentStep='two'">Go directly to 'two'</a>
  888. * <a ng-click="currentStep=0">Done</a>
  889. * </li>
  890. * </ul>
  891. */
  892. angular.module('ui.tour', [])
  893. .directive('uiTour', ['$timeout', '$parse', function($timeout, $parse){
  894. return {
  895. link: function($scope, $element, $attributes) {
  896. var model = $parse($attributes.uiTour);
  897. // Watch model and change steps
  898. $scope.$watch($attributes.uiTour, function(newVal, oldVal){
  899. if (angular.isNumber(newVal)) {
  900. showStep(newVal)
  901. } else {
  902. if (angular.isString(newVal)) {
  903. var stepNumber = 0,
  904. children = $element.children()
  905. angular.forEach(children, function(step, index) {
  906. if (angular.element(step).attr('name') === newVal)
  907. stepNumber = index+1;
  908. });
  909. model.assign($scope, stepNumber);
  910. } else {
  911. model.assign($scope, newVal && 1 || 0);
  912. }
  913. }
  914. });
  915. // Show step
  916. function showStep(stepNumber) {
  917. var elm, at, children = $element.children().removeClass('active');
  918. elm = children.eq(stepNumber - 1);
  919. if (stepNumber) {
  920. at = elm.attr('at');
  921. $timeout(function(){
  922. var target = angular.element(elm.attr('target'))[0];
  923. if (elm.attr('overlay') !== undefined) {
  924. $('.tour-overlay').addClass('active').css({
  925. // marginLeft: target.offsetLeft + target.offsetWidth / 2 - 150,
  926. // marginTop: target.offsetTop + target.offsetHeight / 2 - 150
  927. }).addClass('in');
  928. }
  929. // offset = {};
  930. //
  931. // offset.top = target.offsetTop;
  932. // offset.left = target.offsetLeft;
  933. elm.addClass('active');
  934. // if (at.indexOf('bottom') > -1) {
  935. // offset.top += target.offsetHeight;
  936. // } else if (at.indexOf('top') > -1) {
  937. // offset.top -= elm[0].offsetHeight;
  938. // } else {
  939. // offset.top += target.offsetHeight / 2 - elm[0].offsetHeight / 2;
  940. // }
  941. // if (at.indexOf('left') > -1) {
  942. // offset.left -= elm[0].offsetWidth;
  943. // } else if (at.indexOf('right') > -1) {
  944. // offset.left += target.offsetWidth;
  945. // } else {
  946. // offset.left += target.offsetWidth / 2 - elm[0].offsetWidth / 2;
  947. // }
  948. //
  949. // elm.css(offset);
  950. });
  951. } else {
  952. $('.tour-overlay').removeClass('in');
  953. $('.tour-overlay').removeClass('active');
  954. }
  955. }
  956. }
  957. };
  958. }]);
  959. });