Программный интерфейс

Показывать:
  1. /**
  2. * Глобальные переменные и общие методы фреймворка __metadata.js__ <i>Oknosoft data engine</i>
  3. *
  4. * &copy; Evgeniy Malyarov http://www.oknosoft.ru 2014-2016
  5. *
  6. * @module common
  7. * @submodule common.ui
  8. */
  9.  
  10.  
  11. /**
  12. * Описание структуры колонки формы списка
  13. * @class Col_struct
  14. * @param id
  15. * @param width
  16. * @param type
  17. * @param align
  18. * @param sort
  19. * @param caption
  20. * @constructor
  21. */
  22. function Col_struct(id,width,type,align,sort,caption){
  23. this.id = id;
  24. this.width = width;
  25. this.type = type;
  26. this.align = align;
  27. this.sort = sort;
  28. this.caption = caption;
  29. }
  30.  
  31. /**
  32. * ### Объекты интерфейса пользователя
  33. * @class InterfaceObjs
  34. * @static
  35. * @menuorder 40
  36. * @tooltip Контекст UI
  37. */
  38. function InterfaceObjs(){
  39.  
  40. var iface = this;
  41.  
  42. /**
  43. * Очищает область (например, удаляет из div все дочерние элементы)
  44. * @method clear_svgs
  45. * @param area {HTMLElement|String}
  46. */
  47. this.clear_svgs = function(area){
  48. if(typeof area === "string")
  49. area = document.getElementById(area);
  50. while (area.firstChild)
  51. area.removeChild(area.firstChild);
  52. };
  53.  
  54. /**
  55. * Возвращает координату левого верхнего угла элемента относительно документа
  56. * @method get_offset
  57. * @param elm {HTMLElement} - элемент, координату которого, необходимо определить
  58. * @return {Object} - {left: number, top: number}
  59. */
  60. this.get_offset = function(elm) {
  61. var offset = {left: 0, top:0};
  62. if (elm.offsetParent) {
  63. do {
  64. offset.left += elm.offsetLeft;
  65. offset.top += elm.offsetTop;
  66. } while (elm = elm.offsetParent);
  67. }
  68. return offset;
  69. };
  70.  
  71. /**
  72. * Заменяет в строке критичные для xml символы
  73. * @method normalize_xml
  74. * @param str {string} - исходная строка, в которой надо замаскировать символы
  75. * @return {XML|string}
  76. */
  77. this.normalize_xml = function(str){
  78. if(!str) return "";
  79. var entities = { '&': '&amp;', '"': '&quot;', "'": '&apos;', '<': '&lt;', '>': '&gt;'};
  80. return str.replace( /[&"'<>]/g, function (s) {return entities[s];});
  81. };
  82.  
  83. /**
  84. * Масштабирует svg
  85. * @method scale_svg
  86. * @param svg_current {String} - исходная строка svg
  87. * @param size {Number|Object} - требуемый размер картинки
  88. * @param padding {Number} - отступ от границы viewBox
  89. * @return {String} - отмасштабированная строка svg
  90. */
  91. this.scale_svg = function(svg_current, size, padding){
  92. var j, k, svg_head, svg_body, head_ind, vb_ind, svg_head_str, vb_str, viewBox, svg_j = {};
  93.  
  94. var height = typeof size == "number" ? size : size.height,
  95. width = typeof size == "number" ? (size * 1.5).round(0) : size.width,
  96. max_zoom = typeof size == "number" ? Infinity : (size.zoom || Infinity);
  97.  
  98. head_ind = svg_current.indexOf(">");
  99. svg_head_str = svg_current.substring(5, head_ind);
  100. svg_head = svg_head_str.split(' ');
  101. svg_body = svg_current.substr(head_ind+1);
  102. svg_body = svg_body.substr(0, svg_body.length - 6);
  103.  
  104. // получаем w, h и формируем viewBox="0 0 400 100"
  105. for(j in svg_head){
  106. svg_current = svg_head[j].split("=");
  107. if("width,height,x,y".indexOf(svg_current[0]) != -1){
  108. svg_current[1] = Number(svg_current[1].replace(/"/g, ""));
  109. svg_j[svg_current[0]] = svg_current[1];
  110. }
  111. }
  112.  
  113. if((vb_ind = svg_head_str.indexOf("viewBox="))!=-1){
  114. vb_str = svg_head_str.substring(vb_ind+9);
  115. viewBox = 'viewBox="' + vb_str.substring(0, vb_str.indexOf('"')) + '"';
  116. }else{
  117. viewBox = 'viewBox="' + (svg_j.x || 0) + ' ' + (svg_j.y || 0) + ' ' + (svg_j.width - padding) + ' ' + (svg_j.height - padding) + '"';
  118. }
  119.  
  120. var init_height = svg_j.height,
  121. init_width = svg_j.width;
  122.  
  123. k = (height - padding) / init_height;
  124. svg_j.height = height;
  125. svg_j.width = (init_width * k).round(0);
  126.  
  127. if(svg_j.width > width){
  128. k = (width - padding) / init_width;
  129. svg_j.height = (init_height * k).round(0);
  130. svg_j.width = width;
  131. }
  132.  
  133. if(k > max_zoom){
  134. k = max_zoom;
  135. svg_j.height = (init_height * k).round(0);
  136. svg_j.width = (init_width * k).round(0);
  137. }
  138.  
  139. svg_j.x = (svg_j.x * k).round(0);
  140. svg_j.y = (svg_j.y * k).round(0);
  141.  
  142. return '<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" ' +
  143. 'width="' + svg_j.width + '" ' +
  144. 'height="' + svg_j.height + '" ' +
  145. 'x="' + svg_j.x + '" ' +
  146. 'y="' + svg_j.y + '" ' +
  147. 'xml:space="preserve" ' + viewBox + '>' + svg_body + '</svg>';
  148. };
  149.  
  150. /**
  151. * Добавляет в форму функциональность вызова справки
  152. * @method bind_help
  153. * @param wnd {dhtmlXWindowsCell}
  154. * @param [path] {String} - url справки
  155. */
  156. this.bind_help = function (wnd, path) {
  157.  
  158. function frm_help(win){
  159. if(!win.help_path){
  160. $p.msg.show_msg({
  161. title: "Справка",
  162. type: "alert-info",
  163. text: $p.msg.not_implemented
  164. });
  165. return;
  166. }
  167. }
  168.  
  169. if(wnd instanceof dhtmlXCellObject) {
  170. // TODO реализовать кнопку справки для приклеенной формы
  171. }else{
  172. if(!wnd.help_path && path)
  173. wnd.help_path = path;
  174.  
  175. wnd.button('help').show();
  176. wnd.button('help').enable();
  177. wnd.attachEvent("onHelp", frm_help);
  178. }
  179.  
  180. };
  181.  
  182. /**
  183. * Устанавливает hash url для сохранения истории и последующей навигации
  184. * @method set_hash
  185. * @param [obj] {String|Object} - имя класса или объект со свойствами к установке в хеш адреса
  186. * @param [ref] {String} - ссылка объекта
  187. * @param [frm] {String} - имя формы объекта
  188. * @param [view] {String} - имя представления главной формы
  189. */
  190. this.set_hash = function (obj, ref, frm, view ) {
  191.  
  192. var ext = {},
  193. hprm = $p.job_prm.parse_url();
  194.  
  195. if(arguments.length == 1 && typeof obj == "object"){
  196. ext = obj;
  197. if(ext.hasOwnProperty("obj")){
  198. obj = ext.obj;
  199. delete ext.obj;
  200. }
  201. if(ext.hasOwnProperty("ref")){
  202. ref = ext.ref;
  203. delete ext.ref;
  204. }
  205. if(ext.hasOwnProperty("frm")){
  206. frm = ext.frm;
  207. delete ext.frm;
  208. }
  209. if(ext.hasOwnProperty("view")){
  210. view = ext.view;
  211. delete ext.view;
  212. }
  213. }
  214.  
  215. if(obj === undefined)
  216. obj = hprm.obj || "";
  217. if(ref === undefined)
  218. ref = hprm.ref || "";
  219. if(frm === undefined)
  220. frm = hprm.frm || "";
  221. if(view === undefined)
  222. view = hprm.view || "";
  223.  
  224. var hash = "obj=" + obj + "&ref=" + ref + "&frm=" + frm + "&view=" + view;
  225. for(var key in ext){
  226. hash += "&" + key + "=" + ext[key];
  227. }
  228.  
  229. if(location.hash.substr(1) == hash)
  230. iface.hash_route();
  231. else
  232. location.hash = hash;
  233. };
  234.  
  235. /**
  236. * Выполняет навигацию при изменении хеша url
  237. * @method hash_route
  238. * @param event {HashChangeEvent}
  239. * @return {Boolean}
  240. */
  241. this.hash_route = function (event) {
  242.  
  243. var hprm = $p.job_prm.parse_url(),
  244. res = $p.eve.callEvent("hash_route", [hprm]),
  245. mgr;
  246.  
  247. if((res !== false) && (!iface.before_route || iface.before_route(event) !== false)){
  248.  
  249. if($p.ajax.authorized){
  250.  
  251. if(hprm.ref && typeof _md != "undefined"){
  252. // если задана ссылка, открываем форму объекта
  253. mgr = _md.mgr_by_class_name(hprm.obj);
  254. if(mgr)
  255. mgr[hprm.frm || "form_obj"](iface.docs, hprm.ref)
  256.  
  257. }else if(hprm.view && iface.swith_view){
  258. // если задано имя представления, переключаем главную форму
  259. iface.swith_view(hprm.view);
  260.  
  261. }
  262.  
  263. }
  264. }
  265.  
  266. if(event)
  267. return iface.cancel_bubble(event);
  268. };
  269.  
  270. /**
  271. * Запрещает всплывание события
  272. * @param e {MouseEvent|KeyboardEvent}
  273. * @returns {Boolean}
  274. */
  275. this.cancel_bubble = function(e) {
  276. var evt = (e || event);
  277. if (evt && evt.stopPropagation)
  278. evt.stopPropagation();
  279. if (evt && !evt.cancelBubble)
  280. evt.cancelBubble = true;
  281. return false
  282. };
  283.  
  284. /**
  285. * Конструктор описания колонки динамического списка
  286. * @type {Col_struct}
  287. * @constructor
  288. */
  289. this.Col_struct = Col_struct;
  290.  
  291. /**
  292. *
  293. * @param items {Array} - закладки сайдбара
  294. * @param buttons {Array} - кнопки дополнительной навигации
  295. * @param [icons_path] {String} - путь к иконам сайдбара
  296. */
  297. this.init_sidebar = function (items, buttons, icons_path) {
  298.  
  299. // наблюдатель за событиями авторизации и синхронизации
  300. iface.btn_auth_sync = new iface.OBtnAuthSync();
  301.  
  302. // кнопки навигации справа сверху
  303. iface.btns_nav = function (wrapper) {
  304. return iface.btn_auth_sync.bind(new iface.OTooolBar({
  305. wrapper: wrapper,
  306. class_name: 'md_otbnav',
  307. width: '260px', height: '28px', top: '3px', right: '3px', name: 'right',
  308. buttons: buttons,
  309. onclick: function (name) {
  310. iface.main.cells(name).setActive(true);
  311. return false;
  312. }
  313. }))
  314. };
  315.  
  316. // основной сайдбар
  317. iface.main = new dhtmlXSideBar({
  318. parent: document.body,
  319. icons_path: icons_path || "dist/imgs/",
  320. width: 180,
  321. header: true,
  322. template: "tiles",
  323. autohide: true,
  324. items: items,
  325. offsets: {
  326. top: 0,
  327. right: 0,
  328. bottom: 0,
  329. left: 0
  330. }
  331. });
  332.  
  333. // подписываемся на событие навигации по сайдбару
  334. iface.main.attachEvent("onSelect", function(id){
  335.  
  336. var hprm = $p.job_prm.parse_url();
  337. if(hprm.view != id)
  338. iface.set_hash(hprm.obj, hprm.ref, hprm.frm, id);
  339.  
  340. iface["view_" + id](iface.main.cells(id));
  341.  
  342. });
  343.  
  344. // включаем индикатор загрузки
  345. iface.main.progressOn();
  346.  
  347. // активируем страницу
  348. var hprm = $p.job_prm.parse_url();
  349. if(!hprm.view || iface.main.getAllItems().indexOf(hprm.view) == -1){
  350. iface.set_hash(hprm.obj, hprm.ref, hprm.frm, "doc");
  351. } else
  352. setTimeout(iface.hash_route);
  353. };
  354.  
  355. /**
  356. * ### Страница "Все объекты"
  357. * похожа на подменю "все функции" тонкого клиента 1С
  358. *
  359. * @class All_meta_objs
  360. * @param cont {dhtmlXCellObject} - контейнер для размещения страницы
  361. * @param [classes] {Array} - список классов. Если параметр пропущен, будут показаны все классы метаданных
  362. * @param [frm_attr] {Object} - дополнительные настройки, которые будут переданы создаваемым формам списка
  363. * @constructor
  364. */
  365. function All_meta_objs(cont, classes, frm_attr) {
  366.  
  367. this.layout = cont.attachLayout({
  368. pattern: "2U",
  369. cells: [{
  370. id: "a",
  371. text: "Разделы",
  372. collapsed_text: "Разделы",
  373. width: 220
  374. }, {
  375. id: "b",
  376. text: "Раздел",
  377. header: false
  378. }],
  379. offsets: { top: 0, right: 0, bottom: 0, left: 0}
  380. });
  381.  
  382. // дерево используемых метаданных
  383. this.tree = this.layout.cells("a").attachTreeView();
  384. this.tree.attachEvent("onSelect", function (name, mode) {
  385. if(!mode)
  386. return;
  387. var mgr = $p.md.mgr_by_class_name(name);
  388. if(mgr instanceof DataProcessorsManager){
  389. // для отчетов и обработок используем форму отчета
  390. mgr.form_rep(this.layout.cells("b"), frm_attr || {hide_header: true});
  391.  
  392. }else if(mgr){
  393. // для остальных объектов показываем форму списка
  394. mgr.form_list(this.layout.cells("b"), frm_attr || {hide_header: true});
  395. }
  396.  
  397. }.bind(this));
  398.  
  399.  
  400. if(!classes){
  401. var md_classes = $p.md.get_classes();
  402. classes = [];
  403. for(var cl in md_classes){
  404. if(md_classes[cl].length)
  405. classes.push(cl);
  406. }
  407. }
  408.  
  409. // если тип объектов только один, скрываем иерархию
  410. if(classes.length == 1){
  411. $p.md.get_classes()[classes[0]].forEach(function (name) {
  412. var key = classes[0]+"."+name,
  413. meta = $p.md.get(key);
  414. if(!meta.hide){
  415. this.tree.addItem(key, meta.list_presentation || meta.synonym);
  416. this.tree.setItemIcons(key, {file: "icon_1c_"+classes[0]});
  417. }
  418. }.bind(this));
  419.  
  420. }else{
  421. classes.forEach(function (id) {
  422. this.tree.addItem(id, $p.msg["meta_"+id]);
  423. this.tree.setItemIcons(id, {file: "icon_1c_"+id, folder_opened: "icon_1c_"+id, folder_closed: "icon_1c_"+id});
  424. $p.md.get_classes()[id].forEach(function (name) {
  425. var key = id+"."+name,
  426. meta = $p.md.get(key);
  427. if(!meta.hide){
  428. this.tree.addItem(key, meta.list_presentation || meta.synonym, id);
  429. this.tree.setItemIcons(key, {file: "icon_1c_"+id});
  430. }
  431. }.bind(this));
  432. }.bind(this));
  433. }
  434. }
  435.  
  436. /**
  437. * ### Страница "Все объекты"
  438. * @property All_meta_objs
  439. * @type {All_meta_objs}
  440. */
  441. this.All_meta_objs = All_meta_objs;
  442.  
  443. /**
  444. * ### Страница общих настроек
  445. * @param cont {dhtmlXCellObject} - контейнер для размещения страницы
  446. * @constructor
  447. */
  448. function Setting2col(cont) {
  449.  
  450. // закладка основных настроек
  451. cont.attachHTMLString($p.injected_data['view_settings.html']);
  452. this.cont = cont.cell.querySelector(".dhx_cell_cont_tabbar");
  453. this.cont.style.overflow = "auto";
  454.  
  455. // первая колонка настроек
  456. this.form2 = (function (cont) {
  457.  
  458. var form = new dhtmlXForm(cont, [
  459.  
  460. { type:"settings", labelWidth:80, position:"label-left" },
  461.  
  462. {type: "label", labelWidth:320, label: "Адрес CouchDB", className: "label_options"},
  463. {type:"input" , inputWidth: 220, name:"couch_path", label:"Путь:", validate:"NotEmpty"},
  464. {type:"template", label:"",value:"",
  465. note: {text: "Можно указать как относительный, так и абсолютный URL публикации CouchDB", width: 320}},
  466.  
  467. {type: "label", labelWidth:320, label: "Адрес http сервиса 1С", className: "label_options"},
  468. {type:"input" , inputWidth: 220, name:"rest_path", label:"Путь", validate:"NotEmpty"},
  469. {type:"template", label:"",value:"",
  470. note: {text: "Можно указать как относительный, так и абсолютный URL публикации 1С OData", width: 320}},
  471.  
  472. {type: "label", labelWidth:320, label: "Значение разделителя данных", className: "label_options"},
  473. {type:"input" , inputWidth: 220, name:"zone", label:"Зона:", numberFormat: ["0", "", ""], validate:"NotEmpty,ValidInteger"},
  474. {type:"template", label:"",value:"", note: {text: "Для неразделенной публикации, зона = 0", width: 320}},
  475.  
  476. {type: "label", labelWidth:320, label: "Суффикс базы пользователя", className: "label_options"},
  477. {type:"input" , inputWidth: 220, name:"couch_suffix", label:"Суффикс:"},
  478. {type:"template", label:"",value:"",
  479. note: {text: "Назначается абоненту при регистрации", width: 320}},
  480.  
  481. {type:"block", blockOffset: 0, name:"block_buttons", list:[
  482. {type: "button", name: "save", value: "<i class='fa fa-floppy-o fa-lg'></i>", tooltip: "Применить настройки и перезагрузить программу"},
  483. {type:"newcolumn"},
  484. {type: "button", offsetLeft: 20, name: "reset", value: "<i class='fa fa-refresh fa-lg'></i>", tooltip: "Стереть справочники и перезаполнить данными сервера"}
  485. ]
  486. }
  487. ]);
  488.  
  489. form.cont.style.fontSize = "100%";
  490.  
  491. // инициализация свойств
  492. ["zone", "couch_path", "couch_suffix", "rest_path"].forEach(function (prm) {
  493. if(prm == "zone")
  494. form.setItemValue(prm, $p.wsql.get_user_param(prm));
  495. else
  496. form.setItemValue(prm, $p.wsql.get_user_param(prm) || $p.job_prm[prm]);
  497. });
  498.  
  499. form.attachEvent("onChange", function (name, value, state){
  500. $p.wsql.set_user_param(name, name == "enable_save_pwd" ? state || "" : value);
  501. });
  502.  
  503. form.disableItem("couch_suffix");
  504.  
  505. if(!$p.job_prm.rest_path)
  506. form.disableItem("rest_path");
  507.  
  508. form.attachEvent("onButtonClick", function(name){
  509.  
  510. if(name == "save"){
  511.  
  512. // завершаем синхронизацию
  513. $p.wsql.pouch.log_out();
  514.  
  515. // перезагружаем страницу
  516. setTimeout(function () {
  517. $p.eve.redirect = true;
  518. location.reload(true);
  519. }, 1000);
  520.  
  521. } else if(name == "reset"){
  522.  
  523. dhtmlx.confirm({
  524. title: "Сброс данных",
  525. text: "Стереть справочники и перезаполнить данными сервера?",
  526. cancel: $p.msg.cancel,
  527. callback: function(btn) {
  528. if(btn)
  529. $p.wsql.pouch.reset_local_data();
  530. }
  531. });
  532. }
  533. });
  534.  
  535. return form;
  536.  
  537. })(this.cont.querySelector("[name=form2]").firstChild);
  538.  
  539. // вторая колонка настроек
  540. this.form1 = (function (cont) {
  541.  
  542. var form = new dhtmlXForm(cont, [
  543. { type:"settings", labelWidth:320, position:"label-left" },
  544.  
  545. {type: "label", label: "Тип устройства", className: "label_options"},
  546. { type:"block", blockOffset: 0, name:"block_device_type", list:[
  547. { type:"settings", labelAlign:"left", position:"label-right" },
  548. { type:"radio" , name:"device_type", labelWidth:120, label:'<i class="fa fa-desktop"></i> Компьютер', value:"desktop"},
  549. { type:"newcolumn" },
  550. { type:"radio" , name:"device_type", labelWidth:150, label:'<i class="fa fa-mobile fa-lg"></i> Телефон, планшет', value:"phone"}
  551. ] },
  552. {type:"template", label:"",value:"", note: {text: "Класс устройства определяется автоматически, но пользователь может задать его явно", width: 320}},
  553.  
  554. {type: "label", label: "Сохранять пароль пользователя", className: "label_options"},
  555. {type:"checkbox", name:"enable_save_pwd", label:"Разрешить:", labelWidth:90, checked: $p.wsql.get_user_param("enable_save_pwd", "boolean")},
  556. {type:"template", label:"",value:"", note: {text: "Не рекомендуется, если к компьютеру имеют доступ посторонние лица", width: 320}},
  557. {type:"template", label:"",value:"", note: {text: "", width: 320}},
  558.  
  559. {type: "label", label: "Подключаемые модули", className: "label_options"},
  560. {type:"input" , position:"label-top", inputWidth: 320, name:"modifiers", label:"Модификаторы:", value: $p.wsql.get_user_param("modifiers"), rows: 3, style:"height:80px;"},
  561. {type:"template", label:"",value:"", note: {text: "Список дополнительных модулей", width: 320}}
  562.  
  563. ]);
  564.  
  565. form.cont.style.fontSize = "100%";
  566.  
  567. // инициализация свойств
  568. form.checkItem("device_type", $p.job_prm.device_type);
  569.  
  570. // подключаем обработчик изменения значений в форме
  571. form.attachEvent("onChange", function (name, value, state){
  572. $p.wsql.set_user_param(name, name == "enable_save_pwd" ? state || "" : value);
  573.  
  574. });
  575.  
  576. form.disableItem("modifiers");
  577. form.getInput("modifiers").onchange = function () {
  578. $p.wsql.set_user_param("modifiers", this.value);
  579. };
  580.  
  581. return form;
  582.  
  583. })(this.cont.querySelector("[name=form1]").firstChild);
  584.  
  585. }
  586.  
  587. /**
  588. * ### Страница общих настроек
  589. * @property Setting2col
  590. * @type {Setting2col}
  591. */
  592. this.Setting2col = Setting2col;
  593. }
  594.  
  595. $p.__define({
  596.  
  597. /**
  598. * Объекты интерфейса пользователя
  599. * @property iface
  600. * @for MetaEngine
  601. * @type InterfaceObjs
  602. * @static
  603. */
  604. iface: {
  605. value: new InterfaceObjs(),
  606. writable: false
  607. },
  608.  
  609. /**
  610. * ### Текущий пользователь
  611. * Свойство определено после загрузки метаданных и входа впрограмму
  612. * @property current_user
  613. * @type CatUsers
  614. * @final
  615. */
  616. current_user: {
  617. get: function () {
  618. return $p.cat && $p.cat.users ?
  619. $p.cat.users.by_id($p.wsql.get_user_param("user_name")) :
  620. $p.utils.blank.guid;
  621. }
  622. },
  623.  
  624. /**
  625. * ### Права доступа текущего пользователя.
  626. * Свойство определено после загрузки метаданных и входа впрограмму
  627. * @property current_acl
  628. * @type CcatUsers_acl
  629. * @final
  630. */
  631. current_acl: {
  632. get: function () {
  633. var res = {};
  634. if($p.cat && $p.cat.users_acl){
  635. $p.cat.users_acl.find_rows({owner: $p.current_user}, function (o) {
  636. res = o;
  637. return false;
  638. })
  639. }
  640. return res;
  641. }
  642. },
  643.  
  644. /**
  645. * Загружает скрипты и стили синхронно и асинхронно
  646. * @method load_script
  647. * @for MetaEngine
  648. * @param src {String} - url ресурса
  649. * @param type {String} - "link" или "script"
  650. * @param [callback] {Function} - функция обратного вызова после загрузки скрипта
  651. * @async
  652. */
  653. load_script: {
  654. value: function (src, type, callback) {
  655.  
  656. return new Promise(function(resolve, reject){
  657.  
  658. var s = document.createElement(type);
  659. if (type == "script") {
  660. s.type = "text/javascript";
  661. s.src = src;
  662. s.async = true;
  663. s.addEventListener('load', callback ? function () {
  664. callback();
  665. resolve();
  666. } : resolve, false);
  667.  
  668. } else {
  669. s.type = "text/css";
  670. s.rel = "stylesheet";
  671. s.href = src;
  672. }
  673. document.head.appendChild(s);
  674.  
  675. if(type != "script")
  676. resolve()
  677.  
  678. });
  679. }
  680. }
  681.  
  682. });