zintool.js 32 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930
  1. /**
  2. * @typedef {'list'|'form'|'detail'} ZinPageLayout
  3. */
  4. /**
  5. * @typedef {Object} ZinItemProps
  6. * @property {string} type
  7. * @property {string} text
  8. * @property {string} icon
  9. */
  10. /**
  11. * @typedef {Object} ZinFeatureBar
  12. * @property {ZinItemProps[]} [items]
  13. * @property {string} [current]
  14. * @property {string} [linkParams]
  15. */
  16. /**
  17. * @typedef {Object} ZinDtableProps
  18. * @property {Object[]} [cols]
  19. * @property {Object[]} [data]
  20. * @property {string[]} [plugins]
  21. * @property {boolean} [footPager]
  22. * @property {{items: Object[]}} [footToolbar]
  23. * @property {string[]} [footer]
  24. */
  25. /**
  26. * @typedef {Object} ZinFormGroup
  27. * @property {string} type
  28. * @property {string} [label]
  29. * @property {string} [labelClass]
  30. * @property {string|number} [labelWidth]
  31. * @property {string} [tip]
  32. * @property {string} [tipClass]
  33. * @property {string} [value]
  34. * @property {string} [placeholder]
  35. * @property {string} [class]
  36. * @property {string|ZinFormControl} [control]
  37. * @property {string} [name]
  38. * @property {string} [readonly]
  39. * @property {string} [disabled]
  40. * @property {string} [required]
  41. */
  42. /**
  43. * @typedef {Object} ZinFormRow
  44. * @property {ZinFormGroup[]} items
  45. * @property {string} [width]
  46. */
  47. /**
  48. * @typedef {Object} ZinPageInfo
  49. * @property {string} url
  50. * @property {string} title
  51. * @property {ZinPageLayout} layout
  52. * @property {string} moduleName
  53. * @property {string} methodName
  54. * @property {ZinFeatureBar} featureBar
  55. * @property {ZinItemProps[]} toolbar
  56. * @property {ZinDtableProps} dtable
  57. * @property {boolean} tableCustomCols
  58. * @property {{type: string}} sidebar
  59. * @property {{title: string, toolbar: ZinItemProps[]}} mainHeader
  60. * @property {{submitBtnText?: string, rows: ZinFormRow[]}} form
  61. */
  62. function getIconName(iconClass)
  63. {
  64. const names = iconClass.split(' ');
  65. const excludeSet = new Set(['sm', 'lg', '2x', '3x', '4x', 'xs', 'xl']);
  66. for(const name of names)
  67. {
  68. if(!name.length || !name.startsWith('icon-') || name.startsWith('icon-common-')) continue;
  69. const icon = name.replace('icon-', '');
  70. if(excludeSet.has(icon)) continue;
  71. return icon;
  72. }
  73. return '';
  74. }
  75. function convertClassName(className, exclude = 'dropdown-toggle input-group-btn')
  76. {
  77. const excludeSet = new Set(exclude.split(' '));
  78. return className.split(' ').reduce((list, name) =>
  79. {
  80. if(!name.length || excludeSet.has(name)) return list;
  81. if(name.endsWith('-primary')) list.push('primary');
  82. else if(name.endsWith('-secondary')) list.push('secondary');
  83. else if(name.endsWith('-warning')) list.push('warning');
  84. else if(name.endsWith('-success')) list.push('success');
  85. else if(name.endsWith('-danger')) list.push('danger');
  86. else if(name === 'btn-link') list.push('ghost');
  87. else list.push(name);
  88. return list;
  89. }, []).join(' ');
  90. }
  91. function getZinItemProps($item, props)
  92. {
  93. if($item.hasClass('dropdown-menu')) return;
  94. if($item.is('.btn-group,.dropdown'))
  95. {
  96. const items = [];
  97. $item.children().each(function()
  98. {
  99. const $this = $(this);
  100. if(!$this.is('a,.btn')) return;
  101. items.push(getZinItemProps($this));
  102. });
  103. return {type: 'btnGroup', items, ...props};
  104. }
  105. const $icon = $item.find('.icon,i');
  106. const item = {
  107. icon: $icon.length ? getIconName($icon.attr('class')) : undefined,
  108. text: $item.text().trim(),
  109. class: convertClassName($item.attr('class'), 'btn'),
  110. ...props
  111. };
  112. if($item.data('toggle') === 'dropdown')
  113. {
  114. item.type = 'dropdown';
  115. item.items = [];
  116. const $menu = $item.next('.dropdown-menu');
  117. if($menu.length)
  118. {
  119. const $listGroup = $menu.children('.list-group');
  120. if($listGroup.length)
  121. {
  122. $listGroup.children('a').each(function()
  123. {
  124. item.items.push($(this).text().trim());
  125. });
  126. }
  127. else
  128. {
  129. $menu.children('li').each(function()
  130. {
  131. item.items.push($(this).text().trim());
  132. });
  133. }
  134. }
  135. }
  136. return item;
  137. }
  138. function getFeatureBarInfo($)
  139. {
  140. const $featureBar = $('#mainMenu .btn-toolbar.pull-left,#mainMenu .btn-toolBar.pull-left');
  141. if(!$featureBar.length) return;
  142. const featureBar = {items: []};
  143. $featureBar.children().each(function()
  144. {
  145. const $this = $(this);
  146. const id = $this.attr('id') || '';
  147. if($this.is('.querybox-toggle'))
  148. {
  149. featureBar.items.push({type: 'searchToggle'});
  150. return;
  151. }
  152. if($this.is('.checkbox-primary'))
  153. {
  154. featureBar.items.push({type: 'checkbox', text: $this.find('label').text().trim() || $this.text().trim()});
  155. return;
  156. }
  157. if($this.is('a.btn'))
  158. {
  159. if($this.hasClass('btn-active-text')) featureBar.current = id ? id.replace('Tab', '') : ($this.find('.text').text().trim() || $this.text().trim());
  160. return;
  161. }
  162. });
  163. if(!featureBar.items.length) return;
  164. return featureBar;
  165. }
  166. function getToolbarInfo($)
  167. {
  168. const $toolbar = $('#mainMenu .btn-toolbar.pull-right');
  169. if(!$toolbar.length) return;
  170. const toolbar = [];
  171. $toolbar.children().each(function()
  172. {
  173. toolbar.push(getZinItemProps($(this)));
  174. });
  175. return toolbar.length ? toolbar : null;
  176. }
  177. function getTableInfo($)
  178. {
  179. const info = {};
  180. const $table = $('#mainContent .table:not(.table-form)').first();
  181. if(!$table.length) return;
  182. if($('#mainContent .datatable').length)
  183. {
  184. return alert('zin: Please switch the table to simple table mode.');
  185. }
  186. const colTypesMap =
  187. {
  188. name: 'link',
  189. pri: 'pri',
  190. id: 'id',
  191. status: 'status',
  192. actions: 'actions',
  193. progress: 'circleProgress',
  194. assignedTo: 'avatarBtn',
  195. };
  196. const getColName = ($cell) => ($cell.attr('class').toLowerCase().split(' ').find(x => x.startsWith('c-')) || '').replace('c-', '');
  197. /**
  198. * @type {ZinDtableProps}
  199. */
  200. const setting = {cols: [], data: [], plugins: [], footer: []};
  201. let flexed = false;
  202. $table.find('thead>tr:first>th').each(function()
  203. {
  204. const $this = $(this);
  205. const data = $this.data();
  206. const title = $this.attr('title') || $this.text().trim();
  207. const name = getColName($this) || title;
  208. if(!flexed && data.flex) flexed = true;
  209. const col =
  210. {
  211. title,
  212. name,
  213. width: data.width.endsWith('px') ? Number.parseInt(data.width, 10) : 200,
  214. flex: data.width === 'auto' ? 1 : 0,
  215. fixed: data.flex ? false : (flexed ? 'right' : 'left'),
  216. type: colTypesMap[name],
  217. sortType: !!$this.find('a.sort-up,a.sort-down,a.header').length
  218. };
  219. setting.cols.push(col);
  220. });
  221. $table.find('tbody>tr').each(function(idx)
  222. {
  223. const $this = $(this);
  224. const rowData = $this.data();
  225. $this.children('td').each(function()
  226. {
  227. $td = $(this);
  228. const name = getColName($td);
  229. if(name && rowData[name] === undefined)
  230. {
  231. rowData[name] = $td.text().trim();
  232. if(name === 'progress' && rowData[name]) rowData[name] = rowData[name].replace('%', 0);
  233. }
  234. });
  235. if(rowData.id === undefined) rowData.id = idx;
  236. rowData.id = String(rowData.id);
  237. setting.data.push(rowData);
  238. if(setting.data.length > 5) return;
  239. });
  240. const tableJs = $('#mainContent [data-ride="table"]').data('zui.table');
  241. if(tableJs)
  242. {
  243. if(tableJs.options.checkable) setting.plugins.push('checkable');
  244. if(tableJs.options.sortable) setting.plugins.push('sortable');
  245. if(tableJs.options.nested || $table.find('tbody>tr.table-parent,tbody>tr.has-child').length) setting.plugins.push('nested');
  246. }
  247. else
  248. {
  249. if($table.find('td .checkbox-primary').length) setting.plugins.push('checkable');
  250. if($table.hasClass('has-sort-head')) setting.plugins.push('sortable');
  251. if($table.find('tbody>tr.table-parent,tbody>tr.has-child').length) setting.plugins.push('nested');
  252. }
  253. if(setting.plugins.includes('checkable'))
  254. {
  255. const idCol = setting.cols.find(x => x.name === 'id');
  256. if(idCol) idCol.checkbox = true;
  257. }
  258. if(setting.plugins.includes('nested') && !setting.cols.find(x => x.nestedToggle))
  259. {
  260. const nameCol = setting.cols.find(x => x.name === 'name');
  261. if(nameCol) nameCol.nestedToggle = true;
  262. }
  263. const $footer = $('#mainContent .table-footer');
  264. if($footer.length)
  265. {
  266. if(setting.plugins.includes('checkable')) setting.footer.push('checkbox', 'divider');
  267. const $actions = $footer.find('.table-actions,.btn-toolbar');
  268. if($actions.length)
  269. {
  270. const actions = [];
  271. $actions.children().each(function()
  272. {
  273. actions.push(getZinItemProps($(this)));
  274. });
  275. if(actions.length) setting.footer.push('toolbar');
  276. setting.footToolbar = {items: actions.filter(Boolean)};
  277. setting.footer.push('toolbar');
  278. }
  279. const $statistic = $footer.find('.table-statistic');
  280. if($statistic.length) setting.footer.push($statistic.text().trim());
  281. const $pager = $footer.find('.pager');
  282. if($pager.length)
  283. {
  284. setting.footer.push('flex', 'pager');
  285. setting.footPager = true;
  286. }
  287. }
  288. info.dtable = setting;
  289. info.layout = 'list';
  290. info.sidebar = $('#sidebar').length ? {type: 'moduleMenu'} : undefined;
  291. if($('#tableCustomBtn').length) info.tableCustomCols = true;
  292. return info;
  293. }
  294. function getMainHeaderInfo($)
  295. {
  296. const $mainHeader = $('#mainContent .main-header');
  297. if(!$mainHeader.length) return;
  298. const mainHeader = {title: $mainHeader.find('h2').text().trim()};
  299. const $toolbar = $mainHeader.find('.btn-toolbar');
  300. if($toolbar.length)
  301. {
  302. const toolbar = [];
  303. $toolbar.children().each(function()
  304. {
  305. toolbar.push(getZinItemProps($(this)));
  306. });
  307. mainHeader.toolbar = toolbar.filter(Boolean);
  308. }
  309. return mainHeader;
  310. }
  311. function getFormGroupProps($control)
  312. {
  313. if($control.hasClass('input-group'))
  314. {
  315. const info = {type: 'inputGroup', items: []};
  316. $control.children('.form-control,.input-group-addon,.input-group-btn').each(function()
  317. {
  318. const $this = $(this);
  319. if($this.hasClass('form-control')) info.items.push(getFormGroupProps($this));
  320. else if($this.hasClass('input-group-addon')) info.items.push({type: 'addon', text: $this.text().trim()});
  321. else if($this.hasClass('input-group-btn')) info.items.push({type: 'btn', ...getZinItemProps($this)});
  322. });
  323. return info;
  324. }
  325. if($control.hasClass('file-input-list'))
  326. {
  327. const $file = $control.find('input[type="file"]');
  328. return {type: 'file', name: $control.attr('name') || $file.attr('name'), multiple: $file.prop('multiple')};
  329. }
  330. if($control.hasClass('checkbox-primary'))
  331. {
  332. const $checkboxes = $control.find('input[type="checkbox"]');
  333. const info = {type: 'checkList', name: $control.attr('name') || $checkboxes.attr('name'), items: [], inline: true};
  334. $checkboxes.each(function()
  335. {
  336. const $checkbox = $(this);
  337. info.items.push({checked: $checkbox.prop('checked'), value: $checkbox.val(), disabled: $checkbox.prop('disabled'), text: $checkbox.parent().find('label').text().trim()});
  338. if(info.items.length > 5) return;
  339. });
  340. if($checkboxes.closest('td').prev('td').length) info.class = 'ml-4';
  341. return info;
  342. }
  343. if($control.hasClass('radio') || $control.hasClass('radio-inline'))
  344. {
  345. const $radioes = $control.closest('td').find('input[type="radio"]');
  346. const info = {type: 'radioList', inline: $control.hasClass('radio-inline'), name: $radioes.attr('name'), items: []};
  347. $radioes.each(function()
  348. {
  349. const $radio = $(this);
  350. info.items.push({checked: $radio.prop('checked'), value: $radio.val(), disabled: $radio.prop('disabled'), text: $radio.parent().text().trim()});
  351. if(info.items.length > 5) return;
  352. });
  353. return info;
  354. }
  355. const info = {name: $control.attr('name'), id: $control.attr('id'), placeholder: $control.attr('placeholder'), value: $control.val(), disabled: $control.prop('disabled'), type: $control.is('input') ? $control.attr('type') : null};
  356. if($control.attr('onchange')) info.onchange = $control.attr('onchange').split('(').shift().trim();
  357. if($control.is('textarea'))
  358. {
  359. info.rows = +$control.attr('rows') || null;
  360. info.type = $control.data('keditor') ? 'editor' : 'textarea';
  361. return info;
  362. }
  363. if($control.is('select'))
  364. {
  365. info.items = [];
  366. info.type = $control.prop('multiple') ? {type: 'picker', multiple: true} : 'picker';
  367. $control.children().each(function()
  368. {
  369. const $option = $(this);
  370. info.items.push({value: $option.attr('value'), text: $option.text().trim()});
  371. if(info.items.length > 5) return;
  372. });
  373. return info;
  374. }
  375. if($control.is('.form-date,.form-time,.form-datetime'))
  376. {
  377. info.type = $control.hasClass('form-date') ? 'date' : $control.hasClass('form-time') ? 'time' : 'datetime';
  378. }
  379. return info;
  380. }
  381. function getFormInfo($)
  382. {
  383. const $form = $('#mainContent').find('.table-form,.main-form>.table');
  384. if(!$form.length) return;
  385. const form = {items: [], grid: 4, vars: [], hiddens: []};
  386. const $submit = $form.find('#submit');
  387. if($submit.length)
  388. {
  389. form.grid = +$submit.closest('td').attr('colspan') || 1;
  390. form.submitBtnText = $submit.text().trim();
  391. if(form.submitBtnText === '保存') form.submitBtnText = null;
  392. }
  393. $form.find('tbody>tr').each(function()
  394. {
  395. const $tr = $(this);
  396. if($tr.find('#submit').length) return;
  397. let label = '';
  398. const formRow = {items: [], hidden: $tr.is(':hidden')};
  399. $tr.children().each(function()
  400. {
  401. const $thd = $(this);
  402. if($thd.is('th'))
  403. {
  404. label = $thd.text().trim();
  405. }
  406. else
  407. {
  408. const $control = $thd.find('.form-control,.input-group,textarea.kindeditor,.file-input-list,.checkbox-primary,.radio,.radio-inline').first();
  409. if(!$control.length) return;
  410. const colspan = +$thd.attr('colspan') || 1;
  411. const width = colspan !== form.grid ? `${colspan}/${form.grid}` : null;
  412. const formGroup = {label: label, required: $thd.hasClass('required'), width: width, value: ''};
  413. $.extend(formGroup, getFormGroupProps($control));
  414. if(!formGroup.name && formGroup.type !== 'inputGroup') return;
  415. if(formGroup.items)
  416. {
  417. if(formGroup.name)
  418. {
  419. form.vars.push({name: `${formGroup.name.replace('[]', '')}Options`, value: formGroup.items.slice(0, Math.min(formGroup.items.length, 5))});
  420. }
  421. else if(formGroup.type === 'inputGroup')
  422. {
  423. formGroup.items.forEach(item =>
  424. {
  425. if(item.items && item.name) form.vars.push({name: `${item.name.replace('[]', '')}Options`, value: item.items.slice(0, Math.min(item.items.length, 5))});
  426. });
  427. }
  428. }
  429. formRow.items.push(formGroup);
  430. label = '';
  431. }
  432. });
  433. if(formRow.items.length) form.items.push(formRow);
  434. });
  435. $form.find('input[type="hidden"]').each(function()
  436. {
  437. const $input = $(this);
  438. form.hiddens.push({name: $input.attr('name'), value: $input.val()});
  439. });
  440. return {form, layout: 'form'};
  441. }
  442. /**
  443. * Get page info for zin
  444. * @param {Window} win
  445. * @returns {ZinPageInfo}
  446. */
  447. function getPageInfo(win)
  448. {
  449. const {document, config, $, pageVars} = win;
  450. return $.extend(
  451. {
  452. url: win.location.href,
  453. title: document.title.replace(' - 禅道', ''),
  454. moduleName: config.currentModule,
  455. methodName: config.currentMethod,
  456. featureBar: getFeatureBarInfo($),
  457. toolbar: getToolbarInfo($),
  458. mainHeader: getMainHeaderInfo($),
  459. vars: pageVars,
  460. }, getTableInfo($), getFormInfo($));
  461. }
  462. /**
  463. * Indent string lines
  464. * @param {string[]|string} lines
  465. * @param {number} [indent=0]
  466. * @returns {string[]|string}
  467. */
  468. function indentLines(lines, indent = 0)
  469. {
  470. const isArray = Array.isArray(lines);
  471. if(!isArray) lines = lines.split('\n');
  472. const indentStr = ' '.repeat(indent * 4);
  473. lines = lines.map(x => x.includes('\n') ? indentLines(x, indent) : (indentStr + x));
  474. return isArray ? lines : lines.join('\n');
  475. }
  476. /**
  477. * Generate item statement
  478. * @param {ZinItemProps} name
  479. * @param {any} value
  480. * @param {number} [indent=0]
  481. * @returns {string}
  482. */
  483. function genSetStatement(name, value, indent = 0)
  484. {
  485. return indentLines(`set::${name}(${JSON.stringify(value)})`, indent);
  486. }
  487. /**
  488. * Find dotted var name from an object
  489. */
  490. function findVarName(val, vars, preName = '')
  491. {
  492. if(vars === val) return preName;
  493. if(!vars || typeof vars !== 'object') return '';
  494. const props = Object.keys(vars);
  495. for(const prop of props)
  496. {
  497. if(!Number.isNaN(+prop)) return (preName.length && val === vars[prop]) ? `${preName}[${prop}]` : '';
  498. const name = findVarName(val, vars[prop], preName.length ? `${preName}->${prop}` : prop);
  499. if(name.length) return name;
  500. }
  501. return '';
  502. }
  503. /**
  504. * Get var name matches the given value
  505. */
  506. function getVarName(val)
  507. {
  508. if(!pageInfo || !pageInfo.vars || typeof val === 'object') return;
  509. return findVarName(val, pageInfo.vars);
  510. }
  511. /**
  512. * Gen value to php statement
  513. * @param {any} value
  514. * @param {number} indent
  515. * @returns {string}
  516. */
  517. function genValueStatement(value, indent = 0, join = '\n')
  518. {
  519. if(value === undefined) return;
  520. if(value === null) return indentLines('NULL', indent);
  521. if(Array.isArray(value))
  522. {
  523. return indentLines(`array(${value.map(val => genValueStatement(val, indent, join)).join(', ')})`, indent);
  524. }
  525. if(typeof value === 'object')
  526. {
  527. return genArrayStatement(value, null, indent, '', '', join);
  528. }
  529. if(typeof value === 'string' && value.startsWith('RAW_PHP<')) return indentLines(value.substring(8, value.length - 8), indent);
  530. if(typeof value !== 'object' && value !== '')
  531. {
  532. const varName = getVarName(value);
  533. if(varName.length) return `$${varName}`;
  534. }
  535. return indentLines(JSON.stringify(value), indent);
  536. }
  537. /**
  538. * Generate php array statement
  539. * @param {Reacord<string, any>} array Php array object
  540. * @param {string|string[]} props Prop names list
  541. * @param {number} [indent=0]
  542. * @returns {string}
  543. */
  544. function genArrayStatement(array, props = null, indent = 0, prefix = '', suffix = '', join = '\n')
  545. {
  546. if(typeof props === 'string') props = props.split(',');
  547. else if(props === null) props = Object.keys(array);
  548. const propLines = props.reduce((lines, prop) =>
  549. {
  550. const value = genValueStatement(array[prop], 0, join);
  551. if(typeof value === 'string' && value.length)
  552. {
  553. lines.push(`'${prop}' => ${value}`);
  554. }
  555. return lines;
  556. }, []);
  557. if(!propLines.length) return [indentLines('array()', indent)];
  558. return indentLines(
  559. [
  560. `${prefix}array`,
  561. '(',
  562. indentLines(propLines.join(`,${join === '' ? ' ' : join}`), join === '' ? 0 : (1 + indent)),
  563. `)${suffix}`
  564. ], indent).join(join);
  565. }
  566. /**
  567. * Generate item statement
  568. * @param {ZinItemProps} item
  569. * @param {number} [indent=0]
  570. * @returns {string}
  571. */
  572. function genItemStatement(item, indent = 0)
  573. {
  574. if(item.type === 'searchToggle') return indentLines('li(searchToggle())', indent);
  575. return genArrayStatement(item, null, indent, 'item(set(', '))');
  576. }
  577. /**
  578. * Generate php variable statement
  579. * @param {string} name
  580. * @param {any} value
  581. * @param {number} indent
  582. * @param {string} join
  583. */
  584. function genVarStatement(name, value, indent = 0, join = '')
  585. {
  586. return indentLines(`$${name} = ${genValueStatement(value, indent, join)};`, indent);
  587. }
  588. function genInputGroupItemStatement(item, indent = 0)
  589. {
  590. if(item.type === 'addon') return indentLines(genValueStatement(item.text), indent);
  591. if(item.type === 'btn') return indentLines(genArrayStatement(item, null, 0, 'btn(set(', '))'), indent);
  592. item = $.extend({}, item);
  593. if(typeof item.type === 'object') item = $.extend(item, item.type);
  594. if(item.items) item.items = `RAW_PHP<$${item.name.replace('[]', '')}Options>RAW_PHP`;
  595. return indentLines(genArrayStatement(item, null, 0, 'control(set(', '))'), indent);
  596. }
  597. function genInputGroupStatement(items, indent = 0)
  598. {
  599. return indentLines(
  600. [
  601. 'inputGroup',
  602. '(',
  603. items.map(item => genInputGroupItemStatement(item, 1)).join(',\n'),
  604. ')'
  605. ], indent).join('\n');
  606. }
  607. function genFormGroupStatement(formGroup, indent = 0)
  608. {
  609. return indentLines(
  610. [
  611. 'formGroup',
  612. '(',
  613. indentLines(
  614. [
  615. formGroup.width ? `set::width(${JSON.stringify(formGroup.width)})` : null,
  616. formGroup.name ? `set::name(${JSON.stringify(formGroup.name)})` : null,
  617. formGroup.label ? `set::label(${genValueStatement(formGroup.label)})` : null,
  618. formGroup.class ? `set::className(${JSON.stringify(formGroup.class)})` : null,
  619. formGroup.disabled ? 'set::disabled(true)' : null,
  620. formGroup.required ? 'set::required(true)' : null,
  621. formGroup.value ? `set::value(${JSON.stringify(formGroup.value)})` : null,
  622. formGroup.type && formGroup.type !== 'inputGroup' ? `set::control(${JSON.stringify(formGroup.type)})` : null,
  623. formGroup.id && formGroup.id !== formGroup.name ? `set::id(${JSON.stringify(formGroup.id)})` : null,
  624. formGroup.items && formGroup.type !== 'inputGroup' ? `set::items($${formGroup.name.replace('[]', '')}Options)` : null,
  625. formGroup.type === 'inputGroup' ? genInputGroupStatement(formGroup.items) : null,
  626. ].filter(x => typeof x === 'string'), 1).join(',\n'),
  627. ')'
  628. ].filter(x => typeof x === 'string'), indent).join('\n');
  629. }
  630. function getFormRowStatement(formRow, indent = 0)
  631. {
  632. return indentLines(
  633. [
  634. 'formRow',
  635. '(',
  636. formRow.hidden ? ' set::hidden(true),' : null,
  637. formRow.items.map(item => genFormGroupStatement(item, 1)).join(',\n'),
  638. ')'
  639. ].filter(Boolean), indent).join('\n');
  640. }
  641. /**
  642. * Get page template
  643. * @param {ZinPageInfo} info
  644. * @returns {string}
  645. */
  646. function getPageTemplate(info)
  647. {
  648. /** @type {string[]} */
  649. const lines = [];
  650. const {featureBar = {}, toolbar, dtable, form, mainHeader = {}} = info;
  651. const variables = [];
  652. const widgets = [];
  653. if(featureBar && featureBar.current && !info.vars.browseType)
  654. {
  655. lines.push
  656. (
  657. '/* zin: Set variable $browseType to store the current active item in feature bar. */',
  658. `${genVarStatement('browseType', featureBar.current)} // the variable may already defined in control method.`,
  659. ''
  660. );
  661. }
  662. if(dtable)
  663. {
  664. lines.push('/* zin: Set variables to define columns and rows data for dtable. */');
  665. lines.push(genVarStatement('dtableCols', dtable.cols));
  666. lines.push(genVarStatement('dtableRows', dtable.data));
  667. variables.push('dtableCols', 'dtableRows');
  668. if(dtable.footToolbar)
  669. {
  670. variables.push('dtableToolbar');
  671. lines.push(genVarStatement('dtableToolbar', dtable.footToolbar));
  672. }
  673. }
  674. if(form && form.vars.length)
  675. {
  676. lines.push('/* zin: Set variables to define picker options for form. */');
  677. if(mainHeader.title !== info.title) lines.push(genVarStatement('formTitle', mainHeader.title));
  678. form.vars.forEach(item => lines.push(genVarStatement(item.name, item.value)));
  679. variables.push(...form.vars.map(item => item.name));
  680. }
  681. lines.push('\n\n/* ====== Define the page structure with zin widgets ====== */\n');
  682. if(featureBar && featureBar.items)
  683. {
  684. widgets.push('featureBar');
  685. lines.push
  686. (
  687. '/* zin: Define the feature bar on main menu. */',
  688. 'featureBar',
  689. '(',
  690. indentLines(
  691. [
  692. featureBar.current ? 'set::current($browseType)' : null,
  693. featureBar.linkParams ? `set::linkParams(${JSON.stringify(featureBar.linkParams)}),` : null,
  694. ...featureBar.items.map(item => genItemStatement(item))
  695. ].filter(x => typeof x === 'string'), 1).join(',\n'),
  696. ');',
  697. ''
  698. );
  699. }
  700. if(toolbar && toolbar.length)
  701. {
  702. lines.push
  703. (
  704. '/* zin: Define the toolbar on main menu. */',
  705. 'toolbar',
  706. '(',
  707. indentLines(toolbar.map(item => genItemStatement(item)).filter(x => typeof x === 'string'), 1).join(',\n'),
  708. ');',
  709. ''
  710. );
  711. }
  712. if(info.sidebar)
  713. {
  714. widgets.push('sidebar');
  715. lines.push
  716. (
  717. '/* zin: Define the sidebar in main content. */',
  718. 'sidebar',
  719. '(',
  720. ` ${info.sidebar.type}()`,
  721. ');',
  722. ''
  723. );
  724. }
  725. if(dtable)
  726. {
  727. widgets.push('dtable');
  728. lines.push
  729. (
  730. '/* zin: Define the dtable in main content. */',
  731. 'dtable',
  732. '(',
  733. indentLines(
  734. [
  735. "set::className('shadow rounded')",
  736. 'set::cols($dtableCols)',
  737. 'set::data($dtableRows)',
  738. dtable.plugins ? `set::plugins(array(${dtable.plugins.map(x => JSON.stringify(x)).join(', ')}))` : null,
  739. (dtable.plugins && dtable.plugins.includes('checkable')) ? 'set::checkable(true)' : null,
  740. (dtable.plugins && dtable.plugins.includes('nested')) ? 'set::nested(true)' : null,
  741. dtable.footToolbar ? 'set::toolbar($dtableToolbar)' : null,
  742. dtable.footPager ? 'set::footPager(usePager())' : null,
  743. dtable.footer ? `set::footer(array(${dtable.footer.map(x => JSON.stringify(x)).join(', ')}))` : null,
  744. ].filter(x => typeof x === 'string'), 1).join(',\n'),
  745. ');',
  746. ''
  747. );
  748. }
  749. else if(form)
  750. {
  751. widgets.push('formPanel', 'formRow', 'inputGroup', 'input', 'control');
  752. const mainHeader = info.mainHeader || {};
  753. lines.push
  754. (
  755. '/* zin: Define the form in main content. */',
  756. 'formPanel',
  757. '(',
  758. indentLines
  759. ([
  760. mainHeader.title !== info.title ? "set::title($formTitle), // The form title is diffrent from the page title" : null,
  761. form.items.map((formRow) =>
  762. {
  763. if(!formRow.hidden && formRow.items.length === 1)
  764. {
  765. return genFormGroupStatement(formRow.items[0]);
  766. }
  767. return getFormRowStatement(formRow);
  768. }).join(',\n'),
  769. ].filter(x => typeof x === 'string'), 1).join(',\n'),
  770. ');',
  771. ''
  772. );
  773. }
  774. lines.push
  775. (
  776. '\n/* ====== Render page ====== */\n',
  777. 'render();'
  778. );
  779. lines.unshift(
  780. '<?php',
  781. 'declare(strict_types=1);',
  782. '/**',
  783. ` * The ${info.methodName} view file of ${info.moduleName} module of ZenTaoPMS.`,
  784. ' *',
  785. ' * @copyright Copyright 2009-2023 禅道软件(青岛)有限公司(ZenTao Software (Qingdao) Co., Ltd. www.zentao.net)',
  786. ' * @license ZPL(https://zpl.pub/page/zplv12.html) or AGPL(https://www.gnu.org/licenses/agpl-3.0.en.html)',
  787. ' * @author yourname<yourname@easycorp.ltd>',
  788. ` * @package ${info.moduleName}`,
  789. ' * @link http://www.zentao.net',
  790. ' */',
  791. '',
  792. '/*',
  793. ' ======= Attention ======',
  794. '',
  795. ' This file is generated by zin-tool, you should check the following to-do list.',
  796. '',
  797. ` + Familiar with the use of these widgets in zin: ${widgets.join(', ')}.`,
  798. variables.length ? ` + Check the following variables which used in widgets: ${variables.map(x => `$${x}`).join(', ')}.` : null,
  799. featureBar.items && featureBar.items.length ? ` + Check the ${featureBar.items.length} items definition in featureBar widget.` : null,
  800. toolbar && toolbar.length ? ` + Check the ${toolbar.length} items definition in toolbar widget.` : null,
  801. ` + Check the origin code in module/${info.moduleName}/view/${info.methodName}.html.php, and ensure that all features have been implemented.`,
  802. ` + Check the origin js code in module/${info.moduleName}/js/common.js and module/${info.moduleName}/js/${info.methodName}.js.`,
  803. ` + Check the origin css code in module/${info.moduleName}/css/common.css and module/${info.moduleName}/css/${info.methodName}.css.`,
  804. ' + Remove the comments which start with "zin:".',
  805. ' + Test according to the new design draft and the original implementation.',
  806. ' */',
  807. '',
  808. 'namespace zin;',
  809. '',
  810. '/* ====== Preparing and processing page data ====== */',
  811. '',
  812. );
  813. return lines.filter(x => typeof x === 'string').join('\n');
  814. }
  815. function zin(win)
  816. {
  817. if(!win)
  818. {
  819. if(config.currentModule === 'index' && config.currentMethod === 'index') win = $.apps.getLastApp().$iframe[0].contentWindow;
  820. else win = window;
  821. }
  822. if(!win.config) return $.zui.messager.danger('zin: Current page is not supported yet, may be it rendered by zin already!');
  823. const pageInfo = window.pageInfo = getPageInfo(win);
  824. if(!pageInfo) return $.zui.messager.danger('zin: Current page is not supported temporarily.');
  825. const template = getPageTemplate(pageInfo);
  826. console.log('> [ZIN-TOOL] pageInfo', pageInfo);
  827. console.log('> [ZIN-TOOL] template', template);
  828. const $dialog = bootbox.dialog(
  829. {
  830. title: '<strong><i class="icon icon-magic"></i> zin view file</strong>',
  831. message: `<div class="strong">module/${pageInfo.moduleName}/ui/${pageInfo.methodName}.html.php</div><pre class="prettyprint"><code></code></pre>`,
  832. size: 'large',
  833. buttons:
  834. {
  835. copy:
  836. {
  837. label: 'Copy to clipboard',
  838. className: 'btn-primary',
  839. callback: () =>
  840. {
  841. navigator.clipboard.writeText(template);
  842. $.zui.messager.success(`zin view file copied to clipboard, you can create file module/${pageInfo.moduleName}/ui/${pageInfo.methodName}.html.php and paste to it.`);
  843. }
  844. },
  845. close:
  846. {
  847. label: 'Close',
  848. className: 'btn-default',
  849. callback: () => {}
  850. },
  851. }
  852. });
  853. $dialog.find('.modal-dialog').width(1200).find('pre>code').text(template);
  854. if(!window.prettyPrint) $.getScript(config.webRoot + 'js/kindeditor/plugins/code/prettify.js', () => window.prettyPrint());
  855. else window.prettyPrint();
  856. }
  857. $(function()
  858. {
  859. if(!config) return;
  860. if(config.currentModule === 'index' && config.currentMethod === 'index')
  861. {
  862. $('<button type="button" class="btn btn-danger code">zin()</button>').prependTo('#globalBarLogo').on('click', () =>
  863. {
  864. const app = $.apps.getLastApp();
  865. if(!app) return;
  866. zin(app.$iframe[0].contentWindow);
  867. });
  868. }
  869. });