From e97ab9686b138e4e09c57cc7943c97f6765beab1 Mon Sep 17 00:00:00 2001 From: sunhao Date: Tue, 25 Jul 2023 17:42:27 +0800 Subject: [PATCH] + add zin and zui3 files. --- framework/base/control.class.php | 105 +- framework/base/helper.class.php | 109 +- framework/base/router.class.php | 60 +- framework/control.class.php | 11 +- framework/helper.class.php | 260 + lib/base/front/front.class.php | 49 +- lib/trace/trace.class.php | 107 + lib/zin/config.php | 25 + lib/zin/core/context.class.php | 169 + lib/zin/core/context.func.php | 40 + lib/zin/core/data.func.php | 53 + lib/zin/core/directive.class.php | 66 + lib/zin/core/dom.class.php | 395 + lib/zin/core/h.class.php | 300 + lib/zin/core/h.func.php | 47 + lib/zin/core/item.class.php | 33 + lib/zin/core/on.class.php | 24 + lib/zin/core/portal.class.php | 27 + lib/zin/core/props.class.php | 284 + lib/zin/core/rawcontent.class.php | 23 + lib/zin/core/render.func.php | 60 + lib/zin/core/selector.func.php | 165 + lib/zin/core/set.class.php | 30 + lib/zin/core/to.class.php | 23 + lib/zin/core/wg.class.php | 752 + lib/zin/core/wg.func.php | 305 + lib/zin/core/zin.class.php | 76 + lib/zin/func.php | 1691 + lib/zin/helper.php | 131 + lib/zin/utils/classlist.class.php | 314 + lib/zin/utils/data.class.php | 84 + lib/zin/utils/dataset.class.php | 288 + lib/zin/utils/debug.func.php | 45 + lib/zin/utils/deep.func.php | 38 + lib/zin/utils/flat.func.php | 20 + lib/zin/utils/hx.class.php | 126 + lib/zin/utils/style.class.php | 174 + lib/zin/wg/actionitem/v1.php | 139 + lib/zin/wg/avatar/v1.php | 297 + lib/zin/wg/backbtn/v1.php | 101 + lib/zin/wg/batchactions/js/v1.js | 72 + lib/zin/wg/batchactions/v1.php | 39 + lib/zin/wg/blockpanel/v1.php | 77 + lib/zin/wg/btn/v1.php | 105 + lib/zin/wg/btngroup/v1.php | 44 + lib/zin/wg/burn/v1.php | 41 + lib/zin/wg/cell/v1.php | 49 + lib/zin/wg/center/v1.php | 16 + lib/zin/wg/checkbox/v1.php | 75 + lib/zin/wg/checkboxgroup/css/v1.css | 1 + lib/zin/wg/checkboxgroup/js/v1.js | 41 + lib/zin/wg/checkboxgroup/v1.php | 67 + lib/zin/wg/checklist/v1.php | 66 + lib/zin/wg/col/v1.php | 26 + lib/zin/wg/collapsebtn/v1.php | 39 + lib/zin/wg/colorpicker/v1.php | 83 + lib/zin/wg/commentbtn/v1.php | 42 + lib/zin/wg/commentdialog/v1.php | 35 + lib/zin/wg/commentform/v1.php | 40 + lib/zin/wg/control/v1.php | 167 + lib/zin/wg/dashboard/v1.php | 60 + lib/zin/wg/datepicker/v1.php | 85 + lib/zin/wg/datetimepicker/v1.php | 12 + lib/zin/wg/detailbody/css/v1.css | 1 + lib/zin/wg/detailbody/js/v1.js | 9 + lib/zin/wg/detailbody/v1.php | 82 + lib/zin/wg/detailheader/v1.php | 51 + lib/zin/wg/detailside/css/v1.css | 6 + lib/zin/wg/detailside/v1.php | 21 + lib/zin/wg/docmenu/css/v1.css | 19 + lib/zin/wg/docmenu/js/v1.js | 61 + lib/zin/wg/docmenu/v1.php | 416 + lib/zin/wg/dragul/css/v1.css | 1 + lib/zin/wg/dragul/v1.php | 123 + lib/zin/wg/dropdown/v1.php | 165 + lib/zin/wg/dropmenu/css/v1.css | 5 + lib/zin/wg/dropmenu/v1.php | 104 + lib/zin/wg/dtable/css/v1.css | 5 + lib/zin/wg/dtable/v1.php | 184 + lib/zin/wg/dynamic/css/v1.css | 27 + lib/zin/wg/dynamic/v1.php | 137 + lib/zin/wg/echarts/v1.php | 34 + lib/zin/wg/editor/css/v1.css | 12 + lib/zin/wg/editor/v1.php | 69 + lib/zin/wg/entitylabel/css/v1.css | 1 + lib/zin/wg/entitylabel/v1.php | 105 + lib/zin/wg/featurebar/v1.php | 148 + lib/zin/wg/fileinput/v1.php | 13 + lib/zin/wg/filelist/css/v1.css | 12 + lib/zin/wg/filelist/js/v1.js | 127 + lib/zin/wg/filelist/v1.php | 89 + lib/zin/wg/floatprenextbtn/css/v1.css | 2 + lib/zin/wg/floatprenextbtn/js/v1.js | 24 + lib/zin/wg/floatprenextbtn/v1.php | 44 + lib/zin/wg/floattoolbar/css/v1.css | 4 + lib/zin/wg/floattoolbar/v1.php | 99 + lib/zin/wg/form/v1.php | 88 + lib/zin/wg/formbase/v1.php | 114 + lib/zin/wg/formbatch/v1.php | 167 + lib/zin/wg/formbatchitem/v1.php | 122 + lib/zin/wg/formbatchpanel/v1.php | 84 + lib/zin/wg/formgroup/v1.php | 74 + lib/zin/wg/formitemdropdown/css/v1.css | 6 + lib/zin/wg/formitemdropdown/v1.php | 86 + lib/zin/wg/formlabel/v1.php | 34 + lib/zin/wg/formpanel/js/v1.js | 62 + lib/zin/wg/formpanel/v1.php | 167 + lib/zin/wg/formrow/v1.php | 39 + lib/zin/wg/formrowgroup/css/v1.css | 1 + lib/zin/wg/formrowgroup/v1.php | 40 + lib/zin/wg/formsettingbtn/css/v1.css | 6 + lib/zin/wg/formsettingbtn/js/v1.js | 117 + lib/zin/wg/formsettingbtn/v1.php | 75 + lib/zin/wg/fragment/v1.php | 26 + lib/zin/wg/globalsearch/v1.php | 43 + lib/zin/wg/header/v1.php | 440 + lib/zin/wg/heading/v1.php | 103 + lib/zin/wg/history/css/v1.css | 16 + lib/zin/wg/history/js/v1.js | 78 + lib/zin/wg/history/v1.php | 253 + lib/zin/wg/hr/v1.php | 11 + lib/zin/wg/icon/v1.php | 36 + lib/zin/wg/input/v1.php | 33 + lib/zin/wg/inputcontrol/v1.php | 72 + lib/zin/wg/inputgroup/v1.php | 53 + lib/zin/wg/inputgroupaddon/v1.php | 27 + lib/zin/wg/label/v1.php | 30 + lib/zin/wg/main/v1.php | 134 + lib/zin/wg/mainmenu/v1.php | 71 + lib/zin/wg/mainnavbar/v1.php | 107 + lib/zin/wg/menu/v1.php | 37 + lib/zin/wg/modal/v1.php | 32 + lib/zin/wg/modaldialog/css/v1.css | 0 lib/zin/wg/modaldialog/v1.php | 125 + lib/zin/wg/modalheader/v1.php | 69 + lib/zin/wg/modaltrigger/v1.php | 90 + lib/zin/wg/modulemenu/css/v1.css | 4 + lib/zin/wg/modulemenu/v1.php | 158 + lib/zin/wg/monaco/js/v1.js | 79 + lib/zin/wg/monaco/v1.php | 51 + lib/zin/wg/nav/v1.php | 36 + lib/zin/wg/navbar/v1.php | 302 + lib/zin/wg/overviewblock/css/v1.css | 1 + lib/zin/wg/overviewblock/v1.php | 150 + lib/zin/wg/page/v1.php | 54 + lib/zin/wg/pagebase/v1.php | 107 + lib/zin/wg/pageform/v1.php | 20 + lib/zin/wg/pager/v1.php | 11 + lib/zin/wg/panel/v1.php | 105 + lib/zin/wg/pastedialog/css/v1.css | 1 + lib/zin/wg/pastedialog/js/v1.js | 61 + lib/zin/wg/pastedialog/v1.php | 56 + lib/zin/wg/picker/v1.php | 130 + lib/zin/wg/popovers/v1.php | 44 + lib/zin/wg/prilabel/v1.php | 31 + lib/zin/wg/pripicker/v1.php | 78 + lib/zin/wg/productmenu/css/v1.css | 11 + lib/zin/wg/productmenu/v1.php | 76 + lib/zin/wg/programmenu/css/v1.css | 10 + lib/zin/wg/programmenu/v1.php | 87 + lib/zin/wg/radio/v1.php | 13 + lib/zin/wg/radiolist/v1.php | 13 + lib/zin/wg/risklabel/v1.php | 44 + lib/zin/wg/row/v1.php | 26 + lib/zin/wg/searchform/v1.php | 11 + lib/zin/wg/searchtoggle/css/v1.css | 3 + lib/zin/wg/searchtoggle/js/v1.js | 25 + lib/zin/wg/searchtoggle/v1.php | 39 + lib/zin/wg/section/v1.php | 74 + lib/zin/wg/sectioncard/v1.php | 50 + lib/zin/wg/sectionlist/v1.php | 23 + lib/zin/wg/select/v1.php | 139 + lib/zin/wg/severitylabel/v1.php | 36 + lib/zin/wg/severitypicker/v1.php | 79 + lib/zin/wg/sidebar/css/v1.css | 97 + lib/zin/wg/sidebar/js/v1.js | 13 + lib/zin/wg/sidebar/v1.php | 38 + lib/zin/wg/stepseditor/css/v1.css | 23 + lib/zin/wg/stepseditor/js/v1.js | 225 + lib/zin/wg/stepseditor/v1.php | 106 + lib/zin/wg/switcher/v1.php | 13 + lib/zin/wg/tablechart/v1.php | 85 + lib/zin/wg/tabledata/css/v1.css | 3 + lib/zin/wg/tabledata/v1.php | 127 + lib/zin/wg/tabpane/v1.php | 33 + lib/zin/wg/tabs/css/v1.css | 8 + lib/zin/wg/tabs/v1.php | 138 + lib/zin/wg/textarea/v1.php | 43 + lib/zin/wg/timepicker/v1.php | 74 + lib/zin/wg/toolbar/v1.php | 51 + lib/zin/wg/tooltip/v1.php | 13 + lib/zin/wg/tree/css/v1.css | 0 lib/zin/wg/tree/js/v1.js | 6 + lib/zin/wg/tree/v1.php | 61 + lib/zin/wg/upload/v1.php | 53 + lib/zin/wg/uploadimgs/v1.php | 42 + lib/zin/wg/useravatar/v1.php | 64 + lib/zin/zentao/bind.class.php | 38 + lib/zin/zentao/pager.func.php | 64 + lib/zin/zentao/zentao.func.php | 135 + lib/zin/zin.class.php | 17 + lib/zin/zui/toggle.class.php | 23 + lib/zin/zui/toggle.func.php | 25 + lib/zin/zui/zui.class.php | 211 + lib/zin/zui/zui.func.php | 22 + module/common/lang/menu.php | 14 + module/common/model.php | 23 +- module/repo/ui/create.html.php | 6 + www/install.php.tmp | 36 - www/js/zui3/@zentao/dtable/assign-modal.html | 18 + www/js/zui3/@zentao/fonts/Oswald-Light.ttf | Bin 0 -> 85176 bytes www/js/zui3/@zentao/fonts/Oswald-Medium.ttf | Bin 0 -> 85324 bytes www/js/zui3/@zentao/fonts/Oswald-Regular.ttf | Bin 0 -> 85104 bytes www/js/zui3/@zentao/icons/ZentaoIcon.eot | Bin 0 -> 71390 bytes www/js/zui3/@zentao/icons/ZentaoIcon.json | 51609 +++++++++++++++++ www/js/zui3/@zentao/icons/ZentaoIcon.svg | 302 + www/js/zui3/@zentao/icons/ZentaoIcon.ttf | Bin 0 -> 71212 bytes www/js/zui3/@zentao/icons/ZentaoIcon.woff | Bin 0 -> 71288 bytes www/js/zui3/@zentao/icons/icons.json | 318 + www/js/zui3/dashboard/block-1.html | 6 + www/js/zui3/dashboard/block-2.html | 11 + www/js/zui3/dashboard/block-3.html | 66 + www/js/zui3/dashboard/block-4.html | 8 + www/js/zui3/dashboard/block-5.html | 8 + www/js/zui3/dashboard/block-6.html | 567 + www/js/zui3/dashboard/block-8.html | 13 + www/js/zui3/dashboard/block-9.html | 13 + www/js/zui3/zin.js | 978 + www/js/zui3/zui.zentao.css | 1 + www/js/zui3/zui.zentao.js | 10617 ++++ www/js/zui3/zui.zentao.js.map | 1 + www/js/zui3/zui.zentao.umd.cjs | 4 + www/js/zui3/zui.zentao.umd.cjs.map | 1 + www/upgrade.php.tmp | 83 - 234 files changed, 82310 insertions(+), 178 deletions(-) create mode 100644 lib/trace/trace.class.php create mode 100644 lib/zin/config.php create mode 100644 lib/zin/core/context.class.php create mode 100644 lib/zin/core/context.func.php create mode 100644 lib/zin/core/data.func.php create mode 100644 lib/zin/core/directive.class.php create mode 100644 lib/zin/core/dom.class.php create mode 100644 lib/zin/core/h.class.php create mode 100644 lib/zin/core/h.func.php create mode 100644 lib/zin/core/item.class.php create mode 100644 lib/zin/core/on.class.php create mode 100644 lib/zin/core/portal.class.php create mode 100644 lib/zin/core/props.class.php create mode 100644 lib/zin/core/rawcontent.class.php create mode 100644 lib/zin/core/render.func.php create mode 100644 lib/zin/core/selector.func.php create mode 100644 lib/zin/core/set.class.php create mode 100644 lib/zin/core/to.class.php create mode 100644 lib/zin/core/wg.class.php create mode 100644 lib/zin/core/wg.func.php create mode 100644 lib/zin/core/zin.class.php create mode 100644 lib/zin/func.php create mode 100644 lib/zin/helper.php create mode 100644 lib/zin/utils/classlist.class.php create mode 100644 lib/zin/utils/data.class.php create mode 100644 lib/zin/utils/dataset.class.php create mode 100644 lib/zin/utils/debug.func.php create mode 100644 lib/zin/utils/deep.func.php create mode 100644 lib/zin/utils/flat.func.php create mode 100644 lib/zin/utils/hx.class.php create mode 100644 lib/zin/utils/style.class.php create mode 100644 lib/zin/wg/actionitem/v1.php create mode 100644 lib/zin/wg/avatar/v1.php create mode 100644 lib/zin/wg/backbtn/v1.php create mode 100644 lib/zin/wg/batchactions/js/v1.js create mode 100644 lib/zin/wg/batchactions/v1.php create mode 100644 lib/zin/wg/blockpanel/v1.php create mode 100644 lib/zin/wg/btn/v1.php create mode 100644 lib/zin/wg/btngroup/v1.php create mode 100644 lib/zin/wg/burn/v1.php create mode 100644 lib/zin/wg/cell/v1.php create mode 100644 lib/zin/wg/center/v1.php create mode 100644 lib/zin/wg/checkbox/v1.php create mode 100644 lib/zin/wg/checkboxgroup/css/v1.css create mode 100644 lib/zin/wg/checkboxgroup/js/v1.js create mode 100644 lib/zin/wg/checkboxgroup/v1.php create mode 100644 lib/zin/wg/checklist/v1.php create mode 100644 lib/zin/wg/col/v1.php create mode 100644 lib/zin/wg/collapsebtn/v1.php create mode 100644 lib/zin/wg/colorpicker/v1.php create mode 100644 lib/zin/wg/commentbtn/v1.php create mode 100644 lib/zin/wg/commentdialog/v1.php create mode 100644 lib/zin/wg/commentform/v1.php create mode 100644 lib/zin/wg/control/v1.php create mode 100644 lib/zin/wg/dashboard/v1.php create mode 100644 lib/zin/wg/datepicker/v1.php create mode 100644 lib/zin/wg/datetimepicker/v1.php create mode 100644 lib/zin/wg/detailbody/css/v1.css create mode 100644 lib/zin/wg/detailbody/js/v1.js create mode 100644 lib/zin/wg/detailbody/v1.php create mode 100644 lib/zin/wg/detailheader/v1.php create mode 100644 lib/zin/wg/detailside/css/v1.css create mode 100644 lib/zin/wg/detailside/v1.php create mode 100644 lib/zin/wg/docmenu/css/v1.css create mode 100644 lib/zin/wg/docmenu/js/v1.js create mode 100644 lib/zin/wg/docmenu/v1.php create mode 100644 lib/zin/wg/dragul/css/v1.css create mode 100644 lib/zin/wg/dragul/v1.php create mode 100644 lib/zin/wg/dropdown/v1.php create mode 100644 lib/zin/wg/dropmenu/css/v1.css create mode 100644 lib/zin/wg/dropmenu/v1.php create mode 100644 lib/zin/wg/dtable/css/v1.css create mode 100644 lib/zin/wg/dtable/v1.php create mode 100644 lib/zin/wg/dynamic/css/v1.css create mode 100644 lib/zin/wg/dynamic/v1.php create mode 100644 lib/zin/wg/echarts/v1.php create mode 100644 lib/zin/wg/editor/css/v1.css create mode 100644 lib/zin/wg/editor/v1.php create mode 100644 lib/zin/wg/entitylabel/css/v1.css create mode 100644 lib/zin/wg/entitylabel/v1.php create mode 100644 lib/zin/wg/featurebar/v1.php create mode 100644 lib/zin/wg/fileinput/v1.php create mode 100644 lib/zin/wg/filelist/css/v1.css create mode 100644 lib/zin/wg/filelist/js/v1.js create mode 100644 lib/zin/wg/filelist/v1.php create mode 100644 lib/zin/wg/floatprenextbtn/css/v1.css create mode 100644 lib/zin/wg/floatprenextbtn/js/v1.js create mode 100644 lib/zin/wg/floatprenextbtn/v1.php create mode 100644 lib/zin/wg/floattoolbar/css/v1.css create mode 100644 lib/zin/wg/floattoolbar/v1.php create mode 100644 lib/zin/wg/form/v1.php create mode 100644 lib/zin/wg/formbase/v1.php create mode 100644 lib/zin/wg/formbatch/v1.php create mode 100644 lib/zin/wg/formbatchitem/v1.php create mode 100644 lib/zin/wg/formbatchpanel/v1.php create mode 100644 lib/zin/wg/formgroup/v1.php create mode 100644 lib/zin/wg/formitemdropdown/css/v1.css create mode 100644 lib/zin/wg/formitemdropdown/v1.php create mode 100644 lib/zin/wg/formlabel/v1.php create mode 100644 lib/zin/wg/formpanel/js/v1.js create mode 100644 lib/zin/wg/formpanel/v1.php create mode 100644 lib/zin/wg/formrow/v1.php create mode 100644 lib/zin/wg/formrowgroup/css/v1.css create mode 100644 lib/zin/wg/formrowgroup/v1.php create mode 100644 lib/zin/wg/formsettingbtn/css/v1.css create mode 100644 lib/zin/wg/formsettingbtn/js/v1.js create mode 100644 lib/zin/wg/formsettingbtn/v1.php create mode 100644 lib/zin/wg/fragment/v1.php create mode 100644 lib/zin/wg/globalsearch/v1.php create mode 100644 lib/zin/wg/header/v1.php create mode 100644 lib/zin/wg/heading/v1.php create mode 100644 lib/zin/wg/history/css/v1.css create mode 100644 lib/zin/wg/history/js/v1.js create mode 100644 lib/zin/wg/history/v1.php create mode 100644 lib/zin/wg/hr/v1.php create mode 100644 lib/zin/wg/icon/v1.php create mode 100644 lib/zin/wg/input/v1.php create mode 100644 lib/zin/wg/inputcontrol/v1.php create mode 100644 lib/zin/wg/inputgroup/v1.php create mode 100644 lib/zin/wg/inputgroupaddon/v1.php create mode 100644 lib/zin/wg/label/v1.php create mode 100644 lib/zin/wg/main/v1.php create mode 100644 lib/zin/wg/mainmenu/v1.php create mode 100644 lib/zin/wg/mainnavbar/v1.php create mode 100644 lib/zin/wg/menu/v1.php create mode 100644 lib/zin/wg/modal/v1.php create mode 100644 lib/zin/wg/modaldialog/css/v1.css create mode 100644 lib/zin/wg/modaldialog/v1.php create mode 100644 lib/zin/wg/modalheader/v1.php create mode 100644 lib/zin/wg/modaltrigger/v1.php create mode 100644 lib/zin/wg/modulemenu/css/v1.css create mode 100644 lib/zin/wg/modulemenu/v1.php create mode 100644 lib/zin/wg/monaco/js/v1.js create mode 100644 lib/zin/wg/monaco/v1.php create mode 100644 lib/zin/wg/nav/v1.php create mode 100644 lib/zin/wg/navbar/v1.php create mode 100644 lib/zin/wg/overviewblock/css/v1.css create mode 100644 lib/zin/wg/overviewblock/v1.php create mode 100644 lib/zin/wg/page/v1.php create mode 100644 lib/zin/wg/pagebase/v1.php create mode 100644 lib/zin/wg/pageform/v1.php create mode 100644 lib/zin/wg/pager/v1.php create mode 100644 lib/zin/wg/panel/v1.php create mode 100644 lib/zin/wg/pastedialog/css/v1.css create mode 100644 lib/zin/wg/pastedialog/js/v1.js create mode 100644 lib/zin/wg/pastedialog/v1.php create mode 100644 lib/zin/wg/picker/v1.php create mode 100644 lib/zin/wg/popovers/v1.php create mode 100644 lib/zin/wg/prilabel/v1.php create mode 100644 lib/zin/wg/pripicker/v1.php create mode 100644 lib/zin/wg/productmenu/css/v1.css create mode 100644 lib/zin/wg/productmenu/v1.php create mode 100644 lib/zin/wg/programmenu/css/v1.css create mode 100644 lib/zin/wg/programmenu/v1.php create mode 100644 lib/zin/wg/radio/v1.php create mode 100644 lib/zin/wg/radiolist/v1.php create mode 100644 lib/zin/wg/risklabel/v1.php create mode 100644 lib/zin/wg/row/v1.php create mode 100644 lib/zin/wg/searchform/v1.php create mode 100644 lib/zin/wg/searchtoggle/css/v1.css create mode 100644 lib/zin/wg/searchtoggle/js/v1.js create mode 100644 lib/zin/wg/searchtoggle/v1.php create mode 100644 lib/zin/wg/section/v1.php create mode 100644 lib/zin/wg/sectioncard/v1.php create mode 100644 lib/zin/wg/sectionlist/v1.php create mode 100644 lib/zin/wg/select/v1.php create mode 100644 lib/zin/wg/severitylabel/v1.php create mode 100644 lib/zin/wg/severitypicker/v1.php create mode 100644 lib/zin/wg/sidebar/css/v1.css create mode 100644 lib/zin/wg/sidebar/js/v1.js create mode 100644 lib/zin/wg/sidebar/v1.php create mode 100644 lib/zin/wg/stepseditor/css/v1.css create mode 100644 lib/zin/wg/stepseditor/js/v1.js create mode 100644 lib/zin/wg/stepseditor/v1.php create mode 100644 lib/zin/wg/switcher/v1.php create mode 100644 lib/zin/wg/tablechart/v1.php create mode 100644 lib/zin/wg/tabledata/css/v1.css create mode 100644 lib/zin/wg/tabledata/v1.php create mode 100644 lib/zin/wg/tabpane/v1.php create mode 100644 lib/zin/wg/tabs/css/v1.css create mode 100644 lib/zin/wg/tabs/v1.php create mode 100644 lib/zin/wg/textarea/v1.php create mode 100644 lib/zin/wg/timepicker/v1.php create mode 100644 lib/zin/wg/toolbar/v1.php create mode 100644 lib/zin/wg/tooltip/v1.php create mode 100644 lib/zin/wg/tree/css/v1.css create mode 100644 lib/zin/wg/tree/js/v1.js create mode 100644 lib/zin/wg/tree/v1.php create mode 100644 lib/zin/wg/upload/v1.php create mode 100644 lib/zin/wg/uploadimgs/v1.php create mode 100644 lib/zin/wg/useravatar/v1.php create mode 100644 lib/zin/zentao/bind.class.php create mode 100644 lib/zin/zentao/pager.func.php create mode 100644 lib/zin/zentao/zentao.func.php create mode 100644 lib/zin/zin.class.php create mode 100644 lib/zin/zui/toggle.class.php create mode 100644 lib/zin/zui/toggle.func.php create mode 100644 lib/zin/zui/zui.class.php create mode 100644 lib/zin/zui/zui.func.php create mode 100644 module/repo/ui/create.html.php delete mode 100644 www/install.php.tmp create mode 100644 www/js/zui3/@zentao/dtable/assign-modal.html create mode 100644 www/js/zui3/@zentao/fonts/Oswald-Light.ttf create mode 100644 www/js/zui3/@zentao/fonts/Oswald-Medium.ttf create mode 100644 www/js/zui3/@zentao/fonts/Oswald-Regular.ttf create mode 100644 www/js/zui3/@zentao/icons/ZentaoIcon.eot create mode 100644 www/js/zui3/@zentao/icons/ZentaoIcon.json create mode 100644 www/js/zui3/@zentao/icons/ZentaoIcon.svg create mode 100644 www/js/zui3/@zentao/icons/ZentaoIcon.ttf create mode 100644 www/js/zui3/@zentao/icons/ZentaoIcon.woff create mode 100644 www/js/zui3/@zentao/icons/icons.json create mode 100644 www/js/zui3/dashboard/block-1.html create mode 100644 www/js/zui3/dashboard/block-2.html create mode 100644 www/js/zui3/dashboard/block-3.html create mode 100644 www/js/zui3/dashboard/block-4.html create mode 100644 www/js/zui3/dashboard/block-5.html create mode 100644 www/js/zui3/dashboard/block-6.html create mode 100644 www/js/zui3/dashboard/block-8.html create mode 100644 www/js/zui3/dashboard/block-9.html create mode 100644 www/js/zui3/zin.js create mode 100644 www/js/zui3/zui.zentao.css create mode 100644 www/js/zui3/zui.zentao.js create mode 100644 www/js/zui3/zui.zentao.js.map create mode 100644 www/js/zui3/zui.zentao.umd.cjs create mode 100644 www/js/zui3/zui.zentao.umd.cjs.map delete mode 100644 www/upgrade.php.tmp diff --git a/framework/base/control.class.php b/framework/base/control.class.php index ef66f235bc..82c725b057 100644 --- a/framework/base/control.class.php +++ b/framework/base/control.class.php @@ -394,19 +394,20 @@ class baseControl * * @param string $moduleName module name * @param string $methodName method name + * @param string $viewDir * @access public * @return string the view file */ - public function setViewFile($moduleName, $methodName) + public function setViewFile(string $moduleName, string $methodName, string $viewDir = 'view') { $moduleName = strtolower(trim($moduleName)); $methodName = strtolower(trim($methodName)); $modulePath = $this->app->getModulePath($this->appName, $moduleName); - $viewExtPath = $this->app->getModuleExtPath($this->appName, $moduleName, 'view'); + $viewExtPath = $this->app->getModuleExtPath($moduleName, $viewDir); $viewType = $this->viewType == 'mhtml' ? 'html' : $this->viewType; - $mainViewFile = $modulePath . 'view' . DS . $this->devicePrefix . $methodName . '.' . $viewType . '.php'; + $mainViewFile = $modulePath . $viewDir . DS . $this->devicePrefix . $methodName . '.' . $viewType . '.php'; $viewFile = $mainViewFile; if(!empty($viewExtPath)) @@ -416,7 +417,7 @@ class baseControl $viewFile = file_exists($commonExtViewFile) ? $commonExtViewFile : $mainViewFile; $viewFile = (!empty($siteExtViewFile) and file_exists($siteExtViewFile)) ? $siteExtViewFile : $viewFile; - if(!is_file($viewFile)) $this->app->triggerError("the view file $viewFile not found", __FILE__, __LINE__, $exit = true); + if(!is_file($viewFile)) $this->app->triggerError("the view file $viewFile not found", __FILE__, __LINE__, true); $commonExtHookFiles = glob($viewExtPath['common'] . $this->devicePrefix . $methodName . ".*.{$viewType}.hook.php"); $siteExtHookFiles = empty($viewExtPath['site']) ? '' : glob($viewExtPath['site'] . $this->devicePrefix . $methodName . ".*.{$viewType}.hook.php"); @@ -907,12 +908,106 @@ class baseControl * @access public * @return void */ - public function display($moduleName = '', $methodName = '') + public function display(string $moduleName = '', string $methodName = '') { + if($this->config->debug && $this->viewType === 'html' && (!isset($_GET['zin']) || $_GET['zin'] != '0')) + { + if(empty($moduleName)) $moduleName = $this->moduleName; + if(empty($methodName)) $methodName = $this->methodName; + $modulePath = $this->app->getModulePath($this->appName, $moduleName); + $viewType = $this->viewType == 'mhtml' ? 'html' : $this->viewType; + $mainViewFile = $modulePath . 'ui' . DS . $this->devicePrefix . $methodName . '.' . $viewType . '.php'; + if(file_exists($mainViewFile)) return $this->render($moduleName, $methodName); + } + if(empty($this->output)) $this->parse($moduleName, $methodName); + echo $this->output; } + /** + * 向浏览器输出内容。 + * Print the content of the view. + * + * @param string $moduleName module name + * @param string $methodName method name + * @access public + * @return void + */ + public function render($moduleName = '', $methodName = '') + { + if(isset($_GET['zin']) && $_GET['zin'] == '0') + { + $this->display($moduleName, $methodName); + return; + } + + if(empty($moduleName)) $moduleName = $this->moduleName; + if(empty($methodName)) $methodName = $this->methodName; + + /* Load zin lib */ + $this->app->loadClass('zin', true); + \zin\loadConfig(); + + /** + * 设置视图文件。(PHP7有一个bug,不能直接$viewFile = $this->setViewFile())。 + * Set viewFile. (Can't assign $viewFile = $this->setViewFile() directly because one php7's bug.) + */ + $results = $this->setViewFile($moduleName, $methodName, 'ui'); + + $viewFile = $results; + if(is_array($results)) extract($results); + + /** + * 获得当前页面的CSS和JS。 + * Get css and js codes for current method. + */ + $css = $this->getCSS($moduleName, $methodName, '.ui'); + $js = $this->getJS($moduleName, $methodName, '.ui'); + if($css) $this->view->pageCSS = $css; + if($js) $this->view->pageJS = $js; + + /** + * 切换到视图文件所在的目录,以保证视图文件里面的include语句能够正常运行。 + * Change the dir to the view file to keep the relative paths work. + */ + $currentPWD = getcwd(); + chdir(dirname($viewFile)); + + /** + * Set zin context data + */ + \zin\zin::$globalRenderList = array(); + \zin\zin::$enabledGlobalRender = true; + \zin\zin::$rendered = false; + \zin\zin::$rawContentCalled = false; + + \zin\zin::$data = (array)$this->view; + \zin\zin::$data['zinDebug'] = array(); + if($this->config->debug && $this->config->debug >= 2) + { + \zin\zin::$data['zinDebug']['trace'] = $this->app->loadClass('trace')->getTrace(); + } + + /** + * 使用extract和ob方法渲染$viewFile里面的代码。 + * Use extract and ob functions to eval the codes in $viewFile. + */ + extract(\zin\zin::$data); + ob_start(); + include $viewFile; + if(!\zin\zin::$rendered) \zin\render(); + $content = ob_get_clean(); + ob_start(); + echo $content; + + /** + * 渲染完毕后,再切换回之前的路径。 + * At the end, chang the dir to the previous. + */ + chdir($currentPWD); + } + /** * 直接输出data数据,通常用于ajax请求中。 * Send data directly, for ajax requests. diff --git a/framework/base/helper.class.php b/framework/base/helper.class.php index e8fb36f49d..f7a58e6556 100644 --- a/framework/base/helper.class.php +++ b/framework/base/helper.class.php @@ -680,15 +680,21 @@ class baseHelper * 检查是否是AJAX请求。 * Check is ajax request. * + * @param ?string $type zin|modal|fetch * @static * @access public * @return bool */ - public static function isAjaxRequest() + public static function isAjaxRequest(?string $type = null): bool { - if(isset($_SERVER['HTTP_X_REQUESTED_WITH']) && $_SERVER['HTTP_X_REQUESTED_WITH'] == 'XMLHttpRequest') return true; - if(isset($_GET['HTTP_X_REQUESTED_WITH']) && $_GET['HTTP_X_REQUESTED_WITH'] == 'XMLHttpRequest') return true; - return false; + $isAjax = (isset($_SERVER['HTTP_X_REQUESTED_WITH']) && $_SERVER['HTTP_X_REQUESTED_WITH'] === 'XMLHttpRequest') || (isset($_GET['HTTP_X_REQUESTED_WITH']) && $_GET['HTTP_X_REQUESTED_WITH'] === 'XMLHttpRequest'); + if($isAjax === false) return false; + + if($type === 'zin') return array_key_exists('HTTP_X_ZIN_OPTIONS', $_SERVER); + if($type === 'modal') return isset($_SERVER['HTTP_X_ZUI_MODAL']) && $_SERVER['HTTP_X_ZUI_MODAL'] == true; + if($type === 'fetch') return !array_key_exists('HTTP_X_ZIN_OPTIONS', $_SERVER) && !(isset($_SERVER['HTTP_X_ZUI_MODAL']) && $_SERVER['HTTP_X_ZUI_MODAL'] == true); + + return $isAjax; } /** @@ -788,6 +794,101 @@ class baseHelper return null; } + /** + * Send a cookie. + * + * @param string $name + * @param string $value + * @param int|null $expire + * @param string|null $path + * @param string $domain + * @param bool|null $secure + * @param bool $httponly + * @static + * @access public + * @return bool + */ + public static function setcookie(string $name, string $value = '', int $expire = null, string $path = null, string $domain = '', bool $secure = null, bool $httponly = true) + { + global $config, $app; + + if($expire === null) $expire = $config->cookieLife; + if($path === null) $path = $config->webRoot; + if($secure === null) $secure = $config->cookieSecure; + + if(isset($app->worker)) + { + $app->worker->response->setCookie($name, $value, $expire, $path, $domain, $secure, $httponly); + } + else + { + return setcookie($name, $value, $expire, $path, $domain, $secure, $httponly); + } + } + + /** + * 设置状态码。 + * Set status code. + * + * @param int $code + * @static + * @access public + * @return void + */ + static public function setStatus(int $code) + { + global $app; + + if(isset($app->worker)) + { + $app->worker->response->setStatus($code); + } + else + { + $PHRASES = array( + 100 => 'Continue', 101 => 'Switching Protocols', 102 => 'Processing', + 200 => 'OK', 201 => 'Created', 202 => 'Accepted', 203 => 'Non-Authoritative Information', 204 => 'No Content', 205 => 'Reset Content', 206 => 'Partial Content', 207 => 'Multi-status', 208 => 'Already Reported', + 300 => 'Multiple Choices', 301 => 'Moved Permanently', 302 => 'Found', 303 => 'See Other', 304 => 'Not Modified', 305 => 'Use Proxy', 306 => 'Switch Proxy', 307 => 'Temporary Redirect', + 400 => 'Bad Request', 401 => 'Unauthorized', 402 => 'Payment Required', 403 => 'Forbidden', 404 => 'Not Found', 405 => 'Method Not Allowed', 406 => 'Not Acceptable', 407 => 'Proxy Authentication Required', 408 => 'Request Time-out', 409 => 'Conflict', 410 => 'Gone', 411 => 'Length Required', 412 => 'Precondition Failed', 413 => 'Request Entity Too Large', 414 => 'Request-URI Too Large', 415 => 'Unsupported Media Type', 416 => 'Requested range not satisfiable', 417 => 'Expectation Failed', 418 => 'I\'m a teapot', 422 => 'Unprocessable Entity', 423 => 'Locked', 424 => 'Failed Dependency', 425 => 'Unordered Collection', 426 => 'Upgrade Required', 428 => 'Precondition Required', 429 => 'Too Many Requests', 431 => 'Request Header Fields Too Large', 451 => 'Unavailable For Legal Reasons', + 500 => 'Internal Server Error', 501 => 'Not Implemented', 502 => 'Bad Gateway', 503 => 'Service Unavailable', 504 => 'Gateway Time-out', 505 => 'HTTP Version not supported', 506 => 'Variant Also Negotiates', 507 => 'Insufficient Storage', 508 => 'Loop Detected', 511 => 'Network Authentication Required', + ); + header('HTTP/1.1 ' . (string)$code . ' ' . $PHRASES[$code], true, $code); + } + } + + /** + * 发送HTTP头信息。 + * Send http header. + * + * @param string $key + * @param string $value + * @param bool $replace + * @param int $response_code + * @static + * @access public + * @return void + */ + static public function header(string $key, string $value, bool $replace = true, int $response_code = 0) + { + global $app; + + if(isset($app->worker)) + { + $key = trim(strtolower($key)); + $app->worker->response->setHeader($key, $value); + + if($key == 'location') + { + $app->worker->response->setStatus(302); + helper::end(); + } + } + else + { + header($key . ': ' . $value, $replace, $response_code); + } + } + /** * 检查是否启用缓存。 * Check is enable cache. diff --git a/framework/base/router.class.php b/framework/base/router.class.php index f79c1a63a0..362d5658e8 100644 --- a/framework/base/router.class.php +++ b/framework/base/router.class.php @@ -356,6 +356,14 @@ class baseRouter */ public $siteCode; + /** + * zin 请求时发生的错误信息。 + * The errors occurred when zin request. + * + * @var array + */ + public $zinErrors = array(); + /** * 构造方法, 设置路径,类,超级变量等。注意: * 1.应该使用createApp()方法实例化router类; @@ -2891,7 +2899,7 @@ class baseRouter * @access public * @return void */ - public function saveError($level, $message, $file, $line) + public function saveError(int $level, string $message, string $file, int $line) { if(empty($this->config->debug)) return true; if(!is_dir($this->logRoot)) return true; @@ -2901,9 +2909,9 @@ class baseRouter * 删除设定时间之前的日志。 * Delete the log before the set time. **/ - if(mt_rand(0, 10) == 1) + if(random_int(0, 10) == 1) { - $logDays = isset($this->config->framework->logDays) ? $this->config->framework->logDays : 14; + $logDays = $this->config->framework->logDays ?? 14; $dayTime = time() - $logDays * 24 * 3600; foreach(glob($this->getLogRoot() . '*') as $logFile) { @@ -2915,7 +2923,7 @@ class baseRouter * 忽略该错误:Redefining already defined constructor。 * Skip the error: Redefining already defined constructor. **/ - if(strpos($message, 'Redefining') !== false) return true; + if(str_contains($message, 'Redefining')) return true; /* * 设置错误信息。 @@ -2924,8 +2932,8 @@ class baseRouter if(preg_match('/[^\x00-\x80]/', $message)) $message = helper::convertEncoding($message, 'gbk'); $errorLog = "\n" . date('H:i:s') . " $message in $file on line $line "; - $URI = $this->getURI(); - $errorLog .= "when visiting " . (empty($URI) ? '' : htmlspecialchars($URI)) . "\n"; + $uri = $this->getURI(); + $errorLog .= "when visiting " . (empty($uri) ? '' : htmlspecialchars($uri)) . "\n"; /* * 为了安全起见,对公网环境隐藏脚本路径。 @@ -2942,18 +2950,29 @@ class baseRouter if(!is_file($errorFile)) file_put_contents($errorFile, "\n"); $fh = fopen($errorFile, 'a'); - if($fh) fwrite($fh, strip_tags($errorLog)) and fclose($fh); + if($fh) fwrite($fh, strip_tags(htmlspecialchars_decode($errorLog))) and fclose($fh); /* - * 如果debug > 1,显示warning, notice级别的错误。 - * If the debug > 1, show warning, notice error. + * 如果debug > 1,直接在页面显示非严重错误。 + * If the debug > 1, show non-serious errors on page directly. **/ - if($level == E_NOTICE or $level == E_WARNING or $level == E_STRICT or $level == 8192) // 8192: E_DEPRECATED + if(!empty($this->config->debug) && $this->config->debug > 1) { - if(!empty($this->config->debug) and $this->config->debug > 1) + /* Send non-serious errors to page in zin mode. */ + $isZinRequest = isset($this->config->zin) || isset($_SERVER['HTTP_X_ZIN_OPTIONS']); + $isNonSeriousError = $level !== E_ERROR && $level !== E_PARSE && $level !== E_CORE_ERROR && $level !== E_COMPILE_ERROR; + if($isZinRequest && $isNonSeriousError) + { + $this->zinErrors[] = array('file' => $file, 'line' => $line, 'message' => $message, 'level' => $level); + return; + } + + /* Show non-serious errors to classic page. */ + if($level == E_NOTICE or $level == E_WARNING or $level == E_STRICT or $level == 8192) { $cmd = "vim +$line $file"; $size = strlen($cmd); + echo "
$message: ";
                 echo "
"; } @@ -2963,14 +2982,21 @@ class baseRouter * 如果是严重错误,停止程序。 * If error level is serious, die. * */ - if($level == E_ERROR or $level == E_PARSE or $level == E_CORE_ERROR or $level == E_COMPILE_ERROR or $level == E_USER_ERROR) + if(in_array($level, array(E_ERROR, E_PARSE, E_CORE_ERROR, E_COMPILE_ERROR, E_USER_ERROR))) { - if(empty($this->config->debug)) die(); - if(PHP_SAPI == 'cli') die($errorLog); + if(empty($this->config->debug)) helper::end(); - $htmlError = ""; - $htmlError .= "" . nl2br($errorLog) . ""; - die($htmlError); + if(PHP_SAPI == 'cli') + { + echo $errorLog; + } + else + { + $htmlError = ""; + $htmlError .= "" . nl2br($errorLog) . ""; + echo $htmlError; + helper::end(); + } } } diff --git a/framework/control.class.php b/framework/control.class.php index f19f2d51c0..f0b65f4d94 100644 --- a/framework/control.class.php +++ b/framework/control.class.php @@ -249,19 +249,20 @@ class control extends baseControl * * @param string $moduleName module name * @param string $methodName method name + * @param string $viewDir * @access public * @return string the view file */ - public function setViewFile($moduleName, $methodName) + public function setViewFile(string $moduleName, string $methodName, string $viewDir = 'view') { $moduleName = strtolower(trim($moduleName)); $methodName = strtolower(trim($methodName)); $modulePath = $this->app->getModulePath($this->appName, $moduleName); - $viewExtPath = $this->app->getModuleExtPath($this->appName, $moduleName, 'view'); + $viewExtPath = $this->app->getModuleExtPath($moduleName, $viewDir, 'view'); $viewType = ($this->viewType == 'mhtml' or $this->viewType == 'xhtml') ? 'html' : $this->viewType; - $mainViewFile = $modulePath . 'view' . DS . $this->devicePrefix . $methodName . '.' . $viewType . '.php'; + $mainViewFile = $modulePath . $viewDir . DS . $this->devicePrefix . $methodName . '.' . $viewType . '.php'; /* If the main view file doesn't exist, set the device prefix to empty and reset the main view file. */ if(!file_exists($mainViewFile) and $this->app->clientDevice != 'mobile') @@ -309,8 +310,8 @@ class control extends baseControl $viewFile = $commonExtViewFile; } - if(!is_file($viewFile)) $viewFile = dirname(dirname($viewExtPath['common'])) . DS . 'view' . DS . $this->devicePrefix . $methodName . ".{$viewType}.php"; - if(!is_file($viewFile)) die(js::error($this->lang->notPage) . js::locate('back')); + if(!is_file($viewFile)) $viewFile = dirname((string) $viewExtPath['common'], 2) . DS . 'view' . DS . $this->devicePrefix . $methodName . ".{$viewType}.php"; + if(!is_file($viewFile)) helper::end(js::error($this->lang->notPage) . js::locate('back')); /* Get ext hook files. */ $commonExtHookFiles = glob($viewExtPath['common'] . $this->devicePrefix . $methodName . ".*.{$viewType}.hook.php"); diff --git a/framework/helper.class.php b/framework/helper.class.php index 1147271e04..78dbc6b971 100644 --- a/framework/helper.class.php +++ b/framework/helper.class.php @@ -373,6 +373,18 @@ function isonlybody() return helper::inOnlyBodyMode(); } +/** + * 检查页面是否是弹窗中。 + * Check page is modal. + * + * @access public + * @return bool + */ +function isInModal(): bool +{ + return helper::isAjaxRequest('modal'); +} + /** * Format time. * @@ -406,3 +418,251 @@ function autoloader($class) } spl_autoload_register('autoloader'); + + +/** + * Init page title based on the module name and the method name. + * + * @access public + * @return string + */ +function initPageTitle(): string +{ + global $app, $lang; + $module = $app->rawModule; + $method = $app->rawMethod; + + if(empty($lang->$module)) $app->loadLang($module); + + if(!empty($lang->$module->{$method . 'Action'})) return $lang->$module->{$method . 'Action'}; + if(!empty($lang->$module->$method)) return $lang->$module->$method; + return zget($lang, $method); +} + +/** + * Init page entity based on configuration of objectNameFields. + * + * @param object $object + * @access public + * @return array + */ +function initPageEntity(object $object): array +{ + if(empty($object)) return array(); + + global $app, $config; + $app->loadModuleConfig('action'); + + $module = $app->getModuleName(); + $idField = isset($config->action->objectIdFields[$module]) ? $config->action->objectIdFields[$module] : 'id'; + $titleField = isset($config->action->objectNameFields[$module]) ? $config->action->objectNameFields[$module] : 'title'; + + return array(zget($object, $titleField, ''), zget($object, $idField, 0)); +} + +/** + * Init table data of zin. + * + * @param array $items + * @param array $fieldList + * @param object $model + * @access public + * @return array + */ +function initTableData(array $items, array &$fieldList, object $model = null): array +{ + $items = setParent($items); + if(empty($fieldList['actions'])) return $items; + + foreach($fieldList['actions']['menu'] as $actionMenu) + { + if(is_array($actionMenu)) + { + foreach($actionMenu as $actionMenuKey => $actionName) + { + if($actionMenuKey == 'other') + { + foreach($actionName as $otherActionName) initTableActions($fieldList, $otherActionName); + } + else + { + initTableActions($fieldList, $actionName); + } + } + } + else + { + initTableActions($fieldList, $actionMenu); + } + } + + global $app; + if(empty($model)) + { + $module = $app->getModuleName(); + $model = $app->control->loadModel($module); + } + + $maxActionCount = 0; + foreach($items as $item) + { + $item->actions = array(); + foreach($fieldList['actions']['menu'] as $actionKey => $actionMenu) + { + if(isset($actionMenu['other'])) + { + $currentActionMenu = $actionMenu[0]; + initItemActions($item, $currentActionMenu, $fieldList['actions']['list'], $model); + + $otherActionMenus = $actionMenu['other']; + $otherAction = ''; + foreach($otherActionMenus as $otherActionMenu) + { + $otherActions = explode('|', $otherActionMenu); + foreach($otherActions as $otherActionName) + { + if(in_array($otherActionName, array_column($item->actions, 'name'))) continue; + + if(method_exists($model, 'isClickable') && !$model->isClickable($item, $otherActionName)) $otherAction .= '-'; + $otherAction .= $otherActionName . ','; + } + } + $item->actions[] = 'other:' . $otherAction; + } + elseif($actionKey == 'more') + { + $moreAction = ''; + foreach($actionMenu as $moreActionName) + { + if(method_exists($model, 'isClickable') && !$model->isClickable($item, $moreActionName)) $moreAction .= '-'; + $moreAction .= $moreActionName . ','; + } + + $item->actions[] = 'more:' . $moreAction; + } + elseif(is_array($actionMenu)) // Two or more grups. + { + /* + * Menu可能会有多套,如果只有一套可以直接用一维数组。 + * There are maybe two or more groups of action menus. + */ + $item->actions = array(); + $isClickable = false; + foreach($actionMenu as $actionName) $isClickable |= initItemActions($item, $actionName, $fieldList['actions']['list'], $model); + + if($isClickable) break; // If the action is clickable, use this group. + } + else // Only one group of action menus. + { + initItemActions($item, $actionMenu, $fieldList['actions']['list'], $model); + } + } + + if(count($item->actions) > $maxActionCount) $maxActionCount = count($item->actions); + } + if(isset($fieldList['actions'])) $fieldList['actions']['minWidth'] = $maxActionCount * 24 + 24; + + return array_values($items); +} + +/** + * Set the parent property of the data. + * + * @param array $items + * @access public + * @return array + */ +function setParent(array $items) +{ + foreach($items as $item) + { + /* Set parent attribute. */ + $item->isParent = false; + if(isset($item->parent) && $item->parent == -1) + { + /* When the parent is -1, the hierarchical structure is displayed incorrectly. */ + $item->parent = 0; + $item->isParent = true; + } + + if(!empty($item->parent) && isset($items[$item->parent])) $items[$item->parent]->isParent = true; + } + return $items; +} + +/** + * Init column actions of a table. + * + * @param array $fieldList + * @param string $actionMenu + * @access public + * @return void + */ +function initTableActions(array &$fieldList, string $actionMenu): void +{ + $actions = explode('|', $actionMenu); + foreach($actions as $action) + { + if(!isset($fieldList['actions']['list'][$action])) continue; + + $actionConfig = $fieldList['actions']['list'][$action]; + $actionConfig['text'] = ''; + + if(!empty($actionConfig['url']['module']) && !empty($actionConfig['url']['method'])) + { + $module = $actionConfig['url']['module']; + $method = $actionConfig['url']['method']; + $params = !empty($actionConfig['url']['params']) ? $actionConfig['url']['params'] : array(); + + $actionConfig['url'] = helper::createLink($module, $method, $params); + } + + $fieldList['actions']['actionsMap'][$action] = $actionConfig; + } +} + +/** + * Init row actions of a item. + * + * @param object $item + * @param string $actionMenu + * @param array $actionList + * @param object $model + * @access public + * @return bool + */ +function initItemActions(object &$item, string $actionMenu, array $actionList, object $model): bool +{ + global $app; + $module = $app->getModuleName(); + $method = ''; + + $isClickable = false; + $actions = explode('|', $actionMenu); + foreach($actions as $action) + { + if(!isset($actionList[$action])) continue; + + $actionConfig = $actionList[$action]; + if(!empty($actionConfig['url']['module']) && $module != $actionConfig['url']['module']) + { + $module = $actionConfig['url']['module']; + $model = $app->control->loadModel($module); + } + + $method = $action; + if(!empty($actionConfig['url']['method']) && $method != $actionConfig['url']['method']) $method = $actionConfig['url']['method']; + + if(!method_exists($model, 'isClickable') || $model->isClickable($item, $method)) + { + $isClickable = true; + break; + } + } + + if(!$method || !common::hasPriv($module, $method)) return $isClickable; + + $item->actions[] = array('name' => $action, 'disabled' => !$isClickable); + + return $isClickable; +} diff --git a/lib/base/front/front.class.php b/lib/base/front/front.class.php index 4dee58782e..297aff278e 100644 --- a/lib/base/front/front.class.php +++ b/lib/base/front/front.class.php @@ -1160,18 +1160,8 @@ EOT; return $js; } - /** - * 导出$config到js,因为js的createLink()方法需要获取config信息。 - * Export the config vars for createLink() js version. - * - * @static - * @access public - * @return void - */ - static public function exportConfigVars() + static function getJSConfigVars() { - if(!function_exists('json_encode')) return false; - global $app, $config, $lang; $defaultViewType = $app->getViewType(); $themeRoot = $app->getWebRoot() . 'theme/'; @@ -1180,7 +1170,7 @@ EOT; $clientLang = $app->getClientLang(); $runMode = defined('RUN_MODE') ? RUN_MODE : ''; $requiredFields = ''; - if(isset($config->$moduleName->$methodName->requiredFields)) $requiredFields = str_replace(' ', '', $config->$moduleName->$methodName->requiredFields); + if(isset($config->$moduleName->$methodName->requiredFields)) $requiredFields = str_replace(' ', '', (string) $config->$moduleName->$methodName->requiredFields); $jsConfig = new stdclass(); $jsConfig->webRoot = $config->webRoot; @@ -1199,21 +1189,40 @@ EOT; $jsConfig->clientLang = $clientLang; $jsConfig->requiredFields = $requiredFields; $jsConfig->router = $app->server->SCRIPT_NAME; - $jsConfig->save = isset($lang->save) ? $lang->save : ''; + $jsConfig->save = $lang->save ?? ''; $jsConfig->runMode = $runMode; - $jsConfig->timeout = isset($config->timeout) ? $config->timeout : ''; - $jsConfig->pingInterval = isset($config->pingInterval) ? $config->pingInterval : ''; + $jsConfig->timeout = $config->timeout ?? ''; + $jsConfig->pingInterval = $config->pingInterval ?? ''; $jsConfig->onlybody = zget($_GET, 'onlybody', 'no'); $jsConfig->tabSession = $config->tabSession; if($config->tabSession and helper::isWithTID()) $jsConfig->tid = zget($_GET, 'tid', ''); + return $jsConfig; + } + + /** + * 导出$config到js,因为js的createLink()方法需要获取config信息。 + * Export the config vars for createLink() js version. + * + * @static + * @access public + * @return void + */ + static public function exportConfigVars() + { + if(!function_exists('json_encode')) return false; + + global $lang; + + $jsConfig = static::getJSConfigVars(); + $jsLang = new stdclass(); - $jsLang->submitting = isset($lang->loading) ? $lang->loading : ''; + $jsLang->submitting = $lang->loading ?? ''; $jsLang->save = $jsConfig->save; - $jsLang->expand = isset($lang->expand) ? $lang->expand : ''; - $jsLang->timeout = isset($lang->timeout) ? $lang->timeout : ''; - $jsLang->confirmDraft = isset($lang->confirmDraft) ? $lang->confirmDraft : ''; - $jsLang->resume = isset($lang->resume) ? $lang->resume : ''; + $jsLang->expand = $lang->expand ?? ''; + $jsLang->timeout = $lang->timeout ?? ''; + $jsLang->confirmDraft = $lang->confirmDraft ?? ''; + $jsLang->resume = $lang->resume ?? ''; $jsLang->program = zget($lang->program, 'common', ''); $jsLang->project = zget($lang->project, 'common', ''); $jsLang->product = zget($lang->product, 'common', ''); diff --git a/lib/trace/trace.class.php b/lib/trace/trace.class.php new file mode 100644 index 0000000000..c142def963 --- /dev/null +++ b/lib/trace/trace.class.php @@ -0,0 +1,107 @@ + + * @package trace + * @link https://www.zentao.net + */ + +class trace +{ + /** + * @var array + */ + public $trace = array(); + + protected $app; + + protected $dao; + + public function __construct() + { + global $app, $dao; + $this->app = $app; + $this->dao = $dao; + } + + /** + * 获取请求信息。 + * Get request info. + * + * @return void + */ + public function getRequestInfo() + { + $this->trace['request'] = array( + 'start' => date('Y-m-d H:i:s', (int)$this->app->startTime), + 'url' => $this->app->getURI(true), + 'protocol' => $this->app->server->server_protocol, + 'method' => $this->app->server->request_method, + 'timeUsed' => round(getTime() - $this->app->startTime, 4) * 1000, + 'memory' => round(memory_get_peak_usage() / 1024, 1), + 'querys' => count(dao::$querys), + 'caches' => count(dao::$cache), + 'files' => count(get_included_files()), + 'session' => session_id() + ); + } + + /** + * 获取请求加载的文件。 + * Get request files. + * + * @return void + */ + public function getRequestFiles() + { + $this->trace['files'] = get_included_files(); + } + + /** + * 获取请求的 SQL 语句。 + * Get request SQLs. + * + * @return void + */ + public function getRequestSqls() + { + $this->trace['sqlQuery'] = dao::$querys; + } + + /** + * 获取请求的 SQL profiles。 + * Get request SQL profiles. + * + * @return array + */ + public function getSQLProfiles() + { + $profiling = $this->dao->dbh->query('SHOW PROFILES')->fetchAll(PDO::FETCH_ASSOC); + + $this->trace['profiles'] = $profiling; + } + + /** + * 生成请求 Trace。 + * Generate request trace. + * + * @return array + */ + public function getTrace() + { + $this->getRequestInfo(); + $this->getRequestFiles(); + $this->getRequestSqls(); + $this->getSQLProfiles(); + return $this->trace; + } + + public function __toString(): string + { + return json_encode($this->getTrace()); + } +} diff --git a/lib/zin/config.php b/lib/zin/config.php new file mode 100644 index 0000000000..1c0be501e8 --- /dev/null +++ b/lib/zin/config.php @@ -0,0 +1,25 @@ + + * @package zin + * @version $Id + * @link https://www.zentao.net + */ + +namespace zin; + +function loadConfig() +{ + global $app, $config; + + $config->zin = new \stdClass(); + + $config->zin->lang = $app->getClientLang(); + $config->zin->wgVer = isset($config->wgVer) ? $config->wgVer : '1'; + $config->zin->wgVerMap = isset($config->wgVerMap) ? $config->wgVerMap : array(); + $config->zin->zuiPath = isset($config->zuiPath) ? $config->zuiPath : ($app->getWebRoot() . 'js/zui3/'); +} diff --git a/lib/zin/core/context.class.php b/lib/zin/core/context.class.php new file mode 100644 index 0000000000..e9f8a719dc --- /dev/null +++ b/lib/zin/core/context.class.php @@ -0,0 +1,169 @@ + + * @package zin + * @version $Id + * @link https://www.zentao.net + */ + +namespace zin; + +require_once dirname(__DIR__) . DS . 'utils' . DS . 'dataset.class.php'; +require_once dirname(__DIR__) . DS . 'utils' . DS . 'flat.func.php'; + +class context extends \zin\utils\dataset +{ + public function addImport() + { + return $this->addToList('import', func_get_args()); + } + + public function getImportList() + { + return $this->getList('import'); + } + + public function addCSS() + { + return $this->addToList('css', func_get_args()); + } + + public function getCSS() + { + return trim(implode("\n", $this->getList('css'))); + } + + public function addJS() + { + return $this->addToList('js', func_get_args()); + } + + public function addJSVar($name, $value) + { + return $this->addToList('jsVar', h::createJsVarCode($name, $value)); + } + + public function addWgWithEvents($wg) + { + $list = $this->getWgWithEventsList(); + if(in_array($wg, $list)) return $this; + return $this->addToList('wgWithEvents', $wg); + } + + public function getWgWithEventsList() + { + return $this->getList('wgWithEvents'); + } + + public function addJSCall() + { + $code = call_user_func_array('\zin\h::createJsCallCode', func_get_args()); + return $this->addToList('jsCall', $code); + } + + public function getEventsBindings() + { + $wgs = $this->getList('wgWithEvents'); + $codes = []; + foreach($wgs as $wg) + { + if(!method_exists($wg, 'buildEvents')) continue; + $code = $wg->buildEvents(); + if(!empty($code)) $codes[] = $code; + } + return $codes; + } + + public function getJS() + { + $js = trim(implode("\n", array_merge($this->getList('jsVar'), $this->getList('js'), $this->getEventsBindings(), $this->getList('jsCall')))); + if(empty($js)) return ''; + + if(strpos($js, 'setTimeout') !== false) $js = 'function setTimeout(callback, time){return typeof window.registerTimer === "function" ? window.registerTimer(callback, time) : window.setTimeout(callback, time);}' . $js; + if(strpos($js, 'setInterval') !== false) $js = 'function setInterval(callback, time){return typeof window.registerTimer === "function" ? window.registerTimer(callback, time, "interval") : window.setInterval(callback, time);}' . $js; + + $methods = array('onPageUnmount', 'beforePageUpdate', 'afterPageUpdate', 'onPageRender'); + foreach($methods as $method) + { + if(strpos($js, $method) !== false) $js .= "if(typeof $method === 'function') window.$method = $method;"; + } + return $js; + } + + public static $map = array(); + + public static function js(/* string ...$code */) + { + $context = static::current(); + call_user_func_array(array($context, 'addJS'), \zin\utils\flat(func_get_args())); + } + + public static function jsCall(/* string ...$code */) + { + $context = static::current(); + call_user_func_array(array($context, 'addJSCall'), func_get_args()); + } + + + public static function jsVar($name, $value) + { + $context = static::current(); + $context->addJSVar($name, $value); + } + + public static function css(/* string ...$code */) + { + $context = static::current(); + call_user_func_array(array($context, 'addCSS'), \zin\utils\flat(func_get_args())); + } + + public static function import(/* string ...$files */) + { + $context = static::current(); + call_user_func_array(array($context, 'addImport'), func_get_args()); + } + + /** + * Get current context. + * + * @access public + * @return context + */ + public static function current(): context + { + if(empty(static::$map)) static::$map['current'] = new context(null); + return static::$map['current']; + } + + /** + * Create widget context. + * + * @access public + * @param string $gid The widget gid. + * @return context + */ + public static function create(string $gid): context + { + if(isset(static::$map[$gid])) return static::$map[$gid]; + $context = new context(); + static::$map[$gid] = $context; + return $context; + } + + /** + * Destroy widget context. + * + * @access public + * @param string $gid The widget gid. + * @return void + */ + public static function destroy(string $gid = null): void + { + if($gid === null) unset(static::$map['current']); + elseif(isset(static::$map[$gid])) unset(static::$map[$gid]); + } +} diff --git a/lib/zin/core/context.func.php b/lib/zin/core/context.func.php new file mode 100644 index 0000000000..aec0a86f17 --- /dev/null +++ b/lib/zin/core/context.func.php @@ -0,0 +1,40 @@ + + * @package zin + * @version $Id + * @link https://www.zentao.net + */ + +namespace zin; + +require_once __DIR__ . DS . 'context.class.php'; + +function js() +{ + call_user_func_array('\zin\context::js', func_get_args()); +} + +function jsCall() +{ + call_user_func_array('\zin\context::jsCall', func_get_args()); +} + +function jsVar() +{ + call_user_func_array('\zin\context::jsVar', func_get_args()); +} + +function css() +{ + call_user_func_array('\zin\context::css', func_get_args()); +} + +function import() +{ + call_user_func_array('\zin\context::import', func_get_args()); +} diff --git a/lib/zin/core/data.func.php b/lib/zin/core/data.func.php new file mode 100644 index 0000000000..14fd179a70 --- /dev/null +++ b/lib/zin/core/data.func.php @@ -0,0 +1,53 @@ + + * @package zin + * @version $Id + * @link https://www.zentao.net + */ + +namespace zin; + +function setPageData($name, $value) +{ + if(is_array($value) && empty($name)) + { + foreach ($value as $key => $val) zin::setData($key, $val); + return; + } + zin::setData($name, $value); +} + +function getPageData($name) +{ + if(is_array($name)) + { + $values = array(); + foreach($name as $propName) + { + $values[] = zin::getData($propName); + } + return $values; + } + + return zin::getData($name); +} + +function data(...$args) +{ + if(count($args) >= 2) return setPageData($args[0], $args[1]); + return getPageData($args[0]); +} + +/** + * Set page data + * @deprecated Use data($name, $value) insteadOf useData($name, $value) + */ +function useData($name, $value) +{ + return setPageData($name, $value); +} diff --git a/lib/zin/core/directive.class.php b/lib/zin/core/directive.class.php new file mode 100644 index 0000000000..0871efe0f1 --- /dev/null +++ b/lib/zin/core/directive.class.php @@ -0,0 +1,66 @@ + + * @package zin + * @version $Id + * @link https://www.zentao.net + */ + +namespace zin; + +require_once __DIR__ . DS . 'zin.class.php'; + +class directive +{ + public string $type; + + public mixed $data; + + public ?array $options; + + public ?wg $parent = null; + + /** + * Construct a directive object + * @param string $type + * @param mixed $data + * @param array $options + * @access public + */ + public function __construct(string $type, mixed $data, ?array $options = null) + { + $this->type = $type; + $this->data = $data; + $this->options = $options; + + zin::renderInGlobal($this); + } + + public function __debugInfo(): array + { + return array( + 'type' => $this->type, + 'data' => $this->data, + 'options' => $this->options + ); + } + + public static function is(mixed $item, ?string $type = null): bool + { + return $item instanceof directive && ($type === null || $item->type === $type); + } +} + +function directive($type, $data, $options = null): directive +{ + return new directive($type, $data, $options); +} + +function isDirective(mixed $item, ?string $type = null): bool +{ + return directive::is($item, $type); +} diff --git a/lib/zin/core/dom.class.php b/lib/zin/core/dom.class.php new file mode 100644 index 0000000000..8218c53135 --- /dev/null +++ b/lib/zin/core/dom.class.php @@ -0,0 +1,395 @@ + + * @package zin + * @version $Id + * @link https://www.zentao.net + */ + +namespace zin; + +use stdClass; + +require_once dirname(__DIR__) . DS . 'utils' . DS . 'deep.func.php'; +require_once __DIR__ . DS . 'selector.func.php'; + +class dom +{ + /** + * @var wg + */ + public $wg; + + public $children = array(); + + public $selectors = null; + + public $renderInner = false; + + public $renderType; + + public $dataGetters = null; + + public $dataCommands; + + public $buildList = null; + + public $buildListInner = false; + + /** + * Construct the dom object. + * + * @param wg $wg + * @param array $children + * @param array|string|object $selectors + * @access public + */ + public function __construct($wg, $children, $selectors = null, $renderType = null, $dataCommands = null) + { + $this->wg = $wg; + $this->renderType = $renderType; + + $this->add($children); + $this->addSelectors($selectors); + $this->addDataCommands($dataCommands); + } + + public function __debugInfo() + { + return array( + 'gid' => $this->wg->gid, + 'type' => $this->wg->type(), + 'count' => count($this->children), + 'renderInner' => $this->renderInner, + 'renderType' => $this->renderType, + 'dataCommands' => $this->dataCommands, + 'selectors' => stringifyWgSelectors($this->selectors) + ); + } + + public function add($children) + { + if(empty($children)) return; + + if(!is_array($children)) $children = [$children]; + foreach($children as $child) + { + if(is_array($child)) $this->add($child); + else $this->children[] = $child; + } + } + + public function addDataCommands($commands) + { + if(empty($commands)) return; + + if(is_string($commands)) + { + $commandList = explode(',', $commands); + $commands = array(); + foreach($commandList as $command) + { + $parts = explode(':', $command, 2); + $commands[$parts[0]] = count($parts) > 1 ? $parts[1] : $parts[0]; + } + } + + if($this->dataCommands === null) $this->dataCommands = array(); + $index = 0; + foreach($commands as $key => $command) + { + $this->dataCommands[$index == $key ? $command : $key] = $command; + $index++; + } + } + + public function addSelectors($selectors) + { + if(empty($selectors)) return; + + if($this->selectors === null) $this->selectors = array(); + $selectors = parseWgSelectors($selectors); + foreach($selectors as $selector) + { + if(isset($selector->command) && !empty($selector->command)) $this->addDataCommands([$selector->tag => $selector->command]); + else $this->selectors[] = $selector; + } + } + + public function isMatch($selector) + { + return $this->wg->isMatch($selector); + } + + /** + * Build the children dom list. + * @access public + * @return array + */ + public function build() + { + if($this->buildList !== null && $this->buildListInner === $this->renderInner) return $this->buildList; + + if(empty($this->selectors) && !empty($this->dataCommands)) + { + $this->buildList = array(); + return $this->buildList; + } + + $list = array(); + $children = $this->renderInner ? $this->wg->children() : $this->children; + + if(empty($children)) return $list; + + foreach($children as $child) $list[] = ($child instanceof wg) ? $child->buildDom() : $child; + + if(!empty($this->selectors)) $list = static::filter($list, $this->selectors); + + $this->buildList = $list; + $this->buildListInner = $this->renderInner; + return $list; + } + + public function render() + { + if($this->renderType === 'json') return $this->renderJson(); + if($this->renderType === 'list') return $this->renderList(); + return $this->renderHtml(); + } + + /** + * Render dom to json object. + * + * @access public + * @return object + */ + public function renderJson(): object + { + $list = $this->build(); + $output = new stdClass(); + foreach($list as $name => $item) + { + $output->$name = static::renderItemToJson($item); + } + + if(!empty($this->dataCommands)) + { + $data = array(); + foreach($this->dataCommands as $name => $command) + { + $data[$name] = data($command); + } + $output->data = $data; + } + + return $output; + } + + /** + * Render dom to html string. + * + * @access public + * @return string + */ + public function renderHtml(): string + { + $list = $this->build(); + if(empty($list)) return ''; + + $output = array(); + foreach($list as $item) + { + $result = static::renderItemToHtml($item); + if(!is_string($result)) $result = json_encode($result); + $output[] = $result; + } + return implode('', $output); + } + + /** + * Render dom to list by given selector. + * + * @access public + * @return array + */ + public function renderList(): array + { + $list = $this->build(); + $output = array(); + foreach($list as $name => $item) + { + if(is_array($item) && count($item) === 1) $item = $item[0]; + $renderType = $item instanceof dom ? $item->renderType : 'html'; + if(empty($renderType)) $renderType = 'html'; + $output[] = array('name' => $name, 'data' => static::renderDomItem($item, $renderType), 'type' => $renderType); + } + + if(!empty($this->dataCommands)) + { + foreach($this->dataCommands as $name => $command) + { + $output[] = array('name' => $name, 'data' => data($command), 'type' => 'command'); + } + } + + return $output; + } + + public static function renderDomItem($item, $defaultType = 'html') + { + if($item instanceof dom) + { + $renderType = $item->renderType; + if(empty($renderType)) $renderType = $defaultType; + if($renderType === 'json') return dom::renderItemToJson($item); + return dom::renderItemToHtml($item->build()); + } + + $renderType = $defaultType; + if($renderType === 'json') return static::renderItemToJson($item); + return static::renderItemToHtml($item); + } + + public static function renderItemToJson($item) + { + if($item === null || is_bool($item)) return null; + + if(is_array($item)) + { + $output = array(); + foreach($item as $subItem) $output[] = static::renderItemToJson($subItem); + return $output; + } + + if($item instanceof dom) + { + $json = $item->wg->toJsonData(); + if(!empty($item->dataGetters)) + { + $output = array(); + $props = explode(',', $item->dataGetters); + foreach($props as $prop) + { + $prop = trim($prop); + if(empty($prop)) continue; + + $parts = explode(':', $prop, 2); + $name = $parts[0]; + $namePath = count($parts) > 1 ? $parts[1] : $parts[0]; + $output[$name] = \zin\utils\deepGet($json, $namePath); + } + return $output; + } + return $json; + } + if($item instanceof wg) return dom::renderDomItem($item, 'json'); + if(is_string($item)) return $item; + + if(is_object($item)) + { + if(isDirective($item, 'html')) return $item->data; + if(isDirective($item, 'text')) return htmlspecialchars($item->data); + if(isset($item->html)) return $item->html; + if(isset($item->text)) return htmlspecialchars($item->text); + if(method_exists($item, 'render')) return $item->render(); + } + + return strval($item); + } + + public static function renderItemToHtml($item) + { + if($item === null || is_bool($item)) return ''; + + if(is_array($item)) + { + $output = array(); + foreach($item as $subItem) $output[] = static::renderItemToHtml($subItem); + return implode('', $output); + } + + if($item instanceof dom) return dom::renderItemToHtml($item->build()); + if($item instanceof wg) return $item->render(); + if(is_string($item)) return $item; + + if(is_object($item)) + { + if(isDirective($item, 'html')) return $item->data; + if(isDirective($item, 'text')) return htmlspecialchars($item->data); + if(isset($item->html)) return $item->html; + if(isset($item->text)) return htmlspecialchars($item->text); + if(method_exists($item, 'render')) return $item->render(); + } + + return strval($item); + } + + /** + * Filter the dom list with selector. + * + * @param array $list + * @param object $selector + * @param array $filteredList + * @access public + * @return array + */ + public static function filterList(&$list, $selector, &$filteredList) + { + if(empty($list) || empty($selector)) return []; + + $results = array(); + foreach($list as $item) + { + if(!($item instanceof dom) || in_array($item->wg->gid, $filteredList)) continue; + + if($item->wg->isMatch($selector)) + { + $item->selector = $selector; + $item->renderInner = isset($selector->inner) ? $selector->inner : false; + $item->renderType = isset($selector->type) ? $selector->type : null; + $item->dataGetters = isset($selector->data) ? $selector->data : null; + + $filteredList[] = $item->wg->gid; + $results[] = $item; + } + else + { + $children = $item->build(); + if(!empty($children)) + { + $subResults = static::filterList($children, $selector, $filteredList); + foreach($subResults as $subItem) $results[] = $subItem; + } + } + if($selector->first && !empty($results)) break; + } + return $results; + } + + /** + * Filter the dom list with selectors. + * + * @param array $domList + * @param array $selectors + * @access public + * @return array + */ + public static function filter(&$domList, $selectors) + { + if(empty($selectors)) return $domList; + + $list = array(); + $filteredList = array(); + foreach($selectors as $selector) + { + $results = static::filterList($domList, $selector, $filteredList); + $list[$selector->name] = $results; + } + + return $list; + } +} diff --git a/lib/zin/core/h.class.php b/lib/zin/core/h.class.php new file mode 100644 index 0000000000..44f440d755 --- /dev/null +++ b/lib/zin/core/h.class.php @@ -0,0 +1,300 @@ + + * @package zin + * @version $Id + * @link https://www.zentao.net + */ + +namespace zin; + +require_once dirname(__DIR__) . DS . 'utils' . DS . 'flat.func.php'; +require_once __DIR__ . DS . 'wg.class.php'; +require_once __DIR__ . DS . 'wg.func.php'; + +class h extends wg +{ + protected static array $defineProps = array( + 'tagName: string', + 'selfClose?: bool' + ); + + public function getTagName(): string + { + return $this->props->get('tagName'); + } + + public function isDomElement(): bool + { + return true; + } + + public function isSelfClose() + { + $selfClose = $this->props->get('selfClose'); + if($selfClose !== null) return $selfClose; + + return in_array($this->getTagName(), static::$selfCloseTags); + } + + public function build(): array + { + if($this->isSelfClose()) return array($this->buildSelfCloseTag()); + + return array($this->buildTagBegin(), parent::build(), $this->buildTagEnd()); + } + + public function toJsonData(): array + { + $data = parent::toJsonData(); + $data['type'] = 'h:' . $this->getTagName(); + return $data; + } + + public function type(): string + { + return $this->getTagName(); + } + + public function shortType(): string + { + return $this->getTagName(); + } + + protected function getPropsStr(): string + { + $propStr = $this->props->toStr(array_keys(static::definedPropsList())); + if($this->props->hasEvent() && empty($this->id()) && $this->getTagName() !== 'html') $propStr = "$propStr id='$this->gid'"; + return empty($propStr) ? '' : " $propStr"; + } + + protected function buildSelfCloseTag(): string + { + $tagName = $this->getTagName(); + $propStr = $this->getPropsStr(); + return "<$tagName$propStr />"; + } + + protected function buildTagBegin(): string + { + $tagName = $this->getTagName(); + $propStr = $this->getPropsStr(); + return "<$tagName$propStr>"; + } + + protected function buildTagEnd(): string + { + $tagName = $this->getTagName(); + return ""; + } + + public static function create(): h + { + $args = func_get_args(); + $tagName = array_shift($args); + return new h(is_string($tagName) ? set('tagName', $tagName) : $tagName, $args); + } + + public static function __callStatic(string $tagName, array $args): h + { + return new h(set('tagName', $tagName), $args); + } + + public static function a(): h + { + $a = static::create('a', func_get_args()); + if($a->prop('target') === '_blank' && !$a->hasProp('rel')) $a->prop('rel', 'noopener noreferrer'); + return $a; + } + + public static function button() + { + return static::create('button', set('type', 'button'), func_get_args()); + } + + public static function input() + { + return static::create('input', set('type', 'text'), func_get_args()); + } + + public static function formHidden($name, $value, ...$args) + { + return static::create('input', set('type', 'hidden'), set::name($name), set::value($value), $args); + } + + public static function checkbox() + { + return static::create('input', set('type', 'checkbox'), func_get_args()); + } + + public static function radio() + { + return static::create('input', set('type', 'radio'), func_get_args()); + } + + public static function date() + { + return static::create('input', set('type', 'date'), func_get_args()); + } + + public static function file() + { + return static::create('input', set('type', 'file'), func_get_args()); + } + + public static function textarea(...$args) + { + list($code, $args) = h::splitRawCode($args); + return static::create('textarea', $code, ...$args); + } + + /** + * create a html comment tag + * + * @access public + * @param string $comment + * @return directive + */ + public static function comment(string $comment): directive + { + return html(""); + } + + public static function importJs($src, ...$args) + { + return static::create('script', set('src', $src), ...$args); + } + + public static function importCss($src, ...$args) + { + return static::create('link', set('rel', 'stylesheet'), set('href', $src), ...$args); + } + + public static function import($file, $type = null, ...$args) + { + if(is_array($file)) + { + $children = array(); + foreach($file as $file) + { + $children[] = static::import($file, $type); + } + return $children; + } + if($type === null) $type = pathinfo($file, PATHINFO_EXTENSION); + if($type == 'js' || $type == 'cjs') return static::importJs($file, ...$args); + if($type == 'css') return static::importCss($file, ...$args); + return null; + } + + public static function css(...$args) + { + list($code, $args) = h::splitRawCode($args); + if(empty($code)) return null; + return static::create('style', html(implode("\n", $code)), ...$args); + } + + public static function globalJS(...$args) + { + list($code, $args) = h::splitRawCode($args); + if(empty($code)) return null; + return static::create('script', html(implode("\n", $code)), ...$args); + } + + public static function js(...$args) + { + + list($code, $args) = h::splitRawCode($args); + if(empty($code)) return null; + return static::create('script', html(h::createJsScopeCode($code)), ...$args); + } + + public static function jsVar($name, $value, ...$directives) + { + return static::js(static::createJsVarCode($name, $value), ...$directives); + } + + public static function jsCall($funcName, ...$args) + { + $funcArgs = []; + $directives = []; + foreach($args as $arg) + { + if(isDirective($arg)) $directives[] = $arg; + else $funcArgs[] = $arg; + } + $code = static::createJsCallCode($funcName, $funcArgs); + return static::js($code, ...$directives); + } + + public static function createJsCallCode($func, $args) + { + foreach($args as $index => $arg) + { + $args[$index] = h::encodeJsonWithRawJs($arg, JSON_UNESCAPED_UNICODE); + } + + if($func[0] === '~') + { + $func = substr($func, 1); + return "$(() => $func(" . implode(',', $args) . "));"; + } + return $func . '(' . implode(',', $args) . ');'; + } + + public static function createJsVarCode($name, $value) + { + $vars = is_string($name) ? array($name => $value) : $name; + $jsCode = ''; + foreach($vars as $var => $val) + { + if(empty($var)) continue; + + $val = h::encodeJsonWithRawJs($val); + + if(str_starts_with($var, 'window.')) $jsCode .= "$var=" . $val . ';'; + elseif(str_starts_with($var, '+')) $jsCode .= 'let ' . substr($var, 1) . '=' . $val . ';'; + else $jsCode .= "const $var=" . $val . ';'; + } + return $jsCode; + } + + public static function createJsScopeCode(string|array $codes): string + { + if(is_array($codes)) $codes = implode("\n", $codes); + return ";(function(){\n$codes\n}());"; + } + + public static function jsRaw(): string + { + return 'RAWJS<' . implode("\n", func_get_args()) . '>RAWJS'; + } + + protected static function encodeJsonWithRawJs($data) + { + $json = json_encode($data, JSON_UNESCAPED_UNICODE); + if(empty($json) && (is_array($data) || is_object($data))) return '[]'; + + $json = str_replace('"RAWJS<', '', str_replace('>RAWJS"', '', $json)); + return $json; + } + + protected static function splitRawCode($children) + { + $children = \zin\utils\flat($children); + $code = []; + $args = []; + foreach($children as $key => $child) + { + if(is_string($child)) $code[] = $child; + else $args[] = $child; + } + return [$code, $args]; + } + + public static $selfCloseTags = array('area', 'base', 'br', 'col', 'command', 'embed', 'hr', 'img', 'input', 'keygen', 'link', 'meta', 'param', 'source', 'track', 'wbr'); +} diff --git a/lib/zin/core/h.func.php b/lib/zin/core/h.func.php new file mode 100644 index 0000000000..ef751faf45 --- /dev/null +++ b/lib/zin/core/h.func.php @@ -0,0 +1,47 @@ + + * @package zin + * @version $Id + * @link https://www.zentao.net + */ + +namespace zin; + +require_once __DIR__ . DS . 'h.class.php'; +require_once __DIR__ . DS . 'item.class.php'; +require_once __DIR__ . DS . 'wg.func.php'; +require_once __DIR__ . DS . 'set.class.php'; +require_once __DIR__ . DS . 'to.class.php'; +require_once __DIR__ . DS . 'data.func.php'; +require_once __DIR__ . DS . 'on.class.php'; + +function h(): h {return call_user_func_array('\zin\h::create', func_get_args());} + +function div(): h {return call_user_func_array('\zin\h::div', func_get_args());} +function span(): h {return call_user_func_array('\zin\h::span', func_get_args());} +function code(): h {return call_user_func_array('\zin\h::code', func_get_args());} +function canvas(): h {return call_user_func_array('\zin\h::canvas', func_get_args());} +function br(): h {return call_user_func_array('\zin\h::br', func_get_args());} +function a(): h {return call_user_func_array('\zin\h::a', func_get_args());} +function p(): h {return call_user_func_array('\zin\h::p', func_get_args());} +function img(): h {return call_user_func_array('\zin\h::img', func_get_args());} +function button(): h {return call_user_func_array('\zin\h::button', func_get_args());} +function h1(): h {return call_user_func_array('\zin\h::h1', func_get_args());} +function h2(): h {return call_user_func_array('\zin\h::h2', func_get_args());} +function h3(): h {return call_user_func_array('\zin\h::h3', func_get_args());} +function h4(): h {return call_user_func_array('\zin\h::h4', func_get_args());} +function h5(): h {return call_user_func_array('\zin\h::h5', func_get_args());} +function h6(): h {return call_user_func_array('\zin\h::h6', func_get_args());} +function ul(): h {return call_user_func_array('\zin\h::ul', func_get_args());} +function li(): h {return call_user_func_array('\zin\h::li', func_get_args());} +function template(): h {return call_user_func_array('\zin\h::template', func_get_args());} +function formHidden(): h {return call_user_func_array('\zin\h::formHidden', func_get_args());} +function fieldset(): h {return call_user_func_array('\zin\h::fieldset', func_get_args());} +function legend(): h {return call_user_func_array('\zin\h::legend', func_get_args());} + +function jsRaw(): string {return call_user_func_array('\zin\h::jsRaw', func_get_args());} diff --git a/lib/zin/core/item.class.php b/lib/zin/core/item.class.php new file mode 100644 index 0000000000..a7319de451 --- /dev/null +++ b/lib/zin/core/item.class.php @@ -0,0 +1,33 @@ + + * @package zin + * @version $Id + * @link https://www.zentao.net + */ + +namespace zin; + +require_once __DIR__ . DS . 'wg.class.php'; +require_once __DIR__ . DS . 'wg.func.php'; + +class item extends wg +{ + public function build(): wg + { + if($this->parent instanceof wg && method_exists($this->parent, 'onBuildItem')) + { + return call_user_func(array($this->parent, 'onBuildItem'), $this); + } + return parent::build(); + } +} + +function item() +{ + return new item(func_get_args()); +} diff --git a/lib/zin/core/on.class.php b/lib/zin/core/on.class.php new file mode 100644 index 0000000000..b07a15770f --- /dev/null +++ b/lib/zin/core/on.class.php @@ -0,0 +1,24 @@ + + * @package zin + * @version $Id + * @link https://www.zentao.net + */ + +namespace zin; + +require_once __DIR__ . DS . 'wg.func.php'; + +class on +{ + public static function __callStatic($name, $args) + { + list($callback, $options) = array_merge($args, array(null)); + return on($name, $callback, $options); + } +} diff --git a/lib/zin/core/portal.class.php b/lib/zin/core/portal.class.php new file mode 100644 index 0000000000..11cefe2157 --- /dev/null +++ b/lib/zin/core/portal.class.php @@ -0,0 +1,27 @@ + + * @package zin + * @version $Id + * @link https://www.zentao.net + */ + +namespace zin; + +require_once __DIR__ . DS . 'wg.class.php'; + +class portal extends wg +{ + protected static array $defineProps = array( + 'target:string' + ); + + public static function __callStatic($name, $args) + { + return new portal(set('target', $name), $args); + } +} diff --git a/lib/zin/core/props.class.php b/lib/zin/core/props.class.php new file mode 100644 index 0000000000..bd8478601a --- /dev/null +++ b/lib/zin/core/props.class.php @@ -0,0 +1,284 @@ + + * @package zin + * @version $Id + * @link https://www.zentao.net + */ + +namespace zin; + +use zin\utils\classlist; +use zin\utils\style; + +require_once dirname(__DIR__) . DS . 'utils' . DS . 'dataset.class.php'; +require_once dirname(__DIR__) . DS . 'utils' . DS . 'classlist.class.php'; +require_once dirname(__DIR__) . DS . 'utils' . DS . 'style.class.php'; + +/** + * Manage properties for html element and widgets + */ +class props extends \zin\utils\dataset +{ + /** + * Style property + * + * @access public + * @var style + */ + public style $style; + + /** + * Class property + * + * @access public + * @var classlist + */ + public classlist $class; + + public static array $booleanAttrs = array('allowfullscreen', 'async', 'autofocus', 'autoplay', 'checked', 'controls', 'default', 'defer', 'disabled', 'formnovalidate', 'inert', 'ismap', 'itemscope', 'loop', 'multiple', 'muted', 'nomodule', 'novalidate', 'open', 'playsinline', 'readonly', 'required', 'reversed', 'selected'); + + /** + * Create properties instance + * + * @access public + * @param array $props - Properties list array + */ + public function __construct(?array $props = null) + { + $this->style = new \zin\utils\style(); + $this->class = new \zin\utils\classlist(); + + parent::__construct($props); + } + + /** + * Method for sub class to modify value on setting it + * + * @access public + * @param string $prop - Property name or properties list + * @param mixed $value - Property value + */ + protected function setVal(string $prop, mixed $value): props + { + if($prop === 'class' || $prop === '.') $this->class->set($value); + elseif($prop === 'style' || $prop === '~') $this->style->set($value); + elseif(str_starts_with($prop, '~')) $this->style->set(substr($prop, 1), $value); + elseif($prop === '--') $this->style->cssVar($value); + elseif(str_starts_with($prop, '--')) $this->style->cssVar(substr($prop, 2), $value); + elseif($prop === '!') $this->hx($value); + elseif(str_starts_with($prop, '!')) $this->hx(substr($prop, 1), $value); + elseif(str_starts_with($prop, ':')) $this->set('data-' . substr($prop, 1), $value); + elseif($prop === '@') $this->bindEvent($value); + elseif(str_starts_with($prop, '@')) $this->bindEvent(substr($prop, 1), $value); + else parent::setVal($prop, $value); + return $this; + } + + protected function getVal(string $prop): mixed + { + if($prop === 'class' || $prop === '.') + { + if(!$this->class->count()) return null; + return $this->class->toStr(); + } + if($prop === 'style' || $prop === '~') + { + if(!$this->style->count(true)) return null; + return $this->style->toStr(); + } + return parent::getVal($prop); + } + + /** + * @param string|string[] $name + * @param mixed $value + */ + public function reset(array|string $name, mixed $value = null) + { + if(is_array($name)) + { + foreach($name as $n) $this->reset($n); + return; + } + if($name === 'class') return $this->class->clear(); + if($name === 'style') return $this->style->clear(); + + $this->remove($name); + if($value) $this->setVal($name, $value); + } + + public function bindEvent($name, $callback = null) + { + if(is_array($name)) + { + foreach($name as $key => $value) $this->bindEvent($key, $value); + return; + } + + $events = parent::getVal("@$name") ?? []; + if(is_array($callback)) $events = array_merge($events, $callback); + else $events[] = $callback; + + parent::setVal("@$name", $events); + } + + public function events(): array + { + $events = array(); + foreach($this->data as $name => $value) + { + if(str_starts_with($name, '@')) $events[substr($name, 1)] = $value; + } + + return $events; + } + + public function hasEvent(): bool + { + foreach($this->data as $name => $value) + { + if(str_starts_with($name, '@')) return true; + } + + return false; + } + + public function hx(array|string $name, ?string $value = null) + { + if(is_array($name)) + { + foreach($name as $key => $val) $this->set("hx-$key", $val); + return; + } + + $this->set("hx-$name", $value); + } + + /** + * Convert props to html string + * + * Example: + * + * // Properties data map: + * $map = array( + * 'id' => 'sayHelloBtn', + * 'data-title' => 'Say "Hello"!', + * 'data-content' => null, + * 'data-show' => true, + * ); + * // Output string: id="sayHelloBtn" data-title="Say "Hello"!" data-show="true" + * + * @access public + */ + public function toStr(array|string $skipProps = array()): string + { + if(is_string($skipProps)) $skipProps = explode(',', $skipProps); + + $pairs = array(); + + if($this->class->count()) $pairs[] = 'class="' . $this->class->toStr() . '"'; + if($this->style->count(true)) $pairs[] = 'style="' . $this->style->toStr() . '"'; + + foreach($this->data as $name => $value) + { + /* Handle boolean attributes */ + if(in_array($name, static::$booleanAttrs)) $value = $value ? true : null; + + /* Skip any null value or events setting */ + if($value === null || in_array($name, $skipProps) || $name[0] === '@') continue; + + /* Convert non-string to json */ + if($value === true && !str_starts_with($name, 'data-')) + { + $pairs[] = $name; + } + else + { + if(!is_string($value)) $value = json_encode($value); + + $pairs[] = $name . '="' . htmlspecialchars($value) . '"'; + } + } + + return implode(' ', $pairs); + } + + public function toJsonData(bool $skipEvents = false): array + { + $data = $this->data; + if(!empty($this->style->data)) $data['style'] = $this->style->data; + if(!empty($this->class->list)) $data['class'] = $this->class->toStr(); + + if($skipEvents) + { + foreach($data as $name => $value) + { + if(str_starts_with($name, '@')) unset($data[$name]); + } + } + return $data; + } + + public function skip(array|string $skipProps = array(), bool $skipFalse = false): array + { + if(is_string($skipProps)) $skipProps = explode(',', $skipProps); + + $data = $this->toJsonData(); + foreach($data as $name => $value) + { + if($value === null || $name[0] === '@' || in_array($name, $skipProps)) unset($data[$name]); + if($skipFalse && $value === false) unset($data[$name]); + } + + return $data; + } + + public function split(array|string $firstListProps = array()): array + { + if(is_string($firstListProps)) $firstListProps = explode(',', $firstListProps); + + $data = $this->toJsonData(); + $firstList = array(); + $restList = array(); + foreach($data as $name => $value) + { + if($value === null || $name[0] === '@') continue; + if(in_array($name, $firstListProps)) $firstList[$name] = $value; + else $restList[$name] = $value; + } + + return array($firstList, $restList); + } + + public function pick(array|string $pickProps = array()): array + { + if(is_string($pickProps)) $pickProps = explode(',', $pickProps); + + $data = $this->toJsonData(); + foreach($data as $name => $value) + { + if($value === null || !in_array($name, $pickProps)) unset($data[$name]); + } + + return $data; + } + + /** + * Clone a new instance + * + * @access public + * @return props + */ + public function clone(): props + { + $props = new props($this->data); + $props->style = clone $this->style; + $props->class = clone $this->class; + return $props; + } +} diff --git a/lib/zin/core/rawcontent.class.php b/lib/zin/core/rawcontent.class.php new file mode 100644 index 0000000000..023a73f211 --- /dev/null +++ b/lib/zin/core/rawcontent.class.php @@ -0,0 +1,23 @@ + + * @package zin + * @version $Id + * @link https://www.zentao.net + */ + +namespace zin; + +require_once __DIR__ . DS . 'wg.class.php'; + +class rawContent extends wg +{ + protected function build(): directive + { + return h::comment('{{RAW_CONTENT}}'); + } +} diff --git a/lib/zin/core/render.func.php b/lib/zin/core/render.func.php new file mode 100644 index 0000000000..95499dfdbf --- /dev/null +++ b/lib/zin/core/render.func.php @@ -0,0 +1,60 @@ + + * @package zin + * @link https://www.zentao.net + */ + +namespace zin; + +require_once __DIR__ . DS . 'zin.class.php'; + +/** + * 将视图页面声明的所有内容通过一个部件进行渲染,并输出 HTML。 + * Render page content with a widget to HTML. + * + * @access public + * @param string $wgName + * @param array $options + * @return void + */ +function render(string $wgName = '', array $options = array()) +{ + /* 获取全局渲染部件实例和指令。 Get global render widgets and directives. */ + $globalItems = zin::getGlobalRenderList(); + + /* 决定部件名称,如果是 Ajax 请求则进行特殊处理。 Decide widget name, if is ajax request, then do special process. */ + if(empty($wgName)) + { + $wgName = 'page'; + if(isAjaxRequest('modal')) $wgName = 'modalDialog'; + else if(isAjaxRequest() && !isAjaxRequest('zin')) $wgName = 'fragment'; + } + + /* 判断是否渲染为完整页面。 Check if render in full page. */ + $isFullPage = str_starts_with($wgName, 'page'); + if($isFullPage) $globalItems[] = set::display(false); + + /* 获取部件渲染选项。 Get widget display options. */ + if(empty($options) && isset($_SERVER['HTTP_X_ZIN_OPTIONS']) && !empty($_SERVER['HTTP_X_ZIN_OPTIONS'])) + { + $setting = $_SERVER['HTTP_X_ZIN_OPTIONS']; + $options = $setting[0] === '{' ? json_decode($setting, true) : array('selector' => $setting); + } + + /* 创建部件实例。 Create widget instance. */ + $wg = createWg($wgName, $globalItems); + + /* 如果不是渲染一个完整页面,则使用 fragment 进行渲染。 If not render in full page, then render all items in a fragment. */ + if(!$isFullPage && $wgName !== 'fragment') $wg = fragment($wg); + + /* 渲染并输出 HTML。 Render and display html. */ + $wg->display($options); + + zin::$rendered = true; +} diff --git a/lib/zin/core/selector.func.php b/lib/zin/core/selector.func.php new file mode 100644 index 0000000000..71c8213b48 --- /dev/null +++ b/lib/zin/core/selector.func.php @@ -0,0 +1,165 @@ + + * @package zin + * @version $Id + * @link https://www.zentao.net + */ + +namespace zin; + +/** + * Parse wg selector + * @param string|object $selector + * @return object|null + */ +function parseWgSelector(string|object $selector): ?object +{ + if(is_object($selector)) return $selector; + + $selector = trim($selector); + $len = strlen($selector); + + if($len < 1) return null; + + $result = array( + 'class' => array(), + 'id' => null, + 'tag' => null, + 'inner' => false, + 'name' => null, + 'first' => false, + 'selector' => $selector + ); + if(str_contains($selector, '/')) + { + $parts = explode('/', $selector, 2); + $result['name'] = $parts[0]; + $selector = $parts[1]; + $len = strlen($selector); + } + $selector = str_replace('> *', '>*', $selector); + if(substr($selector, strlen($selector) - 2) == '>*') + { + $result['inner'] = true; + $selector = substr($selector, 0, strlen($selector) - 2); + $len = strlen($selector); + } + + $type = 'tag'; + $current = ''; + $updateResult = function(&$result, $current, $type) + { + if(empty($current)) return; + + if($type === 'class') + { + $result[$type][] = $current; + } + elseif($type === 'option') + { + $options = []; + parse_str($current, $options); + foreach($options as $key => $value) $result[$key] = empty($value) ? true : $value; + } + else + { + $result[$type] = $current; + } + }; + + for($i = 0; $i < $len; $i++) + { + $c = $selector[$i]; + $t = ''; + + if($c === '#' & $type !== 'option') + { + $t = 'id'; + } + elseif($c === '.' & $type !== 'option') + { + $t = 'class'; + } + elseif($c === '(' && $type !== 'option' && str_ends_with($selector, ')')) + { + $command = substr($selector, $i + 1, -1); + if(empty($command)) $command = $current; + $result['command'] = $command; + break; + } + elseif($c === ':') + { + $t = 'option'; + } + + if(empty($t)) + { + $current .= $c; + } + else + { + $updateResult($result, $current, $type); + $current = ''; + $type = $t; + } + } + $updateResult($result, $current, $type); + + if(empty($result['class'])) $result['class'] = null; + if(empty($result['name'])) + { + if(!empty($result['id'])) $result['name'] = $result['id']; + elseif(!empty($result['tag'])) $result['name'] = $result['tag']; + else $result['name'] = $selector; + } + + return (object)$result; +} + +/** + * Parse wg selectors. + * @param object|string|object[]|string[] $selectors + * @return object[] + */ +function parseWgSelectors(object|string|array $selectors): array +{ + if(is_object($selectors)) return array($selectors); + if(is_string($selectors)) $selectors = explode(',', trim($selectors)); + $results = array(); + foreach($selectors as $selector) + { + $selector = parseWgSelector($selector); + if(is_object($selector)) $results[] = $selector; + } + return $results; +} + +/** + * Stringify wg selectors. + * @param object|object[] $selector + * @return string + */ +function stringifyWgSelectors(array|object $selector): string +{ + if(empty($selector)) return ''; + if(is_array($selector)) + { + $result = []; + foreach($selector as $s) $result[] = stringifyWgSelectors($s); + return implode(',', $result); + } + + $result = ''; + if(!empty($selector->name) && $selector->name !== $selector->selector) $result .= $selector->name . '/'; + if(!empty($selector->tag)) $result .= $selector->tag; + if(!empty($selector->id)) $result .= '#' . $selector->id; + if(!empty($selector->class)) $result .= '.' . implode('.', $selector->class); + if(!empty($selector->first)) $result .= ':first'; + if($selector->inner) $result .= '>*'; + return $result; +} diff --git a/lib/zin/core/set.class.php b/lib/zin/core/set.class.php new file mode 100644 index 0000000000..80b0ff69c8 --- /dev/null +++ b/lib/zin/core/set.class.php @@ -0,0 +1,30 @@ + + * @package zin + * @version $Id + * @link https://www.zentao.net + */ + +namespace zin; + +require_once __DIR__ . DS . 'directive.class.php'; + +class set +{ + public static function __callStatic($prop, $args) + { + $value = array_shift($args); + if(is_object($value)) $value = (array)$value; + return directive('prop', array($prop => $value)); + } + + public static function class(...$args) + { + return directive('prop', array('class' => $args)); + } +} diff --git a/lib/zin/core/to.class.php b/lib/zin/core/to.class.php new file mode 100644 index 0000000000..f53c6b8446 --- /dev/null +++ b/lib/zin/core/to.class.php @@ -0,0 +1,23 @@ + + * @package zin + * @version $Id + * @link https://www.zentao.net + */ + +namespace zin; + +require_once __DIR__ . DS . 'wg.func.php'; + +class to +{ + public static function __callStatic($name, $args) + { + return to($name, $args); + } +} diff --git a/lib/zin/core/wg.class.php b/lib/zin/core/wg.class.php new file mode 100644 index 0000000000..d8e814c3e1 --- /dev/null +++ b/lib/zin/core/wg.class.php @@ -0,0 +1,752 @@ + + * @package zin + * @version $Id + * @link https://www.zentao.net + */ + +namespace zin; + +require_once __DIR__ . DS . 'props.class.php'; +require_once __DIR__ . DS . 'directive.class.php'; +require_once __DIR__ . DS . 'zin.class.php'; +require_once __DIR__ . DS . 'context.class.php'; +require_once __DIR__ . DS . 'selector.func.php'; +require_once __DIR__ . DS . 'dom.class.php'; + +class wg +{ + /** + * Define props for the element + * + * @var array + */ + protected static array $defineProps = array(); + + protected static array $defaultProps = array(); + + protected static array $defineBlocks = array(); + + protected static array $wgToBlockMap = array(); + + protected static array $definedPropsMap = array(); + + private static array $pageResources = array(); + + /** + * The props of the element + * + * @access public + * @var props + */ + public props $props; + + public array $blocks = array(); + + public ?wg $parent = null; + + public string $gid; + + public bool $displayed = false; + + protected array $renderOptions = array(); + + public function __construct(/* string|element|object|array|null ...$args */) + { + $this->props = new props(); + + $this->gid = 'zin_' . uniqid(); + $this->setDefaultProps(static::getDefaultProps()); + $this->add(func_get_args()); + $this->created(); + + zin::renderInGlobal($this); + static::checkPageResources(); + + $this->checkErrors(); + } + + public function __debugInfo(): array + { + return $this->toJsonData(); + } + + public function isDomElement(): bool + { + return false; + } + + /** + * Check if the element is match any of the selectors + * @param string|array|object $selectors + */ + public function isMatch(string|array|object $selectors): bool + { + $list = parseWgSelectors($selectors); + foreach($list as $selector) + { + if(isset($selector->command)) continue; + if(!empty($selector->id) && $this->id() !== $selector->id) continue; + if(!empty($selector->tag) && $this->shortType() !== $selector->tag) continue; + if(!empty($selector->class) && !$this->props->class->has($selector->class)) continue; + return true; + } + return false; + } + + /** + * Build dom object + * @return dom + */ + public function buildDom(): dom + { + $before = $this->buildBefore(); + $children = $this->build(); + $after = $this->buildAfter(); + $options = $this->renderOptions; + $selectors = (!empty($options) && isset($options['selector'])) ? $options['selector'] : null; + + return new dom + ( + $this, + [$before, $children, $after], + $selectors, + (!empty($options) && isset($options['type'])) ? $options['type'] : 'html', // TODO: () may not work in lower php + (!empty($options) && isset($options['data'])) ? $options['data'] : null, + ); + } + + /** + * Render widget to html + * @return string + */ + public function render(): string + { + $dom = $this->buildDom(); + $result = $dom->render(); + + return is_string($result) ? $result : json_encode($result); + } + + public function display(array $options = array()): wg + { + zin::disableGlobalRender(); + $this->renderOptions = $options; + + $dom = $this->buildDom(); + $result = $dom->render(); + $context = context::current(); + $css = $context->getCSS(); + $js = $context->getJS(); + + global $app, $config; + $zinDebug = null; + if($config->debug && (!isAjaxRequest() || isAjaxRequest('zin'))) + { + $zinDebug = data('zinDebug'); + if(is_array($zinDebug)) + { + $zinDebug['basePath'] = $app->getBasePath(); + if(isset($app->zinErrors)) $zinDebug['errors'] = $app->zinErrors; + } + } + + $rawContent = ob_get_contents(); + if(!is_string($rawContent)) $rawContent = ''; + ob_end_clean(); + + if(is_object($result)) + { + if($zinDebug && isset($result['zinDebug'])) $result['zinDebug'] = $zinDebug; + $result = json_encode($result); + } + elseif(is_array($result)) + { + foreach($result as $name => $item) + { + if(!isset($item['type']) || $item['type'] !== 'html') continue; + + $item['data'] = str_replace('/*{{ZIN_PAGE_CSS}}*/', $css, $item['data']); + $item['data'] = str_replace('/*{{ZIN_PAGE_JS}}*/', $js, $item['data']); + $item['data'] = str_replace('', $rawContent, $item['data']); + + $result[$name]['data'] = $item['data']; + } + if($zinDebug && isset($result['zinDebug'])) $result['zinDebug'] = $zinDebug; + $result = json_encode($result); + } + else + { + if($zinDebug) $js .= h::createJsVarCode('window.zinDebug', $zinDebug); + $result = str_replace('/*{{ZIN_PAGE_CSS}}*/', $css, $result); + $result = str_replace('/*{{ZIN_PAGE_JS}}*/', $js, $result); + $result = str_replace('', $rawContent, $result); + } + + ob_start(); + echo $result; + + $this->displayed = true; + context::destroy(); + return $this; + } + + protected function created() {} + + protected function buildBefore(): array + { + return $this->block('before'); + } + + protected function buildAfter(): array + { + return $this->block('after'); + } + + protected function build(): array|wg|directive + { + return $this->children(); + } + + public function buildEvents(): ?string + { + $events = $this->props->events(); + if(empty($events)) return null; + + $id = $this->id(); + $code = array($this->shortType() === 'html' ? 'const ele = document;' : 'const ele = document.getElementById("' . (empty($id) ? $this->gid : $id) . '");if(!ele)return;const $ele = $(ele); const events = new Set(($ele.attr("data-zin-events") || "").split(" ").filter(Boolean));'); + foreach($events as $event => $bindingList) + { + $code[] = "\$ele.on('$event.on.zin', function(e){"; + foreach($bindingList as $binding) + { + if(is_string($binding)) $binding = (object)array('handler' => $binding); + $selector = isset($binding->selector) ? $binding->selector : null; + $handler = isset($binding->handler) ? trim($binding->handler) : ''; + $stop = isset($binding->stop) ? $binding->stop : null; + $prevent = isset($binding->prevent) ? $binding->prevent : null; + $self = isset($binding->self) ? $binding->self : null; + + $code[] = '(function(){'; + if($selector) $code[] = "const target = e.target.closest('$selector');if(!target) return;"; + else $code[] = "const target = ele;"; + if($self) $code[] = "if(ele !== e.target) return;"; + if($stop) $code[] = "e.stopPropagation();"; + if($prevent) $code[] = "e.preventDefault();"; + + if(preg_match('/^[$A-Z_][0-9A-Z_$\[\]."\']*$/i', $handler)) $code[] = "($handler).call(target,e);"; + else $code[] = $handler; + + $code[] = '})();'; + } + $code[] = "});events.add('$event');"; + } + $code[] = '$ele.attr("data-zin-events", Array.from(events).join(" "));'; + return h::createJsScopeCode($code); + } + + + protected function onAddBlock(array|string|wg|directive $child, string $name) + { + return $child; + } + + protected function onAddChild(array|string|wg|directive $child) + { + return $child; + } + + protected function onSetProp(array|string $prop, mixed $value) + { + if($prop === 'id' && $value === '$GID') $value = $this->gid; + if($prop[0] === '@') + { + $this->setDefaultProps(array('id' => $this->gid)); + context::current()->addWgWithEvents($this); + } + $this->props->set($prop, $value); + } + + protected function onGetProp(string $prop, mixed $defaultValue): mixed + { + return $this->props->get($prop, $defaultValue); + } + + public function add($item, string $blockName = 'children') + { + if($item === null || is_bool($item)) return $this; + + if(is_array($item)) + { + foreach($item as $child) $this->add($child, $blockName); + return $this; + } + + zin::disableGlobalRender(); + + if($item instanceof wg) $this->addToBlock($blockName, $item); + elseif(is_string($item)) $this->addToBlock($blockName, htmlentities($item)); + elseif(isDirective($item)) $this->directive($item, $blockName); + else $this->addToBlock($blockName, htmlentities(strval($item))); + + zin::enableGlobalRender(); + + return $this; + } + + public function addToBlock(array|string $name, array|string|null|wg|directive $child = null) + { + if(is_array($name)) + { + foreach($name as $blockName => $blockChildren) + { + $this->addToBlock($blockName, $blockChildren); + } + return; + } + if(is_array($child)) + { + foreach($child as $blockChild) + { + $this->addToBlock($name, $blockChild); + } + return; + } + + if($child instanceof wg && empty($child->parent)) $child->parent = &$this; + + if($name === 'children' && $child instanceof wg) + { + $blockName = static::getBlockNameForWg($child); + if($blockName !== null) $name = $blockName; + } + + $result = $name === 'children' ? $this->onAddChild($child) : $this->onAddBlock($child, $name); + + if($result === false) return; + if($result !== null && $result !== true) $child = $result; + + if(isset($this->blocks[$name])) $this->blocks[$name][] = $child; + else $this->blocks[$name] = array($child); + } + + public function children(): array + { + return $this->block('children'); + } + + public function block(string $name): array + { + $list = array(); + if(isset($this->blocks[$name])) + { + $blocks = $this->blocks[$name]; + foreach($blocks as $block) + { + $isWg = $block instanceof wg && $block->shortType() === 'wg'; + $block = $isWg ? $block->children() : $block; + if(is_array($block)) $list = array_merge($list, $block); + else $list[] = $block; + } + } + return $list; + } + + public function hasBlock(string $name): bool + { + return isset($this->blocks[$name]); + } + + /** + * Apply directive + */ + public function directive(directive &$directive, array|string $blockName) + { + $data = $directive->data; + $type = $directive->type; + $directive->parent = &$this; + + if($type === 'prop') + { + $this->setProp($data); + return; + } + if($type === 'class' || $type === 'style') + { + $this->setProp($type, $data); + return; + } + if($type === 'cssVar') + { + $this->setProp('--', $data); + return; + } + if($type === 'html') + { + $this->addToBlock($blockName, $directive); + return; + } + if($type === 'text') + { + $this->addToBlock($blockName, htmlspecialchars($data)); + return; + } + if($type === 'block') + { + foreach($data as $blockName => $blockChildren) + { + $this->add($blockChildren, $blockName); + } + } + } + + public function prop(array|string $name, mixed $defaultValue = null): mixed + { + if(is_array($name)) + { + $values = array(); + foreach($name as $index => $propName) + { + $values[] = $this->onGetProp($propName, is_array($defaultValue) ? (isset($defaultValue[$propName]) ? $defaultValue[$propName] : $defaultValue[$index]) : $defaultValue); + } + return $values; + } + + return $this->onGetProp($name, $defaultValue); + } + + /** + * Set property, an array can be passed to set multiple properties + * + * @access public + * @param props|array|string $prop - Property name or properties list + * @param mixed $value - Property value + */ + public function setProp(props|array|string $prop, mixed $value = null) + { + if($prop instanceof props) $prop = $prop->toJsonData(); + + if(is_array($prop)) + { + foreach($prop as $name => $value) $this->setProp($name, $value); + return $this; + } + + if(!is_string($prop) || empty($prop)) return $this; + + if($prop[0] === '#') + { + $this->add($value, substr($prop, 1)); + return $this; + } + + $this->onSetProp($prop, $value); + return $this; + } + + public function hasProp(): bool + { + $names = func_get_args(); + if(empty($names)) return false; + foreach($names as $name) + { + if(!$this->props->has($name)) return false; + } + return true; + } + + public function setDefaultProps(array $props) + { + if(!is_array($props) || empty($props)) return; + + foreach($props as $name => $value) + { + if($this->props->has($name)) continue; + $this->setProp($name, $value); + } + } + + public function getRestProps(): array + { + return $this->props->skip(array_keys(static::definedPropsList())); + } + + public function getDefinedProps(): array + { + return $this->props->pick(array_keys(static::definedPropsList())); + } + + public function type(): string + { + return get_called_class(); + } + + public function shortType(): string + { + $type = $this->type(); + $pos = strrpos($type, '\\'); + return $pos === false ? $type : substr($type, $pos + 1); + } + + public function id(): ?string + { + return $this->prop('id'); + } + + public function toJsonData(): array + { + $data = array(); + $data['gid'] = $this->gid; + $data['props'] = $this->props->toJsonData(); + + $data['type'] = $this->type(); + if(str_starts_with($data['type'], 'zin\\')) $data['type'] = substr($data['type'], 4); + + $data['blocks'] = array(); + foreach($this->blocks as $key => $value) + { + foreach($value as $index => $child) + { + if($child instanceof wg || (is_object($child) && method_exists($child, 'toJsonData'))) + { + $value[$index] = $child->toJsonData(); + } + elseif(isDirective($child, 'html')) + { + $value[$index] = $child->data; + } + } + if($key === 'children') + { + unset($data['blocks'][$key]); + $data['children'] = $value; + } + else + { + $data['blocks'][$key] = $value; + } + } + + if(empty($data['blocks'])) unset($data['blocks']); + + if(!empty($this->parent)) $data['parent'] = $this->parent->gid; + + return $data; + } + + /** + * Check errors in debug mode. + * + * @access protected + * @return void + */ + protected function checkErrors() + { + global $config; + if(!isset($config->debug) || !$config->debug) return; + + $definedProps = static::definedPropsList(); + foreach($definedProps as $name => $definition) + { + if($this->hasProp($name)) continue; + if(isset($definition['default']) && $definition['default'] !== null) continue; + if(isset($definition['optional']) && $definition['optional']) continue; + + trigger_error("[ZIN] The property \"$name: {$definition['type']}\" of widget \"{$this->type()}#$this->gid\" is required.", E_USER_ERROR); + } + + $wgErrors = $this->onCheckErrors(); + if(empty($wgErrors)) return; + + foreach($wgErrors as $error) + { + if(is_array($error)) trigger_error("[ZIN] $error[0]", count($error) > 1 ? $error[1] : E_USER_WARNING); + else trigger_error("[ZIN] $error", E_USER_ERROR); + } + } + + /** + * The lifecycle method for checking errors in debug mode. + * + * @access protected + * @return array|null + */ + protected function onCheckErrors(): array|null + { + return null; + } + + public static function getPageCSS(): string|false + { + return false; // No css + } + + public static function getPageJS(): string|false + { + return false; // No js + } + + protected static function checkPageResources() + { + $name = get_called_class(); + if(isset(static::$pageResources[$name])) return; + + static::$pageResources[$name] = true; + + $pageCSS = static::getPageCSS(); + $pageJS = static::getPageJS(); + + if(!empty($pageCSS)) context::css($pageCSS); + if(!empty($pageJS)) context::js($pageJS); + } + + public static function wgBlockMap(): array + { + $wgName = get_called_class(); + if(!isset(wg::$wgToBlockMap[$wgName])) + { + $wgBlockMap = array(); + if(!empty(static::$defineBlocks)) + { + foreach(static::$defineBlocks as $blockName => $setting) + { + if(!isset($setting['map'])) continue; + $map = $setting['map']; + if(is_string($map)) $map = explode(',', $map); + foreach($map as $name) $wgBlockMap[$name] = $blockName; + } + } + wg::$wgToBlockMap[$wgName] = $wgBlockMap; + } + return wg::$wgToBlockMap[$wgName]; + } + + public static function getBlockNameForWg(wg|string $wg): ?string + { + $wgType = ($wg instanceof wg) ? $wg->type() : $wg; + $wgBlockMap = static::wgBlockMap(); + if(str_starts_with($wgType, 'zin\\')) $wgType = substr($wgType, 4); + return isset($wgBlockMap[$wgType]) ? $wgBlockMap[$wgType] : null; + } + + protected static function definedPropsList(?string $wgName = null): array + { + if($wgName === null) $wgName = get_called_class(); + + if(!isset(wg::$definedPropsMap[$wgName]) && $wgName === get_called_class()) + { + wg::$definedPropsMap[$wgName] = static::parsePropsDefinition(static::$defineProps); + } + return wg::$definedPropsMap[$wgName]; + } + + protected static function getDefaultProps(?string $wgName = null): array + { + $defaultProps = array(); + foreach(static::definedPropsList($wgName) as $name => $definition) + { + if(!isset($definition['default'])) continue; + $defaultProps[$name] = $definition['default']; + } + return $defaultProps; + } + + /** + * Parse props definition + * @param $definition + * @example + * + * $definition = array('name', 'desc:string', 'title?:string|element', 'icon?:string="star"'); + * $definition = array('name' => 'mixed', 'desc' => 'string', 'title' => array('type' => 'string|element', 'optional' => true), 'icon' => array('type' => 'string', 'default' => 'star', 'optional' => true)))) + */ + private static function parsePropsDefinition(array $definition): array + { + $parentClass = get_parent_class(get_called_class()); + /** + * @var array + */ + $props = $parentClass ? call_user_func("$parentClass::definedPropsList") : array(); + + if($parentClass !== false && $definition === $parentClass::$defineProps) + { + if(!empty(static::$defaultProps) && static::$defaultProps !== $parentClass::$defaultProps) + { + foreach($props as $name => $value) + { + if(isset(static::$defaultProps[$name])) + { + $value['default'] = static::$defaultProps[$name]; + $props[$name] = $value; + } + } + } + return $props; + } + + foreach($definition as $name => $value) + { + $optional = false; + $type = 'mixed'; + $default = (isset($props[$name]) && isset($props[$name]['default'])) ? $props[$name]['default'] : null; + + if(is_int($name) && is_string($value)) + { + $value = trim($value); + if(!str_contains($value, ':')) + { + $name = $value; + $value = ''; + } + else + { + list($name, $value) = explode(':', $value, 2); + } + $name = trim($name); + if($name[strlen($name) - 1] === '?') + { + $name = substr($name, 0, strlen($name) - 1); + $optional = true; + } + } + + if(is_array($value)) + { + $type = isset($value['type']) ? $value['type'] : $type; + $default = isset($value['default']) ? $value['default'] : $default; + $optional = isset($value['optional'])? $value['optional']: $optional; + } + else if(is_string($value)) + { + if(!str_contains($value, '=')) + { + $type = $value; + $default = null; + } + else + { + list($type, $default) = explode('=', $value, 2); + } + $type = trim($type); + + if(is_string($default)) $default = json_decode(trim($default)); + } + + $props[$name] = array('type' => empty($type) ? 'mixed' : $type, 'default' => $default, 'optional' => $default !== null || $optional); + } + + if(static::$defaultProps && (!$parentClass || static::$defaultProps !== $parentClass::$defaultProps)) + { + foreach(static::$defaultProps as $name => $value) + { + if(!isset($props[$name])) continue; + $props[$name]['default'] = $value; + } + } + return $props; + } +} diff --git a/lib/zin/core/wg.func.php b/lib/zin/core/wg.func.php new file mode 100644 index 0000000000..e0c2f450cb --- /dev/null +++ b/lib/zin/core/wg.func.php @@ -0,0 +1,305 @@ + + * @package zin + * @link http://www.zentao.net + */ +namespace zin; + +require_once dirname(__DIR__) . DS . 'utils' . DS . 'flat.func.php'; +require_once __DIR__ . DS . 'props.class.php'; +require_once __DIR__ . DS . 'directive.class.php'; +require_once __DIR__ . DS . 'rawcontent.class.php'; +require_once __DIR__ . DS . 'wg.class.php'; +require_once __DIR__ . DS . 'context.func.php'; + +/** + * Create an new widget. + * + * @return wg + */ +function wg(): wg +{ + return new wg(func_get_args()); +} + +/** + * Set widget properties. + * + * @param string|array|props|null $name + * @param mixed $value + * @return directive|null + */ +function set(string|array|props|null $name, mixed $value = null): ?directive +{ + if($name === null) return null; + + $props = null; + if($name instanceof props) $props = $name; + else if(is_array($name)) $props = $name; + else if(is_object($name)) $props = (array)$name; + else if(is_string($name)) $props = array($name => $value); + return $props ? directive('prop', $props) : null; +} + +/** + * Set widget CSS class attribute. + * + * @param array|string|null ...$classList + * @return directive + */ +function setClass(/* array|string|null ...$classList */): directive +{ + return directive('class', func_get_args()); +} + +/** + * Set widget style attribute. + * + * @return directive + */ +function setStyle(array|string $name, ?string $value = null): directive +{ + return directive('style', is_array($name) ? $name : array($name => $value)); +} + +/** + * Set widget CSS variable. + * + * @return directive + */ +function setCssVar(array|string $name, ?string $value = null): directive +{ + return directive('cssVar', is_array($name) ? $name : array($name => $value)); +} + +/** + * Set widget ID attribute. + * + * @return ?directive + */ +function setID(?string $id = null): directive +{ + return set('id', $id); +} + +/** + * Set widget element tag name. + * + * @return directive + */ +function setTag(string $id): directive +{ + return set('tagName', $id); +} + +/** + * Set widget data-* attribute. + * + * @param string|array $name + * @param mixed $value + * @return directive + */ +function setData(string|array $name, mixed $value = null): directive +{ + $map = is_array($name) ? $name : array($name => $value); + $attrs = array(); + foreach($map as $key => $value) + { + $name = "data-$key"; + if(is_bool($value)) $attrs[$name] = $value ? 'true' : 'false'; + else if(is_array($value)) $attrs[$name] = json_encode($value); + else $attrs[$name] = $value; + } + return set($attrs); +} + +/** + * Add event listener to widget element. + * + * @param string $name + * @param bool|string|array $handler + * @param array $options + */ +function on(string $name, bool|string|array $handler, array|string|bool $options = null): directive +{ + if(is_string($options) && is_string($handler)) + { + $options = array('selector' => $handler, 'handler' => $options); + } + elseif(is_bool($options)) + { + $options = array('capture' => $options, 'handler' => $handler); + } + elseif(is_array($options)) + { + $options['handler'] = $handler; + } + else + { + $options = array('handler' => $handler); + } + if(str_contains($name, '__')) + { + list($name, $flags) = explode('__', $name); + if(str_contains($flags, 'capture')) $options['capture'] = true; + if(str_contains($flags, 'stop')) $options['stop'] = true; + if(str_contains($flags, 'prevent')) $options['prevent'] = true; + if(str_contains($flags, 'self')) $options['self'] = true; + } + return set("@$name", (object)$options); +} + +/** + * Create html content. + * + * @param string ...$lines + * @return directive + */ +function html(/* string ...$lines */): directive +{ + return directive('html', implode("\n", \zin\utils\flat(func_get_args()))); +} + +/** + * Create text content. + * + * @param string ...$lines + * @return directive + */ +function text(/* string ...$lines */): directive +{ + return directive('text', implode("\n", \zin\utils\flat(func_get_args()))); +} + +/** + * Create block content. + * + * @param string $name + * @param mixed ...$wgs + * @return directive + */ +function to(/* string $name, mixed ...$wgs */): directive +{ + $args = func_get_args(); + $name = array_shift($args); + $wg = new wg(count($args) > 1 ? $args : $args[0]); + return directive('block', array($name => $wg)); +} + +/** + * Create content for block "before". + * + * @param string $wgs + * @return directive + */ +function before(/* mixed ...$wgs */): directive +{ + return to('before', func_get_args()); +} + +/** + * Create content for block "after". + * + * @param string $wgs + * @return directive + */ +function after(): directive +{ + return to('after', func_get_args()); +} + +/** + * Create widget contents inherited from the given widget. + * + * @param wg|array $item + * @return array + */ +function inherit(wg|array $item): array +{ + if(!($item instanceof wg)) $item = new wg($item); + return array(set($item->props), directive('block', $item->blocks), $item->children()); +} + +/** + * Divorce widget from parent. + * + * @param wg|array $item + * @return array + */ +function divorce(wg|array $item): wg|array +{ + if($item instanceof wg) + { + $item->parent = null; + } + else if(is_array($item)) + { + foreach($item as $i) divorce($i); + } + return $item; +} + +/** + * Check if the given widget list has the given widget type. + * + * @param wg|array $items + * @param string $type + * @return bool + */ +function hasWgInList(wg|array $items, string $type): bool +{ + if(!is_array($items)) $items = array($items); + foreach($items as $item) + { + if($item instanceof wg && $item->type() == $type) return true; + } + return false; +} + +/** + * Group widgets by type. + * + * @param wg|array $items + * @param string $types + * @return array + */ +function groupWgInList(wg|array $items, string|array $types): array +{ + if(is_string($types)) $types = explode(',', $types); + $typesMap = array(); + $restList = array(); + + foreach($types as $type) $typesMap[$type] = array(); + + foreach($items as $item) + { + if(!($item instanceof wg)) continue; + + $type = $item->shortType(); + if(isset($typesMap[$type])) $typesMap[$type][] = $item; + else $restList[] = $item; + } + + $groups = array(); + foreach($types as $index => $type) $groups[] = $typesMap[$type]; + $groups[] = $restList; + return $groups; +} + +/** + * Create raw content placeholder. + * + * @return rawContent + */ +function rawContent(): rawContent +{ + zin::$rawContentCalled = true; + + return new rawContent(); +} diff --git a/lib/zin/core/zin.class.php b/lib/zin/core/zin.class.php new file mode 100644 index 0000000000..bfb653bfe7 --- /dev/null +++ b/lib/zin/core/zin.class.php @@ -0,0 +1,76 @@ + + * @package zin + * @version $Id + * @link https://www.zentao.net + */ + +namespace zin; + +require_once dirname(__DIR__) . DS . 'utils' . DS . 'deep.func.php'; + +class zin +{ + public static array $globalRenderList = array(); + + public static bool $enabledGlobalRender = true; + + public static array $data = array(); + + public static bool $rendered = false; + + public static bool $rawContentCalled = false; + + public static function getData(string $namePath, mixed $defaultValue = null): mixed + { + return \zin\utils\deepGet(static::$data, $namePath, $defaultValue); + } + + public static function setData(string $namePath, mixed $value) + { + \zin\utils\deepSet(static::$data, $namePath, $value); + } + + public static function enableGlobalRender() + { + static::$enabledGlobalRender = true; + } + + public static function disableGlobalRender() + { + static::$enabledGlobalRender = false; + } + + public static function renderInGlobal(): bool + { + if(!static::$enabledGlobalRender) return false; + + static::$globalRenderList = array_merge(static::$globalRenderList, func_get_args()); + return true; + } + + public static function getGlobalRenderList(bool $clear = true): array + { + $globalItems = array(); + + foreach(static::$globalRenderList as $item) + { + if(is_object($item)) + { + if((isset($item->parent) && $item->parent) || ($item instanceof wg && $item->shortType() === 'wg')) + continue; + } + $globalItems[] = $item; + } + + /* Clear globalRenderList. */ + if($clear) static::$globalRenderList = array(); + + return $globalItems; + } +} diff --git a/lib/zin/func.php b/lib/zin/func.php new file mode 100644 index 0000000000..7fe5866774 --- /dev/null +++ b/lib/zin/func.php @@ -0,0 +1,1691 @@ + + * @package zin + * @version $Id + * @link https://www.zentao.net + */ + +namespace zin; + +require_once __DIR__ . DS . 'core' . DS . 'h.func.php'; +require_once __DIR__ . DS . 'core' . DS . 'render.func.php'; +require_once __DIR__ . DS . 'zui' . DS . 'zui.func.php'; +require_once __DIR__ . DS . 'zentao' . DS . 'zentao.func.php'; +require_once __DIR__ . DS . 'zentao' . DS . 'bind.class.php'; + +/* Form */ + +/** + * Html input. + * + * string name + * ?string type='text' + * ?string id + * ?string class='form-control' + * ?string value + * ?bool required + * ?string placeholder + * ?bool autofocus + * ?bool autocomplete=false + * ?bool disabled + */ +function input(): input +{ + return createWg('input', func_get_args()); +} + +/** + * Html textarea. + * + * string name + * int rows + * int cols + * ?string id + * ?string class='form-control' + * ?bool required + * ?string placeholder + */ +function textarea(): textarea +{ + return createWg('textarea', func_get_args()); +} + +/** + * Radio widget. + * + * string id + * ?string text + * ?bool checked + * ?string name + * ?bool primary=true + * ?bool disabled + * ?string value + * ?string typeClass + * ?string rootClass + */ +function radio(): radio +{ + return createWg('radio', func_get_args()); +} + +/** + * Switcher widget. + * + * string id + * string value + * ?string text + * ?bool checked + * ?string name + * ?bool primary=true + * ?bool disabled + * ?string typeClass + * ?string rootClass + */ +function switcher(): switcher +{ + return createWg('switcher', func_get_args()); +} + +/** + * Checkbox widget. + * + * string id + * ?string text + * ?bool checked + * ?string name + * ?bool primary=true + * ?bool disabled + * ?string value + * ?string typeClass + * ?string rootClass + * ?string labelClass + */ +function checkbox(): checkbox +{ + return createWg('checkbox', func_get_args()); +} + +/** + * Checkbox group widget. + * + * array title + * array items + */ +function checkboxGroup(): checkboxGroup +{ + return createWg('checkboxGroup', func_get_args()); +} + +/** + * Base form widget. + * + * ?string id='$GID' + * ?string method='post' + * ?string url + * ?array actions + * ?string actionsClass + * ?string target + * ?string submitBtnText + * ?string cancelBtnText + */ +function formBase(): formBase +{ + return createWg('formBase', func_get_args()); +} + +/** + * Form widget. + * + * ?string id='$GID' + * ?string method='post' + * ?string url + * ?array actions + * ?string actionsClass + * ?string target + * ?string submitBtnText + * ?string cancelBtnText + * ?bool grid + * ?int labelWidth + */ +function form(): form +{ + return createWg('form', func_get_args()); +} + +/** + * Form panel widget. + * + * ?string method + * ?string url + * ?array actions + * ?string target + * ?array items + * ?bool grid + * ?int labelWidth + */ +function formPanel(): formPanel +{ + return createWg('formPanel', func_get_args()); +} + +/** + * Zentao form batch wg. + * + * ?string id='$GID' + * ?string method='post' + * ?string url + * ?array actions + * ?string actionsClass + * ?string target + * ?array items + * ?int minRows=1 + * ?int maxRows=100 + * ?array data + * ?string mode='add' + */ +function formBatch(): formBatch +{ + return createWg('formBatch', func_get_args()); +} + +/** + * Zentao form batch item wg. + * + * ?string name + * ?string|bool label + * ?string labelClass + * ?string labelProps + * ?bool|string required="auto" + * ?array|string control + * ?string width + * ?string|array value + * ?bool disabled + * ?array items + * ?string placeholder + * ?bool hidden=false + */ +function formBatchItem(): formBatchItem +{ + return createWg('formBatchItem', func_get_args()); +} + +/** + * Form panel widget. + * + * ?string method + * ?string url + * ?array actions + * ?string target + * ?array items + * ?bool grid + * ?int labelWidth + * ?int minRows=1 + * ?int maxRows=100 + * ?array data + * ?string mode='add' + */ +function formBatchPanel(): formBatchPanel +{ + return createWg('formBatchPanel', func_get_args()); +} + +/** + * Batch actions: add, delete. + */ +function batchActions(): batchActions +{ + return createWg('batchActions', func_get_args()); +} + +/** + * Control widget. + * Dynamically create html input. + * + * string type - it can be text, password, email, number, date, time, datetime, month, url, search, tel, color, picker, select, checkbox, radio, checkboxList, radioList, checkboxListInline, radioListInline, file, textarea. + * string name + * ?string id + * ?string value + * ?bool required + * ?string placeholder + * ?bool disabled + * ?string form + * ?array items + */ +function control(): control +{ + return createWg('control', func_get_args()); +} + +/** + * Html select. + * + * string name + * ?string id + * ?string class="form-control" + * ?string value + * ?bool required + * ?bool disabled + * ?bool multiple + * ?array items + * ?int size + */ +function select(): select +{ + return createWg('select', func_get_args()); +} + +/** + * Html label which use id form. + * + * ?string text + * ?bool required + * ?string for + */ +function formLabel(): formLabel +{ + return createWg('formLabel', func_get_args()); +} + +/** + * Form group widget. + * + * ?string name + * ?string labelClass + * ?string tip + * ?array tipProps + * ?string width + * ?bool strong + * ?bool disabled + * ?array items + * ?string placeholder + * bool|string|null required='auto' + * string|array|null tipClass + * string|bool|null label + * string|array|null value + * array|string|null control + */ +function formGroup(): formGroup +{ + return createWg('formGroup', func_get_args()); +} + +/** + * Form row widget. + * + * ?string width + * ?array items + * ?bool hidden + */ +function formRow(): formRow +{ + return createWg('formRow', func_get_args()); +} + +/** + * Form row group widget. + * + * ?string width + * ?array items + * ?bool hidden + */ +function formRowGroup(): formRowGroup +{ + return createWg('formRowGroup', func_get_args()); +} + +/** + * Html input with prefix or suffix. + * + * mixed prefix + * mixed suffix + * string|int prefixWidth + * string|int suffixWidth + */ +function inputControl(): inputControl +{ + return createWg('inputControl', func_get_args()); +} + +/** + * Input group widget. + * + * ?array items + * ?bool seg + */ +function inputGroup(): inputGroup +{ + return createWg('inputGroup', func_get_args()); +} + +/** + * Input group addon widget. + */ +function inputGroupAddon(): inputGroupAddon +{ + return createWg('inputGroupAddon', func_get_args()); +} + +/** + * Checkbox list widget. + * + * ?string name + * ?bool primary=true + * ?array items + * ?bool inline + * string|array|null value + */ +function checkList(): checkList +{ + return createWg('checkList', func_get_args()); +} + +/** + * Radio list widget. + * + * ?string name + * ?bool primary=true + * ?array items + * ?bool inline + * string|array|null value + */ +function radioList(): radioList +{ + return createWg('radioList', func_get_args()); +} + +/** + * Color picker widget which extends input. + * + * ?string heading + * ?string icon + * ?bool closeBtn=true + * ?string syncText + * ?string syncColor + * ?string syncBackground + * ?string syncBorder + */ +function colorPicker(): colorPicker +{ + return createWg('colorPicker', func_get_args()); +} + +/** + * Date picker widget which extends input. + * + * string name + * ?string id + * ?string class + * ?string value + * ?bool required + * ?string placeholder + * ?bool autofocus + * ?bool disabled + * ?bool autocomplete=false + */ +function datePicker(): datePicker +{ + return createWg('datePicker', func_get_args()); +} + +/** + * Datetime picker widget which extends input. + * + * string name + * ?string id + * ?string class + * ?string value + * ?bool required + * ?string placeholder + * ?bool autofocus + * ?bool disabled + * ?bool autocomplete=false + */ +function datetimePicker(): datetimePicker +{ + return createWg('datetimePicker', func_get_args()); +} + +/** + * Time picker widget which extends input. + * + * string name + * ?string id + * ?string class + * ?string value + * ?bool required + * ?string placeholder + * ?bool autofocus + * ?bool disabled + * ?bool autocomplete=false + */ +function timePicker(): timePicker +{ + return createWg('timePicker', func_get_args()); +} + +/** + * Html file input which extends input. + * + * string name + * ?string id + * ?string class + * ?string value + * ?bool required + * ?string placeholder + * ?bool autofocus + * ?bool disabled + * ?bool autocomplete=false + */ +function fileInput(): fileInput +{ + return createWg('fileInput', func_get_args()); +} + +/** + * Page form widget which extends page. + * + * ?array formPanel + * ?string title + * ?array bodyProps + * ?bool zui=true + * ?bool display=true + * array|string|null bodyClass + * string|array|null metas=array('', '', '', '') + * + * ====== blocks ====== + * head = array() + * header = array('map' => 'header') + * main = array('map' => 'main') + * footer = array() + * ==================== + */ +function pageForm(): pageForm +{ + return createWg('pageForm', func_get_args()); +} + +/** + * Icon widget. + * + * string name + * string|int|null size + */ +function icon(): icon +{ + return createWg('icon', func_get_args()); +} + +/** + * Button widget. + * + * ?string icon + * ?string text + * ?bool square + * ?bool disabled + * ?bool active + * ?string url + * ?string target + * ?string trailingIcon + * ?string hint + * ?string type + * ?string btnType + * string|int|null size + * string|bool|null caret + */ +function btn(): btn +{ + return createWg('btn', func_get_args()); +} + +/** + * Page base widget. + * + * ?string title + * ?array bodyProps + * ?bool zui=false + * ?bool display=true + * array|string|null bodyClass + * string|array|null metas=array('', '', '', '') + */ +function pageBase(): pageBase +{ + return createWg('pageBase', func_get_args()); +} + +/** + * Page widget. + * + * ?string title + * ?array bodyProps + * ?array|string bodyClass + * ?bool zui=true + * ?bool display=true + * string|array|null metas=array('', '', '', '') + * + * ====== blocks ====== + * head = array() + * header = array('map' => 'header') + * main = array('map' => 'main') + * footer = array() + * ==================== + */ +function page(): page +{ + return createWg('page', func_get_args()); +} + +/** + * Fragment widget. + * Let you group elements without a wrapper node. + */ +function fragment(): fragment +{ + return createWg('fragment', func_get_args()); +} + +/** + * Button group widget. + * + * ?array items + * ?bool disabled + * ?string size + */ +function btnGroup(): btnGroup +{ + return createWg('btnGroup', func_get_args()); +} + +/** + * Zentao main menu widget. + * + * ?array statuses + * ?array btnGroup + * ?array others + */ +function mainMenu(): mainMenu +{ + return createWg('mainMenu', func_get_args()); +} + +/** + * Row widget. + * + * ?string justify + * ?string align + */ +function row(): row +{ + return createWg('row', func_get_args()); +} + +/** + * Col widget. + * + * ?string justify + * ?string align + */ +function col(): col +{ + return createWg('col', func_get_args()); +} + +/** + * Center widget. + */ +function center(): center +{ + return createWg('center', func_get_args()); +} + +/** + * Cell widget. + * Flex item. + * + * int order + * int grow + * string shrink + * string|int width 'auto'|'flex-start'|'flex-end'|'center'|'baseline'|'stretch' + * string align + * string flex + */ +function cell(): cell +{ + return createWg('cell', func_get_args()); +} + +/** + * Action item widget. + * + * ?string name='action' + * ?string type='item' + * ?string outerTag='li' + * ?string tagName='a' + * ?string icon + * ?string text + * ?string url + * ?string target + * ?bool active + * ?bool disabled + * ?string trailingIcon + * ?array outerProps + * ?string outerClass + * ?props array + * string|array|object|null badge + */ +function actionItem(): actionItem +{ + return createWg('actionItem', func_get_args()); +} + +/** + * Nav widget. + * + * ?array items + */ +function nav(): nav +{ + return createWg('nav', func_get_args()); +} + +/** + * Label widget. + * + * ?string text + */ +function label(): label +{ + return createWg('label', func_get_args()); +} + +/** + * DTable widget. + * + * ?string className + * ?string id + * ?bool customCols + * ?array cols + * ?string module + */ +function dtable(): dtable +{ + return createWg('dtable', func_get_args()); +} + +/** + * Menu widget. + * + * ?array items + */ +function menu(): menu +{ + return createWg('menu', func_get_args()); +} + +/** + * Dropdown widget. + * + * ?array items + * ?string placement + * ?string strategy + * ?int offset + * ?bool flip + * ?string arrow + * ?string trigger + * ?array menu + * ?string target + * ?string id + * ?string menuClass + * ?bool hasIcons + * ?bool staticMenu + */ +function dropdown(): dropdown +{ + return createWg('dropdown', func_get_args()); +} + +/** + * Header widget. + * + * ====== blocks ====== + * heading = array('map' => 'toolbar') + * navbar = array('map' => 'nav') + * toolbar = array('map' => 'btn') + * ==================== + */ +function header(): header +{ + return createWg('header', func_get_args()); +} + +/** + * Heading widget. + * + * array items + * ?bool showAppName=true + */ +function heading(): heading +{ + return createWg('heading', func_get_args()); +} + +/** + * Navbar widget. + * + * ?array items + */ +function navbar(): navbar +{ + return createWg('navbar', func_get_args()); +} + +/** + * Dropmenu widget. + * + * ?string id // ID,当页面有多个 dropmenu 时确保有唯一的 ID。 + * ?string tab // 应用名。 + * ?string module // 模块名。 + * ?string method // 方法名。 + * ?string objectID // 当前选中项的 ID。 + * ?string extra // 额外参数。 + * ?string url // 异步获取下拉菜单选项数据的 URL。 + * ?string text // 选择按钮上显示的文本。 + * ?bool|int cache // 是否启用缓存。 + * ?array data // 手动指定数据。 + * ?string menuID // 指定下拉菜单的ID。 + */ +function dropmenu(): dropmenu +{ + return createWg('dropmenu', func_get_args()); +} + +/** + * Main widget. + * + * ====== blocks ====== + * menu = array('map' => 'featureBar,nav,toolbar') + * sidebar = array('map' => 'sidebar') + * ==================== + */ +function main(): main +{ + return createWg('main', func_get_args()); +} + +/** + * Zentao sidebar widget. + * + * ?string side='left' + * ?bool showToggle=true + */ +function sidebar(): sidebar +{ + return createWg('sidebar', func_get_args()); +} + +/** + * Zentao feature bar widget. + * + * ?array items + * ?string current + * ?string link + * ?string linkParams + * ?string module + * ?string method + * + * ====== blocks ====== + * nav = array('map' => 'nav') + * leading = array() + * trailing = array() + * ==================== + */ +function featureBar(): featureBar +{ + return createWg('featureBar', func_get_args()); +} + +/** + * Avatar widget. + * + * ?string className + * ?array style + * ?int size=32 + * ?bool circle=true + * ?string|int rounded + * ?string background + * ?string foreColor + * ?string text + * ?string code + * ?int maxTextLength=2 + * ?int hueDistance=43 + * ?int saturation=0.4 + * ?int lightness=0.6 + * ?string src + */ +function avatar(): avatar +{ + return createWg('avatar', func_get_args()); +} + +/** + * Zentao user avatar widget. + * + * string className? + * array style? + * int size=32 + * bool circle=true + * string|int rounded + * string background + * string foreColor + * string text + * string code? + * int maxTextLength=2 + * int hueDistance=43 + * int saturation=0.4 + * int lightness=0.6 + * string src? + * string avatar? + * string account? + * string realname? + * array|object user? + */ +function userAvatar(): userAvatar +{ + return createWg('userAvatar', func_get_args()); +} + +/** + * Pager widget. + */ +function pager(): pager +{ + return createWg('pager', func_get_args()); +} + +/** + * Modal widget. + * + * ?string id="$GID" + * ?array modalProps=array() + */ +function modal(): modal +{ + return createWg('modal', func_get_args()); +} + +/** + * Modal trigger widget. + * + * ?string target + * ?bool keyboard + * ?bool moveable + * ?bool animation + * ?int transTime + * ?bool responsive + * ?string type + * ?string loadingText + * ?int loadTimeout + * ?string failedTip + * ?string timeoutTip + * ?string title + * ?string content + * ?object custom + * ?string url + * ?object request + * ?string dataType + * bool|string|null backdrop + * string|int|object|null size + * string|int|object|function|null position + * + * ====== blocks ====== + * trigger = array('map' => 'btn,a') + * modal = array('map' => 'modal') + * ==================== + */ +function modalTrigger(): modalTrigger +{ + return createWg('modalTrigger', func_get_args()); +} + +/** + * Modal header widget. + * + * ?string title + * ?string entityText + * ?int entityID + */ +function modalHeader(): modalHeader +{ + return createWg('modalHeader', func_get_args()); +} + +/** + * Modal dialog widget. + * + * ?string title + * ?int itemID + * ?string headerClass + * ?array headerProps + * ?array actions + * ?array footerActions + * ?string footerClass + * ?array footerProps + * bool|array|null closeBtn=true + */ +function modalDialog(): modalDialog +{ + return createWg('modalDialog', func_get_args()); +} + +/** + * Tabs widget. + * + * ?string direction='h' + * ?bool collapse=false + */ +function tabs(): tabs +{ + return createWg('tabs', func_get_args()); +} + +/** + * Tab pane widget. + * + * ?bool active + * string key + * string title + * + * ====== blocks ====== + * prefix = array() + * suffix = array() + * ==================== + */ +function tabPane(): tabPane +{ + return createWg('tabPane', func_get_args()); +} + +/** + * Panel widget. + * + * ?string class='rounded shadow ring-0 canvas' + * ?string size + * ?string title + * ?string titleClass + * ?array titleProps + * ?string headingClass + * ?array headingProps + * ?array headingActions + * ?string bodyClass + * ?array bodyProps + * ?array footerActions + * ?string footerClass + * ?array footerProps + */ +function panel(): panel +{ + return createWg('panel', func_get_args()); +} + +function pasteDialog(): pasteDialog +{ + return createWg('pasteDialog', func_get_args()); +} + +/** + * Tooltip widget. + */ +function tooltip(): tooltip +{ + return createWg('tooltip', func_get_args()); +} + +/** + * Toolbar widget. + * + * ?array items + * ?string btnClass + * ?array btnProps + */ +function toolbar(): toolbar +{ + return createWg('toolbar', func_get_args()); +} + +/** + * Zentao search form widget. + */ +function searchForm(): searchForm +{ + return createWg('searchForm', func_get_args()); +} + +/** + * Zentao search toggle widget. + * + * ?bool open + */ +function searchToggle(): searchToggle +{ + return createWg('searchToggle', func_get_args()); +} + +/** + * Zentao program menu widget. + * + * ?array programs + * ?string activeClass + * ?string activeIcon + * ?string activeKey + * ?string closeLink + */ +function programMenu(): programMenu +{ + return createWg('programMenu', func_get_args()); +} + +/** + * Zentao product menu widget. + * + * ?string title + * ?array items + * ?string activeKey + * ?string link + */ +function productMenu(): productMenu +{ + return createWg('productMenu', func_get_args()); +} + +/** + * Zentao module menu widget. + * + * array modules + * int activeKey + * string closeLink + */ +function moduleMenu(): moduleMenu +{ + return createWg('moduleMenu', func_get_args()); +} + +/** + * Zentao doc module menu widget. + * + * array modules + * int activeKey + * string closeLink + */ +function docMenu(): docMenu +{ + return createWg('docMenu', func_get_args()); +} + +/** + * Zentao tree widget. + * + * ?array items + */ +function tree(): Tree +{ + return createWg('tree', func_get_args()); +} + +/** + * Zentao file list widget. + * + * ?array files + * ?int objectID + * ?string objectType + */ +function fileList(): fileList +{ + return createWg('fileList', func_get_args()); +} + +/** + * Zentao history records widget. + * + * ?array actions + * ?array users + * ?string methodName + * ?bool commentBtn + */ +function history(): history +{ + return createWg('history', func_get_args()); +} + +/** + * Zentao float toolbar widget. + * + * ?array prefix btns props array. + * ?array main btns props array. + * ?array suffix btns props array. + * ?object object + * + * ====== blocks ====== + * dropdowns = array() + * ==================== + */ +function floatToolbar(): floatToolbar +{ + return createWg('floatToolbar', func_get_args()); +} + +/** + * Zentao customize form item dropdown. + * + * ?string method + * ?string url + * ?array actions + * ?string target + * ?array items + * ?array value + */ +function formItemDropdown(): formItemDropdown +{ + return createWg('formItemDropdown', func_get_args()); +} + +/** + * Zentao editor wg. + * + * ?bool createInput=false + * ?string uploadUrl='' + * ?string placeholder='' + * ?bool fullscreenable=false + * ?bool resizable=false + * ?bool exposeEditor=true + * ?string size='sm' + * ?bool hideMenubar=false + * ?bool bubbleMenu=false + */ +function editor(): editor +{ + return createWg('editor', func_get_args()); +} + +/** + * Zentao comment button wg. + * + * ?string dataTarget + * ?string dataUrl + * ?string dataType + * ?string icon + * ?string text + * ?bool square + * ?bool disabled + * ?bool active + * ?string url + * ?string target + * ?string trailingIcon + * ?string hint + * ?string type + * ?string btnType + * string|int|null size + * string|bool|null caret + */ +function commentBtn(): commentBtn +{ + return createWg('commentBtn', func_get_args()); +} + +/** + * + * Zentao comment dialog wg. + * + * ?string title, + * ?string url + * ?string name='comment' + * ?string method='POST' + */ +function commentDialog(): commentDialog +{ + return createWg('commentDialog', func_get_args()); +} + +/** + * Zentao comment form wg. + * + * ?string url + * ?string name='comment' + * ?string method='POST' + */ +function commentForm(): commentForm +{ + return createWg('commentForm', func_get_args()); +} + +/** + * Zentao priority number wg. + * + * int pri + */ +function priLabel(): priLabel +{ + return createWg('priLabel', func_get_args()); +} + +/** + * Zentao risk label wg. + * + * ?string text + * ?string level 'h'|'m'|'l' + */ +function riskLabel(): riskLabel +{ + return createWg('riskLabel', func_get_args()); +} + +/** + * Zentao severity label wg. + * + * ?string text + * ?int|string level 1|2|3|4 + */ +function severityLabel(): severityLabel +{ + return createWg('severityLabel', func_get_args()); +} + +/** + * Zentao dashboard widget. + * + * ?bool responsive 是否启用响应式。 + * array blocks 区块列表。 + * ?int grid 栅格数。 + * ?int gap 间距。 + * ?int leftStop 区块水平停靠间隔。 + * ?int cellHeight 网格高度。 + * ?stringunction|array blockFetch 区块数据获取 url 或选项。 + * ?array blockDefaultSize 区块默认大小。 + * array blockSizeMap 区块大小映射。 + * ?array blockMenu 区块菜单。 + * ?function onLayoutChange 布局变更事件。 + */ +function dashboard(): dashboard +{ + return createWg('dashboard', func_get_args()); +} + +/** + * Dashboard panel widget. + */ +function blockPanel(): blockPanel +{ + return createWg('blockPanel', func_get_args()); +} + +/** + * Zentao detail page section widget. + * + * string title + * string|array|null content + * ?bool useHtml=false + * + * ====== blocks ====== + * subTitle => array() + * actions => array() + * ==================== + */ +function section(): section +{ + return createWg('section', func_get_args()); +} + +/** + * Zentao detail page section card widget. + * + * ====== blocks ====== + * title => array('map' => 'entityLabel') + * ==================== + */ +function sectionCard(): sectionCard +{ + return createWg('sectionCard', func_get_args()); +} + +/** + * Zentao detail page section list widget. + */ +function sectionList(): sectionList +{ + return createWg('sectionList', func_get_args()); +} + +/** + * Zentao entity label widget. + * + * string|int|null entityID + * string|int|null level + * string text + * + * ====== blocks ====== + * suffix = array() + * ==================== + */ +function entityLabel(): entityLabel +{ + return createWg('entityLabel',func_get_args()); +} + +/** + * Zentao table data widget. + */ +function tableData(): tableData +{ + return createWg('tableData',func_get_args()); +} + +/** + * Zentao detail page header widget. + * + * ====== blocks ====== + * prefix => array() + * title => array() + * suffix => array() + * ==================== + */ +function detailHeader(): detailHeader +{ + return createWg('detailHeader', func_get_args()); +} + +/** + * Zentao detail page side widget. + */ +function detailSide(): detailSide +{ + return createWg('detailSide', func_get_args()); +} + +/** + * Zentao detail page body widget. + */ +function detailBody(): detailBody +{ + return createWg('detailBody', func_get_args()); +} + +/** + * ECharts widget. + */ +function echarts(): echarts +{ + return createWg('echarts', func_get_args()); +} + +/** + * Popovers widget. + * + * ?string placement='bottom' + * ?string strategy='fixed' + * ?bool flip=true + * ?array|bool shift=array('padding' => 5) + * ?bool arrow=false + * ?int offset=1 + * + * ====== blocks ====== + * trigger = array() + * target = array() + * ==================== + */ +function popovers(): popovers +{ + return createWg('popovers', func_get_args()); +} + +/** + * Draggable ul widget. + */ +function dragUl(): dragUl +{ + return createWg('dragUl', func_get_args()); +} + +/** + * Back btn widget. + * + * ?string back + */ +function backBtn(): backBtn +{ + return createWg('backBtn', func_get_args()); +} + +/** + * Collapse btn widget. + * + * string target + * string parent + */ +function collapseBtn(): collapseBtn +{ + return createWg('collapseBtn', func_get_args()); +} + +/** + * Main navbar widget. + * + * ?array items + */ +function mainNavbar(): mainNavbar +{ + return createWg('mainNavbar', func_get_args()); +} + +/** + * Zentao float Pre and Next Button widget. + * + * ?string preLink link of pre-button. + * ?string nextLink link of next-button. + */ +function floatPreNextBtn(): floatPreNextBtn +{ + return createWg('floatPreNextBtn', func_get_args()); +} + +/** + * Upload widget. + * + * string name + * ?string icon + * ?bool showIcon=true + * ?bool showSize=true + * ?bool multiple=true + * ?string listPosition="bottom" + * ?string uploadText + * ?string uploadIcon + * ?bool renameBtn=true + * ?string renameIcon + * ?string renameText + * ?string renameClass + * ?bool deleteBtn=true + * ?string deleteText + * ?string deleteIcon + * ?string deleteClass + * ?string confirmText + * ?string cancelText + * ?bool useIconBtn=true + * ?string tip + * ?string btnClass + * ?callable onAdd + * ?callable onDelete + * ?callable onRename + * ?callable onSizeChange + * ?bool draggable + * ?int limitCount + * ?string accept + * ?object[] defaultFileList + * false|string|null limitSize=false + * ?string duplicatedHint + * ?string exceededSizeHint + * ?string exceededCountHint + */ +function upload(): upload +{ + return createWg('upload', func_get_args()); +} + +/** + * Upload images widget. + * + * string name + * ?bool showSize=true + * ?bool multiple=true + * ?string uploadText + * ?string uploadIcon + * ?bool renameBtn=true + * ?string renameIcon + * ?string renameText + * ?string renameClass + * ?bool deleteBtn=true + * ?string deleteIcon + * ?string deleteText + * ?string deleteClass + * ?string tip + * ?string btnClass + * ?callable onAdd + * ?callable onDelete + * ?callable onRename + * ?callable onSizeChange + * ?int limitCount + * ?string accept + * ?object[] defaultFileList + * false|string|null limitSize=false + * ?string duplicatedHint + * ?string exceededSizeHint + * ?string exceededCountHint + * ?string totalCountText + */ +function uploadImgs(): uploadImgs +{ + return createWg('uploadImgs', func_get_args()); +} + +/** + * Burn widget. + * + * ?string|array data + * ?bool referenceLine + */ +function burn(): burn +{ + return createWg('burn', func_get_args()); +} + +/** + * Monaco widget. + * + * string id + * ?string action + * ?string options + * ?string diffContent + * ?string onMouseDown + */ +function monaco(): monaco +{ + return createWg('monaco', func_get_args()); +} + +/** + * Dynamic widget. + * + * ?array dynamics + * ?array users + * ?string className + */ +function dynamic(): dynamic +{ + return createWg('dynamic', func_get_args()); +} + +/** + * Form setting button. + * + * ?array customFields + * ?string urlParams + */ +function formSettingBtn(): formSettingBtn +{ + return createWg('formSettingBtn', func_get_args()); +} + +/** + * Overview block widget. + * + * string id + * string title + */ +function overviewBlock(): overviewBlock +{ + return createWg('overviewBlock', func_get_args()); +} + +/** + * Picker widget. + */ +function picker(): picker +{ + return createWg('picker', func_get_args()); +} + +/** + * Pri picker widget. + * + * ?string placeholder + * ?string[]|object[]|object|array items + */ +function priPicker(): priPicker +{ + return createWg('priPicker', func_get_args()); +} + +/** + * Severity picker widget. + * + * ?string placeholder + * ?string[]|object[]|object|array items + */ +function severityPicker(): severityPicker +{ + return createWg('severityPicker', func_get_args()); +} + +function hr(): hr +{ + return createWg('hr', func_get_args()); +} + +/** + * Global search widget. + * + * string commonSearchText + * string commonSearchUrl + * array searchItems + */ +function globalSearch(): globalSearch +{ + return createWg('globalSearch', func_get_args()); +} + +/** + * Steps editor widget. + */ +function stepsEditor(): stepsEditor +{ + return createWg('stepsEditor', func_get_args()); +} + +/** + * table and chart widget. + */ +function tableChart(): tableChart +{ + return createWg('tableChart', func_get_args()); +} diff --git a/lib/zin/helper.php b/lib/zin/helper.php new file mode 100644 index 0000000000..2e9d749dcd --- /dev/null +++ b/lib/zin/helper.php @@ -0,0 +1,131 @@ + + * @package zin + * @version $Id + * @link https://www.zentao.net + */ + +namespace zin; + +require_once __DIR__ . DS . 'config.php'; + +function setWgVer($ver, $names = null) +{ + global $config; + $zinConfig = $config->zin; + + if(is_string($names)) $names = explode(',', $names); + if(!is_array($names)) return; + + foreach($names as $name) + { + $name = trim($name); + if(!empty($name)) continue; + + $zinConfig->wgVerMap[$name] = $ver; + } +} + +function getWgVer($name) +{ + global $config; + + return isset($config->zin->verMap[$name]) ? $config->zin->verMap[$name] : $config->zin->wgVer; +} + +function createWg($name, $args): wg +{ + $name = strtolower($name); + $wgVer = getWgVer($name); + + include_once __DIR__ . DS . 'wg' . DS . $name . DS . "v$wgVer.php"; + + $wgName = "\\zin\\$name"; + + return class_exists($wgName) ? (new $wgName($args)) : $wgName($args); +} + +if(!function_exists('str_contains')) +{ + /** + * Determine if a string contains a given substring + * + * @param string $haystack + * @param string $needle + * @return bool + */ + function str_contains($haystack, $needle) + { + return strpos($haystack, $needle) !== false; + } +} +else +{ + function str_contains($haystack, $needle) + { + return \str_contains($haystack, $needle); + } +} + +if(!function_exists('str_starts_with')) +{ + /** + * Checks if a string starts with a given substring + * + * @param string $haystack + * @param string $needle + * @return bool + */ + function str_starts_with($haystack, $needle) + { + return strpos($haystack, $needle) === 0; + } +} +else +{ + function str_starts_with($haystack, $needle) + { + return \str_starts_with($haystack, $needle); + } +} + +if(!function_exists('str_ends_with')) +{ + /** + * Checks if a string starts with a given substring. + * + * @param string $haystack + * @param string $needle + * @return bool + */ + function str_ends_with($haystack, $needle) + { + $length = strlen($needle); + if ($length === 0) return true; + + $position = strpos($haystack, $needle); + return $position !== false && $position === strlen($haystack) - $length; + } +} +else +{ + function str_ends_with($haystack, $needle) + { + return \str_ends_with($haystack, $needle); + } +} + +function uncamelize(string $camelCaps, string $separator = '-'): string +{ + return strtolower(preg_replace('/([a-z])([A-Z])/', "$1" . $separator . "$2", $camelCaps)); +} + +function isHTML(string $string): bool +{ + return $string !== strip_tags($string) ? true : false; +} diff --git a/lib/zin/utils/classlist.class.php b/lib/zin/utils/classlist.class.php new file mode 100644 index 0000000000..4332cf9eff --- /dev/null +++ b/lib/zin/utils/classlist.class.php @@ -0,0 +1,314 @@ + + * @package zin + * @version $Id + * @link https://www.zentao.net + */ + +namespace zin\utils; + +/** + * Manage classname list for html element and widgets + */ +class classlist +{ + /** + * Store classname list, key => value + * + * @access public + * @var array + */ + public $list = array(); + + /** + * Create classname instance + * + * @access public + * @param array ...$list - A string or a class name list + */ + public function __construct(/* ...$list */) + { + $list = func_get_args(); + if(!empty($list)) $this->set($list); + } + + /** + * Convert classnames to string + * + * @access public + * @return string + */ + public function __toString() + { + return $this->toStr(); + } + + /** + * Override __invoke + * + * Example: + * + * $classlist = classlist::create('btn primary'); + * echo $classlist(); // Output: "btn primary" + * + * @access public + * @param array $list - Class name list + * @return string + */ + public function __invoke() + { + $list = func_get_args(); + if(empty($list)) return $this->toStr(); + return $this->set($list); + } + + /** + * Override __call to invoke toggle method conveniently + * + * Example: + * + * $classlist = classlist::new(); + * + * // Add "primary" class + * $classlist->primary(); + * + * // Remove "primary" class + * $classlist->primary(false); + * + * @access public + * @return classlist + */ + public function __call($name, $args) + { + return $this->toggle($name, !count($args) || $args[0]); + } + + /** + * Create classname instance + * + * Example: + * + * // Set class names + * $classlist = new classlist(); + * $classlist->set('btn primary rounded'); + * + * // Set multiple classnames by string list + * $classlist->set(array('btn', 'primary', 'rounded')); + * + * // Set multiple classnames by a mapped array + * $classlist->set(array('btn' => true, 'primary' => true, 'rounded' => $isRounded)); + * + * @access public + * @param string|array $list - A string or a class name list + * @param bool $reset + * @return classlist + */ + public function set($list, $reset = false) + { + if(is_string($list)) $list = explode(' ', $list); + + if(is_array($list)) + { + if($reset) $this->list = array(); + + $expectedKey = 0; + foreach($list as $index => $value) + { + if(is_array($value)) + { + $this->set($value); + continue; + } + + /* If $index is expected numberic key and the $value is string, then use the $value as the name */ + if($expectedKey === $index && is_string($value)) + { + $value = trim($value); + if(strlen($value) > 0) $this->list[$value] = true; + } + /* If index is string, then set $index as name */ + else if(is_string($index)) + { + $index = trim($index); + if(strlen($index) === 0) continue; + + $this->list[$index] = boolval($value); + } + $expectedKey++; + } + } + + return $this; + } + + /** + * Add classnames + * + * Example: + * + * $classlist = new classlist(); + * $classlist->add('btn primary rounded'); + * + * // Add multiple classnames by string list + * $classlist->add('btn', 'primary', 'rounded'); + * + * @access public + * @param array ...$list - classname string joined by space or string array + * @return classlist + */ + public function add(/* ...$list */) + { + return $this->set(func_get_args()); + } + + /** + * Remove classnames + * + * Example: + * + * $classlist = new classlist('btn primary rounded'); + * $classlist->remove('btn primary'); + * + * // Add multiple classnames by string list + * $classlist->remove('btn', 'primary'); + * + * @access public + * @param array|string $list - classname string joined by space or string array + * @return classlist + */ + public function remove($list) + { + if(is_string($list)) $list = explode(' ', $list); + + foreach($list as $name) + { + if(!is_string($name)) continue; + $name = trim($name); + if(!strlen($name)) continue; + + $this->list[$name] = false; + } + return $this; + } + + /** + * Toggle classname + * + * Example: + * + * $classlist = new classlist('btn'); + * $classlist->toggle('btn'); // class list is "" + * + * // Toggle class name by flag + * $classlist->toggle('primary', true); // class list is "primary" + * + * @access public + * @param string $name - classname string + * @return classlist + */ + public function toggle($name, $toggle = null) + { + $name = trim($name); + if(strlen($name)) + { + if($toggle === null) $toggle = !$this->has($name); + $this->list[$name] = $toggle; + } + return $this; + } + + /** + * Check whether has specific class name + * + * Example: + * + * $classlist = new classlist('btn primary rounded'); + * echo $classlist->has('btn'); // Output true + * + * // Check multiple names + * echo $classlist->has('btn primary'); // Output true + */ + public function has($list) + { + if(is_string($list)) $list = explode(' ', $list); + + foreach($list as $name) + { + if(!is_string($name)) continue; + $name = trim($name); + if(!strlen($name)) continue; + + if(!isset($this->list[$name]) || !$this->list[$name]) return false; + } + return true; + } + + public function clear() + { + $this->list = array(); + } + + /** + * Convert classnames to string + * + * @access public + * @return string + */ + public function toStr(): string + { + $names = array(); + foreach($this->list as $name => $toggle) + { + if(!$toggle) continue; + + $name = trim($name); + if(!strlen($name)) continue; + + $names[] = $name; + } + return implode(' ', $names); + } + + /** + * Get class names count + * + * @access public + * @return int + */ + public function count() + { + return count($this->list); + } + + /** + * Create an classlist instance + * + * @param string|array $names - A string or a class name list + * @return classlist + */ + static public function new($names = null) + { + return (new classlist($names)); + } + + /** + * Stringify class list + * + * @param string|array $names - A string or a class name list + * @return string + */ + static public function str($names) + { + return (new classlist($names))->toStr(); + } + + public function toJSON() + { + return $this->list; + } +} diff --git a/lib/zin/utils/data.class.php b/lib/zin/utils/data.class.php new file mode 100644 index 0000000000..1f34a87a83 --- /dev/null +++ b/lib/zin/utils/data.class.php @@ -0,0 +1,84 @@ + + * @package zin + * @version $Id + * @link https://www.zentao.net + */ + +namespace zin\utils; + +require_once __DIR__ . DS . 'dataset.class.php'; + +/** + * Manage data for html element and widgets + */ +class data extends dataset +{ + public function __constructor() + { + $list = func_get_args(); + + foreach($list as $data) $this->set($data); + } + + /** + * Method for sub class to modify value on setting it + * + * @access public + * @param array|string $prop - Property name or properties list + * @param mixed $value - Property value + * @param bool $removeEmpty - Whether to remove empty value + * @return dataset + */ + protected function setVal($prop, $value, $removeEmpty = false) + { + if($prop[0] === '$') $prop = substr($prop, 1); + + if($value === null || ($removeEmpty && empty($value))) return $this->remove($prop); + + $names = explode('.', $prop); + $lastName = array_pop($names); + $data = &$this->data; + if(!empty($names)) + { + foreach($names as $name) + { + if(!is_array($data)) + { + return $this; + } + + if(!isset($data[$name])) $data[$name] = array(); + $data = &$data[$name]; + } + } + + if($value === null || ($removeEmpty && empty($value))) + { + if(isset($data[$lastName])) unset($data[$lastName]); + return $this; + } + + $data[$lastName] = $value; + return $this; + } + + protected function getVal($prop) + { + if($prop[0] === '$') $prop = substr($prop, 1); + + $names = explode('.', $prop); + $data = &$this->data; + foreach($names as $name) + { + if(!is_array($data)) return null; + $data = &$data[$name]; + } + return $data; + } +} diff --git a/lib/zin/utils/dataset.class.php b/lib/zin/utils/dataset.class.php new file mode 100644 index 0000000000..5658f9189e --- /dev/null +++ b/lib/zin/utils/dataset.class.php @@ -0,0 +1,288 @@ + + * @package zin + * @version $Id + * @link https://www.zentao.net + */ + +namespace zin\utils; + +/** + * Manage dataset properties for html element and widgets + */ +class dataset +{ + /** + * Store dataset properties list in an array + * + * @var array + * @access public + */ + public array $data = array(); + + /** + * Create an instance, the initialed data can be passed + * + * @access public + * @param array $data - Properties list array + */ + public function __construct(?array $data = null) + { + if($data !== null) $this->set($data); + } + + /** + * Override __set + * + * @access public + * @param string $prop - Property name + * @param mixed $value - Property value + * @return void + */ + public function __set(string $name, mixed $value) + { + $this->set($name, $value); + } + + /** + * Override __get + * + * @access public + * @param string $prop - Property name + * @return mixed + */ + public function __get(string $name) + { + $this->get($name); + } + + /** + * Override __isset + * + * @access public + * @param string $prop - Property name + * @return bool + */ + public function __isset(string $name): bool + { + return $this->has($name); + } + + /** + * Override __unset + * + * @access public + * @param string $prop - Property name + * @return void + */ + public function __unset(string $name) + { + $this->remove($name); + } + + /** + * Convert dataset to json string + * + * @access public + * @return string + */ + public function __toString(): string + { + return $this->toStr(); + } + + /** + * Override __invoke + * + * @access public + * @return string + */ + public function __invoke(string|array $name = null, mixed $value = null): string + { + if($value !== null || is_array($name)) return $this->set($name, $value); + if(is_string($name)) return $this->get($name); + + return $this->toStr(); + } + + /** + * Override __call for setting property conveniently + * + * Example: + * + * $dataset = dataset::new(); + * + * // Set color property + * $dataset->color('red'); + * + * // Get color property + * echo $dataset->color(); // Output "Red" + * + * @access public + * @return mixed + */ + public function __call(string $name, array $args): mixed + { + if(count($args)) return $this->set($name, $args[0]); + + return $this->get($name); + } + + /** + * Method for sub class to modify value on setting it + * + * @access protected + * @param string $prop - Property name or properties list + * @param mixed $value - Property value + * @return dataset + */ + protected function setVal(string $prop, mixed $value): dataset + { + $this->data[$prop] = $value; + return $this; + } + + protected function getVal(string $prop): mixed + { + return isset($this->data[$prop]) ? $this->data[$prop] : null; + } + + /** + * Get properties count + * + * @access public + * @param bool $skipEmpty - Whether to skip to count empty value + * @return int + */ + public function count($skipEmpty = false): int + { + if(!$skipEmpty) return count($this->data); + + $count = 0; + foreach($this->data as $value) + { + if($value !== null) $count++; + } + return $count; + } + + /** + * Convert dataset to json string + * + * @access public + * @return string + */ + public function toStr(): string + { + return json_encode($this->toJsonData()); + } + + public function toJsonData(): array + { + return $this->data; + } + + /** + * Set property, an array can be passed to set multiple properties + * + * @access public + * @param array|string $prop - Property name or properties list + * @param mixed $value - Property value + * @return dataset + */ + public function set(array|string $prop, mixed $value = null): dataset + { + if(is_array($prop)) + { + foreach($prop as $name => $val) $this->set($name, $val); + return $this; + } + + return $this->setVal($prop, $value); + } + + /** + * Get property value by name + * + * @access public + * @param string $prop - Property name + * @param mixed $defaultValue - Optional default value if actual value is null + * @return mixed + */ + public function get($prop, $defaultValue = null) + { + $val = $this->getVal($prop); + return $val === null ? $defaultValue : $val; + } + + public function addToList($prop, $values) + { + if(!is_array($values)) $values = array($values); + + $list = $this->getList($prop); + $this->set($prop, array_merge($list, $values)); + } + + public function getList($prop) + { + return $this->get($prop, array()); + } + + public function list($prop, $values = null) + { + if($values === null) return $this->getList($prop); + return $this->setList($prop, $values); + } + + /** + * Delete property by name + * + * @access public + * @param string $prop - Property name + * @return dataset + */ + public function remove($prop) + { + return $this->setVal($prop, null); + } + + public function clear() + { + $this->data = array(); + } + + /** + * Check whether has specified property + * + * @access public + * @param string $prop - Property name + * @return boolean + */ + public function has($prop) + { + return $this->getVal($prop) !== null; + } + + /** + * Clone a new instance + * + * @access public + * @return object + */ + public function clone() + { + $className = get_called_class(); + return new $className($this->data); + } + + public function merge($data) + { + if(is_object($data) && isset($data->data)) return $this->set($data->data); + return $this->set($data); + } +} diff --git a/lib/zin/utils/debug.func.php b/lib/zin/utils/debug.func.php new file mode 100644 index 0000000000..eedc48a7d3 --- /dev/null +++ b/lib/zin/utils/debug.func.php @@ -0,0 +1,45 @@ + + * @package zin + * @version $Id + * @link https://www.zentao.net + */ + +namespace zin\utils; + +$logs = array(); + +function log($type, $msg = null, $file) +{ + global $config; + + if(!$config->debug) return; + + if($msg === null) + { + $msg = $type; + $type = 'i'; + } + + if(is_array($msg)) + { + $msgLines = array(); + foreach($msg as $m) $msgLines[] = strval($m); + $msg = implode(' ', $msgLines); + } + else + { + $msg = strval($msg); + } + + $logs[] = array(array('type' => strtolower($type), 'msg' => $msg)); +} + +function logInfo($msg, $file = null) {log('i', $msg, $file);}; +function logWarn($msg, $file = null) {log('w', $msg, $file);}; +function logError($msg, $file = null) {log('e', $msg, $file);}; diff --git a/lib/zin/utils/deep.func.php b/lib/zin/utils/deep.func.php new file mode 100644 index 0000000000..cf67d59820 --- /dev/null +++ b/lib/zin/utils/deep.func.php @@ -0,0 +1,38 @@ +$name)) return $defaultValue; + $data = &$data->$name; + continue; + } + if(!is_array($data) || !isset($data[$name])) return $defaultValue; + $data = &$data[$name]; + } + return $data === null ? $defaultValue : $data; +} + +function deepSet(array &$data, string $namePath, mixed $value) +{ + $names = explode('.', $namePath); + $lastName = array_pop($names); + if(!empty($names)) + { + foreach($names as $name) + { + if(!is_array($data)) return; + + if(!isset($data[$name])) $data[$name] = array(); + $data = &$data[$name]; + } + } + + $data[$lastName] = $value; +} diff --git a/lib/zin/utils/flat.func.php b/lib/zin/utils/flat.func.php new file mode 100644 index 0000000000..f597fbf17b --- /dev/null +++ b/lib/zin/utils/flat.func.php @@ -0,0 +1,20 @@ + $value) + { + if(is_array($value)) + { + $result = array_merge($result, flat($value, $prefix . $key . $separator)); + } + else + { + $result[$prefix . $key] = $value; + } + } + return $result; +} diff --git a/lib/zin/utils/hx.class.php b/lib/zin/utils/hx.class.php new file mode 100644 index 0000000000..231504b0c8 --- /dev/null +++ b/lib/zin/utils/hx.class.php @@ -0,0 +1,126 @@ + + * @package zin + * @version $Id + * @link https://www.zentao.net + */ + +namespace zin\utils; + +require_once __DIR__ . DS . 'dataset.class.php'; + +/** + * Manage hx for html element and widgets + * + * Example: + * + * // Create a hx object an convert to str string + * $hx = hx::new()->boost(); + * + * echo $hx(); // Output 'hx-boost="true"' + * + * @see https://htmx.org/ + * @todo @sunhao: Validate hx properties on modifying + */ +class hx extends dataset +{ + /** + * Method for sub class to modify value on setting it + * + * @access public + * @param array|string $prop - Property name or properties list + * @param mixed $value - Property value + * @param bool $removeEmpty - Whether to remove empty value + * @return hx + */ + protected function setVal($prop, $value, $removeEmpty = false) + { + if(str_starts_with($prop, 'hx-')) $prop = substr($prop, 3); + return parent::setVal($prop, $value); + } + + /** + * Set ajax request + * + * @access public + * @param string $url - The request url + * @param string $trigger - The trigget + * @param string $target - A css selector to specific a element to load remote content + * @param string $method - The request method, default value is "get" + * @return hx + * @see https://htmx.org/docs/#ajax + */ + public function ajax($url, $trigger = '', $target = '', $method = 'get') + { + if(is_array($url)) return $this->set($url); + + return $this->set(array('url' => $url, 'trigger' => $trigger, 'target' => $target, 'method' => $method)); + } + + /** + * Set ajax post request + * + * @access public + * @param string $url - The request url + * @param string $trigger - The trigget + * @param string $target - A css selector to specific a element to load remote content + * @return hx + * @see https://htmx.org/docs/#ajax + */ + public function post($url, $trigger = '', $target = '') + { + return $this->ajax($url, $trigger, $target, 'post'); + } + + /** + * Convert hx properties to str string + * + * @access public + * @return string + */ + public function toStr() + { + $pairs = array(); + + foreach($this->data as $name => $value) + { + /* Skip any null value */ + if($value === null) continue; + + /* Convert non-string to json */ + if(!is_string($value)) $value = json_encode($value); + + $pairs[] = 'hx-' . $name . '="' . htmlspecialchars($value) . '"'; + } + + return implode(' ', $pairs); + } + + /** + * Create an instance + * + * @param string $hx - CSS hx list + * @return hx + */ + static public function new($hx) + { + return new hx($hx); + } + + /** + * Create properties string from hx list + * + * @access public + * @param string $hx - CSS hx list + * @return string + */ + static public function str($props) + { + return (new hx($props))->toStr(); + } +} diff --git a/lib/zin/utils/style.class.php b/lib/zin/utils/style.class.php new file mode 100644 index 0000000000..19b8b7607c --- /dev/null +++ b/lib/zin/utils/style.class.php @@ -0,0 +1,174 @@ + + * @package zin + * @version $Id + * @link https://www.zentao.net + */ + +namespace zin\utils; + +require_once __DIR__ . DS . 'dataset.class.php'; + +/** + * Manage style for html element and widgets + * + * Example: + * + * // Create a style object an convert to css string + * $style = style::create(array('color' => 'red')); + * echo $style(); // Output "color:red" + * + * // Above example same as: + * echo style::css(array('color' => 'red')); + * + * // Modifier style + * $style = style::create(array('color' => 'red')); + * $style->set('background', 'green'); + * + * // Modifier style with property name directly + * $style->background = 'green'; + * + * // Get style value + * echo $style->get('background'); // Output "green" + * + * // Get style value with property name directly + * echo $style->background; // Output "green" + * + * @todo @sunhao: Validate style properties on modifying + */ +class style extends dataset +{ + /** + * Set or get css variable, an array can be passed to set multiple variables + * If only pass variable name, then the variable value will be returned + * If no params passed, then return all setted variables with an array + * + * Notice: no need to prepend prefix '--' to variable name, the method will prepend it automatically, if prepended already, the method will skip to prepend smartly + * + * Example: + * + * // Create a style object and set + * $style = new style(); + * $style->cssVar('text-size', '14px'); + * + * // Set multiple variables + * $style->cssVar(array('text-color' => 'yellow', 'background-image' => 'none')); + * + * // Get variable value + * echo $style->cssVar('text-size'); // Output "14px" + * + * // Get all variables value + * echo $style->cssVar(); + * // Output array('text-size' => '14px', 'color' => 'yellow', 'background' => 'none'); + * + * // Remove variable by setting value with an empty string + * $style->cssVar('text-color', ''); + * + * @access public + * @param array|string $name - Variable name or variables list + * @param mixed $value - Property value + * @return mixed + */ + public function cssVar(array|string $name = '', ?string $value = null): style|array|string + { + /* Support for setting multiple variables by an array */ + if(is_array($name)) + { + foreach($name as $n => $value) $this->set(style::formatVarName($n), $value); + return $this; + } + + /* Return all setted variables without passed any params */ + if(empty($name)) + { + $vars = array(); + foreach ($this->data as $prop => $value) + { + if(!str_starts_with($name, '--')) continue; + $vars[substr($prop, 2)] = $value; + } + return $vars; + } + + $varName = style::formatVarName($name); + + /* Return the specific variable value by name */ + if($value === null) return $this->get($varName); + + /* Set the specific variable value and return style object self */ + $this->set($varName, $value === '' ? null : $value); + return $this; + } + + /** + * Convert to string + * + * @access public + * @return string + */ + public function toStr(): string + { + return $this->toCss(); + } + + /** + * Convert style to css string + * + * @access public + * @return string + */ + public function toCss(): string + { + $pairs = array(); + + foreach($this->data as $prop => $value) + { + /* Skip any empty value */ + if($value === null || $value === '') continue; + + $pairs[] = $prop . ': ' . strval($value) . ';'; + } + + return implode(' ', $pairs); + } + + /** + * Create an instance + * + * @param ?array $style - CSS style list + * @return style + */ + public static function new(?array $style = null): style + { + return new style($style); + } + + /** + * Create css from style list + * + * @access public + * @param ?array $style - CSS style list + * @return string + */ + public static function css(?array $style): string + { + return (new style($style))->toCss(); + } + + /** + * Format CSS variable name with prefix "--" + * + * @access public + * @param string $name - CSS variable name + * @return string + */ + public static function formatVarName(string $name): string + { + return \zin\str_starts_with($name, '--') ? $name : "--$name"; + } +} diff --git a/lib/zin/wg/actionitem/v1.php b/lib/zin/wg/actionitem/v1.php new file mode 100644 index 0000000000..6750cd7457 --- /dev/null +++ b/lib/zin/wg/actionitem/v1.php @@ -0,0 +1,139 @@ +prop(array('icon', 'text', 'trailingIcon', 'textClass')); + + return h::div + ( + set($this->props->skip(array_keys(actionItem::definedPropsList()))), + set($this->prop('props')), + $icon ? icon($icon) : null, + empty($text) ? null : span($text, setClass('text', $textClass)), + $this->children(), + $trailingIcon ? icon($trailingIcon) : null, + ); + } + + protected function buildDropdownItem() + { + list($dropdown, $items, $icon, $text, $trailingIcon, $active, $disabled, $badge, $props, $caret, $textClass) = $this->prop(array('dropdown', 'items', 'icon', 'text', 'trailingIcon', 'active', 'disabled', 'badge', 'props', 'caret', 'textClass')); + + if(is_string($badge)) + { + $badge = label($badge); + } + elseif(is_array($badge)) + { + $badge = label(set($badge)); + } + + $dropdown = new dropdown + ( + set::items($items), + set($dropdown), + h::a( + setClass(array('active' => $active, 'disabled' => $disabled)), + set($this->getRestProps()), + set($props), + $icon ? icon($icon) : null, + span($text, setClass('text', $textClass)), + $badge, + $this->children(), + $trailingIcon ? icon($trailingIcon) : null, + h::span(setClass(is_string($caret) ? "caret-$caret" : 'caret')) + ) + ); + return $dropdown; + } + + protected function buildBtnItem() + { + return new btn($this->props->skip('tagName,type,name,outerTag,outerProps,props'), set($this->prop('props')),$this->children()); + } + + protected function buildCheckboxItem() + { + return new checkbox($this->props->skip('tagName,type,name,outerTag,outerProps,props'), set($this->prop('props')),$this->children()); + } + + protected function buildBtnGroupItem() + { + return new btnGroup($this->props->skip('tagName,type,name,outerTag,outerProps,props'), set($this->prop('props')),$this->children()); + } + + protected function buildItem() + { + $type = $this->prop('type'); + $methodName = "build{$type}Item"; + if(method_exists($this, $methodName)) return $this->$methodName(); + + list($tagName, $icon, $text, $trailingIcon, $url, $target, $active, $disabled, $badge, $textClass) = $this->prop(array('tagName', 'icon', 'text', 'trailingIcon', 'url', 'target', 'active', 'disabled', 'badge', 'textClass')); + + if(is_string($badge)) $badge = label($badge); + else if(is_array($badge)) $badge = label(set($badge)); + + return h::create + ( + $tagName, + set($tagName === 'a' ? array('href' => $url, 'target' => $target) : array('data-url' => $url, 'data-target' => $target)), + setClass(array('active' => $active, 'disabled' => $disabled)), + set($this->getRestProps()), + set($this->prop('props')), + $icon ? icon($icon) : null, + span($text, setClass('text', $textClass)), + $badge, + $this->children(), + $trailingIcon ? icon($trailingIcon) : null, + ); + } + + protected function build(): wg + { + list($name, $type, $outerTag, $outerProps, $outerClass) = $this->prop(array('name', 'type', 'outerTag', 'outerProps', 'outerClass')); + + return h::create + ( + $outerTag, + setClass(($type !== 'item' && $type !== 'divider') ? 'nav-item' : '', "$name-$type", $outerClass), + set($outerProps), + $this->buildItem() + ); + } +} diff --git a/lib/zin/wg/avatar/v1.php b/lib/zin/wg/avatar/v1.php new file mode 100644 index 0000000000..41a534f294 --- /dev/null +++ b/lib/zin/wg/avatar/v1.php @@ -0,0 +1,297 @@ + 20, 'sm' => 24, 'lg' => 48, 'xl' => 80); + private $actualSize = 32; + private $finalClass = array('avatar'); + private $finalStyle; + + protected function onAddChild($child) + { + if(is_string($child) && !$this->props->has('text')) + { + $this->setProp('text', $child); + return false; + } + + return $child; + } + + protected function build(): wg + { + /* Attach classes. */ + $this->finalClass[] = $this->prop('className'); + + /* Init style. */ + $this->finalStyle = new stdClass(); + $this->finalStyle->background = $this->prop('background'); + $this->finalStyle->color = $this->prop('foreColor'); + + foreach($this->props->style->data as $attr => $val) $this->finalStyle->{$attr} = $val; + + /* Init avatar size. */ + $this->initSize(); + /* Init avatar shape. */ + $this->initShape(); + + $content = $this->getContent(); + $finalStyle = json_decode(json_encode($this->finalStyle), true); + return h::div + ( + setClass($this->finalClass), + setStyle($finalStyle), + set($this->getRestProps()), + $content, + $this->children() + ); + } + + private function initSize() + { + $size = $this->prop('size'); + $this->actualSize = $size; + + if(!$size) return; + + if(is_numeric($size)) + { + $fontSize = intval($size/2) > 12 ? intval($size/2) : 12; + $this->finalStyle->width = "{$size}px"; + $this->finalStyle->height = "{$size}px"; + $this->finalStyle->{'font-size'} = "{$fontSize}px"; + + return; + } + + $this->finalClass[] = "size-{$size}"; + $this->actualSize = isset($this->sizeMap[$size]) ? $this->sizeMap[$size] : 20; + } + + private function initShape() + { + $circle = $this->prop('circle'); + $rounded = $this->prop('rounded'); + + /* Set circle. */ + if($circle) + { + $this->finalClass[] = 'rounded-full'; + } + else if($rounded) + { + if(is_numeric($rounded)) $this->finalStyle->{'border-radius'} = "{$rounded}px"; + else $this->finalClass[] = "rounded-{$rounded}"; + } + } + + private function getAvatarText() + { + $maxTextLen = intval($this->prop('maxTextLength')); + $text = strtoupper($this->prop('text', '')); + $this->textLen = strlen($text); + + if(preg_match('/[\x{4e00}-\x{9fa5}\s]+$/u', $text)) + { + $this->textLen = mb_strlen($text); + $text = $this->textLen <= $maxTextLen ? $text : mb_substr($text, $this->textLen - $maxTextLen); + $this->displayTextLen = mb_strlen($text); + return $text; + } + + if(preg_match('/[A-Za-z\d\s]+$/', $text)) + { + $this->displayTextLen = 1; + return substr($text, 0, 1); + } + + return $this->textLen <= $maxTextLen ? $text : substr($text, 0, $maxTextLen); + } + + /** + * Convert HSL values to RGB value. + * + * @param int $h + * @param int $s + * @param int $l + * @access private + * @return array + */ + private function hslToRgb($h, $s, $l) + { + $h = ($h % 360) / 360; + $s = ($s > 0 ? $s : 0); + $s = ($s > 255) ? 255 : $s; + $l = ($l > 0 ? $l : 0); + $l = ($l > 255) ? 255 : $l; + + $m2 = ($l <= 0.5) ? ($l * ($s + 1)) : ($l + $s - $l * $s); + $m1 = $l * 2 - $m2; + + $hueFn = function($val, $m1, $m2) + { + $val = $val < 0 ? $val + 1 : ($val > 1 ? $val - 1 : $val); + + if($val * 6 < 1) return $m1 + ($m2 - $m1) * $val * 6; + elseif($val * 2 < 1) return $m2; + elseif($val * 3 < 2) return $m1 + ($m2 - $m1) * (2/3 - $val) * 6; + + return $m1; + }; + + return array( + 'r' => $hueFn($h + 1/3, $m1, $m2) * 255, + 'g' => $hueFn($h, $m1, $m2) * 255, + 'b' => $hueFn($h - 1/3, $m1, $m2) * 255 + ); + } + + private function hex2Rgb($hex) + { + if(!str_starts_with($hex, '#') || !preg_match('/#[0-9A-F]{3,6}$/', $hex)) throw new \Exception('incorrect data format'); + + $r = 0; + $g = 0; + $b = 0; + if(strlen($hex) == 4) list($r, $g, $b) = sscanf($hex, "#%01x%01x%01x"); + elseif(strlen($hex) == 7) list($r, $g, $b) = sscanf($hex, "#%02x%02x%02x"); + else throw new \Exception('incorrect RGB value'); + + return array( + 'r' => $r, + 'g' => $g, + 'b' => $b + ); + } + + /* + * Get contrast color. + * + * @param array|string $rgb + * @param string $theme dark|light + * @access private + * @return string + */ + private function contrastColor($rgb, $themeDark = null, $themeLight = null) + { + $rgb = is_array($rgb) ? $rgb : $this->hex2Rgb($rgb); + + $r = $rgb['r']; + $g = $rgb['g']; + $b = $rgb['b']; + if(($r * 0.299 + $g * 0.587 + $b * 0.114) > 186) + { + /* Is light color. */ + return $themeDark ? $themeDark : '#333333'; + } + + return $themeLight ? $themeLight : '#ffffff'; + } + + private function getTextStyle() + { + $hueDistance = intval($this->prop('hueDistance')); + $saturation = $this->prop('saturation'); + $lightness = $this->prop('lightness'); + $background = $this->prop('background'); + $foreColor = $this->prop('foreColor'); + $code = $this->prop('code'); + $avatarCode = $code ? $code : $this->prop('text'); + + if(!$background) + { + $val = 0; + if(is_numeric($avatarCode)) + { + $val = intval($avatarCode); + } + else + { + for($i = 0; $i < strlen($avatarCode); $i++) $val += ord($avatarCode[$i]); + } + + $hue = $val * $hueDistance % 360; + $actualSat = $saturation * 100; + $actualLight = $lightness * 100; + $this->finalStyle->background = "hsl({$hue}, {$actualSat}%, {$actualLight}%)"; + + if(!$foreColor) + { + $rgb = $this->hslToRgb($hue, $saturation, $lightness); + $this->finalStyle->color = $this->contrastColor($rgb); + } + } + elseif (!$foreColor && $background) + { + $this->finalStyle->color = $this->contrastColor($background); + } + + $textStyle = array(); + if($this->actualSize and $this->actualSize < (14 * $this->displayTextLen)) + { + $textStyle = array( + 'transform' => 'scale(' . $this->actualSize / (14 * $this->displayTextLen) . ')', + 'white-space' => 'nowrap' + ); + } + + return $textStyle; + } + + private function getContent() + { + $src = $this->prop('src'); + $text = $this->prop('text'); + + /* With avatar. */ + if($src) + { + $this->finalClass[] = 'has-img'; + + return h::img + ( + setClass('avatar-img'), + set('src', $src ), + set('alt', $text) + ); + } + + /* Without text and image. */ + if(!$text) return null; + + $displayText = $this->getAvatarText(); + + $this->finalClass[] = 'has-text'; + $this->finalClass[] = 'has-text-' . $this->textLen; + + $textStyle = $this->getTextStyle(); + return h::div + ( + setClass('avatar-text'), + set('data-actualSize', $this->actualSize), + $textStyle ? setStyle($textStyle) : null, + $displayText + ); + } +} diff --git a/lib/zin/wg/backbtn/v1.php b/lib/zin/wg/backbtn/v1.php new file mode 100644 index 0000000000..8e53e04d60 --- /dev/null +++ b/lib/zin/wg/backbtn/v1.php @@ -0,0 +1,101 @@ + + * @package zin + * @link http://www.zentao.net + */ + +namespace zin; + +require_once dirname(__DIR__) . DS . 'btn' . DS . 'v1.php'; + +/** + * 后退按钮(backBtn)部件类。 + * The back button widget class. + * + * @author Hao Sun + */ +class backBtn extends btn +{ + /** + * Define widget properties. + * + * @var array + * @access protected + */ + protected static array $defineProps = array( + 'back?: string="APP"' // 定义返回行为,可以为 `'APP'`(默认值,返回打开当前页面时的上一个历史记录)、 `'GLOBAL'`(返回上一个全局历史记录)、`'moduleName-methodName'`(从历史记录中向后查找符合指定路径的历史记录)。 + ); + + /** + * Override the getProps method. + * + * @access protected + * @return array + */ + protected function getProps(): array + { + global $app; + + $backs = array( + 'task' => 'execution-task,my-work,my-contribute,', + 'story' => 'product-browse,projectstory-story,execution-story,my-work,my-contribute,productplan-view', + 'bug' => 'bug-browse,project-bug,my-work,my-contribute,', + 'testcase' => 'testcase-browse,project-testcase,my-work,my-contribute,', + 'testsuite' => 'testsuite-browse,testsuite-view,', + 'testtask' => 'testtask-browse,testtask-cases,', + 'testreport' => 'testreport-browse,project-testreport', + 'tree' => 'product-browse,project-browse,execution-task,bug-browse,projectstory-story', + 'doc' => 'doc-mySpace,doc-productSpace,doc-projectSpace,doc-teamSpace', + 'design' => 'design-browse', + 'release' => 'release-browse,release-view', + 'projectrelease' => 'projectrelease-browse', + 'build' => 'execution-build,build-view', + 'projectbuild' => 'projectbuild-browse,projectbuild-view', + 'mr' => 'mr-browse', + 'repo' => 'repo-log,repo-browse', + 'compile' => 'compile-browse', + ); + + $props = parent::getProps(); + $back = $this->prop('back'); + if($back != 'APP') + { + $props['data-back'] = $back; + } + elseif(isset($backs[$app->rawModule])) + { + $props['data-back'] = $backs[$app->rawModule]; + + if(!$this->prop('url')) + { + $backLinks = explode(',', $backs[$app->rawModule]); + $props['data-url'] = $backLinks[0]; + } + } + else + { + $props['data-back'] = empty($back) ? 'APP' : $back; + } + + return $props; + } + + /** + * Override the getClassList method. + * + * @access protected + * @return array + */ + protected function getClassList(): array + { + $classList = parent::getClassList(); + $classList['open-url'] = true; + return $classList; + } +} diff --git a/lib/zin/wg/batchactions/js/v1.js b/lib/zin/wg/batchactions/js/v1.js new file mode 100644 index 0000000000..c83bd1e1e5 --- /dev/null +++ b/lib/zin/wg/batchactions/js/v1.js @@ -0,0 +1,72 @@ +/** + * Add new item. + * + * @param obj e + * @access public + * @return void + */ +function addItem(e) +{ + const obj = e.target + const newItem = $(obj).closest('.form-row').clone(); + let index = 0; + + newItem.find('.add-btn').on('click', addItem); + newItem.find('.del-btn').on('click', removeItem); + + let inputName = newItem.find('input').length > 0 ? newItem.find('input').first().attr('name') : newItem.find('select').first().attr('name'); + inputName = inputName.slice(0, inputName.indexOf('[')); + $('form').find("[name^='" + inputName + "']").each(function() { + let $name = $(this).attr('name'); + + let id = parseInt($name.slice($name.indexOf('[')+1, $name.indexOf(']'))); + if(isNumeric(id) && id >= index) index = id + 1; + }) + + /* Fix id and value. */ + newItem.addClass('newItem'); + newItem.find('.form-label').html(''); + newItem.find('input').each(function() + { + let name = $(this).attr('name'); + name = name.slice(0, name.indexOf('[')+1) + String(index) + name.slice(name.indexOf(']')); + $(this).attr('name', name); + $(this).attr('id', name); + $(this).val(''); + }); + newItem.find('select').each(function() + { + let name = $(this).attr('name'); + name = name.slice(0, name.indexOf('[')+1) + String(index) + name.slice(name.indexOf(']')); + $(this).attr('name', name); + $(this).attr('id', name); + $(this).val(''); + }); + + $(obj).closest('.form-row').after(newItem); +} + +/** + * Remove item. + * + * @param obj e + * @access public + * @return void + */ +function removeItem(e) +{ + const obj = e.target + + /* Dsiabled btn can't remove line. */ + if($(obj).closest('.btn').hasClass('disabled')) return false; + + $(obj).closest('.form-row').remove(); + + let chosenProducts = 0; + $("select[name^='products']").each(function() + { + if($(this).val() > 0) chosenProducts ++; + }); + + (chosenProducts.length > 1 && (model == 'waterfall' || model == 'waterfallplus')) ? $('.stageBy').removeClass('hide') : $('.stageBy').addClass('hide'); +} diff --git a/lib/zin/wg/batchactions/v1.php b/lib/zin/wg/batchactions/v1.php new file mode 100644 index 0000000000..a751a69def --- /dev/null +++ b/lib/zin/wg/batchactions/v1.php @@ -0,0 +1,39 @@ +prop('actionClass')), + btn + ( + icon('plus', set::size('lg')), + setClass('bg-white ring-0 rounded bg-opacity-20 add-btn'), + on::click('addItem'), + ), + btn + ( + icon('close', set::size('lg')), + setClass('bg-white ring-0 rounded bg-opacity-20 del-btn'), + on::click('removeItem'), + ), + ) + ); + } +} diff --git a/lib/zin/wg/blockpanel/v1.php b/lib/zin/wg/blockpanel/v1.php new file mode 100644 index 0000000000..2322911cef --- /dev/null +++ b/lib/zin/wg/blockpanel/v1.php @@ -0,0 +1,77 @@ + + * @package zin + * @link http://www.zentao.net + */ + +namespace zin; + +require_once dirname(__DIR__) . DS . 'panel' . DS . 'v1.php'; + +/** + * 仪表盘区块面板(blockPanel)部件类。 + * The block panel widget class. + * + * @author Hao Sun + */ +class blockPanel extends panel +{ + protected static array $defineProps = array + ( + 'class?: string="rounded bg-canvas panel-block"', // 类名。 + 'id?: string', // ID。 + 'name?: string', // 区块内部名称。 + 'block?: object|array', // 区块对象。 + 'title?: string', // 标题。 + 'headingClass?: string="border-b"', // 标题栏类名。 + 'moreLink?: string' // 更多链接。 + ); + + protected function created() + { + global $lang; + $props = array(); + + $name = $this->prop('name'); + $block = $this->prop('block', data('block')); + + if(is_array($block)) $block = (object)$block; + if(empty($name) && !empty($block)) + { + $name = $block->code; + $props['name'] = $name; + + if(empty($this->prop('id'))) $props['id'] = $block->module . '-' . $block->code . '-' . $block->id; + } + + $moreLink = $this->prop('moreLink'); + if(empty($moreLink) && !empty($block) && isset($block->moreLink)) $moreLink = $block->moreLink; + if(empty($this->prop('headingActions')) && !empty($moreLink)) + { + $props['headingActions'] = array(array('type' => 'ghost', 'url' => $moreLink, 'text' => $lang->more, 'caret' => 'right', 'size' => 'sm')); + } + + if(empty($this->prop('title'))) $props['title'] = empty($block) ? $lang->block->titleList[$name] : $block->title; + + $this->setProp($props); + } + + protected function buildProps(): array + { + $props = parent::buildProps(); + $name = $this->prop('name'); + if(!empty($name)) + { + $props[] = setData('block', $name); + $props[] = setClass("block-{$name}"); + $props[] = setID($this->prop('id')); + } + return $props; + } +} diff --git a/lib/zin/wg/btn/v1.php b/lib/zin/wg/btn/v1.php new file mode 100644 index 0000000000..e2ef19c5ce --- /dev/null +++ b/lib/zin/wg/btn/v1.php @@ -0,0 +1,105 @@ +props->has('text')) + { + $this->props->set('text', $child); + return false; + } + } + + protected function getProps() + { + $url = $this->prop('url'); + $target = $this->prop('target'); + $props = array_merge($this->getRestProps(), array('title' => $this->prop('hint'))); + + if(empty($url)) + { + $props['type'] = $this->prop('btnType'); + if(!isset($props['data-target'])) $props['data-target'] = $target; + return $props; + } + + $props['tagName'] = 'a'; + if(!isset($props['href'])) $props['href'] = $url; + if(!isset($props['target'])) $props['target'] = $target; + return $props; + } + + private function getChildren() + { + list($caret, $text, $icon, $iconClass, $trailingIcon, $trailingIconClass) = $this->prop(array('caret', 'text', 'icon', 'iconClass', 'trailingIcon', 'trailingIconClass')); + + $children = array(); + if(!empty($icon)) $children[] = icon($icon, setClass($iconClass)); + if(!empty($text)) $children[] = h::span($text, setClass('text')); + $children[] = parent::build(); + if(!empty($trailingIcon)) $children[] = icon($trailingIcon, setClass($trailingIconClass)); + if(!empty($caret)) $children[] = h::span(setClass(is_string($caret) ? "caret-$caret" : 'caret')); + + return $children; + } + + protected function getClassList() + { + list($url, $type, $caret, $text, $icon, $trailingIcon) = $this->prop(array('url', 'type', 'caret', 'text', 'icon', 'trailingIcon')); + $onlyCaret = empty($text) && !empty($caret) && empty($icon) && empty($trailingIcon); + $classList = array( + 'btn' => true, + 'disabled' => $this->prop('disabled'), + 'active' => $this->prop('active'), + 'btn-caret' => $onlyCaret, + 'square' => $this->prop('square') + ); + + if(empty($type) && !empty($url)) $type = 'btn-default'; + else if($type === 'link') $type = 'btn-link'; + else if($type === 'default') $type = 'btn-default'; + if(!empty($type)) $classList[$type] = true; + + if(empty($text) && !empty($icon) && !isset($classList['square'])) $classList['square'] = true; + + $size = $this->prop('size'); + if(!empty($size)) $classList["size-$size"] = true; + + return $classList; + } + + protected function build(): wg + { + $props = $this->getProps(); + $children = $this->getChildren(); + $classList = $this->getClassList(); + + return button + ( + set($props), + setClass($classList), + $children + ); + } +} diff --git a/lib/zin/wg/btngroup/v1.php b/lib/zin/wg/btngroup/v1.php new file mode 100644 index 0000000000..6bdb96898d --- /dev/null +++ b/lib/zin/wg/btngroup/v1.php @@ -0,0 +1,44 @@ +prop('disabled'); + $size = $this->prop('size'); + + $className = 'btn-group'; + if(!empty($disabled)) $className .= ' disabled'; + if(!empty($size)) $className .= " size-$size"; + + return $className; + } + + protected function build(): wg + { + $items = $this->prop('items'); + $className = $this->getclassName(); + + return div + ( + setClass($className), + set($this->getRestProps()), + is_array($items) ? array_map(array($this, 'onBuildItem'), $items) : null, + $this->children() + ); + } +} diff --git a/lib/zin/wg/burn/v1.php b/lib/zin/wg/burn/v1.php new file mode 100644 index 0000000000..bb1c038e53 --- /dev/null +++ b/lib/zin/wg/burn/v1.php @@ -0,0 +1,41 @@ + + * @package zin + * @link http://www.zentao.net + */ + +namespace zin; + +/** + * 仪表盘(burn)部件类。 + * The burn widget class. + * + * @author Hao Sun + */ +class burn extends wg +{ + /** + * Define widget properties. + * + * @var array + * @access protected + */ + protected static array $defineProps = array( + 'data?: string|array', // 数据源 + 'referenceLine?: bool=false' // 参考线 + ); + + /** + * Build widget. + */ + protected function build(): zui + { + return zui::burn(inherit($this)); + } +} diff --git a/lib/zin/wg/cell/v1.php b/lib/zin/wg/cell/v1.php new file mode 100644 index 0000000000..3914ea6ee1 --- /dev/null +++ b/lib/zin/wg/cell/v1.php @@ -0,0 +1,49 @@ +prop('width'); + $flex = $this->prop('flex'); + if(!empty($width)) + { + $basis = $width; + if(is_numeric($width)) $basis = $width . 'px'; + elseif(preg_match('/^(\d+)\/(\d+)$/', $width, $matches) !== 0) $basis = ((int)$matches[1] / (int)$matches[2] * 100) . '%'; + } + if(!empty($flex)) + { + if(strpos($flex, ' ') !== false) $style['flex'] = $flex; + else $class[] = "flex-$flex"; + } + + $style = array(); + $style['order'] = $this->prop('order'); + $style['flex-grow'] = $this->prop('grow'); + $style['flex-shrink'] = $this->prop('shrink'); + $style['flex-basis'] = $basis; + $style['align-self'] = $this->prop('align'); + + return div + ( + setClass($class), + setStyle($style), + set($this->getRestProps()), + $this->children() + ); + } +} diff --git a/lib/zin/wg/center/v1.php b/lib/zin/wg/center/v1.php new file mode 100644 index 0000000000..74255b09e9 --- /dev/null +++ b/lib/zin/wg/center/v1.php @@ -0,0 +1,16 @@ +getRestProps()), + $this->children() + ); + } +} diff --git a/lib/zin/wg/checkbox/v1.php b/lib/zin/wg/checkbox/v1.php new file mode 100644 index 0000000000..c1cb7af0e5 --- /dev/null +++ b/lib/zin/wg/checkbox/v1.php @@ -0,0 +1,75 @@ +props->has('text')) + { + $this->props->set('text', $child); + return false; + } + } + + protected function buildPrimary() + { + list($id, $text, $name, $checked, $disabled, $type, $typeClass, $rootClass, $labelClass, $value) = $this->prop(array('id', 'text', 'name', 'checked', 'disabled', 'type', 'typeClass', 'rootClass', 'labelClass', 'value')); + + if(empty($typeClass)) $typeClass = $type; + if(empty($id)) $id = $name . '_' . $value; + + return div + ( + setClass("$typeClass-primary", $rootClass, array('disabled' => $disabled)), + h::input + ( + set::type($type), + set::id($id), + set::name($name), + set::checked($checked), + set($this->props->skip('text,primary,typeClass,rootClass,id,labelClass')), + ), + h::label + ( + set::for($id), + setClass($labelClass), + $text, + ), + $this->children() + ); + } + + protected function build(): wg + { + if($this->prop('primary')) return $this->buildPrimary(); + list($text, $type, $typeClass) = $this->prop(array('text', 'type', 'typeClass')); + + return h::label + ( + setClass(empty($typeClass) ? $type : $typeClass), + h::input + ( + set::type($type), + set($this->props->skip('text,primary,typeClass')), + ), + is_string($text) ? span($text, set::class('text')) : $text, + $this->children() + ); + } +} diff --git a/lib/zin/wg/checkboxgroup/css/v1.css b/lib/zin/wg/checkboxgroup/css/v1.css new file mode 100644 index 0000000000..5c16e7b8ea --- /dev/null +++ b/lib/zin/wg/checkboxgroup/css/v1.css @@ -0,0 +1 @@ +.checkbox-list {border-left: 1px solid var(--color-gray-400);} diff --git a/lib/zin/wg/checkboxgroup/js/v1.js b/lib/zin/wg/checkboxgroup/js/v1.js new file mode 100644 index 0000000000..d54bafadf8 --- /dev/null +++ b/lib/zin/wg/checkboxgroup/js/v1.js @@ -0,0 +1,41 @@ +window.handleCheckboxGroupClick = function(event) +{ + const $target = $(event.target); + const $checkboxGroup = $target.closest('.checkbox-group'); + if($target.closest('.checkbox-title').length > 0) + { + const $checkboxTitle = $target.closest('.checkbox-title'); + $checkboxGroup + .find('.checkbox-child') + .prop('checked', $checkboxTitle.prop('checked')); + return; + } + + if($target.closest('.checkbox-child').length > 0) + { + let checkedCount = 0; + const $checkboxChildren = $checkboxGroup.find('.checkbox-child'); + $checkboxChildren.each((_i, input) => + { + if(input.checked === true) checkedCount++; + }); + + const checkboxTitle = $checkboxGroup.find('.checkbox-title')[0]; + if(checkedCount === 0) + { + checkboxTitle.checked = false; + checkboxTitle.indeterminate = false; + } + else if(checkedCount === $checkboxChildren.length) + { + $checkboxGroup.find('.checkbox-title').prop('checked', true); + checkboxTitle.checked = true; + checkboxTitle.indeterminate = false; + } + else + { + checkboxTitle.checked = false; + checkboxTitle.indeterminate = true; + } + } +} diff --git a/lib/zin/wg/checkboxgroup/v1.php b/lib/zin/wg/checkboxgroup/v1.php new file mode 100644 index 0000000000..be6cb382a6 --- /dev/null +++ b/lib/zin/wg/checkboxgroup/v1.php @@ -0,0 +1,67 @@ + false, + 'disabled' => false, + ); + + public static function getPageCSS(): string|false + { + return file_get_contents(__DIR__ . DS . 'css' . DS . 'v1.css'); + } + + public static function getPageJS(): string|false + { + return file_get_contents(__DIR__ . DS . 'js' . DS . 'v1.js'); + } + + private function buildTitle(): wg + { + $title = array_merge(self::$checkboxProps, $this->prop('title')); + return checkbox(set($title), setClass('checkbox-title')); + } + + private function buildCheckboxList(): wg + { + $items = $this->prop('items'); + $title = array_merge(self::$checkboxProps, $this->prop('title')); + $list = ul(setClass('flex', 'flex-wrap', 'ml-1.5', 'checkbox-list', 'pl-3')); + foreach($items as $item) + { + $item = array_merge(self::$checkboxProps, $item); + if($title['checked'] === true) $item['checked'] = true; + if($title['disabled'] === true) $item['disabled'] = true; + $list->add + ( + li + ( + setClass('basis-1/2'), + checkbox(set($item), setClass('checkbox-child')) + ) + ); + } + return $list; + } + + public function build(): wg + { + return div + ( + set('data-on', 'click'), + set('data-call', 'window.handleCheckboxGroupClick'), + set('data-params', 'event'), + setClass('checkbox-group'), + $this->buildTitle(), + $this->buildCheckboxList(), + ); + } +} diff --git a/lib/zin/wg/checklist/v1.php b/lib/zin/wg/checklist/v1.php new file mode 100644 index 0000000000..8470b3735a --- /dev/null +++ b/lib/zin/wg/checklist/v1.php @@ -0,0 +1,66 @@ +prop('value'); + if(is_null($value)) return array(); + + if($this->prop('type') === 'checkbox') return is_array($value) ? $value : explode(',', $value); + return [$value]; + } + + public function onBuildItem($item): checkbox + { + if($item instanceof item) $item = $item->props->toJsonData(); + + if(!isset($item['checked'])) + { + $value = isset($item['value']) ? $item['value'] : ''; + $valueList = $this->getValueList(); + + $item['checked'] = in_array($value, $valueList); + } + + $props = $this->props->pick(['primary', 'type', 'name']); + return new checkbox(set($props), set($item)); + } + + protected function build(): wg + { + list($items, $inline) = $this->prop(['items', 'inline']); + + if(!empty($items)) + { + $valueList = $this->getValueList(); + foreach($items as $key => $item) + { + if(!is_array($item)) $item = array('text' => $item, 'value' => $key); + if(!isset($item['checked'])) $item['checked'] = in_array($item['value'], $valueList); + $items[$key] = $this->onBuildItem($item); + } + } + + return div + ( + setClass($inline ? 'check-list-inline' : 'check-list'), + set($this->getRestProps()), + $items, + $this->children() + ); + } +} diff --git a/lib/zin/wg/col/v1.php b/lib/zin/wg/col/v1.php new file mode 100644 index 0000000000..cfa83ee973 --- /dev/null +++ b/lib/zin/wg/col/v1.php @@ -0,0 +1,26 @@ +prop(array('justify', 'align')); + if(!empty($justify)) $classList .= ' justify-' . $justify; + if(!empty($align)) $classList .= ' items-' . $align; + + return div + ( + setClass($classList), + set($this->getRestProps()), + $this->children() + ); + } +} diff --git a/lib/zin/wg/collapsebtn/v1.php b/lib/zin/wg/collapsebtn/v1.php new file mode 100644 index 0000000000..9d5b3665cb --- /dev/null +++ b/lib/zin/wg/collapsebtn/v1.php @@ -0,0 +1,39 @@ +prop('target'); + $parent = $this->prop('parent'); + + return btn + ( + setClass('btn-link', 'collapse-btn'), + set($this->getRestProps()), + set::icon('angle-down'), + on::click + ( + << + * @package zin + * @link http://www.zentao.net + */ + +namespace zin; + +/** + * 颜色选择器(colorPicker)部件类 + * The colorPicker widget class + */ +class colorPicker extends wg +{ + /** + * Define widget properties. + * + * @var array + * @access protected + */ + protected static array $defineProps = array( + 'id?: string="$GID"', // 组件根元素的 ID。 + 'formID?: string', // 组件隐藏的表单元素 ID。 + 'className?: string|array', // 类名。 + 'style?: array', // 样式。 + 'tagName?: string', // 组件根元素的标签名。 + 'attrs?: array', // 附加到组件根元素上的属性。 + 'clickType?: "toggle"|"open"', // 点击类型,`toggle` 表示点击按钮时切换显示隐藏,`open` 表示点击按钮时只打。 + 'afterRender?: function', // 渲染完成后的回调函数。 + 'beforeDestroy?: function', // 销毁前的回调函数。 + 'name?: string', // 作为表单项的名称。 + 'value?: string|string[]', // 默认值。 + 'onChange?: function', // 值变更回调函数。 + 'disabled?: boolean', // 是否禁用。 + 'multiple?: boolean|number=false', // 是否允许选择多个值,如果指定为数字,则限制多选的数目,默认 `false`。 + 'required?: boolean', // 是否必选(不允许空值,不可以被清除)。 + 'items?: string | string[]', // 颜色选项列表。 + 'icon?: string|array="color"', // 将触发按钮显示为图标。 + 'syncValue?: string', // 指定选择器同步颜色值作为文本到的元素。 + 'syncColor?: string', // 指定选择器同步文字颜色到的元素。 + 'syncBackground?: string', // 指定选择器同步背景颜色到的元素。 + 'syncBorder?: string', // 指定选择器同步边框颜色到的元素。 + 'hint?: string', // 提示文字。 + 'closeBtn?: boolean', // 是否在弹出面板上显示关闭按钮。 + 'heading?: ComponentChildren' // 弹出面板的标题。 + ); + + /** + * Build widget. + * + * @access protected + */ + protected function build(): wg + { + list($props, $restProps) = $this->props->split(array_keys(static::definedPropsList())); + if(isset($props['id'])) + { + $props['_id'] = $props['id']; + unset($props['id']); + } + + if(!isset($props['items'])) + { + global $app, $lang; + $moduleName = $app->getModuleName(); + if(isset($lang->$moduleName->colorList)) $props['items'] = $lang->$moduleName->colorList; + } + return zui::colorPicker + ( + set::_class('form-group-wrapper'), + set::_map(array('value' => 'defaultValue', 'items' => 'colors', 'formID' => 'id')), + set::_props($restProps), + set($props), + $this->children(), + ); + } +} diff --git a/lib/zin/wg/commentbtn/v1.php b/lib/zin/wg/commentbtn/v1.php new file mode 100644 index 0000000000..b8d26f08d5 --- /dev/null +++ b/lib/zin/wg/commentbtn/v1.php @@ -0,0 +1,42 @@ +prop('dataTarget'); + $dataUrl = $this->prop('dataUrl'); + $dataType = $this->prop('dataType'); + $props = parent::getProps(); + + $props['data-toggle'] = 'modal'; + $props['data-type'] = $dataType; + $props['data-url'] = $dataUrl; + $props['data-target'] = $dataTarget; + + return $props; + } +} diff --git a/lib/zin/wg/commentdialog/v1.php b/lib/zin/wg/commentdialog/v1.php new file mode 100644 index 0000000000..b28da9b9de --- /dev/null +++ b/lib/zin/wg/commentdialog/v1.php @@ -0,0 +1,35 @@ +prop('title'); + $name = $this->prop('name'); + $url = $this->prop('url'); + $method = $this->prop('method'); + if(empty($title)) $title = $lang->action->create; + + return modal + ( + set::id('comment-dialog'), + set::title($title), + commentForm + ( + set::url($url), + set::method($method), + set::name($name), + ) + ); + } +} diff --git a/lib/zin/wg/commentform/v1.php b/lib/zin/wg/commentform/v1.php new file mode 100644 index 0000000000..1e39d90bfa --- /dev/null +++ b/lib/zin/wg/commentform/v1.php @@ -0,0 +1,40 @@ +prop('url'); + $name = $this->prop('name'); + $method = $this->prop('method'); + if(empty($name)) $name = 'comment'; + + return form + ( + set::url($url), + set::method($method), + setClass('comment-form'), + editor + ( + setID($name), + set::name($name) + ), + set::actions + ( + array( + 'submit', + array('data-dismiss' => 'modal', 'text' => $lang->close) + ) + ) + ); + } +} diff --git a/lib/zin/wg/control/v1.php b/lib/zin/wg/control/v1.php new file mode 100644 index 0000000000..215b56a420 --- /dev/null +++ b/lib/zin/wg/control/v1.php @@ -0,0 +1,167 @@ +prop('id') === null && $this->prop('name') !== null) + { + $name = $this->prop('name'); + $id = substr($name, -2) == '[]' ? substr($name, 0, - 2) : $name; + $this->setProp('id', $id); + } + } + + /** + * Build control with static content. + * + * @return wg + */ + protected function buildStatic(): wg + { + return div + ( + set::class('form-control-static'), + set($this->props->skip(array('type', 'name', 'value', 'required', 'disabled', 'placeholder', 'items', 'required'))), + set('data-name', $this->prop('name')), + $this->prop('value') + ); + } + + protected function buildTextarea(): wg + { + return new textarea(set($this->props->skip('type'))); + } + + protected function buildInputControl(): wg + { + $controlProps = array(); + $allProps = $this->props->skip('type'); + $propsNames = array_keys(inputControl::definedPropsList()); + + foreach($propsNames as $propName) + { + if(!isset($allProps[$propName])) continue; + + $controlProps[$propName] = $allProps[$propName]; + unset($allProps[$propName]); + } + + return new inputControl + ( + set($controlProps), + new input(set($allProps)), + ); + } + + protected function buildCheckbox(): wg + { + if($this->hasProp('items')) return $this->buildCheckList(); + return new checkList + ( + new checkbox(set($this->props->skip('type'))) + ); + } + + protected function buildCheckList(): wg + { + return new checkList + ( + set($this->props->skip('type')) + ); + } + + protected function buildRadioList(): wg + { + return new radioList + ( + set($this->props->skip('type')) + ); + } + + protected function buildCheckListInline(): wg + { + return new checkList + ( + set::inline(true), + set($this->props->skip('type')) + ); + } + + protected function buildRadioListInline(): wg + { + return new radioList + ( + set::inline(true), + set($this->props->skip('type')) + ); + } + + protected function buildDate(): wg + { + return new datePicker(set($this->props->skip('type'))); + } + + protected function buildTime(): wg + { + return new timePicker(set($this->props->skip('type'))); + } + + protected function buildPri(): wg + { + return new priPicker(set($this->props->skip('type'))); + } + + protected function buildSeverity(): wg + { + return new severityPicker(set($this->props->skip('type'))); + } + + protected function buildColor(): wg + { + return new colorPicker(set($this->props->skip('type'))); + } + + protected function build(): wg + { + $type = $this->prop('type'); + if(empty($type)) $type = $this->hasProp('items') ? 'picker' : 'text'; + + $methodName = "build{$type}"; + if(method_exists($this, $methodName)) return $this->$methodName(); + + $wgName = "\\zin\\$type"; + if(class_exists($wgName)) return new $wgName(set($this->props->skip('type')), $this->children()); + + return input(set($this->props)); + } +} diff --git a/lib/zin/wg/dashboard/v1.php b/lib/zin/wg/dashboard/v1.php new file mode 100644 index 0000000000..105877f9b8 --- /dev/null +++ b/lib/zin/wg/dashboard/v1.php @@ -0,0 +1,60 @@ + + * @package zin + * @link http://www.zentao.net + */ + +namespace zin; + +/** + * 仪表盘(dashboard)部件类。 + * The dashboard widget class. + * + * @author Hao Sun + */ +class dashboard extends wg +{ + /** + * Define widget properties. + * + * @var array + * @access protected + */ + protected static array $defineProps = array( + 'id?: string', // ID。 + 'cache?: bool|string', // 是否启用缓存。 + 'responsive?: bool', // 是否启用响应式。 + 'blocks: array', // 区块列表。 + 'grid?: int', // 栅格数。 + 'gap?: int', // 间距。 + 'leftStop?: int', // 区块水平停靠间隔。 + 'cellHeight?: int', // 网格高度。 + 'blockFetch?: string|function|array', // 区块数据获取 url 或选项。 + 'blockDefaultSize?: array', // 区块默认大小。 + 'blockSizeMap?: array', // 区块大小映射。 + 'blockMenu?: array', // 区块菜单。 + 'onLayoutChange?: function' // 布局变更事件。 + ); + + static $dashboardID = 0; + + protected function created() + { + $this->setDefaultProps(array('id' => static::$dashboardID ? static::$dashboardID : 'dashboard', 'cache' => data('app.user.account'))); + static::$dashboardID++; + } + + /** + * Build widget. + */ + protected function build(): wg + { + return zui::dashboard(set($this->props->skip(array('id'))), set('_id', $this->prop('id'))); + } +} diff --git a/lib/zin/wg/datepicker/v1.php b/lib/zin/wg/datepicker/v1.php new file mode 100644 index 0000000000..49bb22f4f2 --- /dev/null +++ b/lib/zin/wg/datepicker/v1.php @@ -0,0 +1,85 @@ + + * @package zin + * @link http://www.zentao.net + */ +namespace zin; + +require_once dirname(__DIR__) . DS . 'input' . DS . 'v1.php'; + +/** + * 日期选择器(datePicker)部件类 + * The datePicker widget class + */ +class datePicker extends wg +{ + /** + * Define widget properties. + * + * @var array + * @access protected + */ + protected static array $defineProps = array + ( + 'id?: string="$GID"', // 组件根元素的 ID。 + 'formID?: string', // 组件隐藏的表单元素 ID。 + 'className?: string|array', // 类名。 + 'style?: array', // 样式。 + 'tagName?: string', // 组件根元素的标签名。 + 'attrs?: array', // 附加到组件根元素上的属性。 + 'clickType?: "toggle"|"open"', // 点击类型,`toggle` 表示点击按钮时切换显示隐藏,`open` 表示点击按钮时只打。 + 'afterRender?: function', // 渲染完成后的回调函数。 + 'beforeDestroy?: function', // 销毁前的回调函数。 + 'name?: string', // 作为表单项的名称。 + 'value?: string|string[]', // 默认值。 + 'onChange?: function', // 值变更回调函数。 + 'disabled?: boolean', // 是否禁用。 + 'multiple?: boolean|number=false', // 是否允许选择多个值,如果指定为数字,则限制多选的数目,默认 `false`。 + 'required?: boolean', // 是否必选(不允许空值,不可以被清除)。 + 'placeholder?: string', // 选择框上的占位文本。 + 'format?: string', // 日期格式,默认 yyyy-MM-dd。 + 'icon?: string|array="calendar"', // 在输入框右侧显示的图标。 + 'weekNames?: string[]', // 星期名称,索引为 0 表示周日。 + 'monthNames?: string[]', // 月份名称,索引为 0 表示一月份。 + 'yearText?: string', // 用于显示年份的格式化文本。 + 'todayText?: string', // 用于显示“今天”的文本。 + 'clearText?: string', // 用于显示“清除”的文本。 + 'weekStart?: int', // 一周从星期几开始,默认 1。 + 'minDate?: string|int', // 最小可选的日期。 + 'maxDate?: string|int', // 最大可选的日期。 + 'menu?: array', // 左侧显示的菜单设置。 + 'actions?: array', // 底部工具栏设置。 + 'onInvalid?: function', // 日期值无效时的回调函数。 + ); + + /** + * Build the widget. + * + * @access protected + * @return wg + */ + protected function build(): wg + { + list($props, $restProps) = $this->props->split(array_keys(static::definedPropsList())); + if(isset($props['id'])) + { + $props['_id'] = $props['id']; + unset($props['id']); + } + + return zui::datePicker + ( + set::_class('form-group-wrapper'), + set::_map(array('value' => 'defaultValue', 'formID' => 'id')), + set($props), + set::_props($restProps), + $this->children(), + ); + } +} diff --git a/lib/zin/wg/datetimepicker/v1.php b/lib/zin/wg/datetimepicker/v1.php new file mode 100644 index 0000000000..ec89ec91a4 --- /dev/null +++ b/lib/zin/wg/datetimepicker/v1.php @@ -0,0 +1,12 @@ + 'datetime-local' + ); +} diff --git a/lib/zin/wg/detailbody/css/v1.css b/lib/zin/wg/detailbody/css/v1.css new file mode 100644 index 0000000000..857c3bb4d9 --- /dev/null +++ b/lib/zin/wg/detailbody/css/v1.css @@ -0,0 +1 @@ +.detail-body > .form-actions:last-child {margin-top: 0;} diff --git a/lib/zin/wg/detailbody/js/v1.js b/lib/zin/wg/detailbody/js/v1.js new file mode 100644 index 0000000000..18e065eaa0 --- /dev/null +++ b/lib/zin/wg/detailbody/js/v1.js @@ -0,0 +1,9 @@ +$(() => +{ + const $formActions = $('form.detail-body + .form-actions'); + if($formActions.length) + { + $detailBody = $formActions.prev(); + $detailBody.append($formActions); + } +}); diff --git a/lib/zin/wg/detailbody/v1.php b/lib/zin/wg/detailbody/v1.php new file mode 100644 index 0000000000..bbbff10d64 --- /dev/null +++ b/lib/zin/wg/detailbody/v1.php @@ -0,0 +1,82 @@ + array('map' => 'sectionList'), + 'side' => array('map' => 'detailSide'), + 'bottom' => array('map' => 'history,fileList'), + 'floating' => array('map' => 'floatToolbar'), + ); + + public static function getPageCSS(): string|false + { + return file_get_contents(__DIR__ . DS . 'css' . DS . 'v1.css'); + } + + public static function getPageJS(): string|false + { + return file_get_contents(__DIR__ . DS . 'js' . DS . 'v1.js'); + } + + protected function build(): wg + { + $main = $this->block('main'); + $side = $this->block('side'); + $bottom = $this->block('bottom'); + $floating = $this->block('floating'); + $isForm = $this->prop('isForm'); + + if(!$isForm) + { + return div + ( + setClass('detail-body rounded flex gap-1'), + set($this->getRestProps()), + div + ( + setClass('col gap-1 grow'), + $main, + $bottom, + center(setClass('pt-6'), $floating), + ), + $side + ); + } + + return formBase + ( + set::actionsClass('h-14 flex flex-none items-center justify-center shadow'), + setClass('detail-body rounded col overflow-y-hidden bg-white'), + set($this->getRestProps()), + setStyle('height', 'calc(100vh - 120px)'), + div + ( + setClass('flex-auto overflow-y-auto'), + div + ( + setClass('flex'), + setStyle('min-height', '100%'), + div + ( + setClass('col grow'), + $main, + $bottom, + ), + div + ( + setClass('w-1'), + setStyle('background', 'var(--zt-page-bg)'), + ), + $side, + ), + ) + ); + } +} diff --git a/lib/zin/wg/detailheader/v1.php b/lib/zin/wg/detailheader/v1.php new file mode 100644 index 0000000000..d7063b996d --- /dev/null +++ b/lib/zin/wg/detailheader/v1.php @@ -0,0 +1,51 @@ + array(), + 'title' => array(), + 'suffix' => array(), + ); + + private function backBtn(): wg + { + global $lang; + return backBtn + ( + set::icon('back'), + set::type('secondary'), + set::back($this->prop('back')), + set::url($this->prop('backUrl')), + $lang->goback + ); + } + + protected function build(): wg + { + $prefix = $this->block('prefix'); + $title = $this->block('title'); + $suffix = $this->block('suffix'); + + if(empty($prefix) && !isAjaxRequest('modal')) $prefix = $this->backBtn(); + + return div + ( + setClass('detail-header flex justify-between mb-3'), + div + ( + setClass('flex', 'items-center', 'gap-x-4'), + $prefix, + $title, + ), + $suffix + ); + } +} diff --git a/lib/zin/wg/detailside/css/v1.css b/lib/zin/wg/detailside/css/v1.css new file mode 100644 index 0000000000..98a86465fd --- /dev/null +++ b/lib/zin/wg/detailside/css/v1.css @@ -0,0 +1,6 @@ +.detail-side {width: 370px;} +.detail-side .tab-content>.tab-pane {padding-left: 0 !important;} +.detail-side {background: #fff; height: min-content;} +.detail-side .tabs:not(:first-child) {border-top: 1px solid #E6EAF1;} +.detail-side .tabs {padding-top: 12px; padding-bottom: 20px;} +.detail-side > .table-data {margin-top: 16px;} diff --git a/lib/zin/wg/detailside/v1.php b/lib/zin/wg/detailside/v1.php new file mode 100644 index 0000000000..fa3c67ec34 --- /dev/null +++ b/lib/zin/wg/detailside/v1.php @@ -0,0 +1,21 @@ +getRestProps()), + $this->children() + ); + } +} diff --git a/lib/zin/wg/docmenu/css/v1.css b/lib/zin/wg/docmenu/css/v1.css new file mode 100644 index 0000000000..f6695f89cd --- /dev/null +++ b/lib/zin/wg/docmenu/css/v1.css @@ -0,0 +1,19 @@ +.module-menu {max-height: calc(100vh - 105px);} +.module-menu {padding: 0 0 8px;} +.module-menu .active {color: var(--color-primary-600); font-weight: 500;} +.module-menu header a:hover > .icon {color: var(--color-primary-600) !important;} +.module-menu .tree-item * {white-space: nowrap;} + +#docDropmenu .is-leading {display: none;} +#docDropmenu .primary {--tw-ring-color: var(--btn-border-color); background-color: var(--btn-bg); color: inherit; width: 100%;} + +.module-menu .tree .tree-item .tree-link {text-overflow: clip; overflow: hidden; flex: 1 10 auto;} +.module-menu .tree .tree-item .tree-actions {margin-left: 0;} +.module-menu .tree .tree-item .tree-actions .icon {display: none;} +.module-menu .tree .tree-item .tree-item-content:hover .tree-actions .icon {display: block;} +.module-menu .tree .tree-item .tree-item-content .tree-actions .with-popover-show .icon {display: block;} + +.tree > .tree-item > .project-tree-title {font-size: 16px; margin-bottom: 0.5rem;} +.tree > .tree-item > .project-tree-title .text {margin-left: 0.5rem;} +.tree > .tree-item > .project-tree-title .tree-icon {color: var(--nav-active-color); opacity: 1;} +.tree > .tree-item > .project-tree-title > .tree-toggle-icon {display: none;} diff --git a/lib/zin/wg/docmenu/js/v1.js b/lib/zin/wg/docmenu/js/v1.js new file mode 100644 index 0000000000..a840b3e745 --- /dev/null +++ b/lib/zin/wg/docmenu/js/v1.js @@ -0,0 +1,61 @@ +window.saveModule = function() +{ + const name = $(this).val(); + if(!name) return $(this).closest('.tree-item').remove(); + + const {id, type, lib, module} = $(this).data(); + const parentID = $(this).data('parent'); + + const $element = $(`div[data-id='${id}']`); + $.ajaxSubmit({ + url: $.createLink('tree', 'ajaxCreateModule'), + data: { + name : name, + libID : lib, + parentID : parentID, + objectID : id, + moduleType : module, + isUpdate : false, + createType : type, + } + }); +} + +window.addModule = function(id, addType) +{ + const $element = $(`div[data-id='${id}']`); + const {lib, type, module} = $element.data(); + + let parentID = ['docLib'].includes(type) ? '0' : $element.data('parent'); + if(addType == 'child') parentID = id; + + const level = addType == 'same' ? $element.data('level') : $element.data('level') + 1; + const style = `style="margin-left: calc(${level} * var(--tree-indent, 20px))"`; + + let inputTpl = '
  • '; + inputTpl += `
    `; + inputTpl += ``; + inputTpl += '
  • '; + + if(addType == 'same') + { + $(`div[data-id='${id}']`).before(inputTpl); + } + else + { + if(!$element.parent().hasClass('show')) $element.find('.tree-toggle-icon').trigger('click'); + + setTimeout(function() + { + if($element.next('.tree').length == 0) $element.after(``); + + $(`div[data-id='${id}']`).next('.tree').prepend(inputTpl); + }, 1); + } + + setTimeout(function() + { + $('#moduleName').trigger('focus'); + document.getElementById("moduleName").addEventListener('blur', saveModule); + }, 1); +} diff --git a/lib/zin/wg/docmenu/v1.php b/lib/zin/wg/docmenu/v1.php new file mode 100644 index 0000000000..8e61f4d53f --- /dev/null +++ b/lib/zin/wg/docmenu/v1.php @@ -0,0 +1,416 @@ +url; + if(!empty($url)) return $url; + if(in_array($item->type, array('apiLib', 'docLib'))) + { + $this->libID = $item->id; + $this->moduleID = 0; + } + if($item->type == 'module') $this->moduleID = $item->id; + + $linkParams = sprintf($this->linkParams, "libID={$this->libID}&moduleID={$this->moduleID}"); + if(in_array($this->spaceType, array('product', 'project', 'custom'))) $linkParams = "objectID={$this->objectID}&{$linkParams}"; + + $objectType = $this->objectType; + + $moduleName = $this->spaceType == 'api' ? 'api' : 'doc'; + $methodName = ''; + if($this->spaceType == 'api') + { + $methodName = 'index'; + $linkParams = substr($linkParams, 1); + } + else if($item->type == 'annex') + { + $methodName = 'showFiles'; + $linkParams = "type={$objectType}&objectID={$item->objectID}"; + } + else if(in_array($item->type, array('text', 'word', 'ppt', 'excel'))) + { + $methodName = 'view'; + $linkParams = "docID={$this->moduleID}"; + } + else if($objectType == 'execution') + { + $moduleName = 'execution'; + $methodName = 'doc'; + } + else + { + $methodName = $this->spaceMethod[$objectType] ? $this->spaceMethod[$objectType] : 'teamSpace'; + if(in_array($objectType, $this->mineTypes)) + { + $moduleID = $item->id; + if(in_array($item->type, array('docLib', 'annex', 'api', 'execution'))) $moduleID = 0; + + $type = in_array(strtolower($item->type), $this->mineTypes) ? strtolower($item->type) : 'mine'; + $linkParams = "type={$type}&libID={$this->libID}&moduleID={$moduleID}"; + } + if($item->type == 'module' && $item->object == 'api') + { + $linkParams = str_replace(array('browseType=&', 'param=0'), array('browseType=byrelease&', "param={$this->release}"), $linkParams); + } + } + return helper::createLink($moduleName, $methodName, $linkParams); + } + + private function buildMenuTree(array $items, int $parentID = 0): array + { + if(empty($items)) $items = $this->modules; + if(empty($items)) return array(); + + $activeKey = $this->prop('activeKey'); + foreach($items as $setting) + { + $setting->parentID = $parentID; + + $itemID = 0; + if(!in_array(strtolower($setting->type), $this->mineTypes)) $itemID = $setting->id ? $setting->id : $parentID; + + $item = array( + 'key' => $itemID, + 'text' => $setting->name, + 'icon' => $this->getIcon($setting), + 'url' => $this->buildLink($setting), + 'data-id' => $itemID, + 'data-lib' => $setting->type == 'docLib' ? $itemID : $setting->libID, + 'data-type' => $setting->type, + 'data-parent' => $setting->parentID, + 'data-module' => $this->currentModule, + 'active' => zget($setting, 'active', $itemID == $activeKey), + 'actions' => $this->getActions($setting) + ); + + $children = zget($setting, 'children', array()); + if(!empty($children)) + { + $children = $this->buildMenuTree($children, $itemID); + $item['items'] = $children; + } + + $parentItems[] = $item; + } + return $parentItems; + } + + private function setMenuTreeProps(): void + { + global $app, $lang; + $this->lang = $lang; + $this->rawModule = $app->rawModule; + $this->rawMethod = $app->rawMethod; + $this->currentModule = $app->moduleName; + + $this->release = $this->prop('release', 0); + $this->libID = $this->prop('libID'); + $this->moduleID = $this->prop('moduleID'); + $this->modules = $this->prop('modules'); + $this->linkParams = $this->prop('linkParams', '%s'); + $this->spaceType = $this->prop('spaceType', ''); + $this->objectType = $this->prop('objectType', ''); + $this->objectID = $this->prop('objectID', 0); + $this->spaceMethod = $this->prop('spaceMethod'); + + if($this->rawModule == 'api' && $this->rawMethod == 'view') $this->spaceType = 'api'; + if($this->spaceType != 'project') + { + $this->setProp('items', $this->buildMenuTree(array(), $this->libID)); + } + else + { + $items = array(); + $index = 0; + foreach($this->modules as $treeType => $modules) + { + if($treeType == 'project') + { + $treeTitle = $lang->projectCommon; + $treeIcon = 'project'; + } + elseif($treeType == 'execution') + { + $treeTitle = $lang->execution->common; + $treeIcon = 'run'; + } + else + { + $treeTitle = $lang->files; + $treeIcon = 'paper-clip'; + } + $items[] = array( + 'text' => $treeTitle, + 'icon' => $treeIcon, + 'class' => 'project-tree-title ' . ($index > 0 ? 'border-t mt-2 pt-2' : ''), + ); + + $items = array_merge($items, $this->buildMenuTree($modules, $this->libID)); + $index ++; + } + $this->setProp('items', $items); + } + } + + private function getActions($item): array|null + { + if(isset($item->hasAction) && !$item->hasAction) return null; + if(in_array($item->type, array('mine', 'view', 'collect', 'createdBy', 'editedBy'))) return null; + + $actions = $this->getOperateItems($item); + if(empty($actions)) return null; + + return array( + array( + 'key' => 'more', + 'icon' => 'ellipsis-v', + 'type' => 'dropdown', + 'caret' => false, + 'dropdown' => array( + 'placement' => 'bottom-end', + 'items' => $actions, + ) + ) + ); + } + + private function getOperateItems($item): array + { + $menus = array(); + if(in_array($item->type, array('docLib', 'apiLib'))) + { + $itemID = $item->id ? $item->id : $item->parentID; + if(hasPriv($this->currentModule, 'addCatalog')) + { + $menus[] = array( + 'key' => 'adddirectory', + 'icon' => 'add-directory', + 'text' => $this->lang->doc->libDropdown['addModule'], + 'onClick' => jsRaw("() => addModule({$itemID}, 'child')") + ); + } + + if(hasPriv($this->currentModule, 'editCatalog')) + { + $menus[] = array( + 'key' => 'editlib', + 'icon' => 'edit', + 'text' => $this->lang->doc->libDropdown['editLib'], + 'data-toggle' => 'modal', + 'data-url' => createlink($this->currentModule, 'editlib', "libID={$itemID}"), + ); + } + + if(hasPriv($this->currentModule, 'deleteCatalog')) + { + $menus[] = array( + 'key' => 'dellib', + 'icon' => 'trash', + 'text' => $this->lang->doc->libDropdown['deleteLib'], + 'class' => 'ajax-submit', + 'data-url' => createLink($this->currentModule, 'deleteLib', "libID={$itemID}"), + 'data-confirm' => $this->lang->doc->confirmDeleteLib, + ); + } + } + elseif($item->type == 'module') + { + if(hasPriv($this->currentModule, 'addCatalog')) + { + $menus[] = array( + 'key' => 'adddirectory', + 'icon' => 'add-directory', + 'text' => $this->lang->doc->libDropdown['addSameModule'], + 'onClick' => jsRaw("() => addModule({$item->id}, 'same')") + ); + $menus[] = array( + 'key' => 'addsubdirectory', + 'icon' => 'add-directory', + 'text' => $this->lang->doc->libDropdown['addSubModule'], + 'onClick' => jsRaw("() => addModule({$item->id}, 'child')") + ); + } + + if(hasPriv($this->currentModule, 'editCatalog')) + { + $menus[] = array( + 'key' => 'editmodule', + 'icon' => 'edit', + 'text' => $this->lang->doc->libDropdown['editModule'], + 'link' => '', + 'data-toggle' => 'modal', + 'data-url' => createlink($this->currentModule, 'editCatalog', "moduleID={$item->id}&type=" . ($this->rawModule == 'api' ? 'api' : 'doc')), + ); + } + + if(hasPriv($this->currentModule, 'deleteCatalog')) + { + $menus[] = array( + 'key' => 'delmodule', + 'icon' => 'trash', + 'text' => $this->lang->doc->libDropdown['delModule'], + 'class' => 'ajax-submit', + 'data-url' => createLink($this->currentModule, 'deleteCatalog', "rootID={$item->parentID}&moduleID={$item->id}"), + 'data-confirm' => $this->lang->api->confirmDeleteLib, + ); + } + } + + return $menus; + } + + private function getIcon($item): string + { + $type = $item->type; + if($type == 'apiLib') return 'interface-lib'; + if($type == 'docLib') return 'wiki-lib'; + if($type == 'annex') return 'annex-lib'; + if($type == 'execution') return 'execution'; + if($type == 'text') return 'file-text'; + if($type == 'word') return 'file-word'; + if($type == 'ppt') return 'file-powerpoint'; + if($type == 'excel') return 'file-excel'; + return ''; + } + + private function getTitle(): string + { + global $lang; + $activeKey = $this->prop('activeKey'); + + if(empty($activeKey)) return $this->prop('title'); + + foreach($this->modules as $module) + { + if($module->id == $activeKey) return $module->name; + } + + return ''; + } + + private function buildBtns(): wg|null + { + $settingLink = $this->prop('settingLink'); + $settingText = $this->prop('settingText'); + if(!$settingLink) return null; + + global $app; + $lang = $app->loadLang('datatable')->datatable; + $currentModule = $app->rawModule; + $currentMethod = $app->rawMethod; + + if(!$settingText) $settingText = $lang->moduleSetting; + + $datatableId = $app->moduleName . ucfirst($app->methodName); + + return div + ( + setClass('col gap-2 py-3 px-7'), + $settingLink + ? a + ( + setClass('btn'), + setStyle('background', '#EEF5FF'), + setStyle('box-shadow', 'none'), + set('data-app', $app->tab), + set('data-size', 'sm'), + set('data-toggle', 'modal'), + set::href($settingLink), + $settingText + ) + : null, + ); + } + + private function buildCloseBtn(): ?wg + { + $activeKey = $this->prop('activeKey'); + if(empty($activeKey)) return null; + + return a + ( + set('href', $this->prop('closeLink')), + icon('close', setStyle('color', 'var(--color-slate-600)')) + ); + } + + private function buildDropDownMenu() + { + return menu + ( + setID('dropdownMenu'), + set::items(array()) + ); + } + + protected function build(): wg + { + $this->setMenuTreeProps(); + $title = $this->getTitle(); + $menuLink = $this->prop('menuLink', ''); + + return div + ( + setClass('module-menu rounded shadow-sm bg-white col rounded-sm'), + $title && empty($menuLink) ? h::header + ( + setClass('h-10 flex items-center pl-4 flex-none gap-3'), + span + ( + setClass('module-title text-lg font-semibold'), + html($title) + ), + $this->buildCloseBtn(), + ) : null, + $menuLink ? dropmenu + ( + set::id('docDropmenu'), + set::text($title), + set::url($menuLink), + ) : null, + h::main + ( + setClass($menuLink ? 'pt-3' : ''), + setClass('col flex-auto overflow-y-auto overflow-x-hidden pl-4 pr-1'), + zui::tree(set($this->props->pick(array('items', 'activeClass', 'activeIcon', 'activeKey', 'onClickItem', 'defaultNestedShow', 'changeActiveKey', 'isDropdownMenu', 'hover')))) + ), + $this->buildBtns(), + $this->buildDropDownMenu(), + ); + } +} diff --git a/lib/zin/wg/dragul/css/v1.css b/lib/zin/wg/dragul/css/v1.css new file mode 100644 index 0000000000..7d2732e2f1 --- /dev/null +++ b/lib/zin/wg/dragul/css/v1.css @@ -0,0 +1 @@ +.hold {border: 1px dashed #ccc; box-sizing: border-box;} diff --git a/lib/zin/wg/dragul/v1.php b/lib/zin/wg/dragul/v1.php new file mode 100644 index 0000000000..fb7cb027ba --- /dev/null +++ b/lib/zin/wg/dragul/v1.php @@ -0,0 +1,123 @@ +prop('tagName') !== 'li') return false; + + $child->setProp('draggable', 'true'); + return $child; + } + + private function bindDragstartEvent() + { + $func = <<ul->gid}"]'); + const liList = Array.from(ul.children); + ul.dataset.dragIndex = liList.indexOf(dragLi); + DRAGSTART; + + $this->ul->add(on::dragstart($func)); + } + + private function bindDragendEvent() + { + $func = <<ul->add(on::dragend($func)); + } + + private function bindDragoverEvent() + { + $func = <<ul->add(on::dragover($func)); + } + + private function bindDragexitEvent() + { + $func = <<ul->add(on::dragexit($func)); + } + + private function bindDragenterEvent() + { + $func = <<ul->gid}"]'); + const liList = Array.from(ul.children); + ul.dataset.enterIndex = liList.indexOf(enterLi); + DRAGENTER; + $this->ul->add(on::dragenter($func)); + } + + private function bindDragleaveEvent() + { + $func = <<ul->add(on::dragleave($func)); + } + + private function bindDropEvent() + { + $func = <<ul->gid}"]'); + const dragIndex = ul.dataset.dragIndex; + const enterIndex = ul.dataset.enterIndex; + const dragLi = Array.from(ul.children)[ul.dataset.dragIndex]; + const enterLi = Array.from(ul.children)[ul.dataset.enterIndex]; + enterLi.classList.remove('hold'); + if(dragIndex < enterIndex) { + enterLi.after(dragLi); + } else if(dragIndex > enterIndex) { + enterLi.before(dragLi); + } + DROP; + $this->ul->add(on::drop($func)); + } + + protected function build(): wg + { + $ul = ul + ( + setClass('drag-ul'), + set($this->getRestProps()), + $this->children(), + ); + + $ul->setProp('data-zin-gid', $ul->gid); + $this->ul = $ul; + $this->bindDragstartEvent(); + $this->bindDragendEvent(); + $this->bindDragoverEvent(); + $this->bindDragexitEvent(); + $this->bindDragenterEvent(); + $this->bindDragleaveEvent(); + $this->bindDropEvent(); + return $ul; + } +} diff --git a/lib/zin/wg/dropdown/v1.php b/lib/zin/wg/dropdown/v1.php new file mode 100644 index 0000000000..8b9eb2f06a --- /dev/null +++ b/lib/zin/wg/dropdown/v1.php @@ -0,0 +1,165 @@ + array('map' => 'btn,a'), + 'menu' => array('map' => 'menu'), + 'items' => array('map' => 'item') + ); + + protected function build(): array + { + list($items, $placement, $strategy, $offset, $flip, $arrow, $trigger, $menuProps, $target, $id, $menuClass, $hasIcons, $staticMenu) = $this->prop(array('items', 'placement', 'strategy', 'offset', 'flip', 'arrow', 'trigger', 'menu', 'target', 'id', 'menuClass', 'hasIcons', 'staticMenu')); + + $triggerBlock = $this->block('trigger'); + $menu = $this->block('menu'); + $itemsList = $this->block('items'); + + if(empty($id)) $id = $this->gid; + if(empty($target) && empty($items)) $target = "#$id"; + if(empty($menuProps)) $menuProps = array(); + + if(empty($triggerBlock)) $triggerBlock = h::a($this->children()); + elseif(is_array($triggerBlock)) $triggerBlock = $triggerBlock[0]; + $triggerID = ''; + if($triggerBlock instanceof wg) + { + if($triggerBlock instanceof btn) $triggerBlock->setDefaultProps(array('caret' => true)); + $triggerBlock->setProp($this->getRestProps()); + + $triggerProps = array + ( + 'data-target' => $triggerBlock->hasProp('target', 'href') ? null : $target, + 'data-toggle' => 'dropdown', + 'data-placement' => $placement, + 'data-strategy' => $strategy, + 'data-offset' => $offset, + 'data-flip' => $flip, + 'data-arrow' => $arrow, + 'data-trigger' => $trigger + ); + $triggerBlock->setProp($triggerProps); + + $triggerID = $triggerBlock->id(); + if(empty($triggerID)) + { + $triggerID = "$id-toggle"; + $triggerBlock->setProp('id', $triggerID); + } + } + + if(empty($menu)) + { + if($staticMenu) + { + $menu = new menu + ( + setClass('dropdown-menu'), + set::items($items), + divorce($itemsList), + ); + + if($hasIcons === null) + { + if(is_array($items)) + { + foreach($items as $item) + { + if((is_array($item) and isset($item['icon'])) || (($item instanceof wg) && $item->hasProp('icon'))) + { + $hasIcons = true; + break; + } + } + } + if(!$hasIcons) + { + foreach($itemsList as $item) + { + if(($item instanceof wg) && $item->hasProp('icon')) + { + $hasIcons = true; + break; + } + } + } + } + } + else + { + if(empty($items)) $items = array(); + if(!empty($itemsList)) + { + foreach($itemsList as $item) + { + if(!($item instanceof item)) continue; + $items[] = $item->props->toJsonData(); + } + } + foreach($items as $index => $item) + { + if(!isset($item['icon']) || empty($item['icon']) || str_starts_with($item['icon'], 'icon-')) continue; + $items[$index]['icon'] = 'icon-' . $item['icon']; + } + + if(!is_array($menuProps)) $menuProps = array(); + $menuProps['items'] = $items; + + $menu = zui::dropdown + ( + set(array + ( + '_to' => "#$triggerID", + 'trigger' => $trigger, + 'placement' => $placement, + 'strategy' => $strategy, + 'arrow' => $arrow, + 'flip' => $flip, + 'offset' => $offset, + 'target' => $target, + 'className' => $menuClass, + 'hasIcons' => $hasIcons, + 'menu' => $menuProps + )) + ); + } + } + elseif(is_array($menu)) + { + $menu = $menu[0]; + } + + if($menu instanceof menu) + { + $menu->setProp($menuProps); + $menu->setProp('class', $menuClass); + $menu->setProp('id', $id); + if($hasIcons) $menu->setProp('class', 'has-icons'); + } + + return array($triggerBlock, $menu); + } +} diff --git a/lib/zin/wg/dropmenu/css/v1.css b/lib/zin/wg/dropmenu/css/v1.css new file mode 100644 index 0000000000..afabac8977 --- /dev/null +++ b/lib/zin/wg/dropmenu/css/v1.css @@ -0,0 +1,5 @@ +#pick-pop-admin-menu {width: 130px !important;} +#pick-pop-admin-menu .dropmenu-list {padding: 0;} +#pick-pop-admin-menu .admin-menu-item > .dropmenu-item {padding-left: 10px !important;} +#pick-pop-admin-menu .admin-menu-item > .dropmenu-item.active {background-color: unset;} +#pick-pop-admin-menu .admin-menu-item > .dropmenu-item:hover {color: rgba(var(--color-primary-500-rgb),var(--tw-text-opacity)); background-color: rgba(var(--color-primary-50-rgb),var(--tw-bg-opacity));} diff --git a/lib/zin/wg/dropmenu/v1.php b/lib/zin/wg/dropmenu/v1.php new file mode 100644 index 0000000000..e5f8b71d35 --- /dev/null +++ b/lib/zin/wg/dropmenu/v1.php @@ -0,0 +1,104 @@ + + * @package zin + * @link http://www.zentao.net + */ + +namespace zin; + +/** + * 1.5 级导航(dropmenu)部件类。 + * The dropmenu widget class. + * + * @author Hao Sun + */ +class dropmenu extends wg +{ + /** + * Define the properties. + * + * @var array + * @access protected + */ + protected static array $defineProps = array + ( + 'id?: string="dropmenu"', // ID,当页面有多个 dropmenu 时确保有唯一的 ID。 + 'tab?: string,', // 应用名。 + 'module?: string,', // 模块名。 + 'method?: string,', // 方法名。 + 'objectID?: string,', // 对象 ID。 + 'extra?: string,', // 额外参数。 + 'url?: string', // 异步获取下拉菜单选项数据的 URL,如果已经指定 module, method,objectID,extra 等参数则可以忽略。 + 'text?: string', // 选择按钮上显示的文本。 + 'cache?: bool|int=true', // 是否启用缓存。 + 'data?: array', // 手动指定数据。 + 'menuID?: string="$GID"', // 指定下拉菜单的ID。 + ); + + /** + * Load the css file. + * + * @access public + * @return string|false + */ + public static function getPageCSS(): string|false + { + return file_get_contents(__DIR__ . DS . 'css' . DS . 'v1.css'); + } + + /** + * Override the build method. + * + * @access protected + * @return wg + */ + protected function build(): zui + { + list($url, $text, $objectID, $cache, $tab, $module, $method, $extra, $id, $data, $menuID) = $this->prop(array('url', 'text', 'objectID', 'cache', 'tab', 'module', 'method', 'extra', 'id', 'data', 'menuID')); + + $app = data('app'); + $lang = data('lang'); + + if(empty($tab)) $tab = $app->tab; + if(empty($module)) $module = $app->rawModule; + if(empty($method)) $method = $app->rawMethod; + if(empty($extra)) $extra = ''; + if(empty($objectID)) $objectID = data($tab . 'ID'); + if(empty($objectID)) + { + $object = data($tab); + if(isset($object->id)) $objectID = $object->id; + } + + if($tab == 'admin') + { + $currentMenuKey = $app->control->loadModel('admin')->getMenuKey(); + $text = $lang->admin->menuList->{$currentMenuKey}['name']; + $url = createLink('admin', 'ajaxGetDropMenu', "currentMenuKey={$currentMenuKey}"); + $menuID = 'admin-menu'; + } + + if(empty($url) && empty($data)) $url = createLink($tab, 'ajaxGetDropMenu', "objectID=$objectID&module=$module&method=$method&extra=$extra"); + if(empty($text) && !empty($tab) && !empty($objectID)) + { + $object = $app->control->loadModel($tab)->getByID((int)$objectID); + $text = $object->name; + } + + return zui::dropmenu + ( + setID($menuID), + set('_id', $id), + set('_props', array('data-fetcher' => $url)), + set('data', $data), + set(array('fetcher' => $url, 'text' => $text, 'defaultValue' => $objectID, 'cache' => $cache)), + set($this->getRestProps()) + ); + } +} diff --git a/lib/zin/wg/dtable/css/v1.css b/lib/zin/wg/dtable/css/v1.css new file mode 100644 index 0000000000..5f1714c784 --- /dev/null +++ b/lib/zin/wg/dtable/css/v1.css @@ -0,0 +1,5 @@ +.menu.menu-dtable-actions {display: flex; min-width: auto;} +.menu.menu-dtable-actions .icon {position: static!important; opacity: 1!important;} +.menu.menu-dtable-actions span.text {display: none;} +.menu.menu-dtable-actions > .menu-item > a {padding: 0;} +.menu.menu-dtable-actions > .menu-item > a:hover {background: none; color: var(--menu-hover-bg);} diff --git a/lib/zin/wg/dtable/v1.php b/lib/zin/wg/dtable/v1.php new file mode 100644 index 0000000000..86d102a5f0 --- /dev/null +++ b/lib/zin/wg/dtable/v1.php @@ -0,0 +1,184 @@ +rawModule-$app->rawMethod"; + $this->setDefaultProps(array('id' => static::$dtableID ? ($defaultID . static::$dtableID) : $defaultID)); + static::$dtableID++; + + if($this->prop('customCols') === true) + { + $app->loadLang('datatable'); + $this->setProp('customCols', array( + 'custom' => array( + 'url' => createLink('datatable', 'ajaxcustom', "module=$app->moduleName&method=$app->methodName"), + 'text' => $app->lang->datatable->custom + ), + 'setGlobal' => array( + 'url' => createLink('datatable', 'ajaxsaveglobal', "module={$app->moduleName}&method={$app->methodName}"), + 'text' => $app->lang->datatable->setGlobal, + ), + 'reset' => array( + 'url' => createLink('datatable', 'ajaxreset', "module={$app->moduleName}&method={$app->methodName}"), + 'text' => $app->lang->datatable->reset, + ), + 'resetGlobal' => array( + 'url' => createLink('datatable', 'ajaxreset', "module={$app->moduleName}&method={$app->methodName}&system=1"), + 'text' => $app->lang->datatable->resetGlobal, + ), + )); + } + + $module = $this->prop('module', $app->rawModule); + if(!isset($app->lang->$module)) $app->loadLang($module); + + /* Set col default name and title. */ + $colConfigs = $this->prop('cols'); + foreach($colConfigs as $field => &$config) + { + if(is_object($config)) $config = (array)$config; + + if(!isset($config['name'])) $config['name'] = $field; + if(!isset($config['title'])) $config['title'] = zget($app->lang->{$module}, $config['name'], zget($app->lang, $config['name'])); + if(isset($config['link']) && is_array($config['link'])) $config['link'] = $this->getLink($config['link']); + if(isset($config['assignLink']) && is_array($config['assignLink'])) $config['assignLink'] = $this->getLink($config['assignLink']); + + if(!empty($config['type']) && $config['type'] == 'control') + { + if(!empty($config['control']) && is_string($config['control'])) $config['control'] = array('type' => $config['control']); + + if(isset($config['controlItems'])) + { + if(empty($config['control'])) $config['control'] = array('type' => 'picker'); + + $items = $config['controlItems']; + $newItems = array(); + foreach($items as $key => $value) + { + if(is_numeric($key) && is_array($value)) $newItems[] = $value; + else $newItems[] = array('text' => $value, 'value' => $key); + } + $config['control']['props']['items'] = $newItems; + unset($config['controlItems']); + } + } + + if(!empty($config['actionsMap'])) + { + foreach($config['actionsMap'] as &$action) + { + if(isset($action['data-toggle']) && !isset($action['data-position'])) $action['data-position'] = 'center'; + + if(!empty($action['ajaxSubmit'])) + { + if(empty($action['className'])) $action['className'] = 'ajax-submit'; + if(!isset($action['data-confirm'])) $action['data-confirm'] = zget($app->lang->$module, 'confirmDelete'); + } + } + } + } + $this->setProp('cols', array_values($colConfigs)); + + $tableData = $this->prop('data', array()); + $this->setProp('data', array_values($tableData)); + + /* Add dtable load info to pager links. */ + $pager = $this->prop('footPager'); + if(!empty($pager) && isset($pager['items'])) + { + if(!isset($pager['btnProps'])) $pager['btnProps'] = array('data-load' => 'table', 'type' => 'ghost', 'size' => 'sm'); + foreach($pager['items'] as $index => $item) + { + if($item['type'] !== 'size-menu') continue; + if(isset($item['itemProps'])) $pager['items'][$index]['itemProps']['data-load'] = 'table'; + else $pager['items'][$index]['itemProps'] = array('data-load' => 'table'); + } + $this->setProp('footPager', $pager); + } + } + + public static function getPageCSS(): string|false + { + return file_get_contents(__DIR__ . DS . 'css' . DS . 'v1.css'); + } + + /** + * 获取字段链接。 + * Get link to the field. + * + * @param array $setting + * @access protected + * @return array|string + */ + protected function getLink(array $setting): array|string + { + if(!empty($setting['url'])) + { + $url = $setting['url']; + if(!empty($url['module']) && !empty($url['method'])) + { + $setting['url'] = ''; + if(hasPriv($url['module'], $url['method'])) $setting['url'] = createLink($url['module'], $url['method'], zget($url, 'params', ''), '', !empty($setting['onlybody'])); + } + return $setting; + } + else if(!empty($setting['module']) && !empty($setting['method'])) + { + if(!hasPriv($setting['module'], $setting['method'])) return ''; + return createLink($setting['module'], $setting['method'], zget($setting, 'params', ''), '', !empty($setting['onlybody'])); + } + return $setting; + } + + protected function build(): wg + { + if(empty($this->prop('data'))) + { + global $lang; + $createLink = !empty($this->prop('createLink')) ? $this->getLink($this->prop('createLink')) : ''; + return div + ( + setClass('canvas text-center py-8'), + p + ( + setClass('py-8 my-8'), + span + ( + setClass('text-gray'), + !empty($this->prop('emptyTip')) ? $this->prop('emptyTip') : $lang->noData, + ), + !empty($createLink) + ? a + ( + setClass('btn primary-pale bd-primary ml-0.5'), + set::href($createLink), + icon('plus'), + !empty($this->prop('createTip')) ? $this->prop('createTip') : $lang->create, + ) + : '', + ) + ); + } + return zui::dtable(inherit($this)); + } +} diff --git a/lib/zin/wg/dynamic/css/v1.css b/lib/zin/wg/dynamic/css/v1.css new file mode 100644 index 0000000000..4b8c6417fc --- /dev/null +++ b/lib/zin/wg/dynamic/css/v1.css @@ -0,0 +1,27 @@ +.dynamic > li {position: relative; list-style: none} +.dynamic > li:before, .dynamic > li > div:after {position: absolute; left: -20px; display: block; width: 15px; height: 15px; content: ' '; border-radius: 50%} +.dynamic > li:before {top: 12px; left: -17px; z-index: 3; width: 10px; height: 10px; background-color: #c4c4c4; border: none; border: 2px solid #fff;} +.dynamic > li > div:after {top: 17px; left: -15px; z-index: 3; width: 6px; height: 6px; background-color: var(--color-gray-300); border-radius: 50%; opacity: 0} +.dynamic > li.blue > div:after {background-color: #2e7fff;} +.dynamic > li.green > div:after {background-color: rgb(25, 190, 131);} +.dynamic > li.yellow > div:after {background-color: rgb(255, 210, 169);} +.dynamic > li.trophy > div:after {background-color: unset;} +.dynamic > li:after {position: absolute; top: 12px; bottom: -13px; left: -13px; z-index: 1; display: block; content: ' '; border-left: 2px solid #eee;} +.dynamic > li > div:after {opacity: 1} +.dynamic > li:before {top: 12px; left: -20px; width: 16px; height: 16px; background-color: var(--color-slate-100); border: none;} +.dynamic > li.trophy:before {background: url('static/svg/trophy.svg') no-repeat; width: 18px; height: 18px; background-size: 100%; left: -21px; top: 13px; background-color: var(--color-slate-100);} +.dynamic > li > div {display: block; padding: 3px 0px; line-height: 20px} +.dynamic > li .dynamic-text {padding: 2px 5px;} +.dynamic > li.trophy .dynamic-text {background: #fff;} +.dynamic > li.trophy .dynamic-text img {opacity: 0.2; transform: rotate(6deg); position: relative; max-width: 2.25rem; top: -15px; left: -35px; width: 2.25rem; height: 2.25rem;} + +.dynamic-tag {position: absolute; top: 5px; left: -110px; font-size: 12px} +.dynamic-tag-left {padding-left: 110px;} +.dynamic-tag.has-time {left: -65px;} +.has-time .dynamic-tag-left {padding-left: 65px;} + +.dynamic-sm {font-size: 12px} +.dynamic-sm > li:before, .dynamic-sm > li > div:after {top: 10px; left: -20px; width: 11px; height: 11px} +.dynamic-sm > li.blue:before, .dynamic-sm > li:before {top: 10px; left: -18px; width: 11px; height: 11px; background: 0 0; border: 1px solid #eee} +.dynamic-sm > li > div {line-height: 20px} +.dynamic-sm > li > div:after {top: 13px; left: -15px; width: 5px; height: 5px} diff --git a/lib/zin/wg/dynamic/v1.php b/lib/zin/wg/dynamic/v1.php new file mode 100644 index 0000000000..7b4408bf61 --- /dev/null +++ b/lib/zin/wg/dynamic/v1.php @@ -0,0 +1,137 @@ +action); + $objectType = strtolower($dynamic->objectType); + + // if($dynamic->major) return 'active'; + + if($objectType == 'release' && $action == 'opened') return 'trophy'; + if($objectType == 'project' && $action == 'closed') return 'trophy'; + + if(strpos($action, 'assigned') !== false) return 'blue'; + if(strpos($action, 'finished') !== false || strpos($action, 'resolved') !== false || ($action == 'closed' && $objectType != 'product')) return 'green'; + + return ''; + } + + protected function dynamicItem(object $dynamic, array $users): wg + { + global $config; + $dynamicLabel = zget($dynamic, 'dynamicLabel', ''); + if(empty($dynamicLabel)) $dynamicLabel = zget($dynamic, 'actionLabel', ''); + + $objectLabel = array(); + if($dynamic->action != 'login' && $dynamic->action != 'logout') + { + $objectLabel[] = span + ( + $dynamic->objectLabel, + ); + $objectID = $dynamic->objectID && strpos(',module,chartgroup,', ",$dynamic->objectType,") !== false && strpos(',created,edited,moved,', "$dynamic->action") !== false ? trim($dynamic->extra, ',') : $dynamic->objectID; + $objectLabel[] = $objectID ? span + ( + setClass('label light-outline mx-2 font-sm'), + $objectID, + ) : null; + + if(($config->edition == 'max' && strpos($config->action->assetType, ",{$dynamic->objectType},") !== false) && empty($dynamic->objectName)) + { + $objectLabel[] = span("#{$dynamic->objectID}"); + } + elseif(empty($dynamic->objectID) and $dynamic->extra) + { + $objectLabel[] = span("#{$dynamic->extra}"); + } + elseif(empty($dynamic->objectLink)) + { + $objectLabel[] = span($dynamic->objectName); + } + else + { + $objectLabel[] = a + ( + set::href($dynamic->objectLink), + set::title($dynamic->objectName), + $dynamic->objectName + ); + } + } + + $dynamicClass = $this->getStatusClass($dynamic); + return li + ( + setClass($dynamicClass), + div + ( + span + ( + setClass('dynamic-tag p-1 text-gray'), + isset($dynamic->time) ? $dynamic->time : $dynamic->date, + ), + div + ( + setClass('dynamic-text flex flex-nowrap justify-between items-center'), + div + ( + setClass('clip p-1'), + zget($users, $dynamic->actor), + span + ( + setClass('text-gray px-1'), + $dynamicLabel + ), + $objectLabel, + ), + $dynamicClass == 'trophy' ? + div + ( + setClass('w-0 h-0'), + h::img + ( + set::src('static/svg/trophy.svg'), + setClass('ml-2'), + ) + ) : null + ) + ) + ); + } + + protected function build(): wg + { + $users = $this->prop('users', (array)data('users')); + $dynamics = $this->prop('dynamics', (array)data('dynamics')); + $hasTime = !empty($dynamisc) && isset(reset($dynamics)->time) ? 'has-time' : ''; + + $dynamicListView = h::ul + ( + setClass('dynamic dynamic-tag-left pt-0 overflow-hidden has-time'), + setClass($this->prop('className')), + ); + + foreach($dynamics as $dynamic) + { + if($dynamic->action == 'adjusttasktowait') continue; + $dynamicListView->add($this->dynamicItem($dynamic, $users)); + } + + return $dynamicListView; + } +} diff --git a/lib/zin/wg/echarts/v1.php b/lib/zin/wg/echarts/v1.php new file mode 100644 index 0000000000..a1fc35a255 --- /dev/null +++ b/lib/zin/wg/echarts/v1.php @@ -0,0 +1,34 @@ +setProp('_size', array($width, $height)); + return $this; + } + + public function theme(string|array $value): echarts + { + $this->setProp('theme', $value); + return $this; + } + + public function responsive(bool $value): echarts + { + $this->setProp('responsive', $value); + return $this; + } + + protected function build(): zui + { + global $app; + $jsFile = $app->getWebRoot() . 'js/echarts/echarts.common.min.js'; + + return zui::echarts(inherit($this), set::_call("~((name,selector,options) => $.getScript('$jsFile', null, () => zui.create(name,selector,options)))")); + } +} diff --git a/lib/zin/wg/editor/css/v1.css b/lib/zin/wg/editor/css/v1.css new file mode 100644 index 0000000000..911b4b5122 --- /dev/null +++ b/lib/zin/wg/editor/css/v1.css @@ -0,0 +1,12 @@ +textarea[size="sm"], +tiptap-editor[size="sm"] {min-height: 142px;} +textarea[size="lg"], +tiptap-editor[size="lg"] {min-height: 250px;} + +[data-tippy-root] tiptap-menu-item > button {font-size: inherit; font-family: Arial; line-height: normal;} + +.editor-container {height: auto; width: 100%;} +.editor-container tiptap-editor {display: none;} +.editor-container textarea {height: initial;} +[data-loaded-editor] .editor-container tiptap-editor {display: block;} +[data-loaded-editor] .editor-container textarea {display: none;} diff --git a/lib/zin/wg/editor/v1.php b/lib/zin/wg/editor/v1.php new file mode 100644 index 0000000000..51b5b664fc --- /dev/null +++ b/lib/zin/wg/editor/v1.php @@ -0,0 +1,69 @@ +getWebRoot() . 'js/zeneditor/tiptap-component.esm.js'; + $jsFile = 'https://zui-dist.oop.cc/zeneditor/tiptap-component.esm.js'; + return '$.getScript("' . $jsFile . '", {type: "module"}, () => {document.body.dataset.loadedEditor = true;});'; + } + + protected function build(): wg + { + $editor = new h + ( + setTag('tiptap-editor'), + setClass('form-control', 'p-0', 'h-auto'), + ); + $props = $this->props->pick(array('createInput', 'uploadUrl', 'placeholder', 'fullscreenable', 'resizable', 'exposeEditor', 'size', 'hideMenubar', 'bubbleMenu', 'menubarMode', 'collaborative', 'hocuspocus', 'docName', 'username', 'userColor')); + foreach($props as $key => $value) + { + if($value === true || (is_string($value) && !empty($value))) $editor->add(set(uncamelize($key), $value)); + } + + $customProps = $this->getRestProps(); + if(!isset($customProps['id'])) $customProps['id'] = $customProps['name']; + if(!isset($customProps['class'])) $customProps['class'] = 'w-full'; + + $editor->add(set($customProps)); + $editor->add($this->prop('value')); + $editor->add($this->children()); + + return div + ( + setClass('editor-container'), + $editor, + textarea + ( + $this->prop('value'), + set::rows(1), + set::size($props['size']) + ), + ); + } +} diff --git a/lib/zin/wg/entitylabel/css/v1.css b/lib/zin/wg/entitylabel/css/v1.css new file mode 100644 index 0000000000..64ce09e6a7 --- /dev/null +++ b/lib/zin/wg/entitylabel/css/v1.css @@ -0,0 +1 @@ +.entity-label .label {box-shadow: none; border: 1px solid var(--color-slate-300); color: var(--color-slate-700); background: none;} diff --git a/lib/zin/wg/entitylabel/v1.php b/lib/zin/wg/entitylabel/v1.php new file mode 100644 index 0000000000..fedb6643ab --- /dev/null +++ b/lib/zin/wg/entitylabel/v1.php @@ -0,0 +1,105 @@ + array(), + 'suffix' => array() + ); + + public static function getPageCSS(): string|false + { + return file_get_contents(__DIR__ . DS . 'css' . DS . 'v1.css'); + } + + protected function onAddChild(mixed $child): mixed + { + if(is_string($child) && !$this->props->has('text')) + { + $this->props->set('text', $child); + return false; + } + else + { + $this->props->addToList('titlePrefix', $child); + return false; + } + } + + private function buildEntityID(): ?wg + { + $entityID = $this->prop('entityID'); + $className = $this->prop('idClass'); + if(!isset($entityID)) return null; + + return new label + ( + setClass('justify-center rounded-full px-1.5 h-3.5', $className), + $entityID + ); + } + + private function buildEntityName(): wg + { + $text = $this->prop('text'); + $level = $this->prop('level'); + $className = $this->prop('textClass'); + $href = $this->prop('href'); + $labelProps = $this->prop('labelProps'); + $titlePrefix = $this->prop('titlePrefix'); + + $titleClass = empty($level) + ? "article-content" + : "article-h$level"; + + if(empty($href)) return div + ( + setClass($titleClass, $className), + set($labelProps), + $titlePrefix, + $text + ); + + return a + ( + setClass($titleClass, $className), + set::href($href), + set($labelProps), + $titlePrefix, + $text + ); + } + + protected function build(): wg + { + $reverse = $this->prop('reverse'); + $prefix = $this->block('prefix'); + $suffix = $this->block('suffix'); + $entityID = $this->buildEntityID(); + $entityName = $this->buildEntityName(); + return div + ( + setClass('entity-label', 'flex', 'items-center', 'gap-x-2'), + set($this->getRestProps()), + $prefix, + $reverse ? array($entityName, $entityID) : array($entityID, $entityName), + $suffix + ); + } +} diff --git a/lib/zin/wg/featurebar/v1.php b/lib/zin/wg/featurebar/v1.php new file mode 100644 index 0000000000..d32901b4af --- /dev/null +++ b/lib/zin/wg/featurebar/v1.php @@ -0,0 +1,148 @@ + array('map' => 'nav'), + 'leading' => array(), + 'trailing' => array() + ); + + protected function getItems() + { + $items = $this->prop('items'); + if(!empty($items)) return $items; + + global $app, $lang; + $currentModule = $this->prop('module', $app->rawModule); + $currentMethod = $this->prop('method', $app->rawMethod); + + \common::sortFeatureMenu($currentModule, $currentMethod); + + $rawItems = \customModel::getFeatureMenu($currentModule, $currentMethod); + if(!is_array($rawItems)) return null; + + $current = $this->prop('current', data('browseType')); + $pager = data('pager'); + $recTotal = $pager ? $pager->recTotal : data('recTotal'); + $items = array(); + $link = $this->prop('link'); + $loadID = $this->prop('loadID'); + $load = $this->prop('load'); + + data('activeFeature', $current); + + if(empty($link)) $link = createLink($app->rawModule, $app->rawMethod, $this->prop('linkParams')); + + foreach($rawItems as $item) + { + if(isset($item->hidden)) continue; + + $isActive = $item->name == $current; + + $moreSelects = array(); + if($item->name == 'more' && !empty($lang->$currentModule->moreSelects)) $moreSelects = $lang->$currentModule->moreSelects; + if(isset($lang->$currentModule->moreSelects[$currentMethod][$item->name])) $moreSelects = $lang->$currentModule->moreSelects[$currentMethod][$item->name]; + if($item->name == 'QUERY' && !empty($lang->custom->queryList)) $moreSelects = $lang->custom->queryList; + if(!empty($moreSelects)) + { + $activeText = $item->text; + + $subItems = array(); + $callback = $this->prop($item->name == 'more' ? 'moreMenuLinkCallback' : 'queryMenuLinkCallback'); + $callback = isset($callback[0]) ? $callback[0] : null; + + foreach($moreSelects as $key => $text) + { + $subItem = array(); + $subItem['text'] = $text; + $subItem['active'] = $key == $current; + $subItem['url'] = ($callback instanceof \Closure) ? $callback($key, $text) : str_replace('{key}', (string)$key, $link); + $subItem['attrs'] = ['data-id' => $key, 'data-load' => $load, 'data-target' => $loadID]; + + if($item->name == 'QUERY') + { + $closeLink = createLink('search', 'ajaxRemoveMenu', "queryID={$key}"); + $loadUrl = $subItem['url'] . '#featureBar'; + + $subItem['className'] = 'flex-auto'; + $subItem['rootClass'] = 'row gap-0'; + $subItem['rootChildren'] = array(jsRaw("zui.h('a', {className: 'ajax-submit', 'data-url': '{$closeLink}', 'data-load': '{$loadUrl}'}, zui.h('span', {className: 'close'}))")); + } + + $subItems[] = $subItem; + + if($key === $current) + { + $isActive = true; + $activeText = $text; + } + } + + $items[] = array + ( + 'text' => $activeText, + 'active' => $isActive, + 'type' => 'dropdown', + 'caret' => 'down', + 'items' => $subItems, + 'badge' => $isActive && $recTotal != '' ? array('text' => $recTotal, 'class' => 'size-sm rounded-full white') : null, + 'props' => array('data-id' => $item->name) + ); + + continue; + } + + $items[] = array + ( + 'text' => $item->text, + 'active' => $isActive, + 'url' => str_replace('{key}', $item->name, $link), + 'badge' => $isActive && $recTotal != '' ? array('text' => $recTotal, 'class' => 'size-sm rounded-full white') : null, + 'props' => array('data-id' => $item->name, 'data-load' => $load, 'data-target' => $loadID) + ); + } + + return $items; + } + + protected function buildNav() + { + $nav = $this->block('nav'); + if(!empty($nav) && $nav[0] instanceof nav) return $nav; + return new nav + ( + set::class('nav-feature'), + set::items($this->getItems()), + divorce($this->children()) + ); + } + + protected function build(): wg + { + return div + ( + set::id('featureBar'), + $this->block('leading'), + $this->buildNav(), + $this->block('trailing') + ); + } +} diff --git a/lib/zin/wg/fileinput/v1.php b/lib/zin/wg/fileinput/v1.php new file mode 100644 index 0000000000..9290217235 --- /dev/null +++ b/lib/zin/wg/fileinput/v1.php @@ -0,0 +1,13 @@ + 'file', + 'type' => 'file' + ); +} diff --git a/lib/zin/wg/filelist/css/v1.css b/lib/zin/wg/filelist/css/v1.css new file mode 100644 index 0000000000..4f17f104cb --- /dev/null +++ b/lib/zin/wg/filelist/css/v1.css @@ -0,0 +1,12 @@ +.file {padding-top: 2px;} +ul.files-list {margin-bottom: unset} +.files-list>li>a {display: inline; word-wrap: break-word; color: #313c52; line-height: 24px} +.files-list>li>.right-icon {opacity: 1;} +.fileAction {color: #0c64eb !important;} +.renameFile {display: flex;} +.renameFile .input-group {margin-left: 10px;} +.renameFile .input-group-addon {width: 60px;} +.renameFile > .icon { margin-top: 10px;} +.backgroundColor {background: #eff5ff; } +.icon.icon-file-text {padding-left: 7px} +.right-icon .btn {padding: 0 6px; height: 20px} diff --git a/lib/zin/wg/filelist/js/v1.js b/lib/zin/wg/filelist/js/v1.js new file mode 100644 index 0000000000..68d65b6449 --- /dev/null +++ b/lib/zin/wg/filelist/js/v1.js @@ -0,0 +1,127 @@ +$(document).ready(function() +{ + $('li.file').on('mouseover', function() + { + $(this).children('span.right-icon').removeClass("hidden"); + $(this).addClass('backgroundColor'); + }); + + $('li.file').on('mouseout', function() + { + $(this).children('span.right-icon').addClass("hidden"); + $(this).removeClass('backgroundColor'); + }); +}); + + /** + * Delete a file. + * + * @param int $fileID + * @param object $obj + * @access public + * @return void + */ +window.deleteFile = function(fileID, obj) +{ + if(!fileID) return; + + const method = $(obj).closest('.files-list').parent().data('method'); + const showDelete = $(obj).closest('.files-list').parent().data('showDelete'); + + if(showDelete && method == 'edit') + { + $('').attr('type', 'hidden').attr('name', 'deleteFiles[' + fileID + ']').attr('value', fileID).appendTo('ul.files-list'); + $(obj).closest('li.file').addClass('hidden'); + } + else + { + $.ajaxSubmit( + { + url:$.createLink('file', 'delete', 'fileID=' + fileID), + load:true + }) + } +} + +/** + * Download a file, append the mouse to the link. Thus we call decide to open the file in browser no download it. + * + * @param int $fileID + * @param int $extension + * @param int $imageWidth + * @param string $fileTitle + * @access public + * @return void + */ +window.downloadFile = function(fileID, extension, imageWidth, fileTitle) +{ + if(!fileID) return; + const sessionString = $('ul.files-list').parent().data('session'); + + var fileTypes = 'txt,jpg,jpeg,gif,png,bmp'; + var windowWidth = $(window).width(); + var width = (windowWidth > imageWidth) ? ((imageWidth < windowWidth * 0.5) ? windowWidth * 0.5 : imageWidth) : windowWidth; + var checkExtension = fileTitle.lastIndexOf('.' + extension) == (fileTitle.length - extension.length - 1); + + var url = $.createLink('file', 'download', 'fileID=' + fileID + '&mouse=left'); + url += url.indexOf('?') >= 0 ? '&' : '?'; + url += sessionString; + + if(fileTypes.indexOf(extension) >= 0 && checkExtension) + { + loadModal(url); + } + else + { + loadPage(url, '_blank'); + } + return false; +} + +/** + * Show edit box for editing file name. + * + * @param int $fileID + * @access public + * @return void + */ +window.showRenameBox = function(fileID) +{ + $('#renameFile' + fileID).closest('li').addClass('hidden'); + $('#renameBox' + fileID).closest('li').removeClass('hidden'); +} + +/** + * Show File. + * + * @param int $fileID + * @access public + * @return void + */ +window.showFile = function(fileID) +{ + $('#renameBox' + fileID).closest('li').addClass('hidden'); + $('#renameFile' + fileID).closest('li').removeClass('hidden'); +} + +/** + * Smooth refresh file name. + * + * @param int $fileID + * @access public + * @return void + */ +window.setFileName = function(fileID) +{ + var fileName = $('#fileName' + fileID).val(); + var extension = $('#extension' + fileID).val(); + var postData = {'fileName' : fileName, 'extension' : extension}; + $.ajaxSubmit( + { + url:$.createLink('file', 'edit', 'fileID=' + fileID), + dataType: 'json', + method: 'post', + data: postData, + load:true + }) +} diff --git a/lib/zin/wg/filelist/v1.php b/lib/zin/wg/filelist/v1.php new file mode 100644 index 0000000000..5babfe13ab --- /dev/null +++ b/lib/zin/wg/filelist/v1.php @@ -0,0 +1,89 @@ +prop('files'); + $method = $this->prop('method'); + $showDelete = $this->prop('showDelete'); + $showEdit = $this->prop('showEdit'); + $object = (object)$this->prop('object'); + $fileListView = h::ul(setClass('files-list col relative')); + + foreach($files as $file) + { + $fileItemView = li + ( + setClass('mb-2'), + html($app->loadTarget('file')->printFile($file, $method, $showDelete, $showEdit, $object)) + ); + + $fileListView->add($fileItemView); + } + + return $fileListView; + } + + protected function build(): wg + { + global $lang; + + $fieldset = $this->prop('fieldset'); + $isInModal = isAjaxRequest('modal'); + $px = $isInModal ? 'px-3' : 'px-6'; + $pb = $isInModal ? 'pb-3' : 'pb-6'; + + $method = $this->prop('method'); + $showDelete = $this->prop('showDelete'); + + $fileDiv = div + ( + set + ( + array( + 'data-method' => $method, + 'data-showDelete' => $showDelete, + 'data-session' => session_name() . '=' . session_id(), + ) + ), + $this->fileList() + ); + + return $fieldset ? new section + ( + setClass('files', 'pt-4', $px, $pb, 'canvas'), + set::title($lang->files), + to::actions + ( + icon('paper-clip'), + ), + $fileDiv + ) : $fileDiv; + } +} diff --git a/lib/zin/wg/floatprenextbtn/css/v1.css b/lib/zin/wg/floatprenextbtn/css/v1.css new file mode 100644 index 0000000000..0b2ded40f3 --- /dev/null +++ b/lib/zin/wg/floatprenextbtn/css/v1.css @@ -0,0 +1,2 @@ +.float-btn {background: rgba(49, 60, 82, 0.7); font-size: 24px; height: 20px; width: 20px; border-radius: 50%; opacity: 0.8;} +.float-btn .icon {font-size: 18px; color: white;} diff --git a/lib/zin/wg/floatprenextbtn/js/v1.js b/lib/zin/wg/floatprenextbtn/js/v1.js new file mode 100644 index 0000000000..8d471a64fa --- /dev/null +++ b/lib/zin/wg/floatprenextbtn/js/v1.js @@ -0,0 +1,24 @@ +function updateBtnPosition(margin) +{ + const preBtn = document.querySelector('#preButton'); + const nextBtn = document.querySelector('#nextButton'); + + if(preBtn) + { + $(preBtn).css('top', margin); + } + + if(nextBtn) + { + $(nextBtn).css('top', margin); + } +} + +window.addEventListener('resize', function(event) +{ + updateBtnPosition(event.target.innerHeight / 2); +}); + +$(() => { + updateBtnPosition(window.innerHeight / 2); +}); diff --git a/lib/zin/wg/floatprenextbtn/v1.php b/lib/zin/wg/floatprenextbtn/v1.php new file mode 100644 index 0000000000..057e0400dc --- /dev/null +++ b/lib/zin/wg/floatprenextbtn/v1.php @@ -0,0 +1,44 @@ +prop('preLink'); + $nextLink = $this->prop('nextLink'); + + return fragment + ( + !empty($preLink) ? btn + ( + setID('preButton'), + set::url($preLink), + setClass('float-btn fixed left-0 z-10'), + set::icon('angle-left') + ) : null, + !empty($nextLink) ? btn + ( + setID('nextButton'), + set::url($nextLink), + setClass('float-btn fixed right-0 z-10'), + set::icon('angle-right') + ) : null, + ); + } +} diff --git a/lib/zin/wg/floattoolbar/css/v1.css b/lib/zin/wg/floattoolbar/css/v1.css new file mode 100644 index 0000000000..b4d98560a1 --- /dev/null +++ b/lib/zin/wg/floattoolbar/css/v1.css @@ -0,0 +1,4 @@ +.float-toolbar {background: rgba(49, 60, 82, 0.7);} +.float-toolbar>.divider {background: #838A9D;} +.float-toolbar>.btn:before {background: rgba(0, 0, 0, .1);} +.float-toolbar>.btn:hover {color: #FFF;} diff --git a/lib/zin/wg/floattoolbar/v1.php b/lib/zin/wg/floattoolbar/v1.php new file mode 100644 index 0000000000..2316e1c1aa --- /dev/null +++ b/lib/zin/wg/floattoolbar/v1.php @@ -0,0 +1,99 @@ + array(), + 'main' => array(), + 'suffix' => array(), + ); + + public static function getPageCSS(): string|false + { + return file_get_contents(__DIR__ . DS . 'css' . DS . 'v1.css'); + } + + private function buildDivider(wg|array|null $wg): wg|null + { + if(empty($wg)) return null; + + return div(setClass('divider w-px h-6 mx-2')); + } + + private function buildBtns(array|null $items): array|null + { + if(empty($items)) return null; + + $btns = array(); + foreach ($items as &$item) + { + if(!$item) continue; + + if(!empty($item['url'])) $item['url'] = preg_replace_callback('/\{(\w+)\}/', array($this, 'getObjectValue'), $item['url']); + if(!empty($item['data-url'])) $item['data-url'] = preg_replace_callback('/\{(\w+)\}/', array($this, 'getObjectValue'), $item['data-url']); + + $btns[] = btn(set($item), setClass('ghost text-white')); + } + return $btns; + } + + public function getObjectValue($matches) + { + if(!isset($this->object)) $this->object = $this->prop('object'); + + return zget($this->object, $matches[1]); + } + + private function mergeBtns(array|null $btns, array|wg|null $block): array|wg|null + { + if(empty($btns) && empty($block)) return null; + if(empty($block)) return $btns; + + if($block[0] instanceof btn) + { + $block[0]->add(setClass('ghost', 'text-white')); + } + else + { + foreach($block[0]->children() as $blockBtn) $blockBtn->add(setClass('ghost', 'text-white')); + } + if(empty($btns)) return $block; + + if(!is_array($block)) $block = array($block); + return array_merge($btns, $block); + } + + protected function build(): wg + { + $prefixBtns = $this->buildBtns($this->prop('prefix')); + $mainBtns = $this->buildBtns($this->prop('main')); + $suffixBtns = $this->buildBtns($this->prop('suffix')); + + $prefixBlock = $this->block('prefix'); + $mainBlock = $this->block('main'); + $suffixBlock = $this->block('suffix'); + + $prefixBtns = $this->mergeBtns($prefixBtns, $prefixBlock); + $mainBtns = $this->mergeBtns($mainBtns, $mainBlock); + $suffixBtns = $this->mergeBtns($suffixBtns, $suffixBlock); + + return div + ( + setClass('float-toolbar inline-flex rounded p-1.5 items-center'), + $prefixBtns, + $this->buildDivider($prefixBtns), + $mainBtns, + empty($mainBtns) ? null : $this->buildDivider($suffixBtns), + $suffixBtns, + ); + } +} diff --git a/lib/zin/wg/form/v1.php b/lib/zin/wg/form/v1.php new file mode 100644 index 0000000000..467fc29195 --- /dev/null +++ b/lib/zin/wg/form/v1.php @@ -0,0 +1,88 @@ +getModuleName(); + $method = $app->getMethodName(); + $text = !empty($lang->$module->$method) ? $lang->$module->$method : zget($lang, $method, ''); + + $defaultProps = array(); + $defaultProps['submitBtnText'] = $text; + $defaultProps['class'] = 'px-3 pb-4'; + + $this->setDefaultProps($defaultProps); + } + + public function onBuildItem(item|array $item): wg + { + if(!($item instanceof item)) + { + if($item instanceof wg) return $item; + $item = item(set($item)); + } + + if($this->prop('grid')) return new formRow(inherit($item)); + + return new formGroup(inherit($item)); + } + + protected function buildActions(): wg|null + { + $actions = parent::buildActions(); + if($this->prop('grid') && !empty($actions)) $actions = div(setClass('form-row'), $actions); + return $actions; + } + + protected function buildProps(): array + { + list($grid, $labelWidth) = $this->prop(array('grid', 'labelWidth')); + $props = parent::buildProps(); + if($grid) $props[] = setClass('form-grid'); + if(!empty($labelWidth)) $props[] = setCssVar('form-grid-label-width', $labelWidth); + + return $props; + } + + protected function buildContent(): array + { + list($items, $grid) = $this->prop(array('items', 'grid')); + + $list = is_array($items) ? array_map(array($this, 'onBuildItem'), $items) : array(); + $children = $this->children(); + if(!empty($children)) $list = array_merge($list, $children); + + if($grid) + { + foreach($list as $key => $item) + { + if($item instanceof formGroup) $list[$key] = new formRow($item); + } + } + + return $list; + } +} diff --git a/lib/zin/wg/formbase/v1.php b/lib/zin/wg/formbase/v1.php new file mode 100644 index 0000000000..f3a0e3faa1 --- /dev/null +++ b/lib/zin/wg/formbase/v1.php @@ -0,0 +1,114 @@ + + * @package zin + * @link http://www.zentao.net + */ +namespace zin; + +/** + * 基础表单(formBase)部件类,支持 Ajax 提交 + * The formBase widget class + */ +class formBase extends wg +{ + protected static array $defineProps = array( + 'id?: string="$GID"', // ID,如果不指定则自动生成(使用 zin 部件 GID)。 + 'method?: "get"|"post"="post"', // 表单提交方式。 + 'url?: string', // 表单提交地址。 + 'actions?: array', // 表单操作按钮,如果不指定则使用默认行为的 “保存” 和 “返回” 按钮。 + 'actionsClass?: string', // 表单操作按钮栏类名。 + 'target?: string="ajax"', // 表单提交目标,如果是 `'ajax'` 提交则为 ajax,在禅道中除非特殊目的,都使用 ajax 进行提交。 + 'submitBtnText?: string', // 表单提交按钮文本,如果不指定则使用 `$lang->save` 的值。 + 'cancelBtnText?: string', // 表单取消按钮文本,如果不指定则使用 `$lang->goback` 的值。 + 'back?: string="APP"', // 表单返回行为 + 'backUrl?: string', // 表单返回链接 + 'ajax?:array' // Ajax 表单选项 + ); + + protected static array $defineBlocks = array( + 'actions' => array('toolbar') + ); + + protected function created() + { + if($this->prop('actions') !== null) return; + + $actions = isAjaxRequest('modal') ? array('submit') : array('submit', 'cancel'); + $this->setDefaultProps(array('actions' => $actions)); + } + + protected function buildActions(): wg|null + { + if($this->hasBlock('actions')) return $this->block('actions'); + + $actions = $this->prop('actions'); + if(empty($actions)) return null; + + global $lang; + $submitBtnText = $this->prop('submitBtnText'); + $cancelBtnText = $this->prop('cancelBtnText'); + $backUrl = $this->prop('backUrl'); + $back = $this->prop('back'); + if(empty($submitBtnText)) $submitBtnText = $lang->save; + if(empty($cancelBtnText)) $cancelBtnText = $lang->goback; + foreach($actions as $key => $action) + { + if($action === 'submit') $actions[$key] = array('text' => $submitBtnText, 'btnType' => 'submit', 'type' => 'primary'); + elseif($action === 'cancel') $actions[$key] = array('text' => $cancelBtnText, 'url' => $backUrl, 'back' => $back); + elseif(is_string($action)) $actions[$key] = array('text' => $action); + } + + return toolbar + ( + set::class('form-actions', $this->prop('actionsClass')), + set::items($actions) + ); + } + + protected function buildContent(): array|wg + { + return $this->children(); + } + + protected function buildProps(): array + { + list($url, $target, $method, $id) = $this->prop(array('url', 'target', 'method', 'id')); + return array( + set::class('form load-indicator'), + $target === 'ajax' ? set::class('form-ajax') : null, + set(array( + 'id' => $id, + 'action' => empty($url) ? $_SERVER['REQUEST_URI'] : $url, + 'target' => $target === 'ajax' ? null: $target, + 'method' => $method + )) + ); + } + + protected function buildAfter(): array + { + $after = parent::buildAfter(); + if($this->prop('target') === 'ajax') + { + $after[] = zui::ajaxForm(set::_to('#' . $this->id()), set($this->prop('ajax'))); + } + return $after; + } + + protected function build(): wg + { + return h::form + ( + $this->buildProps(), + set($this->getRestProps()), + $this->buildContent(), + $this->buildActions(), + ); + } +} diff --git a/lib/zin/wg/formbatch/v1.php b/lib/zin/wg/formbatch/v1.php new file mode 100644 index 0000000000..f24efd696a --- /dev/null +++ b/lib/zin/wg/formbatch/v1.php @@ -0,0 +1,167 @@ + + * @package zin + * @link http://www.zentao.net + */ + +namespace zin; + +require_once dirname(__DIR__) . DS . 'formbatchitem' . DS . 'v1.php'; +require_once dirname(__DIR__) . DS . 'formbase' . DS . 'v1.php'; + +/** + * 批量编辑表单(formBatch)部件类,支持 Ajax 提交。 + * The batch operate form widget class. + * + * @author Hao Sun + */ +class formBatch extends formBase +{ + /** + * Define widget properties. + * + * @var array + * @access protected + */ + protected static array $defineProps = array( + 'items?: array[]', // 使用一个列定义对象数组来定义批量表单项。 + 'minRows?: int', // 最小显示的行数目。 + 'maxRows?: int', // 最多显示的行数目。 + 'data?: array[]', // 初始化行数据。 + 'mode?: string', // 批量操作模式,可以为 `'add'`(批量添加) 或 `'edit'`(批量编辑)。 + 'actionsText?: string', // 操作列头部文本,如果不指定则使用 `$lang->actions` 的值。 + 'idKey?: string', // 用于从行数据获取 ID 的属性名。 + 'addRowIcon?: string|false', // 添加行的图标,如果设置为 `false` 则不显示图标 + 'deleteRowIcon?: string|false', // 删除行的图标,如果设置为 `false` 则不显示图标 + 'onRenderRow?: function', // 渲染行时的回调函数。 + 'onRenderRowCol?: function' // 渲染列时的回调函数。 + ); + + /** + * Define default properties. + * + * @var array + * @access protected + */ + protected static array $defaultProps = array( + 'minRows' => 1, + 'maxRows' => 100, + 'mode' => 'add' + ); + + /** + * Handle building inner items. + * + * @param wg|array wg + * @access public + * @return wg + */ + public function onBuildItem(wg|array $item): wg + { + if($item instanceof formBatchItem) return $item; + + if(!($item instanceof item)) + { + if(!is_array($item)) return $item; + $item = item(set($item)); + } + + return new formBatchItem(inherit($item)); + } + + /** + * Build batch form content. + * + * @access protected + * @return array|wg + */ + protected function buildContent(): array|wg + { + $items = array_merge($this->children(), $this->prop('items', array())); + $templateItems = array(); + $headItems = array(); + $otherItems = array(); + + foreach($items as $item) + { + if($item instanceof item || is_array($item)) $item = $this->onBuildItem($item); + if($item instanceof formBatchItem) + { + list($headItem, $templateItem) = $item->build(); + $headItems[] = $headItem; + $templateItems[] = $templateItem; + } + else + { + $otherItems[] = $item; + } + } + + if($this->prop('mode') === 'add') + { + $actionsText = $this->prop('actionsText'); + if($actionsText === null) $actionsText = data('lang.actions'); + $headItems[] = h::th + ( + set('data-name', 'ACTIONS'), + setClass('form-batch-head'), + span(setClass('form-label form-batch-label'), $actionsText) + ); + } + + return array( + div + ( + setClass('form-batch-container'), + h::table + ( + setClass('table form-batch-table'), + h::thead(h::tr($headItems)), + h::tbody(), + ) + ), + template(setClass('form-batch-template'), h::tr($templateItems)), + $otherItems + ); + } + + /** + * Build batch form props. + * + * @access protected + * @return array + */ + protected function buildProps(): array + { + $props = parent::buildProps(); + list($mode, $minRows, $maxRows) = $this->prop(array('mode', 'minRows', 'maxRows')); + + $props[] = setClass('form-batch'); + $props[] = set('data-mode', $mode); + $props[] = set('data-min-rows', $minRows); + $props[] = set('data-max-rows', $maxRows); + + return $props; + } + + /** + * Build content after current widget. + */ + protected function buildAfter(): array + { + $after = parent::buildAfter(); + + $after[] = zui::batchForm + ( + set::_to('#' . $this->id()), + set($this->props->pick(array('minRows', 'maxRows', 'data', 'mode', 'idKey', 'onRenderRow', 'onRenderRowCol', 'addRowIcon', 'deleteRowIcon'))) + ); + return $after; + } +} diff --git a/lib/zin/wg/formbatchitem/v1.php b/lib/zin/wg/formbatchitem/v1.php new file mode 100644 index 0000000000..da08e19436 --- /dev/null +++ b/lib/zin/wg/formbatchitem/v1.php @@ -0,0 +1,122 @@ + + * @package zin + * @link http://www.zentao.net + */ + +namespace zin; + +require_once dirname(__DIR__) . DS . 'control' . DS . 'v1.php'; + +/** + * 批量编辑表单项(formBatchItem)部件类。 + * The batch edit form item widget class. + * + * @author Hao Sun + */ +class formBatchItem extends wg +{ + /** + * Define widget properties. + * + * @var array + * @access protected + */ + protected static array $defineProps = array( + 'name: string', // 表单项名称,无需包含 `[]`。 + 'label: string|bool', // 列标题。 + 'labelClass?: string', // 列标题类名。 + 'labelProps?: string', // 列标题属性,例如 `array('data-toggle' => 'tooltip', 'data-title' 。=> 'This is a tip')` + 'required?:bool|string="auto"', // 是否必填,如果设置为 `"auto"`,则自动从当前模块 config 中查询。 + 'control?: array|string|false', // 控件类型或控件配置。 + 'width?: number|string', // 列宽度,如果设置为 `"auto"` 则自动填充剩余宽度。 + 'minWidth?: number|string', // 列最小宽度。 + 'value?: string|array', // 默认值。 + 'disabled?: bool', // 是否禁用。 + 'items?: array', // 选项,当控件类型为下拉菜单时使用此属性指定下拉菜单项。 + 'placeholder?: string', // 占位文本。 + 'tip?: string', // 显示在列标题上的提示文本。 + 'tipClass?: string', // 列标题上的提示触发按钮类名。 + 'tipIcon?: string="info-sign"', // 列标题上的提示触发按钮图标。 + 'tipProps?: string', // 列标题上的提示触发按钮其他属性。 + 'ditto?: bool', // 是否显示同上按钮。 + 'defaultDitto?:string="on"', // 同上按钮的默认值。 + 'hidden?: bool=false', // 是否隐藏 + 'readonly?: bool=false', // 是否只读 + ); + + /** + * Define default properties. + * + * @access protected + */ + protected function build(): array + { + list($name, $label, $labelClass, $labelProps, $required, $tip, $tipClass, $tipProps, $tipIcon, $control, $width, $strong, $value, $disabled, $items, $placeholder, $ditto, $defaultDitto, $hidden, $readonly, $multiple) = $this->prop(array('name', 'label', 'labelClass', 'labelProps', 'required', 'tip', 'tipClass', 'tipProps', 'tipIcon', 'control', 'width', 'strong', 'value', 'disabled', 'items', 'placeholder', 'ditto', 'defaultDitto', 'hidden', 'readonly', 'multiple')); + + if($required === 'auto') $required = isFieldRequired($name); + + if($control !== false) + { + if(is_string($control)) $control = array('type' => $control, 'name' => $name); + else if(empty($control)) $control = array(); + + if(!isset($control['type'])) $control['type'] = 'text'; + if($required !== null) $control['required'] = $required; + if($name !== null) $control['name'] = $name; + if($value !== null) $control['value'] = $value; + if($disabled !== null) $control['disabled'] = $disabled; + if($multiple !== null) $control['multiple'] = $multiple; + if($items !== null) $control['items'] = $items; + if($placeholder !== null) $control['placeholder'] = $placeholder; + if($readonly !== null) $control['readonly'] = $readonly; + } + + $asIndex = $control['type'] === 'index'; + if($asIndex) $control['type'] = 'static'; + + return array( + h::th + ( + setClass('form-batch-head'), + $hidden ? setClass('hidden') : null, + zui::width($width), + set('data-required', $required), + set('data-ditto', $ditto), + set('data-name', $name), + $ditto ? set('data-default-ditto', $defaultDitto) : null, + $asIndex ? set('data-index', $asIndex) : null, + set($this->getRestProps()), + span + ( + set::class('form-label form-batch-label', $labelClass, $strong ? 'font-bold' : null, $required ? 'required' : null), + set($labelProps), + $label + ), + empty($tip) ? null : new btn + ( + set::class('form-batch-tip state text-gray', $tipClass), + set::size('sm'), + set::type('ghost'), + toggle('tooltip', array('title' => $tip)), + set($tipProps), + set::icon($tipIcon) + ) + ), + h::td + ( + setClass('form-batch-control'), + $hidden ? setClass('hidden') : null, + set('data-name', $name), + empty($control) ? null : new control(set($control)), + $this->children() + ) + ); + } +} diff --git a/lib/zin/wg/formbatchpanel/v1.php b/lib/zin/wg/formbatchpanel/v1.php new file mode 100644 index 0000000000..12e8b70bf9 --- /dev/null +++ b/lib/zin/wg/formbatchpanel/v1.php @@ -0,0 +1,84 @@ + + * @package zin + * @link http://www.zentao.net + */ + +namespace zin; + +require_once dirname(__DIR__) . DS . 'formpanel' . DS . 'v1.php'; + +/** + * 批量编辑表单面板(formBatchPanel)部件类。 + * The batch operate form panel widget class. + * + * @author Hao Sun + */ +class formBatchPanel extends formPanel +{ + /** + * Define widget properties. + * + * @var array + * @access protected + */ + protected static array $defineProps = array( + 'uploadParams?: string|false', // 多图上传的参数,如果设置为 `false` 则不显示按钮 + 'pasteField?: string|false', // 多行录入的字段名,如果设置为 `false` 则不显示按钮 + 'items?: array[]', // 使用一个列定义对象数组来定义批量表单项。 + 'minRows?: int', // 最小显示的行数目。 + 'maxRows?: int', // 最多显示的行数目。 + 'data?: array[]', // 初始化行数据。 + 'mode?: string', // 批量操作模式,可以为 `'add'`(批量添加) 或 `'edit'`(批量编辑)。 + 'actionsText?: string', // 操作列头部文本,如果不指定则使用 `$lang->actions` 的值。 + 'addRowIcon?: string|false', // 添加行的图标,如果设置为 `false` 则不显示图标 + 'deleteRowIcon?: string|false', // 删除行的图标,如果设置为 `false` 则不显示图标 + 'onRenderRow?: function', // 渲染行时的回调函数。 + 'onRenderRowCol?: function' // 渲染列时的回调函数。 + ); + + /** + * Define default properties. + * + * @var array + * @access protected + */ + protected static array $defaultProps = array( + 'uploadParams' => false, + 'pasteField' => false, + 'customFields' => array(), + 'batch' => true + ); + + protected function buildHeadingActions(): ?wg + { + $headingActions = $this->prop('headingActions'); + if(!$headingActions) $headingActions = array(); + + $uploadImage = $this->prop('uploadParams') && hasPriv('file', 'uploadImages'); + $pasteField = $this->prop('pasteField'); + + global $lang; + + /* Upload images. */ + if($uploadImage) $headingActions[] = array('url' => createLink('file', 'uploadImages', $this->prop('uploadParams')), 'class' => 'btn primary-pale bd-primary mr-4', 'data-toggle' => 'modal', 'data-width' => '0.7', 'text' => $lang->uploadImages); + + /* Multi-input. */ + if($pasteField) + { + $headingActions[] = array('class' => 'btn primary-pale bd-primary', 'data-toggle' => 'modal', 'data-target' => '#paste-dialog', 'text' => $lang->pasteText); + + $this->addToBlock('headingActions', pasteDialog(set::field($pasteField))); + } + + $this->setProp('headingActions', $headingActions); + + return parent::buildHeadingActions(); + } +} diff --git a/lib/zin/wg/formgroup/v1.php b/lib/zin/wg/formgroup/v1.php new file mode 100644 index 0000000000..044e2df19a --- /dev/null +++ b/lib/zin/wg/formgroup/v1.php @@ -0,0 +1,74 @@ +prop(['name', 'label', 'labelClass', 'labelProps', 'labelWidth', 'required', 'tip', 'tipClass', 'tipProps', 'control', 'width', 'strong', 'value', 'disabled', 'items', 'placeholder', 'readonly']); + + if($required === 'auto') $required = isFieldRequired($name); + + if(is_string($control)) $control = array('type' => $control, 'name' => $name); + elseif(empty($control) && $name !== null) $control = array('name' => $name); + + if(!empty($control)) + { + if($required !== null) $control['required'] = $required; + if($name !== null) $control['name'] = $name; + if($value !== null) $control['value'] = $value; + if($disabled !== null) $control['disabled'] = $disabled; + if($items !== null) $control['items'] = $items; + if($placeholder !== null) $control['placeholder'] = $placeholder; + if($readonly !== null) $control['readonly'] = $readonly; + } + + return div + ( + set::class('form-group', $required ? 'required' : null, ($label === false || $label === null) ? 'no-label' : null, empty($width) ? null : 'grow-0'), + zui::width($width), + set($this->getRestProps()), + setCssVar('form-grid-label-width', $labelWidth), + empty($label) ? null : new formLabel + ( + set::class($labelClass, $strong ? 'font-bold' : null), + set::required($required), + set($labelProps), + $label + ), + empty($control) ? null : new control(set($control)), + (isset($control['disabled']) && $control['disabled'] && isset($control['name']) && isset($control['value'])) ? h::input(set::type('hidden'), set::name($control['name']), set::value($control['value'])) : null, + $this->children(), + empty($tip) ? null : div + ( + set::class($tipClass), + set($tipProps), + $tip + ) + ); + } +} diff --git a/lib/zin/wg/formitemdropdown/css/v1.css b/lib/zin/wg/formitemdropdown/css/v1.css new file mode 100644 index 0000000000..37b62ddda2 --- /dev/null +++ b/lib/zin/wg/formitemdropdown/css/v1.css @@ -0,0 +1,6 @@ +.form-item-dropdown .check-list-inline .checkbox-primary {width: 45%;} +.form-item-dropdown .form-grid .form-group>.check-list-inline {padding-left: 0; padding-right: 0;} +.form-item-dropdown .dropdown-menu .form-row {width: 240px;} +.form-item-dropdown .dropdown-menu .form-actions {gap: 0;} +.form-item-dropdown .dropdown-menu .form-actions .btn {min-width: 68px; max-width: 68px; margin-left: 16px;} +.form-item-dropdown .dropdown-menu .form-actions .btn:first-child {margin-left: 0;} diff --git a/lib/zin/wg/formitemdropdown/v1.php b/lib/zin/wg/formitemdropdown/v1.php new file mode 100644 index 0000000000..3e05db47cc --- /dev/null +++ b/lib/zin/wg/formitemdropdown/v1.php @@ -0,0 +1,86 @@ +prop(['method', 'url', 'actions', 'target']); + + return form + ( + p + ( + $lang->customField, + setClass('text-base font-bold mb-1') + ), + set($props), + setID('custom'), + setClass('dropdown-menu p-5 bg-white'), + formGroup + ( + setClass('not-hide-menu w-20 mb-1'), + checkList + ( + set + ( + array( + 'class' => 'h-full flex-wrap gap-y-5', + 'primary' => true, + 'inline' => true, + 'value' => $this->prop('value'), + 'items' => $this->prop('items') + ) + ) + ) + ), + set::actions + ( + array( + array('text' => $lang->save, 'class' => 'primary'), + array('text' => $lang->cancel), + array('text' => $lang->restore, 'class' => 'ghost text-primary', 'btnType' => 'reset') + ) + ) + ); + } + + protected function build(): wg + { + return div + ( + setClass('form-item-dropdown'), + btn + ( + set + ( + array( + 'class' => 'ghost', + 'icon' => 'cog', + 'data-target' => 'custom', + 'data-toggle' => 'dropdown' + ) + ) + ), + $this->buildFormPanel() + ); + } +} diff --git a/lib/zin/wg/formlabel/v1.php b/lib/zin/wg/formlabel/v1.php new file mode 100644 index 0000000000..7f4a112ace --- /dev/null +++ b/lib/zin/wg/formlabel/v1.php @@ -0,0 +1,34 @@ +props->has('text')) + { + $this->props->set('text', $child); + return false; + } + } + + protected function build(): wg + { + list($text, $required, $for) = $this->prop(['text', 'required', 'for']); + return h::label + ( + setClass('form-label', $required ? 'required' : null), + set('for', $for), + set($this->getRestProps()), + $text, + $this->children(), + ); + } +} diff --git a/lib/zin/wg/formpanel/js/v1.js b/lib/zin/wg/formpanel/js/v1.js new file mode 100644 index 0000000000..c173efd167 --- /dev/null +++ b/lib/zin/wg/formpanel/js/v1.js @@ -0,0 +1,62 @@ +function hideSingleField(hiddenFields) +{ + if(typeof hiddenFields == 'undefined') return false; + + hiddenFields.forEach(function(field) + { + var $this = $('form [name^="' + field + '"]'); + if($this.length == 0) return; + + $inputGroup = $this.closest('.input-group'); + $formGroup = $this.closest('.form-group'); + if($inputGroup.length == 1) + { + $prev = $this.prev(); + if($prev.hasClass('input-group-addon')) $prev.addClass('hidden'); + + $this.addClass('hidden'); + if($this.hasClass('pick-value')) + { + $pickBox = $this.closest('.pick').parent(); + $pickBox.addClass('hidden'); + + $prev = $pickBox.prev(); + if($prev.hasClass('input-group-addon')) $prev.addClass('hidden'); + } + } + else + { + $formGroup.addClass('hidden'); + } + }) + + $('form .form-row').each(function() + { + var $this = $(this); + if($this.find('.form-group.hidden').length > 0 && $this.find('.form-group:not(.hidden)').length == 0) $this.addClass('hidden'); + }); +} + +function hideBatchField(hiddenFields) +{ + if(typeof hiddenFields == 'undefined') return false; + + hiddenFields.forEach(function(field) + { + $('th.form-batch-head[data-name="' + field + '"]').addClass('hidden'); + $('td.form-batch-control[data-name="' + field + '"]').addClass('hidden'); + $($('template.form-batch-template')[0].content).find('td.form-batch-control[data-name="' + field + '"]').addClass('hidden'); + }); +} + +if(typeof hiddenFields != 'undefined') +{ + if(typeof formBatch == 'undefined' || !formBatch) + { + hideSingleField(hiddenFields); + } + else + { + setTimeout(function(){hideBatchField(hiddenFields)}, 800); + } +} diff --git a/lib/zin/wg/formpanel/v1.php b/lib/zin/wg/formpanel/v1.php new file mode 100644 index 0000000000..c82c615848 --- /dev/null +++ b/lib/zin/wg/formpanel/v1.php @@ -0,0 +1,167 @@ + + * @package zin + * @link http://www.zentao.net + */ + +namespace zin; + +require_once dirname(__DIR__) . DS . 'panel' . DS . 'v1.php'; +require_once dirname(__DIR__) . DS . 'formgroup' . DS . 'v1.php'; +require_once dirname(__DIR__) . DS . 'formrow' . DS . 'v1.php'; +require_once dirname(__DIR__) . DS . 'form' . DS . 'v1.php'; +require_once dirname(__DIR__) . DS . 'formbatch' . DS . 'v1.php'; + +/** + * 表单面板(formPanel)部件类。 + * The form panel widget class. + * + * @author Hao Sun + */ +class formPanel extends panel +{ + /** + * Define widget properties. + * + * @var array + * @access protected + */ + protected static array $defineProps = array( + 'class?: string="panel-form"', // 类名。 + 'size?: string="lg"', // 额外尺寸。 + 'id?: string="$GID"', // ID,如果不指定则自动生成(使用 zin 部件 GID)。 + 'formClass?: string', // 表单样式。 + 'method?: "get"|"post"="post"', // 表单提交方式。 + 'url?: string', // 表单提交地址。 + 'actions?: array', // 表单操作按钮,如果不指定则使用默认行为的 “保存” 和 “返回” 按钮。 + 'actionsClass?: string="form-group no-label"', // 表单操作按钮栏类名。 + 'target?: string="ajax"', // 表单提交目标,如果是 `'ajax'` 提交则为 ajax,在禅道中除非特殊目的,都使用 ajax 进行提交。 + 'submitBtnText?: string', // 表单提交按钮文本,如果不指定则使用 `$lang->save` 的值。 + 'cancelBtnText?: string', // 表单取消按钮文本,如果不指定则使用 `$lang->goback` 的值。 + 'items?: array', // 使用一个列定义对象数组来定义表单项。 + 'grid?: bool=true', // 是否启用网格部件,禅道中所有表单都是网格布局,除非有特殊目的,无需设置此项。 + 'labelWidth?: int', // 标签宽度,单位为像素。 + 'batch?: bool', // 是否为批量操作表单。 + 'shadow?: bool=false', // 是否显示阴影层。 + 'width?: string' // 最大宽度。 + ); + + /** + * Define default properties. + * + * @var array + * @access protected + */ + protected static array $defaultProps = array( + 'customFields' => array(), + ); + + public static function getPageJS(): string|false + { + return file_get_contents(__DIR__ . DS . 'js' . DS . 'v1.js'); + } + + /** + * Build heading actions. + * + * @access protected + * @return ?wg + */ + protected function buildHeadingActions(): ?wg + { + $headingActions = $this->prop('headingActions'); + if(!$headingActions) $headingActions = array(); + + $customFields = $this->prop('customFields'); + + /* Custom fields. */ + if($customFields) + { + global $app; + $urlParams = isset($customFields['urlParams']) ? $customFields['urlParams'] : "module={$app->rawModule}§ion=custom&key=batchCreateFields"; + + $headingActions[] = formSettingBtn(set::customFields($customFields['items']), set::urlParams($urlParams)); + } + + $this->setProp('headingActions', $headingActions); + + return parent::buildHeadingActions(); + } + + /** + * Build form widget by mode. + * + * @access protected + * @return wg + */ + protected function buildForm(): wg + { + $customFields = $this->prop('customFields'); + $hiddenFields = array(); + if(!empty($customFields['items'])) + { + $hiddenFields = array_values(array_filter(array_map(function($item) + { + return $item['show'] ? false : $item['name']; + }, $customFields['items']))); + } + + if($this->prop('batch')) + { + return new formBatch + ( + set($this->props->pick(array_keys(formBatch::definedPropsList()))), + $this->children(), + jsVar('formBatch', true), + $hiddenFields ? jsVar('hiddenFields', $hiddenFields) : null, + ); + } + + return new form + ( + set::class($this->prop('formClass')), + set($this->props->pick(array_keys(form::definedPropsList()))), + $this->children(), + $hiddenFields ? jsVar('hiddenFields', $hiddenFields) : null, + ); + } + + /** + * Build widget props. + * + * @access protected + * @return array + */ + protected function buildProps(): array + { + list($width, $batch, $shadow) = $this->prop(array('width', 'batch', 'shadow')); + $props = parent::buildProps(); + + if($width) $props[] = setCssVar('--zt-page-form-max-width', $width); + elseif($batch) $props[] = setCssVar('--zt-page-form-max-width', 'auto'); + if($shadow) $props[] = setClass('shadow'); + + return $props; + } + + /** + * Build panel body. + * + * @access protected + * @return wg + */ + protected function buildBody(): wg + { + return div + ( + setClass('panel-body'), + $this->buildForm() + ); + } +} diff --git a/lib/zin/wg/formrow/v1.php b/lib/zin/wg/formrow/v1.php new file mode 100644 index 0000000000..86fda43654 --- /dev/null +++ b/lib/zin/wg/formrow/v1.php @@ -0,0 +1,39 @@ +prop(['width', 'items', 'hidden']); + + return div + ( + set::class('form-row', empty($width) ? null : 'grow-0', $hidden ? 'hidden' : ''), + zui::width($width), + is_array($items) ? array_map(array($this, 'onBuildItem'), $items) : null, + set($this->getRestProps()), + $this->children() + ); + } +} diff --git a/lib/zin/wg/formrowgroup/css/v1.css b/lib/zin/wg/formrowgroup/css/v1.css new file mode 100644 index 0000000000..a869fc1191 --- /dev/null +++ b/lib/zin/wg/formrowgroup/css/v1.css @@ -0,0 +1 @@ +.form-row-group .row-group-title {background-color: var(--color-slate-100);} diff --git a/lib/zin/wg/formrowgroup/v1.php b/lib/zin/wg/formrowgroup/v1.php new file mode 100644 index 0000000000..ac312e415a --- /dev/null +++ b/lib/zin/wg/formrowgroup/v1.php @@ -0,0 +1,40 @@ + array(), + 'suffix' => array(), + ); + + public static function getPageCSS(): string|false + { + return file_get_contents(__DIR__ . DS . 'css' . DS . 'v1.css'); + } + + protected function build(): wg + { + return formRow + ( + setClass('form-row-group border-b border-b-1'), + div + ( + $this->block('prefix'), + setClass('row-group-title font-black px-3 py-1'), + $this->prop('title'), + $this->block('suffix'), + ), + $this->prop('items'), + ); + } +} diff --git a/lib/zin/wg/formsettingbtn/css/v1.css b/lib/zin/wg/formsettingbtn/css/v1.css new file mode 100644 index 0000000000..abd9600207 --- /dev/null +++ b/lib/zin/wg/formsettingbtn/css/v1.css @@ -0,0 +1,6 @@ +.form-setting-btn {width: 280px;} +.form-setting-btn .panel-heading, .form-setting-btn .panel-body {padding-left:0px; padding-right:0px;} +.form-setting-btn .panel-body {padding-top: 0px; padding-bottom: 0px;} +.form-setting-btn .panel-body form {display: flex; flex-wrap: wrap;} +.form-setting-btn .panel-body form .checkbox-primary {width: 50%; margin-top: 0.5rem; margin-bottom: 0.5rem;} +.form-setting-btn .panel-body form .form-actions .btn {min-width: auto;} diff --git a/lib/zin/wg/formsettingbtn/js/v1.js b/lib/zin/wg/formsettingbtn/js/v1.js new file mode 100644 index 0000000000..46dd5ea891 --- /dev/null +++ b/lib/zin/wg/formsettingbtn/js/v1.js @@ -0,0 +1,117 @@ +function closeCustomPopupMenu(e) +{ + $(e.target).closest('menu').removeClass('show'); +} + +function revertDefaultFields(e) +{ + $.get($(e.target).closest('button').data('url'), function(data, status) + { + const customFields = []; + const showFields = []; + e.target.closest('form').querySelectorAll('input').forEach(function(field) + { + /* Gather all custom fields. */ + customFields.push(field.value); + if(field.value === 'source') customFields.push('sourceNote'); + + $(field).prop('checked', false); + if(!$(field).data('default')) return; + + /* Gather checked fields to be visible. */ + $(field).prop('checked', true); + showFields.push(field.value); + if(field.value === 'source') showFields.push('sourceNote'); + }); + + hideAndShowFormFields(customFields, showFields); + closeCustomPopupMenu(e); + }); +} + +function onSubmitFormtSetting(e) +{ + const customFields = []; + const showFields = []; + const $form = $(e.target).closest('form'); + + $.post($form.attr('action'), new FormData($form[0]), function(result) + { + $form.find('input[type="checkbox"]').each(function() + { + /* Gather all custom fields. */ + $field = $(this); + customFields.push($field.val()); + if($field.val() === 'source') customFields.push('sourceNote'); + + if(!$field.prop('checked')) return; + + /* Gather checked fields to be visible. */ + showFields.push($field.val()); + if($field.val() === 'source') showFields.push('sourceNote'); + }); + + hideAndShowFormFields(customFields, showFields); + closeCustomPopupMenu(e); + }); +} + +function hideAndShowFormFields(customFields, showFields) +{ + if(typeof formBatch == 'undefined' || !formBatch) return toggleSingleField(customFields, showFields); + if(typeof formBatch != 'undefined' && formBatch) return toggleBatchField(customFields, showFields); +} + +function toggleSingleField(customFields, showFields) +{ + if(typeof customFields == 'undefined') return false; + + customFields.forEach(function(field) + { + var $this = $('form [name^="' + field + '"]'); + if($this.length == 0) return; + + var hidden = !showFields.includes(field); + var $inputGroup = $this.closest('.input-group'); + var $formGroup = $this.closest('.form-group'); + if($inputGroup.length == 1) + { + $prev = $this.prev(); + if($prev.hasClass('input-group-addon')) $prev.toggleClass('hidden', hidden); + + $this.toggleClass('hidden', hidden); + if($this.hasClass('pick-value')) + { + $pickBox = $this.closest('.pick').parent(); + $pickBox.toggleClass('hidden', hidden); + + $prev = $pickBox.prev(); + if($prev.hasClass('input-group-addon')) $prev.toggleClass('hidden', hidden); + } + } + else + { + $formGroup.toggleClass('hidden', hidden); + } + }) + + $('form .form-row').each(function() + { + var $this = $(this); + $this.removeClass('hidden'); + if($this.find('.form-group.hidden').length > 0 && $this.find('.form-group:not(.hidden)').length == 0) $this.addClass('hidden'); + }); +} + +function toggleBatchField(customFields, showFields) +{ + if(typeof customFields == 'undefined') return false; + + customFields.forEach(function(field) + { + var hidden = !showFields.includes(field); + $('th.form-batch-head[data-name="' + field + '"]').toggleClass('hidden', hidden); + $('td.form-batch-control[data-name="' + field + '"]').toggleClass('hidden', hidden); + $($('template.form-batch-template')[0].content).find('td.form-batch-control[data-name="' + field + '"]').toggleClass('hidden', hidden); + }); +} diff --git a/lib/zin/wg/formsettingbtn/v1.php b/lib/zin/wg/formsettingbtn/v1.php new file mode 100644 index 0000000000..baf74b892b --- /dev/null +++ b/lib/zin/wg/formsettingbtn/v1.php @@ -0,0 +1,75 @@ + + * @package zin + * @link http://www.zentao.net + */ + +namespace zin; + +class formSettingBtn extends wg +{ + protected static array $defaultProps = array( + 'customFields' => array(), + 'urlParams' => '' + ); + + public static function getPageCSS(): string|false + { + return file_get_contents(__DIR__ . DS . 'css' . DS . 'v1.css'); + } + + public static function getPageJS(): string|false + { + return file_get_contents(__DIR__ . DS . 'js' . DS . 'v1.js'); + } + + protected function build(): wg + { + $customFields = $this->prop('customFields', array()); + + global $lang; + + $customLink = createLink('custom', 'ajaxSaveCustomFields', $this->prop('urlParams', '')); + return dropdown + ( + set::arrow('false'), + set::placement('bottom-end'), + to::trigger(btn(set::icon('cog-outline'), setClass('ghost'), set::caret(false))), + to::menu(menu + ( + setClass('dropdown-menu'), + on::click('e.stopPropagation();'), + formpanel + ( + setClass('form-setting-btn'), + set::title($lang->customField), + set::url($customLink), + set::actions(array + ( + btn(set::text($lang->save), setClass('primary'), on::click('onSubmitFormtSetting')), + btn(set::text($lang->cancel), set::btnType('reset'), on::click('closeCustomPopupMenu')), + btn(set::text($lang->restore), setClass('text-primary ghost font-bold'), set::href('#'), set('data-url', $customLink), on::click('revertDefaultFields')), + )), + to::headingActions(array(btn(set::icon('close'), setClass('ghost'), set::size('sm'), on::click('closeCustomPopupMenu')))), + array_map(function($field) + { + return checkbox + ( + set::name('fields[]'), + set::value($field['name']), + set::text($field['text']), + set::checked(isset($field['show']) ? $field['show'] : false), + set('data-default', isset($field['default']) ? $field['default'] : false) + ); + }, $customFields), + ) + )) + ); + } +} diff --git a/lib/zin/wg/fragment/v1.php b/lib/zin/wg/fragment/v1.php new file mode 100644 index 0000000000..ebdfb09f54 --- /dev/null +++ b/lib/zin/wg/fragment/v1.php @@ -0,0 +1,26 @@ +prop('rawContent', !zin::$rawContentCalled); + + return array + ( + h::css($css, setClass('zin-page-css')), + $this->children(), + $rawContent ? rawContent() : null, + h::js($js, setClass('zin-page-js')) + ); + } +} diff --git a/lib/zin/wg/globalsearch/v1.php b/lib/zin/wg/globalsearch/v1.php new file mode 100644 index 0000000000..6367ac67da --- /dev/null +++ b/lib/zin/wg/globalsearch/v1.php @@ -0,0 +1,43 @@ +setDefaultProps(array('commonSearchText' => $lang->searchAB)); + + $input = inputGroup + ( + set($this->getRestProps()), + input + ( + setID('globalSearchInput'), + set::placeholder($lang->index->pleaseInput), + ), + btn + ( + set::icon('search'), + set::type('primary'), + ), + ); + $input->setProp('data-zin-id', $input->gid); + $props = array_merge + ( + $this->props->pick(array('commonSearchText', 'commonSearchUrl', 'searchItems')), + array('_to' => "[data-zin-id='{$input->gid}']") + ); + return array( + $input, + zui::globalSearch(set($props)) + ); + } +} diff --git a/lib/zin/wg/header/v1.php b/lib/zin/wg/header/v1.php new file mode 100644 index 0000000000..51e638db88 --- /dev/null +++ b/lib/zin/wg/header/v1.php @@ -0,0 +1,440 @@ + array('map' => 'heading'), + 'headingToolbar' => array('map' => 'toolbar'), + 'dropmenu' => array('map' => 'dropmenu'), + 'navbar' => array('map' => 'nav'), + 'toolbar' => array('map' => 'btn') + ); + + protected function buildHeading() + { + if($this->hasBlock('heading')) return $this->block('heading'); + $headingToolbar = $this->block('headingToolbar'); + $dropmenu = $this->block('dropmenu'); + return new heading($headingToolbar, $dropmenu); + } + + protected function buildNavbar() + { + $navbar = $this->block('navbar'); + if(empty($navbar)) $navbar = new navbar(); + return $navbar; + } + + protected function buildToolbar() + { + $toolbar = $this->block('toolbar'); + if(empty($toolbar)) + { + $toolbar = new toolbar + ( + setClass('gap-5'), + static::quickAddMenu(), + static::userBar(), + static::visionSwitcher() + ); + } + return h::div + ( + set::id('toolbar'), + $toolbar + ); + } + + /** + * Build. + * + * @access protected + */ + protected function build(): wg + { + return h::header + ( + setID('header'), + h::div + ( + setClass('container'), + $this->buildHeading(), + $this->buildNavbar(), + $this->buildToolbar() + ) + ); + } + + static function visionSwitcher() + { + global $lang, $app, $config; + + if(!isset($app->user)) return; + + if(!isset($app->user->visions)) $app->user->visions = trim($config->visions, ','); + $currentVision = $app->config->vision; + $userVisions = array_filter(explode(',', $app->user->visions)); + $configVisions = array_filter(explode(',', trim($config->visions, ','))); + + /* The standalone lite version removes the lite interface button */ + if(trim($config->visions, ',') == 'lite') return true; + + if(count($userVisions) < 2 || count($configVisions) < 2) return btn($lang->visionList[$currentVision]); + + $items = array(); + foreach($userVisions as $vision) + { + $items[] = array + ( + 'active' => $currentVision == $vision, + 'url' => createLink('my', 'ajaxSwitchVision', "vision=$vision"), + 'data-type' => 'ajax', + 'text' => isset($lang->visionList[$vision]) ? $lang->visionList[$vision] : $vision, + ); + } + + return dropdown + ( + btn + ( + setClass('bg-white ring-0 rounded bg-opacity-30'), + set::text($lang->visionList[$currentVision]), + set::caret(false) + ), + + set::id('versionMenu'), + set::trigger('hover'), + set::placement('bottom'), + set::menu(array('style' => array('color' => 'var(--color-fore)'))), + set::arrow(true), + set::items($items) + ); + } + + static function userBar() + { + global $lang, $app, $config; + + if(!isset($app->user)) return; + + $user = $app->user; + $isGuest = $user->account == 'guest'; + $items = array(); + + if(!$isGuest) + { + $noRole = empty($user->role) || !isset($lang->user->roleList[$user->role]); + $items[] = array + ( + 'type' => 'custom', + 'tag' => 'a', + 'href' => createLink('my', 'profile', '', '', true), + 'className' => 'items-center gap-2 px-2 py-1 row text-inherit', + 'style' => array('padding-left' => 0), + 'renders' => array(array('__html' => implode('', array + ( + userAvatar(set::user($user), setClass('flex-none'))->render(), + div + ( + setClass('flex-auto'), + div(setClass('text-lg'), empty($user->realname) ? $user->account : $user->realname), + $noRole ? null : div(setClass('text-gray text-sm'), $lang->user->roleList[$user->role]) + )->render() + )))), + ); + + $items[] = array('type' => 'divider'); + + $items[] = array + ( + 'url' => createLink('my', 'profile', '', '', true), + 'icon' => 'account', + 'text' => $lang->profile, + 'class' => 'iframe', + 'data-width' => 700 + ); + + if($app->config->vision === 'rnd') + { + if(!commonModel::isTutorialMode()) + { + $items[] = array + ( + 'url' => createLink('tutorial', 'start'), + 'icon' => 'guide', + 'text' => $lang->tutorialAB, + 'class' => '800', + 'outerClass' => 'user-tutorial', + 'data-width' => 700, + 'data-class-name' => 'modal-inverse', + 'data-headerless' => true, + 'data-backdrop' => true, + 'data-keyboard' => true + ); + } + + $items[] = array + ( + 'url' => createLink('my', 'preference', 'showTip=false', '', true), + 'icon' => 'controls', + 'text' => $lang->preference, + 'class' => 'iframe', + 'data-width' => 700 + ); + } + + if(common::hasPriv('my', 'changePassword')) + { + $items[] = array + ( + 'url' => createLink('my', 'changepassword', '', '', true), + 'icon' => 'cog-outline', + 'text' => $lang->changePassword, + 'class' => 'iframe', + 'data-width' => 600 + ); + } + + $items[] = array('type' => 'divider'); + } + + $themeItems = array(); + foreach($app->lang->themes as $key => $value) + { + $themeItems[] = array('text' => $value, 'data-value' => $key, 'url' => "javascript:selectTheme(\"$key\")", 'active' => $app->cookie->theme == $key); + } + $items[] = array + ( + 'text' => $lang->theme, + 'icon' => 'theme', + 'items' => $themeItems + ); + + $langItems = array(); + foreach ($app->config->langs as $key => $value) + { + $langItems[] = array('text' => $value, 'data-value' => $key, 'url' => "javascript:selectLang(\"$key\")", 'active' => $app->cookie->lang == $key); + } + $items[] = array('text' => $lang->lang, 'icon' => 'lang', 'items' => $langItems); + + $helpItems = array(); + $manualUrl = ((!empty($config->isINT)) ? $config->manualUrl['int'] : $config->manualUrl['home']) . '&theme=' . $_COOKIE['theme']; + $helpItems[] = array('text' => $lang->manual, 'url' => $manualUrl, 'attrs' => array('data-app' => 'help')); + $helpItems[] = array('text' => $lang->changeLog, 'url' => createLink('misc', 'changeLog'), 'data-toggle' => 'modal'); + $items[] = array('text' => $lang->help, 'icon' => 'help', 'items' => $helpItems); + + /* printClientLink */ + + $items[] = array('text' => $lang->aboutZenTao, 'icon' => 'about', 'url' => createLink('misc', 'about'), 'data-toggle' => 'modal'); + $items[] = array('type' => 'html', 'className' => 'menu-item', 'html' => $lang->designedByAIUX); + + $items[] = array('type' => 'divider'); + + if($isGuest) + { + $items[] = array('text' => $lang->login, 'url' => createLink('user', 'login'), 'target' => '_top'); + } + else + { + $items[] = array('text' => $lang->logout, 'url' => createLink('user', 'logout'), 'target' => '_top', 'icon' => 'exit'); + } + + return dropdown + ( + a + ( + setClass('w-7 h-7 cursor-pointer'), + userAvatar + ( + set::circle(true), + set::size(28), + set::user($user) + ), + set::caret(false) + ), + + set::id('userMenu'), + set::trigger('click'), + set::placement('bottom'), + set::menu(array('style' => array('color' => 'var(--color-fore)'))), + set::strategy('fixed'), + set::arrow(true), + set::items($items) + ); + } + + static function quickAddMenu() + { + global $app, $config, $lang; + + /* Initialize the default values. */ + $showCreateList = $needPrintDivider = false; + + /* Get default product id. */ + $productID = isset($_SESSION['product']) ? $_SESSION['product'] : 0; + if($productID) + { + $product = $app->dbh->query("SELECT id FROM " . TABLE_PRODUCT . " WHERE `deleted` = '0' and vision = '{$config->vision}' and id = '{$productID}'")->fetch(); + if(empty($product)) $productID = 0; + } + if(!$productID and $app->user->view->products) + { + $product = $app->dbh->query("SELECT id FROM " . TABLE_PRODUCT . " WHERE `deleted` = '0' and vision = '{$config->vision}' and id " . helper::dbIN($app->user->view->products) . " order by `order` desc limit 1")->fetch(); + if($product) $productID = $product->id; + } + + if($config->vision == 'lite') + { + $condition = " WHERE `deleted` = '0' AND `vision` = 'lite' AND `model` = 'kanban'"; + if(!$app->user->admin) $condition .= " AND `id` " . helper::dbIN($app->user->view->projects); + + $object = $app->dbh->query("select id from " . TABLE_PROJECT . $condition . ' LIMIT 1')->fetch(); + if(empty($object)) unset($lang->createIcons['story'], $lang->createIcons['task'], $lang->createIcons['execution']); + } + + if($config->edition == 'open') unset($lang->createIcons['effort']); + if($config->systemMode == 'light') unset($lang->createIcons['program']); + + /* Check whether the creation permission is available, and print create buttons. */ + $items = array(); + foreach($lang->createIcons as $objectType => $objectIcon) + { + $createMethod = 'create'; + $module = $objectType == 'kanbanspace' ? 'kanban' : $objectType; + if($objectType == 'effort') $createMethod = 'batchCreate'; + if($objectType == 'kanbanspace') $createMethod = 'createSpace'; + if(str_contains('|bug|execution|kanbanspace|', "|$objectType|")) $needPrintDivider = true; + + if(!common::hasPriv($module, $createMethod)) continue; + + if($objectType == 'doc' and !common::hasPriv('doc', 'tableContents')) continue; + + /* Determines whether to print a divider. */ + if($needPrintDivider and $showCreateList) + { + $items[] = array('type' => 'divider'); + $needPrintDivider = false; + } + + $showCreateList = true; + $isOnlyBody = false; + $item = array('icon' => $objectIcon, 'text' => $lang->createObjects[$objectType]); + + $params = ''; + switch($objectType) + { + case 'doc': + $params = "objectType=&objectID=0&libID=0"; + $createMethod = 'selectLibType'; + $isOnlyBody = true; + $item['class'] = 'iframe'; + $item['data-width'] = '700px'; + break; + case 'project': + if($config->vision == 'lite') + { + $params = "model=kanban"; + } + else if(!defined('TUTORIAL')) + { + $params = "programID=0&from=global"; + $createMethod = 'createGuide'; + $item['data-toggle'] = 'modal'; + } + else + { + $params = "model=scrum&programID=0©ProjectID=0&extra=from=global"; + } + + break; + case 'bug': + $params = "productID=$productID&branch=&extras=from=global"; + break; + case 'story': + if(!$productID and $config->vision == 'lite') + { + $module = 'project'; + $params = "model=kanban"; + } + else + { + $params = "productID=$productID&branch=0&moduleID=0&storyID=0&objectID=0&bugID=0&planID=0&todoID=0&extra=from=global"; + if($config->vision == 'lite') + { + $projectID = isset($_SESSION['project']) ? $_SESSION['project'] : 0; + $projects = $app->dbh->query("SELECT t2.id FROM " . TABLE_PROJECTPRODUCT . " AS t1 LEFT JOIN " . TABLE_PROJECT . " AS t2 ON t1.project = t2.id WHERE t1.`product` = '{$productID}' and t2.`type` = 'project' and t2.id " . helper::dbIN($app->user->view->projects) . " ORDER BY `order` desc")->fetchAll(); + + $projectIdList = array(); + foreach($projects as $project) $projectIdList[$project->id] = $project->id; + if($projectID and !isset($projectIdList[$projectID])) $projectID = 0; + if(empty($projectID)) $projectID = key($projectIdList); + + $params = "productID={$productID}&branch=0&moduleID=0&storyID=0&objectID={$projectID}&bugID=0&planID=0&todoID=0&extra=from=global"; + } + } + + break; + case 'task': + $params = "executionID=0&storyID=0&moduleID=0&taskID=0&todoID=0&extra=from=global"; + break; + case 'testcase': + $params = "productID=$productID&branch=&moduleID=0&from=¶m=0&storyID=0&extras=from=global"; + break; + case 'execution': + $projectID = isset($_SESSION['project']) ? $_SESSION['project'] : 0; + $params = "projectID={$projectID}&executionID=0©ExecutionID=0&planID=0&confirm=no&productID=0&extra=from=global"; + break; + case 'product': + $params = "programID=&extra=from=global"; + break; + case 'program': + $params = "parentProgramID=0&extra=from=global"; + break; + case 'kanbanspace': + $isOnlyBody = true; + $item['class'] = 'iframe'; + $item['data-width'] = '75%'; + break; + case 'kanban': + $isOnlyBody = true; + $item['class'] = 'iframe'; + $item['data-width'] = '75%'; + break; + } + + $item['url'] = createLink($module, $createMethod, $params, '', $isOnlyBody); + + $items[] = $item; + } + + if(!$showCreateList) return ''; + + return dropdown + ( + btn + ( + icon('plus', set::size('lg')), + setClass('bg-white ring-0 rounded bg-opacity-20'), + set::square(true), + set::size('sm'), + set::caret(false) + ), + + set::id('quickAddMenu'), + set::menu(array('style' => array('color' => 'var(--color-fore)'))), + set::trigger('hover'), + set::placement('bottom'), + set::strategy('fixed'), + set::arrow(true), + set::items($items) + ); + } +} diff --git a/lib/zin/wg/heading/v1.php b/lib/zin/wg/heading/v1.php new file mode 100644 index 0000000000..c63158eaf4 --- /dev/null +++ b/lib/zin/wg/heading/v1.php @@ -0,0 +1,103 @@ + array('map' => 'toolbar'), + 'dropmenu' => array('map' => 'dropmenu') + ); + + protected function buildAppName() + { + list($tab, $lang) = data(['app.tab', 'lang']); + $icon = zget($lang->navIconNames, $tab, ''); + + if(!in_array($tab, array('program', 'product', 'project'))) + { + $nav = $lang->mainNav->$tab; + list($title, $currentModule, $currentMethod, $vars) = explode('|', $nav); + if($tab == 'execution') $currentMethod = 'all'; + } + else + { + $currentModule = $tab; + if($tab == 'program' or $tab == 'project') $currentMethod = 'browse'; + if($tab == 'product') $currentMethod = 'all'; + } + + $url = createLink($currentModule, $currentMethod); + return item + ( + set::url($url), + set::hint($lang->$tab->common), + $tab == 'devops' ? setClass('num') : null, + set::icon($icon), + set::text($lang->$tab->common) + ); + } + + /** + * Build dropmenu. + * + * @access protected + * @return dropmenu + */ + protected function buildDropmenu(): dropmenu|array|null + { + global $app, $config; + if($this->hasBlock('dropmenu')) return $this->block('dropmenu'); + + $moduleName = $app->rawModule; + $methodName = $app->rawMethod; + if(in_array("$moduleName-$methodName", is_array($config->excludeDropmenuList) ? $config->excludeDropmenuList : array())) return null; + + if(in_array($app->tab, is_array($config->hasDropmenuApps) ? $config->hasDropmenuApps : array())) + { + $module = $app->tab == 'qa' ? 'product' : $app->tab; + return new dropmenu(set::tab($module)); + } + + return null; + } + + protected function buildToolbar() + { + $showAppName = $this->prop('showAppName'); + if($this->hasBlock('toolbar')) $this->prop('toolbar'); + return new toolbar + ( + $showAppName ? $this->buildAppName() : null, + set::btnClass('primary'), + set::items($this->prop('items')), + $this->children() + ); + } + + /** + * Build. + * + * @access protected + */ + protected function build(): wg + { + return div + ( + set::id('heading'), + $this->buildToolbar(), + $this->buildDropmenu() + ); + } +} diff --git a/lib/zin/wg/history/css/v1.css b/lib/zin/wg/history/css/v1.css new file mode 100644 index 0000000000..a5bd86d062 --- /dev/null +++ b/lib/zin/wg/history/css/v1.css @@ -0,0 +1,16 @@ +.history .btn-mini {width: 14px; height: 14px; overflow: hidden; color: var(--color-slate-500); border-radius: 1px; border: 1px solid var(--color-slate-500); box-shadow: none;} +.history .btn-comment {font-size: 12px; font-weight: 400; color: var(--color-slate-700);} +.history .btn-edit-comment {position: absolute !important;} +.history-list .marker {color: var(--color-slate-700); border-color: var(--color-gray-200); background: var(--color-slate-50);} +.history-list .comment-content {background: var(--color-gray-100);} +.history-list.sort-reverse {flex-direction: column-reverse;} +.history-changes {display: none;} +.history-changes.show {display: block; } + +.history-list > li strong {color: var(--color-slate-800);} +.history-list > li {position: relative;} +.history-list > li::before {display: block; content: ''; position: absolute; width: 1px; height: 100%; background: var(--color-gray-200); top: 12px; left: 10px;} +.history-list > li:last-child::before {display: none;} +.history-list.sort-reverse > li::before {display: block; content: ''; position: absolute; width: 1px; height: 100%; background: var(--color-gray-200); top: 12px; left: 10px;} +.history-list.sort-reverse > li:first-child::before {display: none;} +.history-list.sort-reverse > li:last-child::before {display: block;} diff --git a/lib/zin/wg/history/js/v1.js b/lib/zin/wg/history/js/v1.js new file mode 100644 index 0000000000..f8170463dd --- /dev/null +++ b/lib/zin/wg/history/js/v1.js @@ -0,0 +1,78 @@ +function reverseList(event) +{ + const history = event.target.closest('.history'); + history.querySelector('.history-list').classList.toggle('sort-reverse'); + + const button = event.target.closest('button'); + const icon = button.querySelector('.icon'); + icon.classList.toggle('icon-arrow-up'); + icon.classList.toggle('icon-arrow-down'); +} + +function expandAll(event) +{ + const button = event.target.closest('button'); + const icon = button.querySelector('.icon'); + icon.classList.toggle('icon-plus'); + icon.classList.toggle('icon-minus'); + + const isExpand = icon.classList.contains('icon-minus'); + const history = event.target.closest('.history'); + const changeBoxs = history.querySelectorAll('.history-changes'); + const icons = history.querySelectorAll('.btn-expand > .icon'); + + if(isExpand) + { + changeBoxs.forEach((box) => box.classList.add('show')); + icons.forEach((icon) => + { + icon.classList.remove('icon-plus'); + icon.classList.add('icon-minus'); + }); + } + else + { + changeBoxs.forEach((box) => box.classList.remove('show')); + icons.forEach((icon) => + { + icon.classList.remove('icon-minus'); + icon.classList.add('icon-plus'); + }); + } +} + +function expand(event) +{ + const btn = event.target.closest('button'); + const changeBox = btn.nextElementSibling; + const icon = btn.querySelector('.icon'); + icon.classList.toggle('icon-plus'); + icon.classList.toggle('icon-minus'); + + if(icon.classList.contains('icon-plus')) changeBox.classList.remove('show'); + else changeBox.classList.add('show'); +} + +function editComment(event) +{ + event.target.closest('.article-content.comment').classList.add('hidden'); + const history = event.target.closest('.history'); + const form = history.querySelector('.history-list .comment-edit-form'); + if(!form) return; + + form.classList.remove('hidden'); +} + +function closeCommentForm(event) +{ + const current = event.target; + current.closest('.comment-edit-form').classList.add('hidden'); + + const comment = current.closest('li').querySelector('.article-content.comment'); + if(!comment) return; + + comment.classList.remove('hidden'); +} + +const closeBtn = document.querySelector('#btn-close-form'); +if(closeBtn) closeBtn.addEventListener('click', closeCommentForm); diff --git a/lib/zin/wg/history/v1.php b/lib/zin/wg/history/v1.php new file mode 100644 index 0000000000..fe4d3a3249 --- /dev/null +++ b/lib/zin/wg/history/v1.php @@ -0,0 +1,253 @@ +prop('methodName') !== null ? $this->prop('methodName') : $app->rawMethod; + $actions = $this->prop('actions') !== null ? $this->prop('actions') : data('actions'); + + return (!isset($canBeChanged) || !empty($canBeChanged)) + && !empty($actions) && end($actions) == $action + && trim($action->comment) !== '' + && str_contains(',view,objectlibs,viewcard,', ",$methodName,") + && $action->actor == $app->user->account + && common::hasPriv('action', 'editComment'); + } + + private function expandBtn(): wg + { + global $lang; + return btn + ( + setClass('btn-expand btn-mini px-0'), + set::title($lang->switchDisplay), + h::i(setClass('change-show icon icon-plus icon-sm')), + on::click('expand'), + ); + } + + private function editCommentBtn(): wg + { + global $lang; + return button + ( + setClass('btn btn-link btn-edit-comment right-0'), + set::title($lang->action->editComment), + h::i(setClass('icon icon-pencil')), + on::click('editComment') + ); + } + + private function historyChanges(object $action): wg + { + global $app; + return div + ( + setClass('history-changes ml-7 mt-2'), + html($app->loadTarget('action')->renderChanges($action->objectType, $action->history)), + ); + } + + private function actionItem(object $action, int $i): wg + { + global $app; + return li + ( + setClass('mb-2'), + set::value($i), + $this->marker($i), + html($app->loadTarget('action')->renderAction($action)) + ); + } + + private function getComment(object $action): string + { + if(str_contains($action->comment, '
    '))
    +        {
    +            $before   = explode('
    ', $action->comment);
    +            $after    = explode('
    ', $before[1]); + $htmlCode = $after[0]; + return $before[0] . htmlspecialchars($htmlCode) . $after[1]; + } + + return strip_tags($action->comment) === $action->comment + ? nl2br($action->comment) + : $action->comment; + } + + private function comment(object $action): wg + { + $comment = $this->getComment($action); + $canEdit = $this->checkEditCommentPriv($action); + return div + ( + setClass('article-content comment relative'), + $canEdit ? $this->editCommentBtn() : null, + div + ( + setClass('comment-content mt-2 ml-6 p-2.5'), + isHTML($comment) ? html($comment) : $comment, + ), + ); + } + + private function commentEditForm(object $action): wg + { + global $lang; + + return form + ( + setClass('comment-edit-form hidden mt-2 ml-6'), + set::method('post'), + set::action(createLink('action', 'editComment', "actionID=$action->id")), + editor + ( + set::name('lastComment'), + isHTML($action->comment) ? html($action->comment) : $action->comment + ), + set::actions(array( + 'submit', + array('text' => $lang->close, 'id' => 'btn-close-form') + )) + ); + } + + private function historyList(): wg + { + $actions = $this->prop('actions') !== null ? $this->prop('actions') : data('actions'); + $users = $this->prop('users') !== null ? $this->prop('users') : data('users'); + $historiesListView = h::ol(setClass('history-list col relative')); + + $i = 0; + foreach($actions as $action) + { + if($action->action === 'assigned' || $action->action === 'toaudit') $action->extra = zget($users, $action->extra); + $action->actor = zget($users, $action->actor); + if(str_contains($action->actor, ':')) $action->actor = substr($action->actor, strpos($action->actor, ':') + 1); + + $i++; + $actionItemView = $this->actionItem($action, $i); + + if(!empty($action->history)) + { + $actionItemView->add($this->expandBtn()); + $actionItemView->add($this->historyChanges($action)); + } + if(strlen(trim(($action->comment))) !== 0) + { + $actionItemView->add($this->comment($action)); + + $canEditComment = $this->checkEditCommentPriv($action); + if($canEditComment) $actionItemView->add($this->commentEditForm($action)); + } + $historiesListView->add($actionItemView); + } + + return $historiesListView; + } + + private function reverseBtn(): wg + { + global $lang; + return btn + ( + setClass('btn-reverse btn-mini px-0 ml-3'), + set::title($lang->reverse), + set::icon('arrow-up'), + on::click('reverseList') + ); + } + + private function expandAllBtn(): wg + { + global $lang; + return btn + ( + setClass('btn-mini px-0 btn-expand-all ml-2'), + set::title($lang->switchDisplay), + set::icon('plus'), + on::click('expandAll') + ); + } + + private function commentBtn(): ?wg + { + global $app, $lang; + $methodName = $this->prop('methodName') !== null ? $this->prop('methodName') : $app->rawMethod; + $showCommentBtn = $this->prop('commentBtn', false); + if(!$showCommentBtn && !str_contains(',view,objectlibs,viewcard,', ",$methodName,")) return null; + return commentBtn + ( + set::dataTarget('#comment-dialog'), + setClass('btn-comment ml-4 size-sm ghost'), + set::icon('chat-line'), + set::iconClass('text-primary'), + set::text($lang->action->create) + ); + } + + protected function build(): wg + { + global $lang; + + $commentUrl = $this->prop('commentUrl'); + $isInModal = isAjaxRequest('modal'); + $px = $isInModal ? 'px-3' : 'px-6'; + $pb = $isInModal ? 'pb-3' : 'pb-6'; + + return new section + ( + setClass('history', 'pt-4', $px, $pb, 'canvas'), + set::title($lang->history), + to::actions + ( + div + ( + setClass('flex items-center'), + $this->reverseBtn(), + $this->expandAllBtn(), + $this->commentBtn(), + ) + ), + div(setClass('mt-3'), $this->historyList()), + commentDialog + ( + set::name('comment'), + set::url($commentUrl), + ) + ); + } +} diff --git a/lib/zin/wg/hr/v1.php b/lib/zin/wg/hr/v1.php new file mode 100644 index 0000000000..9dbf422fc5 --- /dev/null +++ b/lib/zin/wg/hr/v1.php @@ -0,0 +1,11 @@ +props->has('name')) + { + $this->props->set('name', $child); + return false; + } + } + + protected function build(): wg + { + list($name, $size) = $this->prop(array('name', 'size')); + return h::i + ( + setClass('icon', empty($name) ? null : "icon-$name"), + is_numeric($size) + ? setStyle('font-size', "{$size}px") + : (is_string($size) + ? setClass("icon-$size") + : null), + set($this->props->skip(array_keys(icon::definedPropsList()))), + $this->children() + ); + } +} diff --git a/lib/zin/wg/input/v1.php b/lib/zin/wg/input/v1.php new file mode 100644 index 0000000000..02ac3bdd77 --- /dev/null +++ b/lib/zin/wg/input/v1.php @@ -0,0 +1,33 @@ + 'text', + 'class' => 'form-control', + ); + + protected function build(): wg + { + $props = $this->props->skip('required'); + $required = $this->prop('required'); + if(!$this->hasProp('id') && isset($props['name'])) $props['id'] = $props['name']; + if(is_bool($props['autocomplete'])) $props['autocomplete'] = $props['autocomplete'] ? 'on' : 'off'; + return h::input(set($props), $required ? setClass('is-required') : null); + } +} diff --git a/lib/zin/wg/inputcontrol/v1.php b/lib/zin/wg/inputcontrol/v1.php new file mode 100644 index 0000000000..f5d12719da --- /dev/null +++ b/lib/zin/wg/inputcontrol/v1.php @@ -0,0 +1,72 @@ + array(), + 'suffix' => array(), + ); + + protected function build(): wg + { + list($prefix, $suffix, $prefixWidth, $suffixWidth) = $this->prop(['prefix', 'suffix', 'prefixWidth', 'suffixWidth']); + + if(empty($prefix)) $prefix = $this->block('prefix'); + if(empty($suffix)) $suffix = $this->block('suffix'); + + $class = array('input-control'); + $vars = array(); + if(!empty($prefix)) + { + if(is_numeric($prefixWidth)) + { + $vars['input-control-prefix'] = $prefixWidth . 'px'; + $class[] = 'has-prefix'; + } + elseif(!empty($prefixWidth)) + { + $class[] = "has-prefix-$prefixWidth"; + } + else + { + $class[] = 'has-prefix'; + } + } + if(!empty($suffix)) + { + if(is_numeric($suffixWidth)) + { + $vars['input-control-suffix'] = $suffixWidth . 'px'; + $class[] = 'has-suffix'; + } + elseif(!empty($suffixWidth)) + { + $class[] = "has-suffix-$suffixWidth"; + } + else + { + $class[] = 'has-suffix'; + } + } + + return div + ( + setClass($class), + empty($vars) ? null : setCssVar($vars), + $this->children(), + empty($prefix) ? null : div(setClass('input-control-prefix'), $prefix), + empty($suffix) ? null : div(setClass('input-control-suffix'), $suffix) + ); + } +} diff --git a/lib/zin/wg/inputgroup/v1.php b/lib/zin/wg/inputgroup/v1.php new file mode 100644 index 0000000000..fd7dd7e9ad --- /dev/null +++ b/lib/zin/wg/inputgroup/v1.php @@ -0,0 +1,53 @@ + 'addon', 'text' => $item))); + elseif(is_array($item)) $item = new item(set($item)); + elseif($item instanceof wg) return $item; + + $type = $item->prop('type'); + + if($type === 'addon') return h::span(setClass('input-group-addon'), set($item->props->skip('type,text')), $item->prop('text')); + if($type === 'btn') return new btn(set($item->props->skip('type'))); + + if($type) + { + $propNames = array_keys(inputControl::definedPropsList()); + return new inputControl + ( + set($item->props->pick($propNames)), + new input(set($item->props->skip(array_merge($propNames, ['type'])))) + ); + } + + return new input(inherit($item)); + } + + protected function build(): wg + { + list($items, $seg) = $this->prop(['items', 'seg']); + $children = $this->children(); + + return div + ( + setClass('input-group', $seg ? 'input-group-segment' : null), + set($this->getRestProps()), + is_array($items) ? array_map(array($this, 'onBuildItem'), $items) : null, + is_array($children) ? array_map(array($this, 'onBuildItem'), $children) : null, + ); + } +} diff --git a/lib/zin/wg/inputgroupaddon/v1.php b/lib/zin/wg/inputgroupaddon/v1.php new file mode 100644 index 0000000000..4da97ef37c --- /dev/null +++ b/lib/zin/wg/inputgroupaddon/v1.php @@ -0,0 +1,27 @@ + + * @package zin + * @link http://www.zentao.net + */ + +namespace zin; + +/** + * 输入框附加部分(inputGroupAddon)部件类。 + * The inputGroupAddon widget class. + * + * @author Hao Sun + */ +class inputGroupAddon extends wg +{ + protected function build(): wg + { + return h::span(setClass('input-group-addon'), set($this->props), $this->children()); + } +} diff --git a/lib/zin/wg/label/v1.php b/lib/zin/wg/label/v1.php new file mode 100644 index 0000000000..8e1ea8c562 --- /dev/null +++ b/lib/zin/wg/label/v1.php @@ -0,0 +1,30 @@ +props->has('text')) + { + $this->props->set('text', $child); + return false; + } + } + + public function build(): wg + { + return span + ( + setClass('label'), + set($this->getRestProps()), + $this->prop('text'), + $this->children() + ); + } +} diff --git a/lib/zin/wg/main/v1.php b/lib/zin/wg/main/v1.php new file mode 100644 index 0000000000..5c094745f3 --- /dev/null +++ b/lib/zin/wg/main/v1.php @@ -0,0 +1,134 @@ + + * @package zin + * @link http://www.zentao.net + */ +namespace zin; + +require_once dirname(__DIR__) . DS . 'pagebase' . DS . 'v1.php'; +require_once dirname(__DIR__) . DS . 'featurebar' . DS . 'v1.php'; +require_once dirname(__DIR__) . DS . 'mainnavbar' . DS . 'v1.php'; + +/** + * 主要内容部件类。 + * The main widget class. + * + * @author Hao Sun + */ +class main extends wg +{ + /** + * Define the blocks. + * + * @var array + * @access protected + */ + protected static array $defineBlocks = array + ( + 'navbar' => array('map' => 'mainNavbar'), + 'menu' => array('map' => 'featureBar,nav,toolbar'), + 'sidebar' => array('map' => 'sidebar') + ); + + /** + * Define the properties. + * + * @access protected + * @return wg + */ + protected function buildMenu(): wg|null + { + $menuBlocks = $this->block('menu'); + if(empty($menuBlocks)) return div(); + + list($featureBarList, $navList, $toolbarList, $restList) = groupWgInList($menuBlocks, array('featureBar', 'nav', 'toolbar')); + + $featureBar = null; + if(!empty($featureBarList)) $featureBar = $featureBarList[0]; + elseif(!empty($navList)) $featureBar = new featureBar($navList); + + $toolbar = null; + if(!empty($toolbarList)) $toolbar = $toolbarList[0]; + if($toolbar instanceof wg && !$toolbar->hasProp('id')) $toolbar->setProp('id', 'actionBar'); + + return div + ( + set::id('mainMenu'), + $featureBar, + $toolbar, + $restList + ); + } + + /** + * Build main content. + * + * @access protected + * @return wg + */ + protected function buildContent() + { + $leftSides = array(); + $rightSides = array(); + $sidebars = $this->block('sidebar'); + + if(!empty($sidebars)) + { + foreach($sidebars as $sidebar) + { + if($sidebar instanceof wg && $sidebar->prop('side') === 'left') $leftSides[] = $sidebar; + else $rightSides[] = $sidebar; + } + } + + return div + ( + set::id('mainContent'), + $leftSides, + set::class(empty($leftSides) && empty($rightSides) ? '' : 'row', empty($leftSides) ? '' : 'has-sidebar-left', empty($rightSides) ? '' : 'has-sidebar-right'), + $this->children(), + $rightSides + ); + } + + /** + * Build main navbar from block. + * + * @access protected + * @return array + */ + protected function buildMainNavbar(): array|mainNavbar + { + $navbar = $this->block('navbar'); + if(!$navbar) return mainNavbar(); + return $navbar; + } + + /** + * Override the build method. + * + * @access protected + * @return wg + */ + protected function build(): wg + { + return div + ( + set::id('main'), + set($this->getRestProps()), + $this->buildMainNavbar(), + div + ( + set::class('container'), + $this->buildMenu(), + $this->buildContent() + ) + ); + } +} diff --git a/lib/zin/wg/mainmenu/v1.php b/lib/zin/wg/mainmenu/v1.php new file mode 100644 index 0000000000..7cdac2b5ab --- /dev/null +++ b/lib/zin/wg/mainmenu/v1.php @@ -0,0 +1,71 @@ +prop('others'); + + if(empty($others)) + { + $otherElms = null; + } + else + { + $otherElms = array(); + foreach($others as $item) $otherElms[] = $this->buildOther($item); + } + + + return div + ( + setID('mainMenu'), + setClass('flex justify-between'), + set($this->getRestProps()), + div + ( + setClass('flex'), + div + ( + toolbar(set(array('items' => $this->prop('statuses')))) + ), + $otherElms + ), + div + ( + setID('featureBarBtns'), + toolbar + ( + setClass('toolbar-btn-group'), + setStyle('gap', '0.625rem'), + set(array('items' => $this->prop('btnGroup'))) + ) + ) + ); + } +} diff --git a/lib/zin/wg/mainnavbar/v1.php b/lib/zin/wg/mainnavbar/v1.php new file mode 100644 index 0000000000..55e866db84 --- /dev/null +++ b/lib/zin/wg/mainnavbar/v1.php @@ -0,0 +1,107 @@ + + * @package zin + * @link http://www.zentao.net + */ + +namespace zin; + +require_once dirname(__DIR__) . DS . 'nav' . DS . 'v1.php'; + +/** + * 主内容导航(三级导航,mainNavbar)部件类。 + * The main navbar widget class. + * + * @author Hao Sun + */ +class mainNavbar extends nav +{ + /** + * Define the blocks. + * + * @var array + * @access protected + */ + protected static array $defineBlocks = array + ( + 'left' => array('map' => 'dropdown'), + 'right' => array('map' => 'toolbar'), + ); + + protected function created() + { + global $app; + + $currentModule = $app->getModuleName(); + $currentMethod = $app->getMethodName(); + if($app->tab == 'admin') $app->control->loadModel('admin')->setMenu(); + + \commonModel::setMainMenu(); + $activeMenu = \commonModel::printMainMenu(false); + $items = \customModel::getModuleMenu($activeMenu); + + if($items) + { + $items = json_decode(json_encode($items), true); + + foreach($items as $key => $item) + { + if(empty($item['link'])) + { + unset($items[$key]); + continue; + } + if(empty($item['alias'])) $item['alias'] = ''; + if(empty($item['exclude'])) $item['exclude'] = ''; + + $link = $item['link']; + $items[$key]['url'] = commonModel::createMenuLink((object)$item, $app->tab); + $items[$key]['data-id'] = $item['name']; + + $active = ''; + $subModule = isset($item['subModule']) ? explode(',', $item['subModule']) : array(); + if($subModule && in_array($currentModule, $subModule)) $active = 'active'; + if($link['module'] == $currentModule && $link['method'] == $currentMethod) $active = 'active'; + if($link['module'] == $currentModule && strpos(",{$item['alias']},", ",{$currentMethod},") !== false) $active = 'active'; + if(strpos(",{$item['exclude']},", ",{$currentModule}-{$currentMethod},") !== false || strpos(",{$item['exclude']},", ",{$currentModule},") !== false) $active = ''; + $items[$key]['class'] = $active; + + unset($items[$key]['name']); + } + + $this->setProp('items', $items); + } + } + + /** + * Override the build method. + * + * @access protected + * @return wg + */ + protected function build(): h + { + if(!$this->prop('items')) return div(); + + $leftBlock = $this->block('left'); + $rightBlock = $this->block('right'); + + return div + ( + setID('mainNavbar'), + div + ( + setClass('container'), + empty($leftBlock) ? null : div(setClass('main-navbar-left'), $leftBlock), + parent::build(), + empty($rightBlock) ? null : div(setClass('main-navbar-right'), $rightBlock) + ) + ); + } +} diff --git a/lib/zin/wg/menu/v1.php b/lib/zin/wg/menu/v1.php new file mode 100644 index 0000000000..d05752314b --- /dev/null +++ b/lib/zin/wg/menu/v1.php @@ -0,0 +1,37 @@ +prop('items'); + return h::menu + ( + setClass('menu'), + set($this->getRestProps()), + is_array($items) ? array_map(array($this, 'onBuildItem'), $this->prop('items')) : null, + $this->children(), + ); + } +} diff --git a/lib/zin/wg/modal/v1.php b/lib/zin/wg/modal/v1.php new file mode 100644 index 0000000000..0a57027bdb --- /dev/null +++ b/lib/zin/wg/modal/v1.php @@ -0,0 +1,32 @@ + array() + ); + + protected function build(): wg + { + list($id, $modalProps) = $this->prop(array('id', 'modalProps')); + + $this->setProp($modalProps); + + return div + ( + setClass('modal'), + setID($id), + set($this->props->skip(array_merge(array_keys($modalProps), array_keys(static::definedPropsList())))), + parent::build() + ); + } +} diff --git a/lib/zin/wg/modaldialog/css/v1.css b/lib/zin/wg/modaldialog/css/v1.css new file mode 100644 index 0000000000..e69de29bb2 diff --git a/lib/zin/wg/modaldialog/v1.php b/lib/zin/wg/modaldialog/v1.php new file mode 100644 index 0000000000..b243d63c31 --- /dev/null +++ b/lib/zin/wg/modaldialog/v1.php @@ -0,0 +1,125 @@ + array('map' => 'modalHeader'), + 'actions' => array(), + 'footer' => array('map' => 'toolbar') + ); + + protected function buildHeader() + { + $title = $this->prop('title'); + $titleClass = $this->prop('titleClass'); + $itemID = $this->prop('itemID'); + $headerBlock = $this->block('header'); + + if(empty($title) && empty($headerBlock)) return null; + + return div + ( + setClass('modal-header', $this->prop('headerClass')), + set($this->prop('headerProps')), + empty($itemID) ? null : label($itemID, setStyle(['min-width' => '30px']), setClass('justify-center')), + empty($title) ? null : div(setClass('modal-title', $titleClass), $title), + $headerBlock + ); + } + + protected function buildActions() + { + list($actions, $closeBtn) = $this->prop(array('actions', 'closeBtn')); + $actionsBlock = $this->block('actions'); + + if(empty($actions) && empty($actionsBlock) && !$closeBtn) return; + + return div + ( + setClass('modal-actions'), + empty($actions) ? null : toolbar(set::items($actions)), + $actionsBlock, + $closeBtn ? btn + ( + set('data-dismiss', 'modal'), + set::square(true), + is_array($closeBtn) ? set($closeBtn) : setClass('ghost'), + span(setClass('close')) + ) : null + ); + } + + protected function buildFooter() + { + $footerActions = $this->prop('footerActions'); + $footerBlock = $this->block('footer'); + + if(empty($footerActions) && empty($footerBlock)) return; + + return div + ( + setClass('modal-footer', $this->prop('footerClass')), + set($this->prop('footerProps')), + $footerBlock, + empty($footerActions) ? null : toolbar(set::items($footerActions)) + ); + } + + protected function buildBody() + { + $rawContent = $this->prop('rawContent', !zin::$rawContentCalled); + return div + ( + setClass('modal-body', $this->prop('bodyClass')), + set($this->prop('bodyProps')), + $this->children(), + $rawContent ? rawContent() : null, + ); + } + + protected function setSize(): ?directive + { + $size = $this->prop('size'); + if(!$size) return null; + if(is_string($size)) return set('data-size', $size); + return setStyle('width', "{$size}px"); + } + + protected function build(): wg + { + return div + ( + setClass('modal-dialog', $this->prop('condensed') ? 'modal-condensed' : ''), + set($this->getRestProps()), + $this->setSize(), + div + ( + setClass('modal-content'), + $this->buildHeader(), + $this->buildActions(), + $this->buildBody(), + $this->buildFooter() + ) + ); + } +} diff --git a/lib/zin/wg/modalheader/v1.php b/lib/zin/wg/modalheader/v1.php new file mode 100644 index 0000000000..6fc5891c7b --- /dev/null +++ b/lib/zin/wg/modalheader/v1.php @@ -0,0 +1,69 @@ + array() + ); + + protected function created() + { + $title = \initPageTitle(); + $entityText = ''; + $entityID = 0; + + global $app; + $module = $app->getModuleName(); + $object = data($module); + if(!empty($object)) + { + $entity = \initPageEntity($object); + if(!empty($entity)) list($entityText, $entityID) = $entity; + } + + $this->setDefaultProps(array('title' => $title, 'entityText' => $entityText, 'entityID' => $entityID)); + } + + protected function build(): wg|array + { + list($title, $entityText, $entityID, $inModal) = $this->prop(array('title', 'entityText', 'entityID', 'inModal')); + if(empty($inModal)) $inModal = false; + + $header = h::div + ( + setClass('flex items-center'), + $title ? div + ( + $title, + set::class($this->prop('titleClass')), + ) : null, + ($entityText || $entityID) ? entityLabel + ( + set::level(1), + setClass('pl-2'), + $entityText ? set::text($entityText) : null, + $entityID ? set::entityID($entityID) : null, + set::reverse(true), + ) : null, + $this->block('suffix') + ); + + if(isAjaxRequest('modal') || $inModal) return $header; + + return h::div + ( + set::class('modal-header panel-form rounded-md canvas mx-auto size-lg'), + $header + ); + } +} diff --git a/lib/zin/wg/modaltrigger/v1.php b/lib/zin/wg/modaltrigger/v1.php new file mode 100644 index 0000000000..db0b4c603a --- /dev/null +++ b/lib/zin/wg/modaltrigger/v1.php @@ -0,0 +1,90 @@ + array('map' => 'btn,a'), + 'modal' => array('map' => 'modal') + ); + + protected function build(): array + { + list($target, $url, $type) = $this->prop(['target', 'url', 'type']); + + $triggerBlock = $this->block('trigger'); + $modalBlock = $this->block('modal'); + + if(empty($target) && !empty($modalBlock)) + { + $modal = $modalBlock[0]; + $target = $modal->id(); + if(empty($target)) + { + $target = $modal->gid; + $modal->setProp('id', $target); + } + $target = "#$target"; + } + if(!empty($url) && empty($type)) $type = 'ajax'; + + if(empty($triggerBlock)) $triggerBlock = h::a($this->children()); + elseif(is_array($triggerBlock)) $triggerBlock = $triggerBlock[0]; + + if($triggerBlock instanceof wg) + { + $triggerBlock->setProp($this->getRestProps()); + + $triggerProps = array( + 'data-toggle' => 'modal', + 'data-target' => $triggerBlock->hasProp('target', 'href') ? null : $target, + 'data-type' => $type, + 'data-url' => $url, + 'data-position' => $this->prop('position'), + 'data-size' => $this->prop('size'), + 'data-backdrop' => $this->prop('backdrop'), + 'data-keyboard' => $this->prop('keyboard'), + 'data-moveable' => $this->prop('moveable'), + 'data-animation' => $this->prop('animation'), + 'data-trans-time' => $this->prop('transTime'), + 'data-responsive' => $this->prop('responsive'), + 'data-loading-text' => $this->prop('loadingText'), + 'data-loadTimeout' => $this->prop('loadTimeout'), + 'data-failed-tip' => $this->prop('failedTip'), + 'data-timeout-tip' => $this->prop('timeoutTip'), + 'data-title' => $this->prop('title'), + 'data-content' => $this->prop('content'), + 'data-custom' => $this->prop('custom'), + 'data-request' => $this->prop('request'), + 'data-data-type' => $this->prop('dataType') + ); + $triggerBlock->setProp($triggerProps); + } + + return array($triggerBlock, $modalBlock); + } +} diff --git a/lib/zin/wg/modulemenu/css/v1.css b/lib/zin/wg/modulemenu/css/v1.css new file mode 100644 index 0000000000..ef2dae65c1 --- /dev/null +++ b/lib/zin/wg/modulemenu/css/v1.css @@ -0,0 +1,4 @@ +.module-menu {max-height: calc(100vh - 105px);} +.module-menu .active {color: var(--color-primary-600); font-weight: 500;} +.module-menu header a:hover > .icon {color: var(--color-primary-600) !important;} +.module-menu .tree-item * {white-space: nowrap;} diff --git a/lib/zin/wg/modulemenu/v1.php b/lib/zin/wg/modulemenu/v1.php new file mode 100644 index 0000000000..5c532a1582 --- /dev/null +++ b/lib/zin/wg/modulemenu/v1.php @@ -0,0 +1,158 @@ +getChildModule($parentID); + if(count($children) === 0) return []; + + $activeKey = $this->prop('activeKey'); + + foreach($children as $child) + { + $item = array( + 'key' => $child->id, + 'text' => $child->name, + 'url' => $child->url, + 'items' => array(), + 'active' => $child->id == $activeKey, + ); + $items = $this->buildMenuTree($item['items'], $child->id); + if(count($items) !== 0) $item['items'] = $items; + else unset($item['items']); + $parentItems[] = $item; + } + return $parentItems; + } + + private function getChildModule(int|string $id): array + { + return array_filter($this->modules, fn($module) => $module->parent == $id); + } + + private function setMenuTreeProps(): void + { + $this->modules = $this->prop('modules'); + $this->setProp('items', $this->buildMenuTree(array(), 0)); + } + + private function getTitle(): string + { + global $lang; + $activeKey = $this->prop('activeKey'); + + if(empty($activeKey)) + { + $allText = $this->prop('allText'); + if(empty($allText)) return $lang->all; + return $allText; + } + + foreach($this->modules as $module) + { + if($module->id == $activeKey) return $module->name; + } + + return ''; + } + + private function buildBtns(): wg|null + { + $settingLink = $this->prop('settingLink'); + $settingText = $this->prop('settingText'); + $showDisplay = $this->prop('showDisplay'); + if(!$settingLink && !$showDisplay) return null; + + global $app; + $lang = $app->loadLang('datatable')->datatable; + $currentModule = $app->rawModule; + $currentMethod = $app->rawMethod; + + if(!$settingText) $settingText = $lang->moduleSetting; + + $datatableId = $app->moduleName . ucfirst($app->methodName); + + return div + ( + setClass('col gap-2 py-3 px-7'), + $settingLink + ? a + ( + setClass('btn'), + setStyle('background', '#EEF5FF'), + setStyle('box-shadow', 'none'), + set('data-app', $app->tab), + set::href($settingLink), + $settingText + ) + : null, + $showDisplay + ? a + ( + setClass('btn white'), + set('data-toggle', 'modal'), + set::href(helper::createLink('datatable', 'ajaxDisplay', "datatableId=$datatableId&moduleName=$app->moduleName&methodName=$app->methodName¤tModule=$currentModule¤tMethod=$currentMethod")), + $lang->displaySetting + ) + : null, + ); + } + + private function buildCloseBtn(): ?wg + { + $activeKey = $this->prop('activeKey'); + if(empty($activeKey)) return null; + + return a + ( + set('href', $this->prop('closeLink')), + icon('close', setStyle('color', 'var(--color-slate-600)')) + ); + } + + protected function build(): wg + { + $this->setMenuTreeProps(); + $title = $this->getTitle(); + + return div + ( + setClass('module-menu rounded shadow-sm bg-white col rounded-sm'), + h::header + ( + setClass('h-10 flex items-center pl-4 flex-none gap-3'), + span + ( + setClass('module-title text-lg font-semibold'), + $title + ), + $this->buildCloseBtn(), + ), + h::main + ( + setClass('col flex-auto overflow-y-auto overflow-x-hidden pl-4 pr-1'), + zui::tree(set($this->props->pick(array('items', 'activeClass', 'activeIcon', 'activeKey', 'onClickItem', 'defaultNestedShow', 'changeActiveKey', 'isDropdownMenu')))) + ), + $this->buildBtns(), + ); + } +} diff --git a/lib/zin/wg/monaco/js/v1.js b/lib/zin/wg/monaco/js/v1.js new file mode 100644 index 0000000000..a257947edc --- /dev/null +++ b/lib/zin/wg/monaco/js/v1.js @@ -0,0 +1,79 @@ +setTimeout(function() +{ + createMonaco(id, action, options, diffContent, onMouseDown, vsPath, clientLang); + resize(id); +}, 200); + +function createMonaco(id, action, options, diffContent, onMouseDown, vsPath, clientLang) +{ + require.config({ + paths: {vs: vsPath}, + 'vs/nls': { + availableLanguages: { + '*': clientLang + } + } + }); + + require(['vs/editor/editor.main'], function () + { + if(action == 'diff') + { + options.renderSideBySide = $.cookie.get('renderSideBySide') == 'true'; + + options.lineNumbers = function(number) + { + var newlc = diffContent.line.new; + return newlc[number - 1]; + }; + console.log('monaco options:', options); + + modifiedEditor = monaco.editor.createDiffEditor(document.getElementById(id), + options); + window.modifiedEditor = modifiedEditor; + + modifiedEditor.setModel({ + original: monaco.editor.createModel(diffContent.code.old.trim("\n"), options.lang), + modified: monaco.editor.createModel(diffContent.code.new.trim("\n"), options.lang), + }); + + editor = modifiedEditor.getModifiedEditor(); + + var getOriginalEditor = modifiedEditor.getOriginalEditor(); + + if(onMouseDown) getOriginalEditor.onMouseDown(function(obj){eval(onMouseDown + '(obj)')}) + } + else + { + console.log('monaco options:', options); + editor = monaco.editor.create(document.getElementById(id), options); + } + + if(onMouseDown) editor.onMouseDown(function(obj){eval(onMouseDown + '(obj)')}) + }); +} + +function resize(id) +{ + var $ = window.$ == undefined ? parent.$ : window.$; + var windowHeight = $(window).height(); + var headerHeight = parseInt($('#header').height()); + var mainNavbar = parseInt($('#mainNavbar').height()); + var mainMenuHeight = parseInt($('#mainMenu').css('padding-top')) + parseInt($('#mainMenu').css('padding-bottom')); + var appTabsHeight = parseInt($('#appTabs').height()); + var appsBarHeight = parseInt($('#appsBar').height()); + var tabsHeight = parseInt($('#fileTabs .tabs-navbar').height()); + + headerHeight = headerHeight ? headerHeight : 0; + appsBarHeight = appsBarHeight ? appsBarHeight : 0; + tabsHeight = tabsHeight ? tabsHeight : 0; + appTabsHeight = appTabsHeight ? appTabsHeight : 0; + mainMenuHeight = mainMenuHeight ? mainMenuHeight : 0; + mainNavbar = mainNavbar ? mainNavbar : 0; + + var codeHeight = windowHeight - headerHeight - appsBarHeight - tabsHeight - appTabsHeight - mainMenuHeight - mainNavbar; + + if(codeHeight > 0) $.cookie.set(id + 'Height', codeHeight); + + $('#' + id).css('height', $.cookie.get(id + 'Height')); +} \ No newline at end of file diff --git a/lib/zin/wg/monaco/v1.php b/lib/zin/wg/monaco/v1.php new file mode 100644 index 0000000000..1586d63bb9 --- /dev/null +++ b/lib/zin/wg/monaco/v1.php @@ -0,0 +1,51 @@ + 'create', + 'options' => array(), + 'diffContent' => array(), + 'onMouseDown' => '', + ); + public static function getPageJS(): string|false + { + return file_get_contents(__DIR__ . DS . 'js' . DS . 'v1.js'); + } + + protected function build(): wg + { + global $app; + $vsPath = $app->getWebRoot() . 'js/monaco-editor/min/vs'; + $clientLang = $app->clientLang; + $id = $this->prop('id'); + $action = $this->prop('action'); + $options = $this->prop('options'); + $diffContent = $this->prop('diffContent'); + $onMouseDown = $this->prop('onMouseDown'); + + if(!$options) $options = new stdclass(); + return div + ( + jsVar('id', $id), + jsVar('action', $action), + jsVar('options', $options), + jsVar('diffContent', $diffContent), + jsVar('onMouseDown', $onMouseDown), + jsVar('vsPath', $vsPath), + jsVar('clientLang', $clientLang), + h::import($app->getWebRoot() . 'js/monaco-editor/min/vs/loader.js'), + setId($id), + ); + } +} diff --git a/lib/zin/wg/nav/v1.php b/lib/zin/wg/nav/v1.php new file mode 100644 index 0000000000..b76cbeb341 --- /dev/null +++ b/lib/zin/wg/nav/v1.php @@ -0,0 +1,36 @@ +prop(array('items', 'type', 'stacked', 'justified')); + return h::menu + ( + setClass('nav', $type ? "nav-$type" : null, $stacked ? 'nav-stacked' : '', $justified ? 'nav-justified' : ''), + set($this->getRestProps()), + is_array($items) ? array_map(array($this, 'onBuildItem'), $items) : null, + $this->children() + ); + } +} diff --git a/lib/zin/wg/navbar/v1.php b/lib/zin/wg/navbar/v1.php new file mode 100644 index 0000000000..1d7cff76ae --- /dev/null +++ b/lib/zin/wg/navbar/v1.php @@ -0,0 +1,302 @@ +dbh->query('SELECT project,type FROM ' . TABLE_EXECUTION . " WHERE `id` = '$executionID'")->fetch(); + if(empty($object)) return; + + $executionPairs = array(); + $userCondition = !$app->user->admin ? " AND `id` " . helper::dbIN($app->user->view->sprints) : ''; + $orderBy = $object->type == 'stage' ? 'ORDER BY `id` ASC' : 'ORDER BY `id` DESC'; + $executionList = $app->dbh->query("SELECT id,name,parent FROM " . TABLE_EXECUTION . " WHERE `project` = '{$object->project}' AND `deleted` = '0' $userCondition $orderBy")->fetchAll(); + foreach($executionList as $execution) + { + if(isset($executionPairs[$execution->parent])) unset($executionPairs[$execution->parent]); + if($execution->id == $executionID) continue; + $executionPairs[$execution->id] = $execution->name; + } + + if(empty($executionPairs)) return; + + $dropItems = array(); + foreach($executionPairs as $executionID => $executionName) + { + $dropItems[] = array( + 'url' => createLink('execution', 'task', "executionID=$executionID"), + 'text' => $executionName, + 'hint' => $executionName, + 'class' => 'text-ellipsis' + ); + + if(count($dropItems) >= 10) break; + } + + if(count($executionPairs) > 10) + { + $dropItems[] = array( + 'url' => createLink('project', 'execution', "status=all&projectID={$object->project}"), + 'text' => "$lang->preview $lang->more", + 'hint' => $lang->more, + 'data-app' => 'project' + ); + } + + return array( + 'type' => 'dropdown', + 'items' => $dropItems, + 'text' => $lang->more, + 'trigger' => 'hover', + 'menu' => array('style' => array('max-width' => '300px')) + ); + } + + protected function getAppBtnItem() + { + if(defined('TUTORIAL')) return; + global $app, $config, $lang; + + $condition = ''; + if(!$app->user->admin) + { + $types = ''; + foreach($config->pipelineTypeList as $pipelineType) + { + if(commonModel::hasPriv($pipelineType, 'browse')) $types .= "'$pipelineType',"; + } + if(empty($types)) return; + $condition .= ' AND `type` in (' . trim($types, ',') . ')'; + } + $pipelineList = $app->dbh->query("SELECT type,name,url FROM " . TABLE_PIPELINE . " WHERE `deleted` = '0' $condition order by type")->fetchAll(); + if(empty($pipelineList)) return; + + $dropItems = array(); + foreach($pipelineList as $pipeline) + { + $dropItems[] = array( + 'url' => $pipeline->url, + 'text' => "[{$pipeline->type}] {$pipeline->name}", + 'hint' => $pipeline->name, + 'class' => 'text-ellipsis', + 'target' => '_blank' + ); + } + + return array( + 'type' => 'dropdown', + 'items' => $dropItems, + 'text' => $lang->app->common, + 'trigger' => 'hover', + 'menu' => array('style' => array('max-width' => '300px')) + ); + } + + protected function getItems() + { + $items = $this->prop('items'); + if(!empty($items)) return $items; + + commonModel::setMainMenu(); + commonModel::checkMenuVarsReplaced(); + + global $app, $lang; + $isTutorialMode = commonModel::isTutorialMode(); + $currentModule = $app->rawModule; + $currentMethod = $app->rawMethod; + + if($isTutorialMode and defined('WIZARD_MODULE')) $currentModule = WIZARD_MODULE; + if($isTutorialMode and defined('WIZARD_METHOD')) $currentMethod = WIZARD_METHOD; + + $menu = \customModel::getMainMenu(); + $tab = $app->tab; + $activeMenu = ''; + $items = array(); + foreach ($menu as $menuItem) + { + if(isset($menuItem->hidden) and $menuItem->hidden and (!isset($menuItem->tutorial) or !$menuItem->tutorial)) continue; + if(empty($menuItem->link)) continue; + + if($menuItem->divider) $items[] = array('type' => 'divider'); + + /* Init the these vars. */ + $subModule = isset($menuItem->subModule) ? explode(',', $menuItem->subModule) : array(); + $class = isset($menuItem->class) ? $menuItem->class : ''; + $exclude = isset($menuItem->exclude) ? $menuItem->exclude : ''; + $isActive = false; + + if($menuItem->name == $currentModule and !str_contains(",$exclude,", ",$currentModule-$currentMethod,")) + { + $isActive = true; + } + elseif($subModule and in_array($currentModule, $subModule) and !str_contains(",$exclude,", ",$currentModule-$currentMethod,")) + { + $isActive = true; + } + + if($menuItem->link['module'] == 'execution' and $menuItem->link['method'] == 'more') + { + $executionID = $menuItem->link['vars']; + $executionMoreItem = $this->getExecutionMoreItem($executionID); + if(!empty($executionMoreItem)) + { + $items[] = array('type' => 'divider'); + $items[] = $executionMoreItem; + } + } + elseif($menuItem->link['module'] == 'app' and $menuItem->link['method'] == 'serverlink') + { + $appBtnItem = $this->getAppBtnItem(); + if(!empty($appBtnItem)) $items[] = $appBtnItem; + } + elseif($menuItem->link) + { + $alias = isset($menuItem->alias) ? $menuItem->alias : ''; + $target = ''; + $module = ''; + $method = ''; + $label = $menuItem->text; + + if(is_array($menuItem->link)) + { + if(isset($menuItem->link['target'])) $target = $menuItem->link['target']; + if(isset($menuItem->link['module'])) $module = $menuItem->link['module']; + if(isset($menuItem->link['method'])) $method = $menuItem->link['method']; + } + + if($module == $currentModule and ($method == $currentMethod or str_contains(",$alias,", ",$currentMethod,")) and !str_contains(",$exclude,", ",$currentMethod,")) + { + $isActive = true; + } + + $dataApp = (isset($lang->navGroup->$module) and $tab != $lang->navGroup->$module) ? $tab : null; + if($isActive && empty($activeMenu)) $activeMenu = $menuItem->name; + else $isActive = false; + + /* Print drop menus. */ + if(isset($menuItem->dropMenu)) + { + $dropItems = array(); + foreach($menuItem->dropMenu as $dropMenuName => $dropMenuItem) + { + if(empty($dropMenuItem)) continue; + if(isset($dropMenuItem->hidden) and $dropMenuItem->hidden) continue; + + /* Parse drop menu link. */ + $dropMenuLink = zget($dropMenuItem, 'link', $dropMenuItem); + + list($subLabel, $subModule, $subMethod, $subParams) = explode('|', $dropMenuLink); + if(!common::hasPriv($subModule, $subMethod)) continue; + + $subLink = createLink($subModule, $subMethod, $subParams); + + $subActive = false; + $activeMainMenu = false; + if($currentModule == strtolower($subModule) and $currentMethod == strtolower($subMethod)) + { + $activeMainMenu = true; + } + else + { + $subModule = isset($dropMenuItem['subModule']) ? explode(',', $dropMenuItem['subModule']) : array(); + if($subModule and in_array($currentModule, $subModule) and !str_contains(",$exclude,", ",$currentModule-$currentMethod,")) $activeMainMenu = true; + } + + if($activeMainMenu) + { + $activeMenu = $dropMenuName; + $isActive = true; + $subActive = true; + $label = $subLabel; + } + + $dropItems[] = array( + 'active' => $subActive, + 'data-id' => $dropMenuName, + 'url' => $subLink, + 'text' => $subLabel, + 'data-app' => $dataApp + ); + } + + if(empty($dropItems)) continue; + $items[] = array( + 'type' => 'dropdown', + 'items' => $dropItems, + 'class' => $class, + 'active' => $isActive, + 'target' => $target, + 'text' => $label, + 'data-id' => $menuItem->name, + 'data-app' => $dataApp, + 'trigger' => 'hover' + ); + } + else + { + $items[] = array( + 'class' => $class, + 'text' => $label, + 'url' => commonModel::createMenuLink($menuItem, $tab), + 'active' => $isActive, + 'target' => $target, + 'data-id' => $menuItem->name, + 'data-app' => $dataApp + ); + } + } + else + { + $items[] = array( + 'class' => $class, + 'text' => $menuItem->text, + 'active' => $isActive, + ); + } + } + + /* Set active menu to global data, make it accessible to other widgets */ + data('activeMenu', $activeMenu); + + return $items; + } + + /** + * Build. + * + * @access protected + */ + protected function build(): wg + { + return h::nav + ( + set::id('navbar'), + new nav + ( + on::click(<<<'FUNC' + const $target = $(e.target); + if(!$target.closest('.nav-divider').length && $target.closest('.nav-item').length) + { + const $navbar = $target.closest('#navbar'); + $navbar.find('.active').removeClass('active'); + $target.closest('.nav-item').find('a').addClass('active'); + } + FUNC), + set::items($this->getItems()), + $this->children() + ) + ); + } +} diff --git a/lib/zin/wg/overviewblock/css/v1.css b/lib/zin/wg/overviewblock/css/v1.css new file mode 100644 index 0000000000..df86bd2ffc --- /dev/null +++ b/lib/zin/wg/overviewblock/css/v1.css @@ -0,0 +1 @@ +.block-overview .cards > .card + .card > :first-child {border-left-width: 1px;} diff --git a/lib/zin/wg/overviewblock/v1.php b/lib/zin/wg/overviewblock/v1.php new file mode 100644 index 0000000000..9d61b10ff6 --- /dev/null +++ b/lib/zin/wg/overviewblock/v1.php @@ -0,0 +1,150 @@ + + * @package zin + * @link https://www.zentao.net + */ + +namespace zin; + +require_once dirname(__DIR__) . DS . 'blockpanel' . DS . 'v1.php'; + +class overviewBlock extends wg +{ + protected static array $defineProps = array( + 'id?: string', + 'title?: string', + 'block?: object', + 'groups?: array' + ); + + public static function getPageCSS(): string|false + { + return file_get_contents(__DIR__ . DS . 'css' . DS . 'v1.css'); + } + + protected function buildCard($card): wg + { + $class = 'text-2xl text-center leading-relaxed num ' . (!empty($card->class) ? $card->class : ''); + + return col + ( + setClass('card justify-center w-1/2'), + empty($card->url) ? span + ( + setClass($class), + $card->value + ) : a + ( + setClass($class), + set::href($card->url), + $card->value + ), + span + ( + setClass('text-center'), + $card->label + ) + ); + } + + protected function buildCards($group): wg + { + $cards = array(); + foreach($group->cards as $card) $cards[] = $this->buildCard($card); + + return row + ( + setClass('w-1/2'), + setStyle(array('margin-top' => '-20px')), + $cards + ); + } + + protected function buildBarChart($group): wg + { + $bars = array(); + $labels = array(); + foreach($group->bars as $bar) + { + $bars[] = h::li + ( + setStyle(array('display' => 'contents')), + span + ( + set::title($bar->value), + setClass('block primary w-2'), + setStyle(array('height' => $bar->rate)) + ) + ); + + $labels[] = span + ( + setClass('text-center'), + $bar->label + ); + } + + return div + ( + setClass('bar-chart flex justify-center w-1/2'), + col + ( + setClass('basis-48'), + span + ( + setClass('mb-2'), + $group->title + ), + div + ( + setClass('border-b'), + h::ul + ( + setClass('flex justify-around items-end w-full'), + setStyle(array('height' => '60px')), + $bars + ) + ), + div + ( + setClass('flex justify-around mt-1.5'), + $labels + ) + ) + ); + } + + protected function buildBody($groups): array + { + $body = array(); + foreach($groups as $group) + { + if($group->type == 'cards') $body[] = $this->buildCards($group); + if($group->type == 'barChart') $body[] = $this->buildBarChart($group); + } + + return $body; + } + + protected function build(): wg + { + list($id, $title, $block, $groups) = $this->prop(array('id', 'title', 'block', 'groups')); + + return new blockPanel + ( + set::block($block), + set::title($title), + set::id($id), + set::headingClass('border-0'), + set::bodyClass('flex block-base p-0'), + + $this->buildBody($groups) + ); + } +} diff --git a/lib/zin/wg/page/v1.php b/lib/zin/wg/page/v1.php new file mode 100644 index 0000000000..01b763bfe3 --- /dev/null +++ b/lib/zin/wg/page/v1.php @@ -0,0 +1,54 @@ + true); + + protected static array $defineBlocks = array( + 'head' => array(), + 'header' => array('map' => 'header'), + 'heading' => array('map' => 'heading'), + 'dropmenu' => array('map' => 'dropmenu'), + 'main' => array('map' => 'main'), + 'footer' => array(), + ); + + protected function buildHeader(): array|wg + { + if($this->hasBlock('header')) return $this->block('header'); + + $headingBlock = $this->block('heading'); + if(!empty($headingBlock)) return new header($headingBlock); + + $dropmenuBlock = $this->block('dropmenu'); + return new header(new heading($dropmenuBlock)); + } + + protected function buildBody(): array + { + if($this->hasBlock('main')) + { + return array + ( + $this->buildHeader(), + $this->block('main'), + $this->children(), + $this->block('footer') + ); + } + + return array + ( + $this->buildHeader(), + new main($this->children()), + $this->block('footer'), + ); + } +} diff --git a/lib/zin/wg/pagebase/v1.php b/lib/zin/wg/pagebase/v1.php new file mode 100644 index 0000000000..488cb3f93a --- /dev/null +++ b/lib/zin/wg/pagebase/v1.php @@ -0,0 +1,107 @@ + false, + 'display' => true, + 'metas' => array('', '', '', '') + ); + + protected static array $defineBlocks = array('head' => array()); + + protected function created() + { + if($this->prop('display')) $this->display(); + } + + protected function buildHead() + { + return $this->block('head'); + } + + protected function buildBody() + { + return $this->children(); + } + + protected function build(): wg + { + global $lang, $config, $app; + + $zui = $this->prop('zui'); + $head = $this->buildHead(); + $body = $this->buildBody(); + + $jsConfig = \js::getJSConfigVars(); + $bodyProps = $this->prop('bodyProps'); + $bodyClass = $this->prop('bodyClass'); + $metas = $this->prop('metas'); + $rawContent = $this->prop('rawContent', !zin::$rawContentCalled); + $title = $this->props->get('title', data('title')) . " - $lang->zentaoPMS"; + $attrs = $this->getRestProps(); + $css = array(data('pageCSS'), '/*{{ZIN_PAGE_CSS}}*/'); + $js = array('/*{{ZIN_PAGE_JS}}*/', data('pageJS')); + $imports = context::current()->getImportList(); + + $jsConfig->zin = true; + if($config->debug) + { + $js[] = h::createJsVarCode('window.zin', array('page' => $this->toJsonData(), 'definedProps' => wg::$definedPropsMap, 'wgBlockMap' => wg::$wgToBlockMap, 'config' => jsRaw('window.config'))); + $js[] = 'console.log("[ZIN] ", window.zin);'; + } + else + { + $js[] = h::createJsVarCode('window.zin', array()); + } + if($zui) array_unshift($js, 'zui.defineFn();'); + + $currentLang = $this->props->get('lang'); + if(empty($currentLang)) $currentLang = $app->getClientLang(); + + return h::html + ( + before(html('')), + set($attrs), + set::lang($currentLang), + h::head + ( + html($metas), + h::title($title), + $this->block('headBefore'), + $zui ? h::importCss($config->zin->zuiPath . 'zui.zentao.css', set::id('zuiCSS')) : null, + $zui ? h::importJs($config->zin->zuiPath . 'zui.zentao.umd.cjs', set::id('zuiJS')) : null, + h::jsVar('window.config', $jsConfig, setID('configJS')), + $zui ? h::importJs($app->getWebRoot() . 'js/zui3/zin.js', set::id('zinJS')) : null, + $head, + ), + h::body + ( + set($bodyProps), + set::class($bodyClass), + empty($imports) ? null : h::import($imports), + h::css($css, setClass('zin-page-css')), + $body, + $rawContent ? rawContent() : null, + h::js($js, setClass('zin-page-js')) + ) + ); + } +} diff --git a/lib/zin/wg/pageform/v1.php b/lib/zin/wg/pageform/v1.php new file mode 100644 index 0000000000..49befd4ecb --- /dev/null +++ b/lib/zin/wg/pageform/v1.php @@ -0,0 +1,20 @@ +prop('formPanel')), parent::children()) + ); + } +} diff --git a/lib/zin/wg/pager/v1.php b/lib/zin/wg/pager/v1.php new file mode 100644 index 0000000000..bbd3147eaf --- /dev/null +++ b/lib/zin/wg/pager/v1.php @@ -0,0 +1,11 @@ + array(), + 'headingActions' => array('map' => 'toolbar'), + 'footer' => array('map' => 'nav') + ); + + protected function buildHeadingActions(): ?wg + { + $actionsBlock = $this->block('headingActions'); + $actions = $this->prop('headingActions'); + + if(empty($actions) && empty($actionsBlock)) return null; + + return div + ( + setClass('panel-actions'), + empty($actions) ? null : toolbar(set::items($actions)), + $actionsBlock + ); + } + + protected function buildHeading(): ?wg + { + list($title, $size) = $this->prop(['title', 'size']); + $headingBlock = $this->block('heading'); + $actions = $this->buildHeadingActions(); + + if(empty($title) && empty($headingBlock) && empty($actions)) return null; + + return div + ( + setClass('panel-heading', $this->prop('headingClass')), + set($this->prop('headingProps')), + empty($title) ? null : div(setClass('panel-title', $this->prop('titleClass', empty($size) ? null : "text-$size")), $title, set($this->prop('titleProps'))), + $headingBlock, + $actions + ); + } + + protected function buildBody(): wg + { + return div + ( + setClass('panel-body ' . $this->prop('bodyClass')), + set($this->prop('bodyProps')), + $this->children() + ); + } + + protected function buildFooter(): ?wg + { + list($footerActions) = $this->prop(array('footerActions')); + $footerBlock = $this->block('footer'); + + if(empty($footerActions) && empty($footerBlock)) return null; + + return div + ( + setClass('panel-footer', $this->prop('footerClass')), + set($this->prop('footerProps')), + $footerBlock, + empty($footerActions) ? null : toolbar(set::items($footerActions)) + ); + } + + protected function buildProps(): array + { + list($class, $size) = $this->prop(array('class', 'size')); + return array(setClass('panel', $class, empty($size) ? null : "size-$size")); + } + + protected function build(): wg + { + return div + ( + $this->buildProps(), + set($this->getRestProps()), + $this->buildHeading(), + $this->buildBody(), + $this->buildFooter() + ); + } +} diff --git a/lib/zin/wg/pastedialog/css/v1.css b/lib/zin/wg/pastedialog/css/v1.css new file mode 100644 index 0000000000..f2af523141 --- /dev/null +++ b/lib/zin/wg/pastedialog/css/v1.css @@ -0,0 +1 @@ +.form-batch-table .form-batch-control .form-control.highlight {background-color: var(--dtable-checked-bg);} diff --git a/lib/zin/wg/pastedialog/js/v1.js b/lib/zin/wg/pastedialog/js/v1.js new file mode 100644 index 0000000000..7d4256611d --- /dev/null +++ b/lib/zin/wg/pastedialog/js/v1.js @@ -0,0 +1,61 @@ +function importLines(target, field) +{ + const $modal = $(target).closest('.modal'); + const $dialog = $modal.find('.modal-dialog'); + const $lines = $modal.find('textarea'); + const $form = $modal.closest('.container').find('form.form-batch'); + const modalID = $modal.attr('id'); + + $dialog.addClass('loading'); + + setTimeout(function() + { + let $currentRow; + + const lines = $lines.val().split('\n'); + $.each(lines, function(index, line) + { + line = line.trim(); + if(!line.length) return true; + + if($currentRow) + { + $row = $currentRow.next(); + } + else + { + $row = $form.find('tbody>tr [name^=' + field + ']').first().closest('tr'); + } + + while($row.length && $row.find('[name^=' + field + ']').val().length) + { + $row = $row.next(); + } + + if(!$row || !$row.length) + { + $form.data('zui.BatchForm').addRow(); + $row = $form.find('tbody>tr [name^=' + field + ']').last().closest('tr'); + } + + $row.find('[name^=' + field + ']').val(line).addClass('highlight'); + + $currentRow = $row; + }); + + $lines.val(''); + $dialog.removeClass('loading'); + + $modal.on('hidden.Modal.zui', function() + { + const $firstRow = $form.find('tbody>tr [name^=' + field + ']').first().closest('tr'); + $firstRow[0].scrollIntoView(); + }); + zui.Modal.hide('#' + modalID); + + setTimeout(function() + { + $form.find('[name^=' + field + '].highlight').removeClass('highlight'); + }, 3000); + }, 200); +}; diff --git a/lib/zin/wg/pastedialog/v1.php b/lib/zin/wg/pastedialog/v1.php new file mode 100644 index 0000000000..623265fe97 --- /dev/null +++ b/lib/zin/wg/pastedialog/v1.php @@ -0,0 +1,56 @@ +prop('field'); + $title = $this->prop('title'); + $name = $this->prop('name'); + if(empty($title)) $title = $lang->pasteText; + + return modal + ( + set::id('paste-dialog'), + set::title($title), + set::footerClass('center pt-2'), + set::footerActions + ( + array + ( + btn + ( + setClass('btn btn-wide primary'), + on::click("importLines(target, '$field')"), + $lang->save + ) + ) + ), + textarea + ( + setID($name), + set::name($name), + set::placeholder($lang->pasteTextInfo) + ) + ); + } +} diff --git a/lib/zin/wg/picker/v1.php b/lib/zin/wg/picker/v1.php new file mode 100644 index 0000000000..9c0939c3e9 --- /dev/null +++ b/lib/zin/wg/picker/v1.php @@ -0,0 +1,130 @@ + + * @package zin + * @link http://www.zentao.net + */ +namespace zin; + +/** + * 下拉选择器(picker)部件类。 + * The picker widget class. + * + * @author Hao Sun + */ +class picker extends wg +{ + /** + * Define widget properties. + * + * @var array + * @access protected + */ + protected static array $defineProps = array( + 'id?: string="$GID"', // 组件根元素的 ID。 + 'formID?: string', // 组件隐藏的表单元素 ID。 + 'className?: string|array', // 类名。 + 'style?: array', // 样式。 + 'tagName?: string', // 组件根元素的标签名。 + 'attrs?: array', // 附加到组件根元素上的属性。 + 'clickType?: "toggle"|"open"', // 点击类型,`toggle` 表示点击按钮时切换显示隐藏,`open` 表示点击按钮时只打。 + 'afterRender?: function', // 渲染完成后的回调函数。 + 'beforeDestroy?: function', // 销毁前的回调函数。 + 'name?: string', // 作为表单项的名称。 + 'value?: string|string[]', // 默认值。 + 'emptyValue?: string', // 允许的空值,使用逗号分隔多个允许的空值。 + 'onChange?: function', // 值变更回调函数。 + 'disabled?: boolean', // 是否禁用。 + 'multiple?: boolean|number=false', // 是否允许选择多个值,如果指定为数字,则限制多选的数目,默认 `false`。 + 'required?: boolean', // 是否必选(不允许空值,不可以被清除)。 + 'placeholder?: string', // 选择框上的占位文本。 + 'valueSplitter?: string', // 多个值的分隔字符串,默认为 `,`。 + 'items: array|function', // 列表项或表项获取方法。 + 'menu?: array', // 附加的菜单选项。 + 'hotkey?: boolean', // 是否启用快捷键。 + 'search?: boolean|number', // 是否启用搜索。 + 'searchDelay?: number', // 搜索延迟时间,单位:毫秒。 + 'searchHint?: string', // 搜索提示文本。 + 'onDeselect?: function', // 当取消选择值时的回调函数。 + 'onSelect?: function', // 当选择值时的回调函数。 + 'onClear?: function', // 当清空值时的回调函数。 + 'popContainer?: string', // 下拉面板容器元素。 + 'popWidth?: number|"auto"|"100%"', // 菜单宽度,如果设置为 `'100%'` 则与选择框宽度一致,默认 `'100%'`。 + 'popHeight?: number|"auto"', // 菜单高度,默认 `'auto'`。 + 'popMaxHeight?: number', // 菜单最大高度,默认 `300`。 + 'popMinHeight?: number', // 菜单最小高度,默认 `32`。 + 'popMaxWidth?: number', // 菜单最大宽度,当宽度设置为 `'auto'` 时生效。 + 'popMinWidth?: number', // 菜单最小宽度,当宽度设置为 `'auto'` 时生效,默认 50。 + 'popPlacement?: "auto"|"bottom"|"top"|"bottom-start"|"top-end"', // 菜单方向,默认 `'auto'`。 + 'popClass?: string|array', // 菜单类名。 + 'popStyle?: array', // 菜单样式。 + 'onPopShow?: function', // 菜单显示时的回调函数。 + 'onPopShown?: function', // 菜单显示后的回调函数。 + 'onPopHide?: function', // 菜单隐藏时的回调函数。 + 'onPopHidden?: function', // 菜单隐藏后的回调函数。 + ); + + /** + * Get picker component properties. + * + * @access protected + * @return array + */ + protected function getPickerProps(): array + { + list($pickerProps, $restProps) = $this->props->split(array_keys(static::definedPropsList())); + $items = $pickerProps['items']; + $pickerItems = array(); + $hasZeroValue = false; + $defaultValue = isset($pickerProps['value']) ? $pickerProps['value'] : (isset($pickerProps['defaultValue']) ? $pickerProps['defaultValue'] : ''); + if(!empty($items)) + { + foreach($items as $key => $item) + { + if(!is_array($item)) $item = array('text' => $item, 'value' => $key); + if(!is_string($item['value'])) $item['value'] = strval($item['value']); + + if($item['value'] === '0') $hasZeroValue = true; + $pickerItems[] = $item; + } + } + + $pickerProps['_props'] = $restProps; + $pickerProps['items'] = $pickerItems; + $pickerProps['defaultValue'] = $defaultValue; + + if(!isset($pickerProps['emptyValue'])) $pickerProps['emptyValue'] = ($hasZeroValue || "$defaultValue" !== '0') ? '' : '0,'; + + if(isset($pickerProps['id'])) + { + $pickerProps['_id'] = $pickerProps['id']; + unset($pickerProps['id']); + } + else + { + $pickerProps['_id'] = $this->gid; + } + return $pickerProps; + } + + /** + * Build the widget. + * + * @access protected + * @return wg + */ + protected function build(): zui + { + return zui::picker + ( + set::_class('form-group-wrapper picker-box'), + set::_map(array('value' => 'defaultValue', 'formID' => 'id')), + set($this->getPickerProps()) + ); + } +} diff --git a/lib/zin/wg/popovers/v1.php b/lib/zin/wg/popovers/v1.php new file mode 100644 index 0000000000..56b50af1f2 --- /dev/null +++ b/lib/zin/wg/popovers/v1.php @@ -0,0 +1,44 @@ + array('padding' => 5), + ); + + protected static array $defineBlocks = array( + 'trigger' => array(), + 'target' => array(), + ); + + protected function build(): array + { + $trigger = $this->block('trigger')[0]->children()[0]; + $target = $this->block('target')[0]; + if(!($target instanceof \zin\zui)) $target = $target->children()[0]; + + $trigger->setProp('data-zin-id', $trigger->gid); + $trigger->setProp('data-target', "[data-zin-id='{$target->gid}']"); + $target->setProp('data-zin-id', $target->gid); + + $props = array_merge($this->props->pick(array('placement', 'strategy', 'flip', 'shift', 'arrow', 'offset')), array('_to' => "[data-zin-id='{$trigger->gid}']")); + + return array( + $trigger, + $target, + zui::popovers(set($props)), + ); + } +} diff --git a/lib/zin/wg/prilabel/v1.php b/lib/zin/wg/prilabel/v1.php new file mode 100644 index 0000000000..2d6c2a1e79 --- /dev/null +++ b/lib/zin/wg/prilabel/v1.php @@ -0,0 +1,31 @@ +props->has('pri')) + { + $this->props->set('pri', $child); + return false; + } + } + + protected function build(): wg + { + $pri = $this->prop('pri'); + + return span + ( + set($this->getRestProps()), + setClass("pri-$pri"), + $pri + ); + } +} diff --git a/lib/zin/wg/pripicker/v1.php b/lib/zin/wg/pripicker/v1.php new file mode 100644 index 0000000000..c0a3d9fe28 --- /dev/null +++ b/lib/zin/wg/pripicker/v1.php @@ -0,0 +1,78 @@ + + * @package zin + * @link http://www.zentao.net + */ + +namespace zin; + +/** + * 优先级选择器(priPicker)部件类 + * The priPicker widget class + */ +class priPicker extends wg +{ + /** + * Define widget properties. + * + * @var array + * @access protected + */ + protected static array $defineProps = array + ( + 'id?: string="$GID"', // 组件根元素的 ID。 + 'formID?: string', // 组件隐藏的表单元素 ID。 + 'className?: string|array', // 类名。 + 'style?: array', // 样式。 + 'tagName?: string', // 组件根元素的标签名。 + 'attrs?: array', // 附加到组件根元素上的属性。 + 'clickType?: "toggle"|"open"', // 点击类型,`toggle` 表示点击按钮时切换显示隐藏,`open` 表示点击按钮时只打。 + 'afterRender?: function', // 渲染完成后的回调函数。 + 'beforeDestroy?: function', // 销毁前的回调函数。 + 'name?: string', // 作为表单项的名称。 + 'value?: string|string[]', // 默认值。 + 'onChange?: function', // 值变更回调函数。 + 'disabled?: boolean', // 是否禁用。 + 'multiple?: boolean|number=false', // 是否允许选择多个值,如果指定为数字,则限制多选的数目,默认 `false`。 + 'required?: boolean', // 是否必选(不允许空值,不可以被清除)。 + 'placeholder?: string', // 选择框上的占位文本。 + 'items?: string[]|array' // 选项列表,默认为 $lang->$moduleName->priList。 + ); + + /** + * Build widget. + * + * @access protected + */ + protected function build(): zui + { + list($props, $restProps) = $this->props->split(array_keys(static::definedPropsList())); + if(isset($props['id'])) + { + $props['_id'] = $props['id']; + unset($props['id']); + } + + if(!isset($props['items'])) + { + global $app, $lang; + $moduleName = $app->getModuleName(); + if(isset($lang->$moduleName->priList)) $props['items'] = $lang->$moduleName->priList; + } + return zui::priPicker + ( + set::_class('form-group-wrapper'), + set::_map(array('value' => 'defaultValue', 'formID' => 'id')), + set::_props($restProps), + set::popWidth('100%'), + set($props), + $this->children(), + ); + } +} diff --git a/lib/zin/wg/productmenu/css/v1.css b/lib/zin/wg/productmenu/css/v1.css new file mode 100644 index 0000000000..c130b2a8d4 --- /dev/null +++ b/lib/zin/wg/productmenu/css/v1.css @@ -0,0 +1,11 @@ +.product-menu {width: 200px; max-height: 100%; margin: -5px 0; justify-content: flex-start;} +.product-menu > .text {flex: auto; display: flex; align-items: center; justify-content: flex-start; font-weight: bold; font-size: 16px; padding: 0 4px;} +.product-menu > .icon.is-leading {width: 18px; height: 18px; border-radius: 50%; border: 1px solid var(--color-primary-500); display: flex; align-items: center; justify-content: center; transform: rotate(90deg); transition: .2s all;} +.product-menu.is-open > .icon.is-leading {color: var(--color-canvas); background: var(--color-primary-500); transform: rotate(-90deg);} +.product-menu > .is-caret {width: 20px; margin-right: -8px; transition: background-color .2s; border-radius: var(--radius)} +.product-menu[data-value=""] > .is-caret, +.product-menu[data-value="all"] > .is-caret, +.product-menu[data-value="0"] > .is-caret {display: none;} +.product-menu > .is-caret:hover {color: var(--color-primary-500); background: var(--color-primary-100);} +.product-menu > .is-caret > .caret {display: none;} +.product-menu > .is-caret::after {content: '\e936'; font-family: ZentaoIcon;} diff --git a/lib/zin/wg/productmenu/v1.php b/lib/zin/wg/productmenu/v1.php new file mode 100644 index 0000000000..8e565816af --- /dev/null +++ b/lib/zin/wg/productmenu/v1.php @@ -0,0 +1,76 @@ +prop('link'); + $items = $this->prop('items'); + + $menus = array(); + foreach($items as $itemKey => $item) + { + if(is_array($item)) + { + $menus[] = $item; + continue; + } + + $menus[] = array('text' => $item, 'key' => $itemKey); + } + return $menus; + } + + private function getTitle() + { + global $lang; + $activeKey = $this->prop('activeKey'); + if(empty($activeKey)) return $lang->product->all; + + $items = $this->prop('items'); + + foreach($items as $itemID => $itemName) + { + if($itemID == $activeKey) return $itemName; + } + + return $lang->product->all; + } + + protected function build(): zui + { + $items = $this->buildMenu(); + + $activeKey = $this->prop('activeKey'); + $link = $this->prop('link'); + $title = $this->prop('title') ? $this->prop('title') : $this->getTitle(); + $closeLink = str_replace('{key}', '', $link); + + return zui::dropmenu + ( + set('_id', 'productMenu'), + set::className('product-menu btn'), + set::defaultValue($activeKey), + set::text($title), + set::caret(true), + set::popWidth(200), + set::popClass('popup text-md'), + set::onClick(jsRaw("(event) => {if(!event.target.closest('.is-caret')) return; openUrl('$closeLink'); return false}")), + set::data(array('search' => false, 'checkIcon' => false, 'link' => $link, 'data' => $items)), + ); + } +} diff --git a/lib/zin/wg/programmenu/css/v1.css b/lib/zin/wg/programmenu/css/v1.css new file mode 100644 index 0000000000..eb59e6a7eb --- /dev/null +++ b/lib/zin/wg/programmenu/css/v1.css @@ -0,0 +1,10 @@ +.program-menu {width: 200px; max-height: 100%; margin: -5px 0; justify-content: flex-start;} +.program-menu > .text {flex: auto; display: flex; align-items: center; justify-content: flex-start; font-weight: bold; font-size: 16px; padding: 0 4px;} +.program-menu > .icon.is-leading {width: 18px; height: 18px; border-radius: 50%; border: 1px solid var(--color-primary-500); display: flex; align-items: center; justify-content: center; transform: rotate(90deg); transition: .2s all;} +.program-menu.is-open > .icon.is-leading {color: var(--color-canvas); background: var(--color-primary-500); transform: rotate(-90deg);} +.program-menu > .is-caret {width: 20px; margin-right: -8px; transition: background-color .2s; border-radius: var(--radius)} +.program-menu[data-value=""] > .is-caret, +.program-menu[data-value="0"] > .is-caret {display: none;} +.program-menu > .is-caret:hover {color: var(--color-primary-500); background: var(--color-primary-100);} +.program-menu > .is-caret > .caret {display: none;} +.program-menu > .is-caret::after {content: '\e936'; font-family: ZentaoIcon;} diff --git a/lib/zin/wg/programmenu/v1.php b/lib/zin/wg/programmenu/v1.php new file mode 100644 index 0000000000..5cb176471c --- /dev/null +++ b/lib/zin/wg/programmenu/v1.php @@ -0,0 +1,87 @@ +getChildProgram($parentID); + if(count($children) === 0) return array(); + + foreach($children as $child) + { + $item = array('id' => $child->id, 'icon' => 'icon-cards-view', 'text' => $child->name, 'items' => array()); + if(isset($child->icon)) $item['icon'] = $child->icon; + + $items = $this->buildMenuTree($item['items'], $child->id); + + if(count($items) !== 0) + { + $item['items'] = $items; + } + else + { + unset($item['items']); + } + + $parent[] = $item; + } + return $parent; + } + + private function getTitle($activeKey) + { + global $lang; + if(empty($activeKey)) return $lang->program->all; + + foreach($this->programs as $program) + { + if($program->id == $activeKey) return $program->name; + } + + return $lang->program->all; + } + + private function getChildProgram($id) + { + return array_filter($this->programs, function($program) use($id) {return $program->parent == $id;}); + } + + protected function build(): zui + { + $this->programs = (array)$this->prop('programs'); + $activeKey = $this->prop('activeKey'); + $link = $this->prop('link'); + $closeLink = str_replace('{id}', '0', $link); + return zui::dropmenu + ( + set('_id', 'programMenu'), + set::className('program-menu btn'), + set::defaultValue($activeKey), + set::text($this->getTitle($activeKey)), + set::caret(true), + set::popWidth(200), + set::popClass('popup text-md'), + set::onClick(jsRaw("(event) => {if(!event.target.closest('.is-caret')) return; openUrl('$closeLink'); return false}")), + set::data(array('search' => false, 'checkIcon' => false, 'title' => data('lang.product.selectProgram'), 'link' => $link, 'data' => $this->buildMenuTree(array(), 0))), + ); + } +} diff --git a/lib/zin/wg/radio/v1.php b/lib/zin/wg/radio/v1.php new file mode 100644 index 0000000000..3a6768ad67 --- /dev/null +++ b/lib/zin/wg/radio/v1.php @@ -0,0 +1,13 @@ + 'radio' + ); +} diff --git a/lib/zin/wg/radiolist/v1.php b/lib/zin/wg/radiolist/v1.php new file mode 100644 index 0000000000..a6bac8b115 --- /dev/null +++ b/lib/zin/wg/radiolist/v1.php @@ -0,0 +1,13 @@ + 'radio' + ); +} diff --git a/lib/zin/wg/risklabel/v1.php b/lib/zin/wg/risklabel/v1.php new file mode 100644 index 0000000000..c50921d812 --- /dev/null +++ b/lib/zin/wg/risklabel/v1.php @@ -0,0 +1,44 @@ +props->has('text')) + { + $this->props->set('text', $child); + return false; + } + } + + private function getThemeClass(): string + { + $level = $this->prop('level'); + if($level === 'h' || $level === 'high') return 'risk-high'; + if($level === 'm' || $level === 'middle') return 'risk-middle'; + return 'risk-low'; + } + + protected function build(): wg + { + $text = $this->prop('text'); + return span + ( + set($this->getRestProps()), + setClass($this->getThemeClass()), + $text + ); + } +} diff --git a/lib/zin/wg/row/v1.php b/lib/zin/wg/row/v1.php new file mode 100644 index 0000000000..d460c85ada --- /dev/null +++ b/lib/zin/wg/row/v1.php @@ -0,0 +1,26 @@ +prop(array('justify', 'align')); + if(!empty($justify)) $classList .= ' justify-' . $justify; + if(!empty($align)) $classList .= ' items-' . $align; + + return div + ( + setClass($classList), + set($this->getRestProps()), + $this->children() + ); + } +} diff --git a/lib/zin/wg/searchform/v1.php b/lib/zin/wg/searchform/v1.php new file mode 100644 index 0000000000..9d00cff4ab --- /dev/null +++ b/lib/zin/wg/searchform/v1.php @@ -0,0 +1,11 @@ +').insertAfter('#mainMenu'); + if(!$form.data('loaded')) + { + const url = $.createLink('search', 'buildForm', 'module=' + moduleName + '&fields=¶ms=&actionURL=&queryID=0&formName=' + formName); + $.get(url, html => + { + $form.html(html).data('loaded', true); + zui.bus.emit('searchform.loaded'); + }); + } +}; diff --git a/lib/zin/wg/searchtoggle/v1.php b/lib/zin/wg/searchtoggle/v1.php new file mode 100644 index 0000000000..8bd65d2192 --- /dev/null +++ b/lib/zin/wg/searchtoggle/v1.php @@ -0,0 +1,39 @@ +prop('module'); + $formName = $this->prop('formName'); + return btn + ( + set::class('ghost search-form-toggle'), + set::icon('search'), + set::text($lang->searchAB), + set('data-module', $this->prop('module')), + set('data-on', 'click'), + set('data-do', "window.toggleSearchForm('$module', '$formName');"), + $this->prop('open') ? h::jsCall('~window.toggleSearchForm', $module, $formName, true) : null + ); + } +} diff --git a/lib/zin/wg/section/v1.php b/lib/zin/wg/section/v1.php new file mode 100644 index 0000000000..3e9b055e6e --- /dev/null +++ b/lib/zin/wg/section/v1.php @@ -0,0 +1,74 @@ + array(), + 'actions' => array(), + ); + + protected function onAddChild($child) + { + if(is_string($child) && !$this->props->has('content')) + { + $this->props->set('content', $child); + return false; + } + } + + private function title(): wg + { + $title = $this->prop('title'); + $actionsView = $this->block('actions'); + + if(empty($actionsView)) return div(setClass('article-h1', 'mb-2'), $title); + + return div + ( + setClass('flex', 'items-center', 'mb-2'), + div(setClass('article-h1'), $title), + $actionsView, + ); + } + + private function content(string|wg $content): wg + { + $useHtml = $this->prop('useHtml') === true && is_string($content); + + return div + ( + setClass('article-content'), + $useHtml ? html($content) : $content, + ); + + } + + private function buildContent(): wg|array|null + { + $content = $this->prop('content'); + if(!isset($content)) return null; + + return $this->content($content); + } + + protected function build(): wg + { + return div + ( + setClass('section'), + set($this->getRestProps()), + $this->title(), + $this->block('subtitle'), + $this->buildContent(), + $this->children() + ); + } +} diff --git a/lib/zin/wg/sectioncard/v1.php b/lib/zin/wg/sectioncard/v1.php new file mode 100644 index 0000000000..765c99841a --- /dev/null +++ b/lib/zin/wg/sectioncard/v1.php @@ -0,0 +1,50 @@ + array('map' => 'entityLabel'), + ); + + private function title(string $text): wg + { + return div + ( + setClass('article-h4', 'mb-1'), + "[$text]" + ); + } + + public function onBuildItem(item $item): wg + { + return div + ( + setClass('py-2', 'pl-3'), + $this->title($item->prop('title')), + $item->children(), + ); + } + + protected function build(): wg + { + $title = $this->block('title'); + + return div + ( + setClass('section-card', 'border', 'rounded-sm'), + div + ( + setClass('h-9', 'flex', 'items-center', 'pl-3'), + setStyle('background', 'var(--color-gray-100)'), + $title + ), + div + ( + setClass('py-1'), + $this->children() + ), + ); + } +} diff --git a/lib/zin/wg/sectionlist/v1.php b/lib/zin/wg/sectionlist/v1.php new file mode 100644 index 0000000000..e61a990b44 --- /dev/null +++ b/lib/zin/wg/sectionlist/v1.php @@ -0,0 +1,23 @@ +getRestProps()), + $this->children() + ); + } +} diff --git a/lib/zin/wg/select/v1.php b/lib/zin/wg/select/v1.php new file mode 100644 index 0000000000..3ce55fe03f --- /dev/null +++ b/lib/zin/wg/select/v1.php @@ -0,0 +1,139 @@ + + * @package zin + * @link http://www.zentao.net + */ +namespace zin; + +/** + * 选择框(select)部件类,支持 Ajax 提交。 + * The select control widget class. + * + * @author Hao Sun + */ +class select extends wg +{ + /** + * Define widget properties. + * + * @var array + * @access protected + */ + protected static array $defineProps = array + ( + 'name: string', + 'id?: string', + 'class?: string="form-control"', + 'value?: string=""', + 'required?: bool', + 'disabled?: bool', + 'multiple?: bool', + 'items?: array', + 'size?: int', + ); + + /** + * The lifecycle method of created. + * + * Set default id with name. + * @access protected + * @return void + */ + protected function created() + { + if($this->prop('id') === null && $this->prop('name') !== null) + { + $name = $this->prop('name'); + $id = substr($name, -2) == '[]' ? substr($name, 0, - 2) : $name; + $this->setProp('id', $id); + } + } + + /** + * Handle building inner options. + * + * @param wg|array wg + * @access public + * @return wg + */ + public function onBuildItem(wg|array $item): wg + { + if($item instanceof item) $item = $item->props->toJsonData(); + + $text = isset($item['text']) ? $item['text'] : ''; + unset($item['text']); + + if(!isset($item['selected'])) + { + $value = isset($item['value']) ? $item['value'] : ''; + $valueList = $this->getValueList(); + + $item['selected'] = in_array($value, $valueList); + } + + return h::option(set($item), $text); + } + + /** + * Get value list. + * + * @access public + * @return array + */ + public function getValueList() + { + list($value, $multiple) = $this->prop(array('value', 'multiple')); + if($multiple) return is_array($value) ? $value : explode(',', (string)$value); + return array($value); + } + + /** + * The lifecycle method of building. + * + * @access protected + * @return wg + */ + protected function build(): wg + { + list($items, $multiple, $required) = $this->prop(array('items', 'multiple', 'required')); + + $hasEmptyItem = false; + $valueList = $this->getValueList(); + if(!empty($items)) + { + foreach($items as $key => $item) + { + if(!is_array($item)) $item = array('text' => $item, 'value' => $key); + if(!is_string($item['value'])) $item['value'] = strval($item['value']); + if(!isset($item['selected'])) $item['selected'] = in_array($item['value'], $valueList); + $items[$key] = $this->onBuildItem($item); + + if($item['value'] === '') $hasEmptyItem = true; + } + } + else + { + $items = array(); + } + + /* Prepend empty option when current select is not required and whitout any empty item. */ + if(!$required && !$hasEmptyItem) array_unshift($items, $this->onBuildItem(array('text' => '', 'value' => '', 'selected' => in_array('', $valueList)))); + + $props = $this->props->skip(['items', 'value', 'multiple', 'required']); + + return h::select + ( + setClass('form-control', $required ? 'is-required' : ''), + set::multiple($multiple), + set($props), + $items, + $this->children() + ); + } +} diff --git a/lib/zin/wg/severitylabel/v1.php b/lib/zin/wg/severitylabel/v1.php new file mode 100644 index 0000000000..efe2c4aaf2 --- /dev/null +++ b/lib/zin/wg/severitylabel/v1.php @@ -0,0 +1,36 @@ +props->has('text')) + { + $this->props->set('text', $child); + return false; + } + } + + protected function build(): wg + { + $text = $this->prop('text'); + $level = $this->prop('level'); + $isIcon = $this->prop('isIcon'); + + return span + ( + set($this->getRestProps()), + setClass($isIcon ? 'severity' : 'severity severity-label'), + set('data-severity', $level), + $text + ); + } +} diff --git a/lib/zin/wg/severitypicker/v1.php b/lib/zin/wg/severitypicker/v1.php new file mode 100644 index 0000000000..885efd0e25 --- /dev/null +++ b/lib/zin/wg/severitypicker/v1.php @@ -0,0 +1,79 @@ + + * @package zin + * @link http://www.zentao.net + */ + +namespace zin; + +/** + * 严重程度选择器(severityPicker)部件类 + * The severityPicker widget class + */ +class severityPicker extends wg +{ + /** + * Define widget properties. + * + * @var array + * @access protected + */ + protected static array $defineProps = array + ( + 'id?: string="$GID"', // 组件根元素的 ID。 + 'formID?: string', // 组件隐藏的表单元素 ID。 + 'className?: string|array', // 类名。 + 'style?: array', // 样式。 + 'tagName?: string', // 组件根元素的标签名。 + 'attrs?: array', // 附加到组件根元素上的属性。 + 'clickType?: "toggle"|"open"', // 点击类型,`toggle` 表示点击按钮时切换显示隐藏,`open` 表示点击按钮时只打。 + 'afterRender?: function', // 渲染完成后的回调函数。 + 'beforeDestroy?: function', // 销毁前的回调函数。 + 'name?: string', // 作为表单项的名称。 + 'value?: string|string[]', // 默认值。 + 'onChange?: function', // 值变更回调函数。 + 'disabled?: boolean', // 是否禁用。 + 'multiple?: boolean|number=false', // 是否允许选择多个值,如果指定为数字,则限制多选的数目,默认 `false`。 + 'required?: boolean', // 是否必选(不允许空值,不可以被清除)。 + 'placeholder?: string', // 选择框上的占位文本。 + 'items?: string[]|array' // 选项列表,默认为 $lang->$moduleName->severityList。 + ); + + /** + * Build widget. + * + * @access protected + */ + protected function build(): zui + { + list($props, $restProps) = $this->props->split(array_keys(static::definedPropsList())); + if(isset($props['id'])) + { + $props['_id'] = $props['id']; + unset($props['id']); + } + + if(!isset($props['items'])) + { + global $app, $lang; + $moduleName = $app->getModuleName(); + if(isset($lang->$moduleName->severityList)) $props['items'] = $lang->$moduleName->severityList; + } + + return zui::severityPicker + ( + set::_class('form-group-wrapper'), + set::_map(array('value' => 'defaultValue', 'formID' => 'id')), + set::_props($restProps), + set::popWidth('100%'), + set($props), + $this->children(), + ); + } +} diff --git a/lib/zin/wg/sidebar/css/v1.css b/lib/zin/wg/sidebar/css/v1.css new file mode 100644 index 0000000000..029801fc5a --- /dev/null +++ b/lib/zin/wg/sidebar/css/v1.css @@ -0,0 +1,97 @@ +.sidebar { + flex: none !important; + position: relative; + transition-duration: .15s; + transition-property: width; + transition-timing-function: cubic-bezier(.4, 0, .2, 1); + width: var(--zt-sidebar-width); +} + +.sidebar-toggle { + align-items: center; + border-radius: var(--radius); + bottom: 0; + cursor: pointer; + display: flex; + justify-content: center; + position: absolute; + top: 0; + transition-duration: 1s; + transition-property: color, background-color, border-color, text-decoration-color, fill, stroke; + transition-timing-function: cubic-bezier(.4, 0, .2, 1); + width: 1rem; +} + +.sidebar-toggle > .icon { + align-items: center; + background-color: rgb(var(--color-canvas-rgb)); + border-color: rgb(212, 212, 216); + border-width: 1px; + display: flex; + height: 2rem; + justify-content: center; + width: 0.75rem; +} + +.sidebar-toggle:hover > .icon { + border-color: rgba(var(--color-primary-500-rgb)); + color: rgba(var(--color-primary-500-rgb)); +} + +.sidebar-left > .sidebar-toggle > .icon { + border-bottom-left-radius: var(--radius-lg); + border-top-left-radius: var(--radius-lg); +} + +.sidebar-right > .sidebar-toggle > .icon { + border-top-right-radius: var(--radius-lg); + border-bottom-right-radius: var(--radius-lg); +} + +.sidebar-left > .sidebar-toggle { + right: -1rem; +} + +.sidebar-right > .sidebar-toggle { + left: -1rem; +} + +.sidebar-toggle:hover { + background-color: #3341550d; +} + +.hide-sidebar-left .sidebar-left > *, +.hide-sidebar-right .sidebar-right > * { + display: none; +} + +.hide-sidebar-left .sidebar.sidebar-left, +.hide-sidebar-right .sidebar.sidebar-right { + margin-left: -0.5rem; + margin-right: -0.5rem; + width: 0; +} + +.hide-sidebar-left .sidebar-left > .sidebar-toggle { + display: flex; + right: -0.5rem; +} +.hide-sidebar-right .sidebar-right > .sidebar-toggle { + display: flex; + left: -0.5rem; +} +.hide-sidebar-left .sidebar-left > .sidebar-toggle > .icon { + transform: rotate(180deg); + border-top-left-radius: 0; + border-bottom-left-radius: 0; + border-top-right-radius: 0.5rem; + border-bottom-right-radius: 0.5rem; +} + +.hide-sidebar-right .sidebar-right > .sidebar-toggle > .icon { + transform: rotate(180deg); + border-top-right-radius: 0; + border-bottom-right-radius: 0; + border-top-left-radius: 0.5rem; + border-bottom-left-radius: 0.5rem; +} diff --git a/lib/zin/wg/sidebar/js/v1.js b/lib/zin/wg/sidebar/js/v1.js new file mode 100644 index 0000000000..b08e5936ad --- /dev/null +++ b/lib/zin/wg/sidebar/js/v1.js @@ -0,0 +1,13 @@ +/** + * @param {{side?: 'left' | 'right', toggle?: boolean, container?: HTMLElement}=} options + */ +function toggleSidebar(options) { + const {side = 'left', toggle, container = document.body} = options || {}; + container.classList.toggle(`hide-sidebar-${side}`, typeof toggle === 'boolean' ? !toggle : undefined); +} + +zui.toggleSidebar = toggleSidebar; + +zui.bus.on('zt_toggleSidebar', (event) => { + toggleSidebar(event.detail); +}); diff --git a/lib/zin/wg/sidebar/v1.php b/lib/zin/wg/sidebar/v1.php new file mode 100644 index 0000000000..ca0419e011 --- /dev/null +++ b/lib/zin/wg/sidebar/v1.php @@ -0,0 +1,38 @@ +prop(array('side', 'showToggle')); + return div + ( + setClass("sidebar sidebar-$side"), + set($this->getRestProps()), + $this->children(), + $showToggle ? div + ( + set::class("sidebar-toggle sidebar-$side-toggle"), + icon("angle-$side"), + on::click("zui.toggleSidebar({side: '$side'})") + ) : null + ); + } +} diff --git a/lib/zin/wg/stepseditor/css/v1.css b/lib/zin/wg/stepseditor/css/v1.css new file mode 100644 index 0000000000..827e01ca5a --- /dev/null +++ b/lib/zin/wg/stepseditor/css/v1.css @@ -0,0 +1,23 @@ +.steps-editor {width: 100%} +.steps-editor-header {padding-right: 40px; color: var(--color-slate-700);} +.steps-editor-row {position: relative; display: flex; align-items: stretch; min-height: 32px; --tw-ring-color: rgba(var(--color-border-strong-rgb), 1); box-shadow: var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color); margin-top: 1px; z-index: 1;} +.steps-editor-row.is-focus {z-index: 100;} +.steps-editor-header .steps-editor-row {border-radius: 2px 2px 0 0; background-color: var(--color-slate-100);} +.steps-editor-col {flex: none; display: flex; align-items: stretch;} +.steps-editor-header .steps-editor-col {padding: 0 12px; align-items: center;} +.steps-editor-col-step {width: 50%;} +.steps-editor-col-add {width: 80px; border-left: 1px solid var(--color-border-strong); border-right: 1px solid var(--color-border-strong); align-items: center; padding: 0!important;} +.steps-editor-col-add > div {width: 50%; display: flex; align-items: center; justify-content: center; position: relative;} +.steps-editor-col-expect {flex: auto;} +.steps-editor-body {padding-right: 40px;} +.steps-editor-col-step.form-control {padding: 0; border-radius: 0; height: auto; z-index: 100;} +.steps-editor-step-name {padding: 6px 0 6px 8px; flex: none; text-align: right; opacity: .5;} +.steps-editor-step-expect, +.steps-editor-step-text {padding: 6px 12px;; flex: auto; border: none; min-height: 32px; background: none; border-radius: 0; resize: none} +.steps-editor-step-text {outline: none;} +.steps-editor-body .steps-editor-col-add > div + div::before {content: ' '; position: absolute; display: block; border-left: 1px solid var(--color-border-strong); top: 4px; bottom: 4px; left: 0; opacity: .7;} +.steps-editor .btn-action {color: var(--color-slate-600);} +.steps-editor .btn-action:hover {color: var(--color-primary-500);} +.steps-editor-col-delete {width: 40px; height: 32px; display: flex; align-items: center; justify-content: center; position: absolute; right: -40px; top: 0} +.steps-editor-row[data-level="1"] {margin-top: 7px;} +.steps-editor-body > .steps-editor-row:first-child {margin-top: 1px;} diff --git a/lib/zin/wg/stepseditor/js/v1.js b/lib/zin/wg/stepseditor/js/v1.js new file mode 100644 index 0000000000..4644a78cc0 --- /dev/null +++ b/lib/zin/wg/stepseditor/js/v1.js @@ -0,0 +1,225 @@ +/** Steps editor component. */ +class StepsEditor extends zui.Component +{ + static NAME = 'StepsEditor'; + + static DEFAULT = + { + name: 'steps', + expectsName: 'expects', + data: ['1', '2', '3'], + sameLevelIcon: 'plus', + subLevelIcon: 'split', + moveIcon: 'move', + deleteIcon: 'trash', + expectDisabledTip: '', + }; + + init() + { + this.update(this.options.data, true, true); + } + + afterInit() + { + this.render(); + this.$element + .on('focus', 'textarea', e => + { + const $textarea = $(e.target); + $textarea.closest('.steps-editor-col-step').addClass('focus'); + $textarea.closest('.steps-editor-row').addClass('is-focus'); + }) + .on('blur', 'textarea', e => + { + const $textarea = $(e.target); + $textarea.closest('.steps-editor-col-step').removeClass('focus'); + $textarea.closest('.steps-editor-row').removeClass('is-focus'); + }) + .on('click', '.btn-action', e => + { + const action = $(e.currentTarget).attr('data-action'); + const name = $(e.currentTarget).closest('.steps-editor-row').attr('data-name'); + if(action === 'delete') this.deleteStep(name); + else if(action === 'sib') this.addSib(name); + else if(action === 'sub') this.addSub(name); + }); + } + + getByID(id) + { + const map = this._map; + const keys = Object.keys(map); + for(let i = 0; i < keys.length; i++) + { + const item = map[keys[i]]; + if(item.id === id) return item; + } + return null; + } + + update(data, reset, skipRender) + { + this._map = (reset ? {} : this._map) || {}; + data.forEach(item =>{this.updateItem(item, true);}); + if(!skipRender) this.render(); + } + + updateItem(item, skipRender) + { + const map = this._map; + if(typeof item === 'string') item = {name: item, id: $.guid++}; + item = $.extend({id: $.guid++, step: '', expect: ''}, item); + const namePath = item.name.split('.'); + item.order = namePath.reduce((total, level, index) => total + (+level * ([1000000, 1000, 1, 0.001, 0.000001][index])), 0); + item.level = namePath.length; + item.selfName = namePath.pop(); + item.parentName = namePath.join('.'); + + const oldItem = this.getByID(item.id); + + if(oldItem && oldItem.name !== item.name) delete map[oldItem.name]; + map[item.name] = oldItem ? $.extend({}, oldItem, item) : item; + + if(!skipRender) this.render(); + } + + render() + { + const map = this._map; + const list = Object.keys(map).map(key => + { + const item = map[key]; + return $.extend(item, {children: [], parent: item.parentName ? map[item.parentName] : null}) + }) + .sort((a, b) => a.order - b.order); + list.forEach(item => + { + if(item.parent) item.parent.children.push(item); + }); + + const $list = this.$element.find('.steps-editor-body'); + const $rows = $list.find('.steps-editor-row').addClass('is-expired'); + const options = this.options; + let $preRow = null; + let $row = null; + list.forEach(item => + { + $row = $rows.filter(`[data-id="${item.id}"]`); + if(!$row.length) + { + $row = $ + ([ + `
    `, + '
    ', + '
    ', + ``, + '
    ', + '
    ', + `
    `, + `
    `, + '
    ', + '
    ', + ``, + '
    ', + '
    ', + `
    `, + '', + '
    ', + '
    ' + ].join('')); + } + if($preRow) $row.insertAfter($preRow); + else $list.prepend($row); + $preRow = $row; + + const hasSub = !!(item.children && item.children.length); + $row.attr('data-level', item.level) + .attr('data-name', item.name) + .toggleClass('has-children', hasSub) + .removeClass('is-expired'); + $row.find('.steps-editor-step-name').css('width', 6 + (item.level * 18)).text(item.name); + $row.find('.steps-editor-step-text').attr('name', `${options.name}[${item.name}]`); + $row.find('.steps-editor-step-type').attr('name', `stepType[${item.name}]`).val(hasSub ? 'item' : 'step'); + const $expect = $row.find('.steps-editor-step-expect').attr( + { + name: `${options.expectsName}[${item.name}]`, + placeholder: hasSub ? options.expectDisabledTip : null, + disabled: hasSub ? 'disabled' : null, + }).toggleClass('disabled', hasSub); + if(hasSub) $expect.val(''); + $row.find('.steps-editor-col-delete .btn').toggleClass('disabled', hasSub).attr('disabled', hasSub ? 'disabled' : null); + $row.find('.steps-editor-col-add .btn-action[data-action="sub"]').attr('disabled', item.level >= 3 ? 'disabled' : null); + }); + $rows.filter('.is-expired').remove(); + } + + deleteStep(name) + { + const item = this._map[name]; + if (!item) return; + delete this._map[name]; + const siblings = item.parent ? item.parent.children.filter(x => x.name !== name) : Object.keys(this._map).map(key => this._map[key]).filter(x => !x.parent); + const updateItems = []; + siblings.forEach((sibling, idx) => + { + const newName = item.parent ? `${item.parent.name}.${idx + 1}` : `${idx + 1}`; + if(sibling.name !== newName) + { + updateItems.push($.extend({}, sibling, {name: newName})); + } + }); + if(updateItems.length) this.update(updateItems, false, true); + if(!Object.keys(this._map).length) this.update(['1'], false, true); + this.render(); + } + + focus(name) + { + const $step = this.$element.find('.steps-editor-row[data-name="' + name + '"] .steps-editor-step-text'); + if($step.length) $step[0].focus(); + } + + addSub(fromName) + { + const item = this._map[fromName]; + if(!item) return; + + const newStepName = item.children ? `${item.name}.${item.children.length + 1}` : `${item.name}.1`; + + this.updateItem({name: newStepName}); + this.focus(newStepName); + } + + addSib(fromName) + { + const item = this._map[fromName]; + if(!item) return; + + const siblings = item.parent ? item.parent.children : Object.keys(this._map).map(key => this._map[key]).filter(item => !item.parent); + const index = siblings.indexOf(item); + const newStepName = item.parent ? `${item.parent.name}.${index + 2}` : `${index + 2}`; + const updateItems = [newStepName]; + if(index < siblings.length - 1) + { + siblings.slice(index + 1).forEach((nextItem, idx) => + { + const newIndex = index + idx + 3; + updateItems.push($.extend({}, nextItem, {name: nextItem.parent ? `${nextItem.parent.name}.${newIndex}` : `${newIndex}`})) + }); + } + this.update(updateItems); + this.focus(newStepName); + } + + addStep(fromName, asSib) + { + if(asSib) this.addSib(fromName); + else this.addSub(fromName); + } +} + +/* Define $.fn.stepsEditor() helper. */ +StepsEditor.defineFn(); + +$.extend(zui, {StepsEditor}); diff --git a/lib/zin/wg/stepseditor/v1.php b/lib/zin/wg/stepseditor/v1.php new file mode 100644 index 0000000000..34467dd6f2 --- /dev/null +++ b/lib/zin/wg/stepseditor/v1.php @@ -0,0 +1,106 @@ + + * @package zin + * @link http://www.zentao.net + */ +namespace zin; + +require_once dirname(__DIR__) . DS . 'input' . DS . 'v1.php'; + +/** + * 步骤编辑(stepsEditor)部件类 + * The stepsEditor widget class + */ +class stepsEditor extends wg +{ + /** + * Define widget properties. + * + * @var array + * @access protected + */ + protected static array $defineProps = array + ( + 'id?: string="$GID"', // 组件根元素的 ID。 + 'name?: string="steps"', // 步骤输入框作为表单项的名称。 + 'expectsName?: string="expects"', // 预期输入框作为表单项的名称。 + 'data?: array', // 默认值。 + 'stepText?: string', // 步骤文本。 + 'expectText?: string', // 预期文本。 + 'sameLevelText?: string', // 同级文本。 + 'subLevelText?: string', // 子级文本。 + 'expectDisabledTip?: string' // 预期输入框禁用提示。 + ); + + public static function getPageJS(): string|false + { + return file_get_contents(__DIR__ . DS . 'js' . DS . 'v1.js'); + } + + public static function getPageCSS(): string|false + { + return file_get_contents(__DIR__ . DS . 'css' . DS . 'v1.css'); + } + + /** + * Build the widget. + * + * @access protected + * @return wg + */ + protected function build(): wg + { + $stepText = $this->prop('stepText', data('lang.testcase.stepDesc')); + $expectText = $this->prop('expectText', data('lang.testcase.stepExpect')); + $sameLevelText = $this->prop('sameLevelText', data('lang.testcase.stepSameLevel')); + $subLevelText = $this->prop('subLevelText', data('lang.testcase.stepSubLevel')); + $id = $this->prop('id'); + $expectDisabledTip = $this->prop('expectDisabledTip', data('lang.testcase.expectDisabledTip')); + + return div + ( + setID($id), + setClass('steps-editor w-full'), + div + ( + setClass('steps-editor-header'), + row + ( + setClass('steps-editor-row'), + cell + ( + set::class('steps-editor-col steps-editor-col-step'), + $stepText + ), + cell + ( + set::class('steps-editor-col steps-editor-col-add'), + div($sameLevelText), + div($subLevelText), + ), + cell + ( + set::class('steps-editor-col steps-editor-col-expect'), + $expectText + ) + ) + ), + div + ( + set::class('steps-editor-body') + ), + zui::stepsEditor + ( + set::_to("#$id"), + set::expectDisabledTip($expectDisabledTip), + set($this->props->pick(array('name', 'expectsName', 'data'))) + ) + ); + } +} diff --git a/lib/zin/wg/switcher/v1.php b/lib/zin/wg/switcher/v1.php new file mode 100644 index 0000000000..a1ac619c54 --- /dev/null +++ b/lib/zin/wg/switcher/v1.php @@ -0,0 +1,13 @@ + 'switch switch' + ); +} diff --git a/lib/zin/wg/tablechart/v1.php b/lib/zin/wg/tablechart/v1.php new file mode 100644 index 0000000000..842f74b609 --- /dev/null +++ b/lib/zin/wg/tablechart/v1.php @@ -0,0 +1,85 @@ +prop('type'); + $title = $this->prop('title'); + $datas = $this->prop('datas'); + $colorList = array('#5470C6', '#91CC75', '#FAC858', '#EE6666', '#73C0DE', '#3BA272', '#FC8452', '#9A60B4', '#EA7CCC'); + $chartOption = array(); + foreach($datas as $key => $data) + { + $color = current($colorList); + $chartOption[] = array('name' => $data->name, 'value' => $type == 'pie' ? $data->value : array('value' => $data->value, 'itemStyle' => array('color' => $color))); + $tableTR[] = h::tr + ( + h::td(label(set::class('label-dot mr-2'), set::style(array('background-color' => $color, '--tw-ring-color' => $color))), $data->name), + h::td($data->value), + h::td(($data->percent * 100) . '%') + ); + if(!next($colorList)) reset($colorList); + } + + return div + ( + set::class('flex border'), + cell + ( + set::width('50%'), + set::class('border-r chart'), + div(set::class('center text-base font-bold py-2'), $title), + echarts + ( + set::color($colorList), + $type != 'pie' ? set::xAxis + ( + array + ( + 'type' => 'category', + 'data' => array_column($chartOption, 'name') + ) + ) : null, + $type != 'pie' ? set::yAxis(array('type' => 'value')) : null, + set::series + ( + array + ( + array + ( + 'data' => $type == 'pie' ? $chartOption : array_column($chartOption, 'value'), + 'type' => $type + ) + ) + ) + )->size('100%', 300), + ), + cell + ( + set::width('50%'), + h::table + ( + set::class('table'), + h::tr + ( + h::th($lang->report->item), + h::th(set::width('100px'), $lang->report->value), + h::th(set::width('120px'), $lang->report->percent) + ), + $tableTR + ) + ) + ); + } +} diff --git a/lib/zin/wg/tabledata/css/v1.css b/lib/zin/wg/tabledata/css/v1.css new file mode 100644 index 0000000000..61a4544198 --- /dev/null +++ b/lib/zin/wg/tabledata/css/v1.css @@ -0,0 +1,3 @@ +.table-data th, +.table-data .table-data-th {min-width: 60px; color: var(--color-slate-600);} + diff --git a/lib/zin/wg/tabledata/v1.php b/lib/zin/wg/tabledata/v1.php new file mode 100644 index 0000000000..c33ca58330 --- /dev/null +++ b/lib/zin/wg/tabledata/v1.php @@ -0,0 +1,127 @@ +prop('trClass')), + h::th + ( + setClass('py-1.5 pr-2 font-normal nowrap text-right', $item->prop('thClass')), + $item->prop('name'), + ), + h::td + ( + setClass('py-1.5 pl-2 w-full', $item->prop('tdClass')), + $item->children() + ) + ); + } + + private function buildItemWithDiv($item): wg + { + if($item->prop('collapse')) + { + return div + ( + setClass('col', 'table-data-tr', $item->prop('trClass')), + div + ( + setClass('py-1.5 pr-2 font-normal nowrap table-data-th', $item->prop('thClass')), + $item->prop('name'), + new collapseBtn + ( + setClass('w-5 h-5 ml-1'), + set::target('.table-data-td'), + set::parent('.table-data-tr') + ), + ), + div + ( + setClass('py-1.5 pl-2 table-data-td', $item->prop('tdClass')), + $item->children() + ) + ); + } + + return div + ( + setClass('flex table-data-tr', $item->prop('trClass')), + div + ( + setClass('py-1.5 pr-2 font-normal nowrap table-data-th', $item->prop('thClass')), + $item->prop('name'), + ), + div + ( + setClass('py-1.5 pl-2 table-data-td', $item->prop('tdClass')), + $item->children() + ) + ); + } + + public function onBuildItem($item): wg + { + $item->setProp(array('thClass' => $this->prop('thClass'), 'tdClass' => $this->prop('tdClass'))); + + $useTable = $this->prop('useTable'); + if($useTable) return $this->buildItemWithTr($item); + + return $this->buildItemWithDiv($item); + } + + private function caption(): ?wg + { + $title = $this->prop('title'); + if(empty($title)) return null; + + return h::caption + ( + setClass('article-h1 text-left mb-2'), + $title + ); + } + + protected function build(): wg + { + $useTable = $this->prop('useTable'); + if($useTable) + { + return h::table + ( + setClass('table-data'), + $this->caption(), + h::tbody($this->children()) + ); + } + + return div + ( + setClass('table-data'), + div + ( + setClass('table-data-body'), + $this->children() + ) + ); + } +} diff --git a/lib/zin/wg/tabpane/v1.php b/lib/zin/wg/tabpane/v1.php new file mode 100644 index 0000000000..bb1b0de2be --- /dev/null +++ b/lib/zin/wg/tabpane/v1.php @@ -0,0 +1,33 @@ + array(), + 'suffix' => array(), + 'divider' => false, + ); + + protected function build(): wg + { + $key = $this->prop('key'); + $active = $this->prop('active'); + + return div + ( + setID($key), + setClass('tab-pane', $active ? 'active' : null), + set($this->getRestProps()), + $this->children() + ); + } +} diff --git a/lib/zin/wg/tabs/css/v1.css b/lib/zin/wg/tabs/css/v1.css new file mode 100644 index 0000000000..ff5bb7e42f --- /dev/null +++ b/lib/zin/wg/tabs/css/v1.css @@ -0,0 +1,8 @@ +.tabs .nav-tabs>.nav-item>a {font-size: 14px; font-weight: 800; padding: 0; padding-right: 0; color: var(--color-slate-800);} +.tabs .nav-tabs>.nav-item>a:after {border-width: 0;} +.tabs .nav-tabs>.nav-item>a:before {background: none;} +.tabs .nav-tabs>.nav-item>a.active {color: var(--color-primary-500);} +.tabs .nav-tabs>.nav-item>a.active:after {border-bottom-color: var(--color-primary-500) !important; border-bottom-width: 2px;} +.tabs .nav-tabs>.divider {height: 20px; border-right: 1px solid #DDD;} +.tabs .tabs-collapse-btn {position: absolute; top: 3px; right: 0; width: 24px; height: 24px;} +.tabs .tab-content {padding-top: 10px;} diff --git a/lib/zin/wg/tabs/v1.php b/lib/zin/wg/tabs/v1.php new file mode 100644 index 0000000000..1b525a38ba --- /dev/null +++ b/lib/zin/wg/tabs/v1.php @@ -0,0 +1,138 @@ +prop('key'); + $title = $tabPane->prop('title'); + $active = $tabPane->prop('active'); + $param = $tabPane->prop('param'); + $prefix = $tabPane->block('prefix'); + $suffix = $tabPane->block('suffix'); + + return li + ( + setClass('nav-item'), + a + ( + set('data-toggle', 'tab'), + set('data-param', $param), + setClass('font-medium', $active ? 'active' : null), + set::href("#$key"), + $prefix, + span($title), + $suffix, + ) + ); + } + + /** + * @param wg[] $titleViews + * @return wg + */ + protected function buildTabHeader(array $titleViews): wg + { + $isVertical = $this->prop('direction') === 'v'; + $collapse = $this->prop('collapse'); + + return ul + ( + setClass('nav nav-tabs gap-x-5', $collapse ? 'relative' : null), + $isVertical ? setClass('nav-stacked') : null, + $titleViews, + $this->buildCollapseBtn(), + ); + } + + /** + * @param wg[] $titleViews + * @return wg + */ + protected function buildTabBody(array $contentViews): wg + { + return div + ( + setClass('tab-content'), + $contentViews + ); + } + + private function filterChildren() + { + foreach ($this->children() as $child) + { + if($child instanceof tabPane) + { + $this->tabPanes[] = $child; + continue; + } + + $this->children[] = $child; + } + } + + private function buildCollapseBtn(): ?wg + { + $collapse = $this->prop('collapse'); + if(!$collapse) return null; + + return collapseBtn + ( + setClass('tabs-collapse-btn'), + set::target('.tab-content'), + set::parent('.tabs') + ); + } + + protected function build(): wg + { + $isVertical = $this->prop('direction') === 'v'; + + $this->filterChildren(); + + $titleViews = array(); + $contentViews = array(); + foreach($this->tabPanes as $tabPane) + { + $titleViews[] = $this->buildTitleView($tabPane); + + $divider = $tabPane->block('divider'); + if($divider) $titleViews[] = div(set::class('divider')); + + $contentViews[] = $tabPane; + } + + return div + ( + setClass('tabs', $isVertical ? 'flex' : null), + set($this->getRestProps()), + + $this->buildTabHeader($titleViews), + $this->buildTabBody($contentViews), + $this->children + ); + } +} diff --git a/lib/zin/wg/textarea/v1.php b/lib/zin/wg/textarea/v1.php new file mode 100644 index 0000000000..62699e8a55 --- /dev/null +++ b/lib/zin/wg/textarea/v1.php @@ -0,0 +1,43 @@ + 'form-control', + 'rows' => 10 + ); + + protected function onAddChild(array|string|wg|directive $child) + { + if(is_string($child) && !$this->props->has('value')) + { + $this->setProp('value', $child); + return false; + } + + return $child; + } + + protected function build(): wg + { + return h::textarea + ( + set($this->props->pick(array('name', 'id', 'class', 'required', 'placeholder', 'rows', 'cols', 'disabled'))), + $this->prop('value'), + $this->children() + ); + } +} diff --git a/lib/zin/wg/timepicker/v1.php b/lib/zin/wg/timepicker/v1.php new file mode 100644 index 0000000000..9dba9fb5a5 --- /dev/null +++ b/lib/zin/wg/timepicker/v1.php @@ -0,0 +1,74 @@ + + * @package zin + * @link http://www.zentao.net + */ +namespace zin; + +require_once dirname(__DIR__) . DS . 'input' . DS . 'v1.php'; + +/** + * 日期选择器(timePicker)部件类 + * The timePicker widget class + */ +class timePicker extends wg +{ + /** + * Define widget properties. + * + * @var array + * @access protected + */ + protected static array $defineProps = array + ( + 'id?: string="$GID"', // 组件根元素的 ID。 + 'formID?: string', // 组件隐藏的表单元素 ID。 + 'className?: string|array', // 类名。 + 'style?: array', // 样式。 + 'tagName?: string', // 组件根元素的标签名。 + 'attrs?: array', // 附加到组件根元素上的属性。 + 'clickType?: "toggle"|"open"', // 点击类型,`toggle` 表示点击按钮时切换显示隐藏,`open` 表示点击按钮时只打。 + 'afterRender?: function', // 渲染完成后的回调函数。 + 'beforeDestroy?: function', // 销毁前的回调函数。 + 'name?: string', // 作为表单项的名称。 + 'defaultValue?: string|string[]', // 默认值。 + 'onChange?: function', // 值变更回调函数。 + 'disabled?: boolean', // 是否禁用。 + 'multiple?: boolean|number=false', // 是否允许选择多个值,如果指定为数字,则限制多选的数目,默认 `false`。 + 'required?: boolean', // 是否必选(不允许空值,不可以被清除)。 + 'placeholder?: string', // 选择框上的占位文本。 + 'format?: string', // 日期格式,默认 hh:mm。 + 'icon?: string|array="time"', // 在输入框右侧显示的图标。 + 'onInvalid?: function', // 日期值无效时的回调函数。 + ); + /** + * Build the widget. + * + * @access protected + * @return wg + */ + protected function build(): zui + { + list($props, $restProps) = $this->props->split(array_keys(static::definedPropsList())); + if(isset($props['id'])) + { + $props['_id'] = $props['id']; + unset($props['id']); + } + + return zui::timePicker + ( + set::_class('form-group-wrapper'), + set::_map(array('value' => 'defaultValue', 'formID' => 'id')), + set($props), + set::_props($restProps), + $this->children(), + ); + } +} diff --git a/lib/zin/wg/toolbar/v1.php b/lib/zin/wg/toolbar/v1.php new file mode 100644 index 0000000000..3cdaaf76fd --- /dev/null +++ b/lib/zin/wg/toolbar/v1.php @@ -0,0 +1,51 @@ +prop('type'); + if($type === 'divider') return div(setClass('toolbar-divider')); + if($type === 'btnGroup') return new btnGroup(inherit($item)); + + list($btnClass, $btnProps) = $this->prop(array('btnClass', 'btnProps')); + $btn = empty($item->prop('back')) ? '\zin\btn' : '\zin\backBtn'; + return new $btn + ( + setClass('toolbar-item', $btnClass), + is_array($btnProps) ? set($btnProps) : null, + inherit($item) + ); + } + + protected function build(): wg + { + $items = $this->prop('items'); + return div + ( + setClass('toolbar'), + set($this->getRestProps()), + is_array($items) ? array_map(array($this, 'onBuildItem'), $items) : null, + $this->children() + ); + } +} diff --git a/lib/zin/wg/tooltip/v1.php b/lib/zin/wg/tooltip/v1.php new file mode 100644 index 0000000000..8dd75f4a43 --- /dev/null +++ b/lib/zin/wg/tooltip/v1.php @@ -0,0 +1,13 @@ +setProp('items', $this->buildTree($this->prop('items'))); + return zui::tree(set($this->props->pick(array('items', 'activeClass', 'activeIcon', 'activeKey', 'onClickItem', 'defaultNestedShow', 'changeActiveKey', 'isDropdownMenu', 'collapsedIcon', 'expandedIcon', 'normalIcon', 'id', 'itemActions', 'hover', 'onClick')))); + } + + private function buildTree(array $items): array + { + $canUpdateOrder = $this->prop('canUpdateOrder'); + $canEdit = $this->prop('canEdit'); + $canDelete = $this->prop('canDelete'); + $editType = $this->prop('type'); + + foreach($items as $key => $item) + { + $item = (array)$item; + $treeItem = array('text' => $item['name'], 'url' => $item['url'], 'id' => $item['id'], 'key' => $item['key']); + if($item['type'] == 'product') + { + $treeItem['icon'] = 'product'; + } + else + { + $treeItem['actions'] = array(); + $treeItem['actions']['items'] = array(); + + if($canEdit) $treeItem['actions']['items'][] = array('key' => 'edit', 'icon' => 'edit', 'id' => $item['id'], 'editType' => $editType, 'onClick' => jsRaw('(event, item) => window.editItem(item)')); + if($canDelete) $treeItem['actions']['items'][] = array('key' => 'delete', 'icon' => 'trash', 'id' => $item['id'], 'class' => 'btn ghost toolbar-item square size-sm rounded ajax-submit','url' => helper::createLink('tree', 'delete', 'module=' . $item['id'])); + $treeItem['actions']['items'][] = array('key' => 'view', 'icon' => 'split', 'url' => $item['url']); + } + + if(isset($item['children'])) $treeItem['items'] = $this->buildTree($item['children']); + + $items[$key] = $treeItem; + } + + return $items; + } +} diff --git a/lib/zin/wg/upload/v1.php b/lib/zin/wg/upload/v1.php new file mode 100644 index 0000000000..979f4ed4a6 --- /dev/null +++ b/lib/zin/wg/upload/v1.php @@ -0,0 +1,53 @@ +prop('class')) $this->setProp('class', 'w-full'); + if(!$this->prop('tip')) $this->setProp('tip', sprintf($lang->noticeDrag, strtoupper(ini_get('upload_max_filesize')))); + + $otherProps = $this->getRestProps(); + return zui::upload(inherit($this), set('_props', $otherProps)); + } +} diff --git a/lib/zin/wg/uploadimgs/v1.php b/lib/zin/wg/uploadimgs/v1.php new file mode 100644 index 0000000000..b766a398a3 --- /dev/null +++ b/lib/zin/wg/uploadimgs/v1.php @@ -0,0 +1,42 @@ +account)) + { + $this->props->set('user', $child); + return false; + } + return parent::onAddChild($child); + } + + protected function build(): wg + { + list($user, $avatar, $account, $realname) = $this->prop(array('user', 'avatar', 'account', 'realname')); + if(is_array($user)) + { + $avatar = isset($user['avatar']) ? $user['avatar'] : $avatar; + $account = isset($user['account']) ? $user['account'] : $account; + $realname = isset($user['realname']) ? $user['realname'] : $realname; + } + elseif(is_object($user)) + { + $avatar = isset($user->avatar) ? $user->avatar : $avatar; + $account = isset($user->account) ? $user->account : $account; + $realname = isset($user->realname) ? $user->realname : $realname; + } + + return avatar + ( + set::src($avatar), + set::code($account), + set::text(empty($realname) ? $account : $realname), + set($this->props->skip(array('avatar', 'account', 'realname', 'user'))) + ); + } +} diff --git a/lib/zin/zentao/bind.class.php b/lib/zin/zentao/bind.class.php new file mode 100644 index 0000000000..830e125745 --- /dev/null +++ b/lib/zin/zentao/bind.class.php @@ -0,0 +1,38 @@ + + * @package zin + * @link https://www.zentao.net + */ + +namespace zin; + +require_once __DIR__ . DS . 'zentao.func.php'; + +/** + * The global event bind class. + * + * @example + * bind::click('alert("ok")'); + */ +class bind +{ + /** + * Bind event by magic __callStatic, the method name as the event name. + * + * @param string $name + * @param array $args + * @return directive + * @access public + */ + public static function __callStatic(string $name, array $args): directive + { + list($callback, $others) = array_merge($args, array(array())); + return bind($name, $callback, $others); + } +} diff --git a/lib/zin/zentao/pager.func.php b/lib/zin/zentao/pager.func.php new file mode 100644 index 0000000000..84ddcf09df --- /dev/null +++ b/lib/zin/zentao/pager.func.php @@ -0,0 +1,64 @@ +setParams(); + $params = $pager->params; + foreach($params as $key => $value) + { + if(strtolower($key) === 'recperpage') $params[$key] = '{recPerPage}'; + if(strtolower($key) === 'pageid') $params[$key] = '{page}'; + } + + $setting = array(); + $setting['page'] = $pager->pageID; + $setting['recTotal'] = $pager->recTotal; + $setting['recPerPage'] = $pager->recPerPage; + $setting['linkCreator'] = createLink($pager->moduleName, $pager->methodName, $params); + $setting['items'] = array(); + $setting['gap'] = 0; + + if($pager->recTotal == 0) + { + $setting['items'][] = array('type' => 'info', 'text' => $pager->lang->pager->noRecord); + } + elseif($extra == 'noTotalCount') + { + $setting['items'][] = array('type' => 'size-menu', 'text' => str_replace('', '', str_replace('', '', $pager->lang->pager->pageSize)), 'dropdown' => array('placement' => 'top')); + $setting['items'][] = array('type' => 'link', 'page' => 'prev', 'hint' => $pager->lang->pager->previousPage, 'icon' => 'icon-angle-left'); + $setting['items'][] = array('type' => 'link', 'page' => 'next', 'hint' => $pager->lang->pager->nextPage, 'icon' => 'icon-angle-right'); + } + else + { + $setting['items'][] = array('type' => 'info', 'text' => $pager->lang->pager->totalCountAB); + if(strpos(',short,', ",{$extra},") === false) $setting['items'][] = array('type' => 'size-menu', 'text' => str_replace('', '', str_replace('', '', $pager->lang->pager->pageSize)), 'dropdown' => array('placement' => 'top'), 'itemProps' => array('onClick' => jsRaw("(e, item) => $.cookie.set('$pager->pageCookie', item.text)"))); + $setting['items'][] = array('type' => 'link', 'page' => 'first', 'hint' => $pager->lang->pager->firstPage, 'icon' => 'icon-first-page'); + $setting['items'][] = array('type' => 'link', 'page' => 'prev', 'hint' => $pager->lang->pager->previousPage, 'icon' => 'icon-angle-left'); + $setting['items'][] = array('type' => 'info', 'text' => '{page}/{pageTotal}'); + $setting['items'][] = array('type' => 'link', 'page' => 'next', 'hint' => $pager->lang->pager->nextPage, 'icon' => 'icon-angle-right'); + $setting['items'][] = array('type' => 'link', 'page' => 'last', 'hint' => $pager->lang->pager->lastPage, 'icon' => 'icon-last-page'); + } + + if(!empty($userSetting)) $setting = array_merge($setting, $userSetting); + + return $setting; +} diff --git a/lib/zin/zentao/zentao.func.php b/lib/zin/zentao/zentao.func.php new file mode 100644 index 0000000000..ef13d52b69 --- /dev/null +++ b/lib/zin/zentao/zentao.func.php @@ -0,0 +1,135 @@ + + * @package zin + * @link https://www.zentao.net + */ + +namespace zin; + +require_once __DIR__ . DS . 'pager.func.php'; + +class helper extends \helper +{ +} + +class html extends \html +{ +} + +class commonModel extends \commonModel +{ +} + +class common extends \commonModel +{ +} + +/** + * Created by typecasting to object. + * + * @link https://php.net/manual/en/reserved.classes.php + */ +class stdClass extends \stdClass +{ +} + +function createLink(string $moduleName, string $methodName = 'index', string|array $vars = array(), string $viewType = '', bool $onlybody = false): string +{ + if(empty($moduleName)) return ''; + return \helper::createLink($moduleName, $methodName, $vars, $viewType, $onlybody); +} + +function inLink(string $methodName = 'index', string|array $vars = '', string $viewType = '', bool $onlybody = false): string +{ + return \inlink($methodName, $vars, $viewType, $onlybody); +} + +function zget(array|object $var, string|int $key, mixed $valueWhenNone = false, mixed $valueWhenExists = false): mixed +{ + return \zget($var, $key, $valueWhenNone, $valueWhenExists); +} + +function getWebRoot(bool $full = false): string +{ + return \getWebRoot($full); +} + +function hasPriv(string $module, string $method, ?object $object = null, string $vars = ''): bool +{ + return \common::hasPriv($module, $method, $object, $vars); +} + +function isFieldRequired(?string $name): bool +{ + if(empty($name)) return false; + + global $config, $app; + $moduleName = $app->moduleName; + $methodName = $app->methodName; + if(isset($config->$moduleName->$methodName->requiredFields)) return in_array($name, explode(',', $config->$moduleName->$methodName->requiredFields)); + + return false; +} + +/** + * Determine whether the request is ajax. + * + * @param ?string $type 'zin'|'modal'|'fetch'|null + */ +function isAjaxRequest(?string $type = null): bool +{ + return \helper::isAjaxRequest($type); +} + +/** + * Bind global event listener to widget element. + * + * @param string $name + * @param bool|string|array $callback + * @param array $options + */ +function bind(string $name, bool|string|array $callback, array|string $options = null): directive +{ + if(is_string($options) && is_string($callback)) + { + $options = array('selector' => $callback, 'call' => $options); + } + elseif(is_array($options)) + { + $options['call'] = $callback; + } + else + { + $options = array('callback' => $callback); + } + if(str_contains($name, '__')) + { + list($name, $flags) = explode('__', $name); + if(str_contains($flags, 'stop')) $options['stop'] = true; + if(str_contains($flags, 'prevent')) $options['prevent'] = true; + if(str_contains($flags, 'self')) $options['self'] = true; + if(str_contains($flags, 'once')) $options['once'] = true; + } + $options['on'] = $name; + return setData($options); +} + +/** + * Render data to json. + * + * @param mixed $data data. + * @param int $flags json encode flags. + * @return void + */ +function renderJson(mixed $data, int $flags = 0) +{ + ob_end_flush(); + zin::$rendered = true; + echo json_encode($data, $flags); +} diff --git a/lib/zin/zin.class.php b/lib/zin/zin.class.php new file mode 100644 index 0000000000..74da6a2488 --- /dev/null +++ b/lib/zin/zin.class.php @@ -0,0 +1,17 @@ + + * @package zin + * @version $Id + * @link https://www.zentao.net + */ + +namespace zin; + +require_once __DIR__ . DS . 'config.php'; +require_once __DIR__ . DS . 'helper.php'; +require_once __DIR__ . DS . 'func.php'; diff --git a/lib/zin/zui/toggle.class.php b/lib/zin/zui/toggle.class.php new file mode 100644 index 0000000000..02306de76e --- /dev/null +++ b/lib/zin/zui/toggle.class.php @@ -0,0 +1,23 @@ + + * @package zin + * @version $Id + * @link https://www.zentao.net + */ + +namespace zin; + +require_once __DIR__ . DS . 'toggle.func.php'; + +class toggle +{ + public static function __callStatic($name, $args) + { + return toggle($name, empty($args) ? null : $args[0]); + } +} diff --git a/lib/zin/zui/toggle.func.php b/lib/zin/zui/toggle.func.php new file mode 100644 index 0000000000..19721d2d25 --- /dev/null +++ b/lib/zin/zui/toggle.func.php @@ -0,0 +1,25 @@ + + * @package zin + * @version $Id + * @link https://www.zentao.net + */ +namespace zin; + +/** + * Add data-toggle=* to widget element and set other data attributes. + * + * @param string $name + * @param array $dataset + */ +function toggle($name, $dataset = null): directive +{ + if(empty($dataset)) $dataset = array(); + $dataset['toggle'] = $name; + return setData($dataset); +} diff --git a/lib/zin/zui/zui.class.php b/lib/zin/zui/zui.class.php new file mode 100644 index 0000000000..05063c1987 --- /dev/null +++ b/lib/zin/zui/zui.class.php @@ -0,0 +1,211 @@ + + * @package zin + * @version $Id + * @link https://www.zentao.net + */ + +namespace zin; + +require_once dirname(__DIR__) . DS . 'core' . DS . 'wg.class.php'; +require_once dirname(__DIR__) . DS . 'core' . DS . 'wg.func.php'; +require_once dirname(__DIR__) . DS . 'core' . DS . 'wg.func.php'; +require_once __DIR__ . DS . 'toggle.func.php'; +require_once __DIR__ . DS . 'toggle.class.php'; + +class zui extends wg +{ + protected static array $defineProps = array( + '_name:string', + '_to?:string', + '_tag:string="div"', + '_map?:array', + '_props?: array', + '_size?: array', + '_id?: string', + '_class?: string', + '_call?: string' + ); + + protected function build(): wg + { + list($name, $target, $tagName, $targetProps, $size, $id, $class, $map, $call) = $this->prop(array('_name', '_to', '_tag', '_props', '_size', '_id', '_class', '_map', '_call')); + list($width, $height) = $size; + + $options = $this->getRestProps(); + $children = $this->children(); + $selector = $target; + if(empty($selector)) + { + if(empty($id)) $id = $this->gid; + $selector = "#$id"; + } + if(is_array($map)) + { + foreach($options as $key => $value) + { + if(!isset($map[$key])) continue; + $options[$map[$key]] = $value; + unset($options[$key]); + } + } + + if(empty($call)) $call = '~zui.create'; + + if(empty($target)) + { + return h + ( + $tagName, + set($targetProps), + setClass($class), + setID($id), + setStyle('width', $width), + setStyle('height', $height), + $children, + h::jsCall($call, $name, $selector, $options) + ); + } + + return h::jsCall($call, $name, $selector, $options); + } + + public static function __callStatic($name, $args) + { + return new zui(set('_name', $name), $args); + } + + public static function toggle($name, $options = null) + { + return toggle($name, $options); + } + + public static function setClass($name, ...$args) + { + $class = array($name => true); + foreach($args as $arg) + { + if(is_bool($arg)) $class[$name] = $arg; + else $class["$name-$arg"] = true; + } + if(isset($class[$name]) && $class[$name] === false) return null; + return setClass($class); + } + + public static function skin($name, $flag = true, $falseValue = null, $cssProp = null) + { + if($flag === null) return null; + + if(is_array($flag)) + { + return array_map(function($value) use($name, $cssProp) {return zui::skin($name, $value, null, $cssProp);}, $flag); + } + + if($flag === false) + { + if(empty($falseValue)) return null; + $flag = 'none'; + } + elseif($cssProp !== null && is_string($flag) && (str_ends_with($flag, 'px') || str_starts_with($flag, '#') || str_contains($flag, '.') || str_contains($flag, '('))) + { + if(str_starts_with($flag, '(')) $flag = substr($flag, 1, -1); + return setStyle($cssProp, $flag); + } + return setClass($flag === true ? $name : "$name-$flag"); + } + + public static function rounded($value = true) + { + return zui::skin('rounded', $value, 'none', 'border-radius'); + } + + public static function shadow($value = true) + { + return zui::skin('shadow', $value, 'none'); + } + + public static function primary($value = true) + { + return zui::skin('primary', $value); + } + + public static function secondary($value = true) + { + return zui::skin('secondary', $value); + } + + public static function success($value = true) + { + return zui::skin('success', $value); + } + + public static function warning($value = true) + { + return zui::skin('warning', $value); + } + + public static function danger($value = true) + { + return zui::skin('danger', $value); + } + + public static function important($value = true) + { + return zui::skin('important', $value); + } + + public static function special($value = true) + { + return zui::skin('special', $value); + } + + public static function bg($value = null) + { + return zui::skin('bg', $value, 'transparent', 'background'); + } + + public static function text($value = null) + { + return zui::skin('text', $value, 'fore', 'color'); + } + + public static function muted($value = true) + { + return $value ? setClass('muted') : null; + } + + public static function opacity($value) + { + return zui::skin('opacity', $value, '0', 'opacity'); + } + + public static function disabled($value = true) + { + return $value ? setClass('disabled') : null; + } + + public static function width($value) + { + return zui::skin('w', $value, '0', 'width'); + } + + public static function height($value) + { + return zui::skin('h', $value, '0', 'width'); + } + + public static function ring(...$args) + { + return zui::skin('ring', $args, '0'); + } + + public static function border(...$args) + { + return zui::skin('border', $args, 'none', 'border'); + } +} diff --git a/lib/zin/zui/zui.func.php b/lib/zin/zui/zui.func.php new file mode 100644 index 0000000000..3bad855484 --- /dev/null +++ b/lib/zin/zui/zui.func.php @@ -0,0 +1,22 @@ +navIcons['bi'] = ""; $lang->navIcons['system'] = ""; $lang->navIcons['admin'] = ""; +$lang->navIconNames = array(); +$lang->navIconNames['my'] = 'menu-my'; +$lang->navIconNames['program'] = 'program'; +$lang->navIconNames['product'] = 'product'; +$lang->navIconNames['project'] = 'project'; +$lang->navIconNames['execution'] = 'run'; +$lang->navIconNames['qa'] = 'test'; +$lang->navIconNames['devops'] = 'devops'; +$lang->navIconNames['kanban'] = 'kanban'; +$lang->navIconNames['doc'] = 'doc'; +$lang->navIconNames['bi'] = 'statistic'; +$lang->navIconNames['system'] = 'group'; +$lang->navIconNames['admin'] = 'cog-outline'; + global $config; list($programModule, $programMethod) = explode('-', $config->programLink); list($productModule, $productMethod) = explode('-', $config->productLink); diff --git a/module/common/model.php b/module/common/model.php index 287e61b8d8..9d8aa61e0b 100644 --- a/module/common/model.php +++ b/module/common/model.php @@ -2498,17 +2498,17 @@ EOF; */ public function checkIframe() { - /** + /* * 忽略如下情况:非 HTML 请求、Ajax 请求、特殊 GET 参数 _single。 * Ignore the following situations: non-HTML request, Ajax request, special GET parameter _single. */ - if($this->app->getViewType() != 'html' || helper::isAjaxRequest() || isset($_GET['_single'])) return; + if($this->app->getViewType() != 'html' || helper::isAjaxRequest() || isset($_GET['_single'])) return true; /** * 忽略无请求头 HTTP_SEC_FETCH_DEST 或者 HTTP_SEC_FETCH_DEST 为 iframe 的请求,较新的浏览器在启用 https 的情况下才会正确发送该请求头。 * Ignore the request without HTTP_SEC_FETCH_DEST or HTTP_SEC_FETCH_DEST is iframe, the latest browser will send this request header correctly when enable https. */ - if(!isset($_SERVER['HTTP_SEC_FETCH_DEST']) || $_SERVER['HTTP_SEC_FETCH_DEST'] == 'iframe') return; + if(!isset($_SERVER['HTTP_SEC_FETCH_DEST']) || $_SERVER['HTTP_SEC_FETCH_DEST'] == 'iframe') return true; /** * 当有 HTTP_REFERER 请求头时,忽略 safari 浏览器,因为 safari 浏览器不会正确发送 HTTP_SEC_FETCH_DEST 请求头。 @@ -2517,17 +2517,23 @@ EOF; if(isset($_SERVER['HTTP_REFERER']) && !empty($_SERVER['HTTP_REFERER'])) { $userAgent = strtolower($_SERVER['HTTP_USER_AGENT']); - if(strpos($userAgent, 'chrome') === false && strpos($userAgent, 'safari') !== false) return; + if(strpos($userAgent, 'chrome') === false && strpos($userAgent, 'safari') !== false) return true; } + /** + * 忽略所有方法名以 ajax 开头的请求。 + * Ignore all requests which it's method name starts with 'ajax'. + */ + $method = $this->app->getMethodName(); + if(strpos($method, 'ajax') === 0) return true; + /** * 以下页面可以允许在非 iframe 中打开,所以要忽略这些页面。 * The following pages can be allowed to open in non-iframe, so ignore these pages. */ $module = $this->app->getModuleName(); - $method = $this->app->getMethodName(); - $whitelist = '|index|tutorial|install|upgrade|sso|cron|misc|user-login|user-deny|user-logout|user-reset|user-forgetpassword|user-resetpassword|my-changepassword|my-preference|file-read|file-download|file-uploadimages|file-ajaxwopifiles|report-annualdata|misc-captcha|execution-printkanban|traincourse-ajaxuploadlargefile|traincourse-playvideo|screen-view|zanode-create|screen-ajaxgetchart|'; - if(strpos($whitelist, "|{$module}|") !== false || strpos($whitelist, "|{$module}-{$method}|") !== false) return; + $whitelist = '|index|tutorial|install|upgrade|sso|cron|misc|user-login|user-deny|user-logout|user-reset|user-forgetpassword|user-resetpassword|my-changepassword|my-preference|file-read|file-download|file-uploadimages|report-annualdata|misc-captcha|execution-printkanban||traincourse-playvideo|'; + if(strpos($whitelist, "|{$module}|") !== false || strpos($whitelist, "|{$module}-{$method}|") !== false) return true; /** * 如果以上条件都不满足,则视为当前页面必须在 iframe 中打开,使用 302 跳转实现。 @@ -2536,7 +2542,8 @@ EOF; $url = helper::safe64Encode($_SERVER['REQUEST_URI']); $redirectUrl = helper::createLink('index', 'index'); $redirectUrl .= strpos($redirectUrl, '?') === false ? "?open=$url" : "&open=$url"; - die(header("location: $redirectUrl")); + helper::header('location', $redirectUrl); + return false; } /** diff --git a/module/repo/ui/create.html.php b/module/repo/ui/create.html.php new file mode 100644 index 0000000000..9d8744aacd --- /dev/null +++ b/module/repo/ui/create.html.php @@ -0,0 +1,6 @@ + - * @package ZenTaoPMS - * @version $Id: install.php 4677 2013-04-26 06:23:58Z chencongzhi520@gmail.com $ - * @link http://www.zentao.net - */ -error_reporting(0); -define('IN_INSTALL', true); - -/* Load the framework. */ -include '../framework/router.class.php'; -include '../framework/control.class.php'; -include '../framework/model.class.php'; -include '../framework/helper.class.php'; - -/* Instance the app. */ -$app = router::createApp('pms', dirname(dirname(__FILE__)), 'router'); - -/* Check installed or not. */ -if(!isset($_SESSION['installing']) and isset($config->installed) and $config->installed) die(header('location: index.php')); - -/* Reset the config params to make sure the install program will be lauched. */ -$config->set('requestType', 'GET'); -$config->set('default.module', 'install'); -$app->setDebug(); - -/* During the installation, if the database params is setted, auto connect the db. */ -if(isset($config->installed) and $config->installed) $app->connectDB(); - -$app->parseRequest(); -$app->loadModule(); diff --git a/www/js/zui3/@zentao/dtable/assign-modal.html b/www/js/zui3/@zentao/dtable/assign-modal.html new file mode 100644 index 0000000000..fa3e9af32b --- /dev/null +++ b/www/js/zui3/@zentao/dtable/assign-modal.html @@ -0,0 +1,18 @@ + diff --git a/www/js/zui3/@zentao/fonts/Oswald-Light.ttf b/www/js/zui3/@zentao/fonts/Oswald-Light.ttf new file mode 100644 index 0000000000000000000000000000000000000000..1d7fce1201cfbd940ad3fb63497dfd2e8c360186 GIT binary patch literal 85176 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 M^a$B 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 literal 0 HcmV?d00001 diff --git a/www/js/zui3/@zentao/fonts/Oswald-Medium.ttf b/www/js/zui3/@zentao/fonts/Oswald-Medium.ttf new file mode 100644 index 0000000000000000000000000000000000000000..d5391bd96939c939d5219378df8624d1bee67232 GIT binary patch literal 85324 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 literal 0 HcmV?d00001 diff --git a/www/js/zui3/@zentao/fonts/Oswald-Regular.ttf b/www/js/zui3/@zentao/fonts/Oswald-Regular.ttf new file mode 100644 index 0000000000000000000000000000000000000000..c6677e09fad73ae0ae92e421876f758593664959 GIT binary patch literal 85104 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 JlRf$Aa$>flQMt)KhpMkTgY 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 + + +Generated by IcoMoon + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/www/js/zui3/@zentao/icons/ZentaoIcon.ttf b/www/js/zui3/@zentao/icons/ZentaoIcon.ttf new file mode 100644 index 0000000000000000000000000000000000000000..20d892b08548e13ccb3057af21c2bf18a384c92d GIT binary patch literal 71212 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 ZZGs9&OnI?|C`n}-=!1#MS9_ns{{uuD+7bW& literal 0 HcmV?d00001 diff --git a/www/js/zui3/@zentao/icons/ZentaoIcon.woff b/www/js/zui3/@zentao/icons/ZentaoIcon.woff new file mode 100644 index 0000000000000000000000000000000000000000..01ec577dccc25b4bb6319bb40a9d57604307ebec GIT binary patch literal 71288 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 literal 0 HcmV?d00001 diff --git a/www/js/zui3/@zentao/icons/icons.json b/www/js/zui3/@zentao/icons/icons.json new file mode 100644 index 0000000000..a4a570f660 --- /dev/null +++ b/www/js/zui3/@zentao/icons/icons.json @@ -0,0 +1,318 @@ +{"zentao": {"code": "e901"}, +"zentao-alt": {"code": "e900"}, +"help": {"code": "e968"}, +"import": {"code": "e904"}, +"download": {"code": "e904"}, +"export": {"code": "e905"}, +"lightbulb": {"code": "e91c"}, +"close": {"code": "e936"}, +"check": {"code": "e5ca"}, +"minus": {"code": "e926"}, +"expand-alt": {"code": "e6f1"}, +"collapse-alt": {"code": "e6f2"}, +"fullscreen": {"code": "e96b"}, +"star-empty": {"code": "e94a"}, +"star": {"code": "e94b"}, +"exclamation-sign": {"code": "e930"}, +"info-sign": {"code": "e9d5"}, +"flag": {"code": "e937"}, +"check-circle": {"code": "e92f"}, +"check-sign": {"code": "e938"}, +"chart-pie": {"code": "e95b"}, +"history": {"code": "e95f"}, +"pencil": {"code": "e254"}, +"search": {"code": "e928"}, +"restart": {"code": "e95e"}, +"cog": {"code": "e93b"}, +"chart-line": {"code": "e95c"}, +"chart-bar": {"code": "e95d"}, +"bar-chart": {"code": "e95d"}, +"exchange": {"code": "e927"}, +"severity": {"code": "e973"}, +"book": {"code": "\f02d"}, +"treemap-alt": {"code": "e971"}, +"severity-solid": {"code": "e902"}, +"chat-line": {"code": "e998"}, +"stack": {"code": "e943"}, +"cube": {"code": "e967"}, +"minus-sign": {"code": "e939"}, +"bars-sign": {"code": "e93a"}, +"chat": {"code": "e940"}, +"message": {"code": "e940"}, +"more": {"code": "e744"}, +"certificate": {"code": "\f0a3"}, +"bell": {"code": "e7f5"}, +"columns": {"code": "\f0db"}, +"envelope-o": {"code": "e92a"}, +"unfold-all": {"code": "e931"}, +"fold-all": {"code": "e932"}, +"bars": {"code": "e948"}, +"cards-view": {"code": "e949"}, +"ellipsis-v": {"code": "e5d4"}, +"spinner-indicator": {"code": "e982"}, +"up-circle": {"code": "e92b"}, +"right-circle": {"code": "e92c"}, +"down-circle": {"code": "e92d"}, +"left-circle": {"code": "e92e"}, +"angle-double-right": {"code": "\f101"}, +"angle-down": {"code": "e313"}, +"angle-left": {"code": "e314"}, +"angle-right": {"code": "e315"}, +"angle-top": {"code": "e316"}, +"first-page": {"code": "e5dc"}, +"last-page": {"code": "e5dd"}, +"caret-down": {"code": "\f0d7"}, +"caret-up": {"code": "\f0d8"}, +"caret-left": {"code": "\f0d9"}, +"caret-right": {"code": "\f0da"}, +"sort": {"code": "\f0dc"}, +"sort-down": {"code": "\f0dd"}, +"sort-up": {"code": "\f0de"}, +"arrow-up": {"code": "e923"}, +"arrow-down": {"code": "e924"}, +"arrow-left": {"code": "e952"}, +"arrow-right": {"code": "e93e"}, +"chevron-left": {"code": "e934"}, +"chevron-right": {"code": "e935"}, +"chevron-double-up": {"code": "e959"}, +"chevron-double-down": {"code": "e95a"}, +"folder-account": {"code": "e942"}, +"folder-move": {"code": "e960"}, +"folder-plus": {"code": "e961"}, +"folder-upload": {"code": "e962"}, +"folder-star": {"code": "e963"}, +"folder-edit": {"code": "e964"}, +"folder-download": {"code": "e965"}, +"folder-outline": {"code": "e966"}, +"folder": {"code": "e944"}, +"folder-o": {"code": "e945"}, +"folder-open-o": {"code": "e946"}, +"folder-open": {"code": "e947"}, +"color": {"code": "e93c"}, +"paper-clip": {"code": "e93d"}, +"text": {"code": "e929"}, +"share": {"code": "\f064"}, +"format-list-bulleted": {"code": "e9a8"}, +"format-bold": {"code": "e953"}, +"format-header-pound": {"code": "e954"}, +"format-italic": {"code": "e955"}, +"format-list-numbers": {"code": "e969"}, +"format-quote-close": {"code": "e96a"}, +"image": {"code": "e96c"}, +"table-large": {"code": "e96d"}, +"aiux": {"code": "e99e"}, +"qc": {"code": "e986"}, +"qc-q": {"code": "e985"}, +"qc-c": {"code": "e987"}, +"sonarqube": {"code": "e9ba"}, +"college": {"code": "e9c8"}, +"ztool": {"code": "e9c1"}, +"contacts": {"code": "e9c3"}, +"chats": {"code": "e9c4"}, +"menu-my": {"code": "e97a"}, +"home": {"code": "e97a"}, +"program": {"code": "e9aa"}, +"lightbulb-alt": {"code": "e98f"}, +"product": {"code": "e98f"}, +"rocket": {"code": "e99c"}, +"project": {"code": "e99c"}, +"run": {"code": "e9a9"}, +"test": {"code": "e956"}, +"infinite": {"code": "e9a3"}, +"devops": {"code": "e9a3"}, +"ops": {"code": "e903"}, +"doc": {"code": "e99b"}, +"menu-doc": {"code": "e99b"}, +"statistic": {"code": "e999"}, +"menu-backend": {"code": "e993"}, +"assets": {"code": "e9ae"}, +"diamond": {"code": "e9ae"}, +"feedback": {"code": "e991"}, +"flow": {"code": "e994"}, +"oa": {"code": "e9a1"}, +"more-circle": {"code": "e988"}, +"controls": {"code": "e995"}, +"account": {"code": "e992"}, +"about": {"code": "e996"}, +"info": {"code": "e996"}, +"cog-outline": {"code": "e997"}, +"backend": {"code": "e997"}, +"exit": {"code": "e99a"}, +"theme": {"code": "e9a0"}, +"globe": {"code": "\f0ac"}, +"lang": {"code": "\f0ac"}, +"seal": {"code": "e9f8"}, +"menu-arrow-left": {"code": "e9f0"}, +"menu-arrow-right": {"code": "e9f1"}, +"add-directory": {"code": "e9ef"}, +"design": {"code": "e9ec"}, +"download-alt": {"code": "e9ea"}, +"blame": {"code": "e9e0"}, +"draft-edit": {"code": "e9dc"}, +"sub-review-user": {"code": "e9db"}, +"sub-review": {"code": "e9df"}, +"ztf": {"code": "e9dd"}, +"save": {"code": "e9d8"}, +"list-box": {"code": "e9b4"}, +"usecase": {"code": "e99d"}, +"code": {"code": "e990"}, +"summary": {"code": "e9ad"}, +"more-alt": {"code": "e9a7"}, +"customer": {"code": "e9d9"}, +"ipd": {"code": "e9f9"}, +"wiki-lib": {"code": "e9f5"}, +"annex-lib": {"code": "e9f6"}, +"interface-lib": {"code": "e9f7"}, +"has-authority-pack": {"code": "e9f3"}, +"without-authority-pack": {"code": "e9f4"}, +"data-structure": {"code": "e9f2"}, +"waterfallplus": {"code": "e9ed"}, +"agileplus": {"code": "e9ee"}, +"horn": {"code": "e9eb"}, +"resume": {"code": "e9e9"}, +"mirror": {"code": "e9e6"}, +"remote": {"code": "e9e7"}, +"moon": {"code": "e9e8"}, +"ticket": {"code": "e9e1"}, +"gantt-alt": {"code": "e9e2"}, +"tree": {"code": "e9c9"}, +"list": {"code": "e9cb"}, +"gantt": {"code": "e9cc"}, +"group-view": {"code": "e9cd"}, +"inherit-space": {"code": "e9c2"}, +"card-archive": {"code": "e9b8"}, +"col-archive": {"code": "e9b9"}, +"col-add-right": {"code": "e9bb"}, +"col-add-left": {"code": "e9bc"}, +"col-split": {"code": "e9bd"}, +"waterfall": {"code": "e9a4"}, +"manual": {"code": "e98d"}, +"kanban": {"code": "e983"}, +"lane": {"code": "e9b1"}, +"snap-house": {"code": "e9e3"}, +"wrench": {"code": "e9e4"}, +"desktop": {"code": "e9e5"}, +"back-circle": {"code": "e9da"}, +"back": {"code": "e9d3"}, +"shield": {"code": "e9ca"}, +"meh": {"code": "e9ce"}, +"frown": {"code": "e9cf"}, +"smile": {"code": "e9d0"}, +"unlock-solid": {"code": "e9d1"}, +"lock-solid": {"code": "e9d2"}, +"ver": {"code": "e9c6"}, +"publish": {"code": "e9c7"}, +"send": {"code": "e9c7"}, +"tag": {"code": "e9be"}, +"tag-lock": {"code": "e9bf"}, +"code-fork": {"code": "\f126"}, +"branch-lock": {"code": "e9c0"}, +"groups": {"code": "e9af"}, +"thumbs-up": {"code": "\f087"}, +"thumbs-down": {"code": "\f088"}, +"thumbs-up-solid": {"code": "e9d6"}, +"thumbs-down-solid": {"code": "e9d7"}, +"hash": {"code": "e9ab"}, +"version": {"code": "e9ab"}, +"p-square": {"code": "e97b"}, +"video-play": {"code": "e97f"}, +"plus-solid-circle": {"code": "e974"}, +"minuse-solid-circle": {"code": "e9b6"}, +"s": {"code": "e975"}, +"c": {"code": "e976"}, +"t": {"code": "e977"}, +"guide": {"code": "e978"}, +"todo": {"code": "e979"}, +"plus": {"code": "e925"}, +"side-left": {"code": "e9b3"}, +"side-right": {"code": "e9b2"}, +"fullscreen-exit": {"code": "e972"}, +"alert": {"code": "e99f"}, +"undo": {"code": "e93f"}, +"redo": {"code": "e9d4"}, +"swap": {"code": "e9b0"}, +"chat-solid": {"code": "e9b5"}, +"clock": {"code": "e97c"}, +"cost": {"code": "e97d"}, +"pencil-alt": {"code": "e984"}, +"size-height": {"code": "e9c5"}, +"file-log": {"code": "e9de"}, +"rich-text": {"code": "e913"}, +"markdown": {"code": "e916"}, +"excel": {"code": "e933"}, +"text-link": {"code": "e94d"}, +"ppt": {"code": "e957"}, +"word": {"code": "e958"}, +"doc-lib": {"code": "e96f"}, +"file": {"code": "\f016"}, +"file-empty": {"code": "\f016"}, +"file-text": {"code": "\f0f6"}, +"file-alt": {"code": "\f15b"}, +"file-text-alt": {"code": "\f15c"}, +"file-pdf": {"code": "\f1c1"}, +"file-word": {"code": "\f1c2"}, +"file-excel": {"code": "\f1c3"}, +"file-powerpoint": {"code": "\f1c4"}, +"file-image": {"code": "\f1c5"}, +"file-archive": {"code": "\f1c6"}, +"file-audio": {"code": "\f1c7"}, +"file-video": {"code": "\f1c8"}, +"file-code": {"code": "\f1c9"}, +"menu-collapse": {"code": "e980"}, +"menu-expand": {"code": "e981"}, +"group": {"code": "e97e"}, +"menu-users": {"code": "e97e"}, +"persons": {"code": "e97e"}, +"team": {"code": "e97e"}, +"estimate": {"code": "e9ac"}, +"sprint": {"code": "e9a2"}, +"shield-check": {"code": "e9a5"}, +"ok": {"code": "e9a6"}, +"printer": {"code": "e906"}, +"bullhorn": {"code": "e910"}, +"person": {"code": "e941"}, +"fields": {"code": "e989"}, +"trigger": {"code": "e98a"}, +"layout": {"code": "e98b"}, +"audit": {"code": "e98c"}, +"cancel": {"code": "e951"}, +"ban-circle": {"code": "e951"}, +"eye": {"code": "e94e"}, +"eye-off": {"code": "e96e"}, +"unlock": {"code": "e94f"}, +"lock": {"code": "e950"}, +"private": {"code": "e950"}, +"move": {"code": "e94c"}, +"hand-right": {"code": "e907"}, +"checked": {"code": "e908"}, +"off": {"code": "e909"}, +"start": {"code": "e90a"}, +"play": {"code": "e90a"}, +"time": {"code": "e90b"}, +"edit": {"code": "e90c"}, +"trash": {"code": "e90d"}, +"link": {"code": "e90e"}, +"unlink": {"code": "e90f"}, +"bug": {"code": "e911"}, +"list-alt": {"code": "e912"}, +"change": {"code": "e970"}, +"alter": {"code": "e970"}, +"glasses": {"code": "e914"}, +"review": {"code": "e914"}, +"sitemap": {"code": "e915"}, +"testcase": {"code": "e915"}, +"pluses": {"code": "e917"}, +"report-list": {"code": "e918"}, +"magic": {"code": "e919"}, +"active": {"code": "e919"}, +"treemap": {"code": "e91a"}, +"confirm": {"code": "e91b"}, +"split": {"code": "e98e"}, +"delay": {"code": "e91d"}, +"calendar": {"code": "e91d"}, +"pause": {"code": "e91e"}, +"ban": {"code": "e91f"}, +"plus-bold": {"code": "e920"}, +"copy": {"code": "e921"}, +"refresh": {"code": "e922"}, +"diff": {"code": "e9b7"}} diff --git a/www/js/zui3/dashboard/block-1.html b/www/js/zui3/dashboard/block-1.html new file mode 100644 index 0000000000..742badf947 --- /dev/null +++ b/www/js/zui3/dashboard/block-1.html @@ -0,0 +1,6 @@ +
    +
    +
    简单区块
    +
    +
    简单区块内容
    +
    diff --git a/www/js/zui3/dashboard/block-2.html b/www/js/zui3/dashboard/block-2.html new file mode 100644 index 0000000000..ab37c16460 --- /dev/null +++ b/www/js/zui3/dashboard/block-2.html @@ -0,0 +1,11 @@ +
    +
    +
    更多链接
    +
    + +
    +
    +
    简单区块内容
    +
    diff --git a/www/js/zui3/dashboard/block-3.html b/www/js/zui3/dashboard/block-3.html new file mode 100644 index 0000000000..ce6548026c --- /dev/null +++ b/www/js/zui3/dashboard/block-3.html @@ -0,0 +1,66 @@ +
    +
    +
    包含表格
    +
    + +
    +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    ID名称状态统计
    1禅道项目管理软件进行中12
    2禅道项目管理软件进行中12
    3禅道项目管理软件进行中12
    4禅道项目管理软件进行中12
    5禅道项目管理软件进行中12
    6禅道项目管理软件进行中12
    7禅道项目管理软件进行中12
    +
    +
    diff --git a/www/js/zui3/dashboard/block-4.html b/www/js/zui3/dashboard/block-4.html new file mode 100644 index 0000000000..f8053a5708 --- /dev/null +++ b/www/js/zui3/dashboard/block-4.html @@ -0,0 +1,8 @@ +
    +
    +
    自定义内容
    +
    +
    +
    自定义内容
    +
    +
    diff --git a/www/js/zui3/dashboard/block-5.html b/www/js/zui3/dashboard/block-5.html new file mode 100644 index 0000000000..38b20db634 --- /dev/null +++ b/www/js/zui3/dashboard/block-5.html @@ -0,0 +1,8 @@ +
    +
    +
    +
    +
    +
    +
    +
    diff --git a/www/js/zui3/dashboard/block-6.html b/www/js/zui3/dashboard/block-6.html new file mode 100644 index 0000000000..556daf20ae --- /dev/null +++ b/www/js/zui3/dashboard/block-6.html @@ -0,0 +1,567 @@ +
    +
    +
    数据表格
    +
    +
    +
    +
    +
    + diff --git a/www/js/zui3/dashboard/block-8.html b/www/js/zui3/dashboard/block-8.html new file mode 100644 index 0000000000..519dbb9d1b --- /dev/null +++ b/www/js/zui3/dashboard/block-8.html @@ -0,0 +1,13 @@ +
    +
    +
    文字排版
    +
    +
    +
    +
    HTML5是HTML最新的修订版本,2014年10月由万维网联盟(W3C)完成标准制定。
    +
    目标是替换1999年所制定的HTML 4.01和XHTML 1.0标准,以期能在互联网应用迅速发展的时候,使网络标准达到匹配当代的网络需求。广义论及HTML5时,实际指的是包括HTML、CSS和JavaScript在内的一套技术组合。它希望能够减少网页浏览器对于需要插件的丰富性网络应用服务(Plug-in-Based Rich Internet Application,RIA),例如:AdobeFlash、Microsoft Silverlight与Oracle JavaFX的需求,并且提供更多能有效加强网络应用的标准集。
    +
    它希望能够减少网页浏览器对于需要插件的丰富性网络应用服务(Plug-in-Based Rich Internet Application,RIA),例如:AdobeFlash、Microsoft Silverlight与Oracle JavaFX的需求,并且提供更多能有效加强网络应用的标准集。
    +
    HTML5是HTML最新的修订版本,2014年10月由万维网联盟(W3C)完成标准制定。目标是替换1999年所制定的HTML 4.01和XHTML 1.0标准,以期能在互联网应用迅速发展的时候,使网络标准达到匹配当代的网络需求。广义论及HTML5时,实际指的是包括HTML、CSS和JavaScript在内的一套技术组合。
    +
    +
    +
    diff --git a/www/js/zui3/dashboard/block-9.html b/www/js/zui3/dashboard/block-9.html new file mode 100644 index 0000000000..bc788eef48 --- /dev/null +++ b/www/js/zui3/dashboard/block-9.html @@ -0,0 +1,13 @@ +
    +
    +
    文字排版
    +
    +
    +

    + HTML5是HTML最新的修订版本,2014年10月由万维网联盟(W3C)完成标准制定。目标是替换1999年所制定的HTML 4.01和XHTML 1.0标准,以期能在互联网应用迅速发展的时候,使网络标准达到匹配当代的网络需求。广义论及HTML5时,实际指的是包括HTML、CSS和JavaScript在内的一套技术组合。它希望能够减少网页浏览器对于需要插件的丰富性网络应用服务(Plug-in-Based Rich Internet Application,RIA),例如:AdobeFlash、Microsoft Silverlight与Oracle JavaFX的需求,并且提供更多能有效加强网络应用的标准集。 +
    +
    + diff --git a/www/js/zui3/zin.js b/www/js/zui3/zin.js new file mode 100644 index 0000000000..7eeced8959 --- /dev/null +++ b/www/js/zui3/zin.js @@ -0,0 +1,978 @@ +(function() +{ + if (config.skipRedirect || window.skipRedirect) return; + + const parent = window.parent; + const currentModule = config.currentModule; + const currentMethod = config.currentMethod; + const isIndexPage = currentModule === 'index' && currentMethod === 'index'; + + const isAllowSelfOpen = isIndexPage + || location.hash === '#_single' + || /(\?|\&)_single/.test(location.search) + || currentModule === 'tutorial' + || currentModule === 'install' + || currentModule === 'upgrade' + || (currentModule === 'user' + && (currentMethod === 'login' || currentMethod === 'deny')) + || (currentModule === 'file' && currentMethod === 'download') + || (currentModule === 'my' && currentMethod === 'changepassword') + || $('body').hasClass('allow-self-open'); + + if (parent === window && !isAllowSelfOpen) + { + const shortUrl = location.pathname + location.search + location.hash; + location.href = $.createLink('index', 'index', `open=${btoa(shortUrl)}`); + return; + } +}()); + +(function() +{ + let config = window.config; + const isIndexPage = config.currentModule === 'index' && config.currentMethod === 'index'; + if(isIndexPage) return; + + const DEBUG = config.debug; + const currentCode = window.name.substring(4); + const isInAppTab = parent.window !== window; + const fetchTasks = new Map(); + const startTime = performance.now(); + const timers = {timeout: [], interval: []}; + let currentAppUrl = isInAppTab ? '' : location.href; + let zinbar = null; + let historyState = parent.window.history.state; + + $.apps = $.extend( + { + currentCode: currentCode, + updateApp: function(code, url, title) + { + const state = typeof code === 'object' ? code : {url: url, title: title}; + const oldState = window.history.state; + + if(title) document.title = title; + + if(oldState && oldState.url === url) return; + + window.history.pushState(state, title, url); + if(DEBUG) console.log('[APP]', 'update:', {code, url, title}); + return state; + }, + isOldPage: () => false, + reloadApp: function(_code, url) + { + loadPage(url); + }, + openApp: function(url, options) + { + loadPage(url, options); + } + }, parent.window.$.apps); + + const renderMap = + { + html: updatePageWithHtml, + body: (data) => $('body').html(data), + title: (data) => document.title = data, + main: (data) => $('#main').html(data), + featureBar: (data) => $('#featureBar').html(data), + pageCSS: (data) => $('style.zin-page-css').html(data), + pageJS: updatePageJS, + configJS: updateConfigJS, + activeFeature: (data) => activeNav(data, '#featureBar'), + activeMenu: (data) => activeNav(data), + navbar: updateNavbar, + heading: updateHeading, + fatal: showFatalError, + zinDebug: (data, _info, options) => showZinDebugInfo(data, options), + zinErrors: (data, _info, options) => showErrors(data, options.id === 'page'), + }; + + function registerRender(name, callback) + { + renderMap[name] = callback; + } + + function showFatalError(data, _info, options) + { + const zinDebug = window.zinDebug; + if(zinDebug && zinDebug.basePath) data = data.split(zinDebug.basePath).join('/'); + if(data.startsWith('
    \n')) data = data.replace('
    \n', ''); + zui.Modal.alert({message: {html: data}, title: `Fatal error: ${options.url}`, actions: [], size: 'lg', custom: {className: 'backdrop-blur border-2 border-canvas bg-opacity-80 rounded-xl', bodyClass: 'font-mono', headerClass: 'text-danger'}}); + } + + function initZinbar() + { + if(!DEBUG || isIndexPage) return; + let $bar = $('#zinbar'); + if($bar.length) return; + + $bar = $('
    ').insertAfter('body'); + zinbar = new zui.Zinbar($bar[0]); + } + + function registerTimer(callback, time, type) + { + type = type || 'timeout'; + const id = type === 'interval' ? setInterval(callback, time) : setTimeout(callback, time); + timers[type].push(id); + return id; + } + + function updateConfigJS(data) + { + $('#configJS').replaceWith(data); + config = window.config; + } + + function updatePageJS(data) + { + if(window.onPageUnmount) window.onPageUnmount(); + + window.onPageUnmount = null; + window.beforePageUpdate = null; + window.afterPageUpdate = null; + window.onPageRender = null; + + if(timers.interval.length) timers.interval.forEach(clearInterval); + if(timers.timeout.length) timers.timeout.forEach(clearTimeout); + timers.interval = []; + timers.timeout = []; + + $('script.zin-page-js').replaceWith(data); + } + + function updateZinbar(perf, errors, basePath) + { + if(!DEBUG) return; + + if(zinbar && zinbar.$) zinbar.$.update(perf, errors, basePath); + else requestAnimationFrame(() => updateZinbar(perf, errors, basePath)); + } + + function updatePerfInfo(options, stage, info) + { + if(!DEBUG) return; + + const perf = {id: options.id, url: options.url || currentAppUrl}; + perf[stage] = performance.now(); + if(stage === 'requestBegin') $.extend(perf, {requestEnd: undefined, renderBegin: undefined, renderEnd: undefined}); + if(info) + { + if(info.perf) $.extend(perf, info.perf); + if(info.dataSize) perf.dataSize = info.dataSize; + } + updateZinbar(perf); + } + + function showZinDebugInfo(data, options) + { + if(!DEBUG) return; + + updateZinbar({id: options.id, trace: data.trace, xhprof: data.xhprof}, data.errors, data.basePath); + } + + function updatePageWithHtml(data) + { + const html = []; + const skipTags = new Set(['SCRIPT', 'META']); + $(data).each(function(_idx, node) + { + const nodeName = node.nodeName; + if(nodeName === '#text') html.push(node.textContent); + else if(nodeName === 'SCRIPT' && node.innerText.startsWith('window.config={')) html.push(node.outerHTML); + else if(nodeName === 'TITLE') document.title = node.innerText; + else if(skipTags.has(nodeName)) return; + else html.push(node.outerHTML); + }); + $('body').html(html.join('')); + window.zin = {config: window.config}; + if(DEBUG) console.log('[ZIN] ', window.zin); + if(DEBUG) zui.Messager.show({content: 'ZIN: load an old page.', close: false}); + } + + function updateNavbar(data) + { + const $navbar = $('#navbar'); + const $newNav = $(data); + if($newNav.text().trim() !== $navbar.text().trim() || $newNav.find('.nav-item>a').map((_, element) => element.href).get().join(' ') !== $navbar.find('.nav-item>a').map((_, element) => element.href).get().join(' ')) return $navbar.empty().append($newNav); + + activeNav($newNav.find('.nav-item>a.active').data('id'), $navbar); + } + + function updateHeading(data) + { + const $data = $(data); + const $heading = $('#heading'); + const $dropmenu = $heading.find('#dropmenu'); + const $nextDropmenu = $data.filter('#dropmenu'); + if($dropmenu.dataset('fetcher') === $nextDropmenu.dataset('fetcher')) + { + const $toolbar = $heading.find('.toolbar'); + const $nextToolbar = $data.filter('.toolbar'); + if($nextToolbar.text().trim() !== $toolbar.text().trim()) $toolbar.replaceWith($nextToolbar); + } + else + { + $heading.html(data); + } + } + + function activeNav(activeID, nav) + { + const $nav = $(nav || '#navbar'); + const $active = $nav.find('.nav-item>a.active'); + if($active.data('id') === activeID) return; + $active.removeClass('active'); + $nav.find('.nav-item>a[data-id="' + activeID + '"]').addClass('active'); + } + + function beforeUpdate($target, info, options) + { + if($target && $target.length) $target.find('[data-zin-events]').off('.on.zin'); + if(window.beforePageUpdate) window.beforePageUpdate($target, info, options); + } + + function afterUpdate($target, info, options) + { + if(window.afterPageUpdate) window.afterPageUpdate($target, info, options); + } + + function renderPartial(info, options) + { + if(window.onPageRender && window.onPageRender(info)) return; + + const render = renderMap[info.name]; + const isHtml = info.type === 'html'; + const selector = parseSelector(info.selector); + const $target = selector ? $(selector.select) : $target; + if(render) + { + if(isHtml) beforeUpdate($target, info, options); + render(info.data, info, options); + if(isHtml) afterUpdate($target, info, options); + return; + } + + /* Common render */ + if(!selector) return console.warn('[APP] ', 'cannot render partial content with data', info); + + if(!$target.length) return console.warn('[APP] ', 'cannot find target element with selector', selector); + if(selector.first) $target = $target.first(); + if(selector.type === 'json') + { + const props = info.data.props; + if(typeof props !== 'object') return; + const component = $target.zui(info.name); + if(typeof component === 'object' && typeof component.render === 'function') component.render(props); + return; + } + + beforeUpdate($target, info, options); + if(selector.inner) $target.html(info.data); + else $target.replaceWith(info.data); + afterUpdate($target, info, options); + } + + function renderPage(list, options) + { + if(DEBUG) console.log('[APP] ', 'render:', list); + list.forEach(item => renderPartial(item, options)); + if(!options.partial) + { + const newState = $.apps.updateApp(currentCode, currentAppUrl, document.title); + if (newState) historyState = newState; + } + } + + function toggleLoading(target, isLoading) + { + const $target = $(target); + const position = $target.css('position'); + if(!['relative', 'absolute', 'fixed'].includes(position)) $target.css('position', 'relative'); + if(!$target.hasClass('load-indicator')) $target.addClass('load-indicator'); + if(isLoading === undefined) isLoading = !$target.hasClass('loading'); + $target.toggleClass('loading', isLoading); + } + + /** + * Request data from remote server + * @param {Object} options + * @param {string} options.id + * @param {string} options.url + * @param {string} options.selector + * @param {string} [options.target] + * @param {string} [options.method] + * @param {FormData|Form|Record} [options.data] + * @param {{selector: string, type: string}} [options.zinOptions] + * @param {function} [options.success] + * @param {function} [options.error] + * @param {function} [options.complete] + * @param {function} [onFinish] + */ + function requestContent(options, onFinish) + { + const target = options.target || '#main'; + const selectors = (Array.isArray(options.selector) ? options.selector : options.selector.split(',')).map(selector => selector.replace(':component', ':type=json&data=props')); + const url = options.url; + + if(DEBUG) console.log('[APP]', 'request', options); + if(DEBUG && !selectors.includes('zinDebug()')) selectors.push('zinDebug()'); + const isDebugRequest = DEBUG && selectors.length === 1 || selectors[0] === 'zinDebug()'; + const ajaxOptions = + { + url: url, + headers: {'X-ZIN-Options': JSON.stringify($.extend({selector: selectors, type: 'list'}, options.zinOptions)), 'X-ZIN-App': currentCode}, + type: options.method || 'GET', + data: options.data, + contentType: options.contentType, + beforeSend: () => + { + updatePerfInfo(options, 'requestBegin'); + if(isDebugRequest) return; + toggleLoading(target, true); + }, + success: (data) => + { + updatePerfInfo(options, 'requestEnd', {dataSize: data.length}); + options.result = 'success'; + let hasFatal = false; + try + { + if(data.includes('RAWJS<')) + { + const func = new Function(`return ${data.split('"RAWJS<').join('').split('>RAWJS"').join('')}`); + data = func(); + } + else + { + data = JSON.parse(data); + } + } + catch(e) + { + if(!isInAppTab && config.zin) return; + hasFatal = data.includes('Fatal error'); + data = [{name: hasFatal ? 'fatal' : 'html', data: data}]; + } + if(!options.partial && !hasFatal) currentAppUrl = url; + if(Array.isArray(data)) + { + data.forEach((item, idx) => item.selector = selectors[idx]); + updatePerfInfo(options, 'renderBegin'); + renderPage(data, options); + updatePerfInfo(options, 'renderEnd'); + $(document).trigger('pagerender.app'); + if(options.success) options.success(data); + if(onFinish) onFinish(null, data); + } + else if(data.load) + { + if(data.load === 'dtable') loadTable(); + else if(typeof data.load === 'string') loadPage(data.load); + else if(data.load === true) loadCurrentPage(); + else if(typeof data.load === 'object') + { + if('back' in data.load) + { + openUrl(data.load); + } + else if('confirm' in data.load) + { + const confirmed = confirm(data.load.confirm); + if(confirmed) loadPage(data.load.confirmed); + else loadPage(data.load.canceled); + } + else + { + loadPage(data.load); + } + } + } + }, + error: (xhr, type, error) => + { + updatePerfInfo(options, 'requestEnd', {error: error}); + if(type === 'abort') return console.log('[ZIN] ', 'Abord fetch data from ' + url, {xhr, type, error});; + if(DEBUG) console.error('[ZIN] ', 'Fetch data failed from ' + url, {xhr, type, error}); + zui.Messager.show('ZIN: Fetch data failed from ' + url); + if(options.error) options.error(data, error); + if(onFinish) onFinish(error); + }, + complete: () => + { + toggleLoading(target, false); + if(options.complete) options.complete(); + $(document).trigger('pageload.app'); + const frameElement = window.frameElement; + if(frameElement) + { + frameElement.classList.remove('loading'); + frameElement.classList.add('in'); + } + } + }; + return $.ajax(ajaxOptions); + } + + function fetchContent(url, selectors, options) + { + if(typeof url === 'object') + { + options = url; + url = options.url; + selectors = options.selector; + } + if(typeof options === 'string') options = {id: options}; + else if(typeof options === 'function') options = {success: options}; + + selectors = Array.isArray(selectors) ? selectors.join(',') : selectors; + const id = options.id || selectors; + options = $.extend({}, options, {url: url, selectors: selectors, id: id}); + + const task = fetchTasks.get(id) || {url: url, selectors: selectors, options: options}; + if(task.xhr) + { + if(task.url === url) return; + task.xhr.abort(); + task.xhr = null; + } + fetchTasks.set(id, task); + if(task.timerID) clearTimeout(task.timerID); + task.timerID = setTimeout(() => + { + task.timerID = 0; + task.xhr = requestContent(options, () => + { + const runID = task.xhr.getResponseHeader('Xhprof-RunID'); + updateZinbar({id: id, xhprof: `${config.webRoot}xhprof/xhprof_html/index.php?run=${runID}&source=${config.currentModule}_${config.currentMethod}`}); + task.xhr = null; + fetchTasks.delete(id); + }); + }, options.delayTime || 0); + } + + /** Load an old page. */ + function loadOldPage(url) + { + let $page = $('#oldPage'); + if(!$page.length) + { + $page = $('
    ') + .append($('