From 59a90678f916e7495cbfb4e2125665cef71d6420 Mon Sep 17 00:00:00 2001
From: tengge1 <930372551@qq.com>
Date: Sun, 14 Jul 2019 09:51:35 +0800
Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E7=89=88ui=E5=90=88=E5=B9=B6=E5=88=B0?=
=?UTF-8?q?web=E4=B8=AD=E3=80=82?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
ShadowEditor.Web/src/ui/Config.css | 34 ++
ShadowEditor.Web/src/ui/canvas/Canvas.jsx | 36 +++
ShadowEditor.Web/src/ui/canvas/css/Canvas.css | 0
ShadowEditor.Web/src/ui/common/Accordion.jsx | 11 +
ShadowEditor.Web/src/ui/common/Buttons.jsx | 11 +
ShadowEditor.Web/src/ui/common/Column.jsx | 11 +
ShadowEditor.Web/src/ui/common/Columns.jsx | 11 +
ShadowEditor.Web/src/ui/common/Content.jsx | 11 +
ShadowEditor.Web/src/ui/common/Row.jsx | 11 +
ShadowEditor.Web/src/ui/common/Rows.jsx | 11 +
ShadowEditor.Web/src/ui/form/Button.jsx | 38 +++
ShadowEditor.Web/src/ui/form/CheckBox.jsx | 61 ++++
ShadowEditor.Web/src/ui/form/Form.jsx | 51 +++
ShadowEditor.Web/src/ui/form/FormControl.jsx | 31 ++
ShadowEditor.Web/src/ui/form/IconButton.jsx | 41 +++
ShadowEditor.Web/src/ui/form/Input.jsx | 56 ++++
ShadowEditor.Web/src/ui/form/Label.jsx | 31 ++
ShadowEditor.Web/src/ui/form/Radio.jsx | 59 ++++
ShadowEditor.Web/src/ui/form/SearchField.jsx | 156 ++++++++++
ShadowEditor.Web/src/ui/form/Select.jsx | 63 ++++
ShadowEditor.Web/src/ui/form/TextArea.jsx | 64 ++++
ShadowEditor.Web/src/ui/form/Toggle.jsx | 58 ++++
ShadowEditor.Web/src/ui/form/css/Button.css | 31 ++
ShadowEditor.Web/src/ui/form/css/CheckBox.css | 21 ++
ShadowEditor.Web/src/ui/form/css/Form.css | 3 +
.../src/ui/form/css/FormControl.css | 13 +
.../src/ui/form/css/IconButton.css | 35 +++
ShadowEditor.Web/src/ui/form/css/Input.css | 9 +
ShadowEditor.Web/src/ui/form/css/Label.css | 6 +
ShadowEditor.Web/src/ui/form/css/Radio.css | 21 ++
.../src/ui/form/css/SearchField.css | 58 ++++
ShadowEditor.Web/src/ui/form/css/Select.css | 3 +
ShadowEditor.Web/src/ui/form/css/TextArea.css | 10 +
ShadowEditor.Web/src/ui/form/css/Toggle.css | 21 ++
ShadowEditor.Web/src/ui/icon/Icon.jsx | 52 ++++
ShadowEditor.Web/src/ui/icon/css/Icon.css | 5 +
ShadowEditor.Web/src/ui/image/Image.jsx | 35 +++
ShadowEditor.Web/src/ui/image/ImageList.jsx | 167 ++++++++++
.../src/ui/image/ImageUploader.jsx | 81 +++++
ShadowEditor.Web/src/ui/image/css/Image.css | 0
.../src/ui/image/css/ImageList.css | 133 ++++++++
.../src/ui/image/css/ImageUploader.css | 15 +
ShadowEditor.Web/src/ui/index.js | 88 ++++++
.../src/ui/layout/AbsoluteLayout.jsx | 41 +++
.../src/ui/layout/AccordionLayout.jsx | 67 ++++
.../src/ui/layout/BorderLayout.jsx | 291 ++++++++++++++++++
ShadowEditor.Web/src/ui/layout/HBoxLayout.jsx | 32 ++
ShadowEditor.Web/src/ui/layout/TabLayout.jsx | 64 ++++
ShadowEditor.Web/src/ui/layout/VBoxLayout.jsx | 32 ++
.../src/ui/layout/css/AbsoluteLayout.css | 4 +
.../src/ui/layout/css/AccordionLayout.css | 5 +
.../src/ui/layout/css/BorderLayout.css | 198 ++++++++++++
.../src/ui/layout/css/Content.css | 3 +
.../src/ui/layout/css/HBoxLayout.css | 6 +
.../src/ui/layout/css/TabLayout.css | 46 +++
.../src/ui/layout/css/VBoxLayout.css | 6 +
.../src/ui/layout/private/AccordionPanel.jsx | 96 ++++++
.../ui/layout/private/css/AccordionPanel.css | 82 +++++
ShadowEditor.Web/src/ui/menu/MenuBar.jsx | 32 ++
.../src/ui/menu/MenuBarFiller.jsx | 27 ++
ShadowEditor.Web/src/ui/menu/MenuItem.jsx | 59 ++++
.../src/ui/menu/MenuItemSeparator.jsx | 32 ++
ShadowEditor.Web/src/ui/menu/css/MenuBar.css | 38 +++
.../src/ui/menu/css/MenuBarFiller.css | 3 +
ShadowEditor.Web/src/ui/menu/css/MenuItem.css | 69 +++++
.../src/ui/menu/css/MenuItemSeparator.css | 11 +
ShadowEditor.Web/src/ui/panel/Panel.jsx | 123 ++++++++
ShadowEditor.Web/src/ui/panel/css/Panel.css | 88 ++++++
.../src/ui/property/ButtonField.jsx | 0
.../src/ui/property/ColorField.jsx | 0
.../src/ui/property/IntegerField.jsx | 0
ShadowEditor.Web/src/ui/property/MapField.jsx | 0
.../src/ui/property/NumberField.jsx | 0
.../src/ui/property/PropertyGrid.jsx | 60 ++++
.../src/ui/property/TextField.jsx | 0
.../src/ui/property/css/PropertyGrid.css | 84 +++++
ShadowEditor.Web/src/ui/svg/SVG.jsx | 30 ++
ShadowEditor.Web/src/ui/svg/css/SVG.css | 0
ShadowEditor.Web/src/ui/table/DataGrid.jsx | 91 ++++++
ShadowEditor.Web/src/ui/table/Pager.jsx | 0
ShadowEditor.Web/src/ui/table/Table.jsx | 34 ++
ShadowEditor.Web/src/ui/table/TableBody.jsx | 34 ++
ShadowEditor.Web/src/ui/table/TableCell.jsx | 34 ++
ShadowEditor.Web/src/ui/table/TableHead.jsx | 34 ++
ShadowEditor.Web/src/ui/table/TableRow.jsx | 34 ++
.../src/ui/table/css/DataGrid.css | 37 +++
ShadowEditor.Web/src/ui/table/css/Table.css | 6 +
.../src/ui/table/css/TableBody.css | 1 +
.../src/ui/table/css/TableCell.css | 5 +
.../src/ui/table/css/TableHead.css | 3 +
.../src/ui/table/css/TableRow.css | 13 +
.../src/ui/table/datagrid/CheckBoxColumn.jsx | 0
.../src/ui/table/datagrid/RowNumberColumn.jsx | 0
ShadowEditor.Web/src/ui/timeline/Timeline.jsx | 146 +++++++++
.../src/ui/timeline/css/Timeline.css | 102 ++++++
.../ui/timeline/private/TimelineControl.jsx | 53 ++++
.../timeline/private/css/TimelineControl.css | 7 +
ShadowEditor.Web/src/ui/toolbar/Toolbar.jsx | 33 ++
.../src/ui/toolbar/ToolbarFiller.jsx | 27 ++
.../src/ui/toolbar/ToolbarSeparator.jsx | 28 ++
.../src/ui/toolbar/css/Toolbar.css | 52 ++++
.../src/ui/toolbar/css/ToolbarFiller.css | 3 +
ShadowEditor.Web/src/ui/tree/Tree.jsx | 208 +++++++++++++
ShadowEditor.Web/src/ui/tree/css/Tree.css | 103 +++++++
ShadowEditor.Web/src/ui/window/Alert.jsx | 71 +++++
ShadowEditor.Web/src/ui/window/Confirm.jsx | 81 +++++
ShadowEditor.Web/src/ui/window/Prompt.jsx | 86 ++++++
ShadowEditor.Web/src/ui/window/Toast.jsx | 36 +++
ShadowEditor.Web/src/ui/window/Window.jsx | 138 +++++++++
ShadowEditor.Web/src/ui/window/css/Alert.css | 4 +
.../src/ui/window/css/Confirm.css | 4 +
ShadowEditor.Web/src/ui/window/css/Prompt.css | 9 +
ShadowEditor.Web/src/ui/window/css/Toast.css | 22 ++
ShadowEditor.Web/src/ui/window/css/Window.css | 105 +++++++
114 files changed, 4936 insertions(+)
create mode 100644 ShadowEditor.Web/src/ui/Config.css
create mode 100644 ShadowEditor.Web/src/ui/canvas/Canvas.jsx
create mode 100644 ShadowEditor.Web/src/ui/canvas/css/Canvas.css
create mode 100644 ShadowEditor.Web/src/ui/common/Accordion.jsx
create mode 100644 ShadowEditor.Web/src/ui/common/Buttons.jsx
create mode 100644 ShadowEditor.Web/src/ui/common/Column.jsx
create mode 100644 ShadowEditor.Web/src/ui/common/Columns.jsx
create mode 100644 ShadowEditor.Web/src/ui/common/Content.jsx
create mode 100644 ShadowEditor.Web/src/ui/common/Row.jsx
create mode 100644 ShadowEditor.Web/src/ui/common/Rows.jsx
create mode 100644 ShadowEditor.Web/src/ui/form/Button.jsx
create mode 100644 ShadowEditor.Web/src/ui/form/CheckBox.jsx
create mode 100644 ShadowEditor.Web/src/ui/form/Form.jsx
create mode 100644 ShadowEditor.Web/src/ui/form/FormControl.jsx
create mode 100644 ShadowEditor.Web/src/ui/form/IconButton.jsx
create mode 100644 ShadowEditor.Web/src/ui/form/Input.jsx
create mode 100644 ShadowEditor.Web/src/ui/form/Label.jsx
create mode 100644 ShadowEditor.Web/src/ui/form/Radio.jsx
create mode 100644 ShadowEditor.Web/src/ui/form/SearchField.jsx
create mode 100644 ShadowEditor.Web/src/ui/form/Select.jsx
create mode 100644 ShadowEditor.Web/src/ui/form/TextArea.jsx
create mode 100644 ShadowEditor.Web/src/ui/form/Toggle.jsx
create mode 100644 ShadowEditor.Web/src/ui/form/css/Button.css
create mode 100644 ShadowEditor.Web/src/ui/form/css/CheckBox.css
create mode 100644 ShadowEditor.Web/src/ui/form/css/Form.css
create mode 100644 ShadowEditor.Web/src/ui/form/css/FormControl.css
create mode 100644 ShadowEditor.Web/src/ui/form/css/IconButton.css
create mode 100644 ShadowEditor.Web/src/ui/form/css/Input.css
create mode 100644 ShadowEditor.Web/src/ui/form/css/Label.css
create mode 100644 ShadowEditor.Web/src/ui/form/css/Radio.css
create mode 100644 ShadowEditor.Web/src/ui/form/css/SearchField.css
create mode 100644 ShadowEditor.Web/src/ui/form/css/Select.css
create mode 100644 ShadowEditor.Web/src/ui/form/css/TextArea.css
create mode 100644 ShadowEditor.Web/src/ui/form/css/Toggle.css
create mode 100644 ShadowEditor.Web/src/ui/icon/Icon.jsx
create mode 100644 ShadowEditor.Web/src/ui/icon/css/Icon.css
create mode 100644 ShadowEditor.Web/src/ui/image/Image.jsx
create mode 100644 ShadowEditor.Web/src/ui/image/ImageList.jsx
create mode 100644 ShadowEditor.Web/src/ui/image/ImageUploader.jsx
create mode 100644 ShadowEditor.Web/src/ui/image/css/Image.css
create mode 100644 ShadowEditor.Web/src/ui/image/css/ImageList.css
create mode 100644 ShadowEditor.Web/src/ui/image/css/ImageUploader.css
create mode 100644 ShadowEditor.Web/src/ui/index.js
create mode 100644 ShadowEditor.Web/src/ui/layout/AbsoluteLayout.jsx
create mode 100644 ShadowEditor.Web/src/ui/layout/AccordionLayout.jsx
create mode 100644 ShadowEditor.Web/src/ui/layout/BorderLayout.jsx
create mode 100644 ShadowEditor.Web/src/ui/layout/HBoxLayout.jsx
create mode 100644 ShadowEditor.Web/src/ui/layout/TabLayout.jsx
create mode 100644 ShadowEditor.Web/src/ui/layout/VBoxLayout.jsx
create mode 100644 ShadowEditor.Web/src/ui/layout/css/AbsoluteLayout.css
create mode 100644 ShadowEditor.Web/src/ui/layout/css/AccordionLayout.css
create mode 100644 ShadowEditor.Web/src/ui/layout/css/BorderLayout.css
create mode 100644 ShadowEditor.Web/src/ui/layout/css/Content.css
create mode 100644 ShadowEditor.Web/src/ui/layout/css/HBoxLayout.css
create mode 100644 ShadowEditor.Web/src/ui/layout/css/TabLayout.css
create mode 100644 ShadowEditor.Web/src/ui/layout/css/VBoxLayout.css
create mode 100644 ShadowEditor.Web/src/ui/layout/private/AccordionPanel.jsx
create mode 100644 ShadowEditor.Web/src/ui/layout/private/css/AccordionPanel.css
create mode 100644 ShadowEditor.Web/src/ui/menu/MenuBar.jsx
create mode 100644 ShadowEditor.Web/src/ui/menu/MenuBarFiller.jsx
create mode 100644 ShadowEditor.Web/src/ui/menu/MenuItem.jsx
create mode 100644 ShadowEditor.Web/src/ui/menu/MenuItemSeparator.jsx
create mode 100644 ShadowEditor.Web/src/ui/menu/css/MenuBar.css
create mode 100644 ShadowEditor.Web/src/ui/menu/css/MenuBarFiller.css
create mode 100644 ShadowEditor.Web/src/ui/menu/css/MenuItem.css
create mode 100644 ShadowEditor.Web/src/ui/menu/css/MenuItemSeparator.css
create mode 100644 ShadowEditor.Web/src/ui/panel/Panel.jsx
create mode 100644 ShadowEditor.Web/src/ui/panel/css/Panel.css
create mode 100644 ShadowEditor.Web/src/ui/property/ButtonField.jsx
create mode 100644 ShadowEditor.Web/src/ui/property/ColorField.jsx
create mode 100644 ShadowEditor.Web/src/ui/property/IntegerField.jsx
create mode 100644 ShadowEditor.Web/src/ui/property/MapField.jsx
create mode 100644 ShadowEditor.Web/src/ui/property/NumberField.jsx
create mode 100644 ShadowEditor.Web/src/ui/property/PropertyGrid.jsx
create mode 100644 ShadowEditor.Web/src/ui/property/TextField.jsx
create mode 100644 ShadowEditor.Web/src/ui/property/css/PropertyGrid.css
create mode 100644 ShadowEditor.Web/src/ui/svg/SVG.jsx
create mode 100644 ShadowEditor.Web/src/ui/svg/css/SVG.css
create mode 100644 ShadowEditor.Web/src/ui/table/DataGrid.jsx
create mode 100644 ShadowEditor.Web/src/ui/table/Pager.jsx
create mode 100644 ShadowEditor.Web/src/ui/table/Table.jsx
create mode 100644 ShadowEditor.Web/src/ui/table/TableBody.jsx
create mode 100644 ShadowEditor.Web/src/ui/table/TableCell.jsx
create mode 100644 ShadowEditor.Web/src/ui/table/TableHead.jsx
create mode 100644 ShadowEditor.Web/src/ui/table/TableRow.jsx
create mode 100644 ShadowEditor.Web/src/ui/table/css/DataGrid.css
create mode 100644 ShadowEditor.Web/src/ui/table/css/Table.css
create mode 100644 ShadowEditor.Web/src/ui/table/css/TableBody.css
create mode 100644 ShadowEditor.Web/src/ui/table/css/TableCell.css
create mode 100644 ShadowEditor.Web/src/ui/table/css/TableHead.css
create mode 100644 ShadowEditor.Web/src/ui/table/css/TableRow.css
create mode 100644 ShadowEditor.Web/src/ui/table/datagrid/CheckBoxColumn.jsx
create mode 100644 ShadowEditor.Web/src/ui/table/datagrid/RowNumberColumn.jsx
create mode 100644 ShadowEditor.Web/src/ui/timeline/Timeline.jsx
create mode 100644 ShadowEditor.Web/src/ui/timeline/css/Timeline.css
create mode 100644 ShadowEditor.Web/src/ui/timeline/private/TimelineControl.jsx
create mode 100644 ShadowEditor.Web/src/ui/timeline/private/css/TimelineControl.css
create mode 100644 ShadowEditor.Web/src/ui/toolbar/Toolbar.jsx
create mode 100644 ShadowEditor.Web/src/ui/toolbar/ToolbarFiller.jsx
create mode 100644 ShadowEditor.Web/src/ui/toolbar/ToolbarSeparator.jsx
create mode 100644 ShadowEditor.Web/src/ui/toolbar/css/Toolbar.css
create mode 100644 ShadowEditor.Web/src/ui/toolbar/css/ToolbarFiller.css
create mode 100644 ShadowEditor.Web/src/ui/tree/Tree.jsx
create mode 100644 ShadowEditor.Web/src/ui/tree/css/Tree.css
create mode 100644 ShadowEditor.Web/src/ui/window/Alert.jsx
create mode 100644 ShadowEditor.Web/src/ui/window/Confirm.jsx
create mode 100644 ShadowEditor.Web/src/ui/window/Prompt.jsx
create mode 100644 ShadowEditor.Web/src/ui/window/Toast.jsx
create mode 100644 ShadowEditor.Web/src/ui/window/Window.jsx
create mode 100644 ShadowEditor.Web/src/ui/window/css/Alert.css
create mode 100644 ShadowEditor.Web/src/ui/window/css/Confirm.css
create mode 100644 ShadowEditor.Web/src/ui/window/css/Prompt.css
create mode 100644 ShadowEditor.Web/src/ui/window/css/Toast.css
create mode 100644 ShadowEditor.Web/src/ui/window/css/Window.css
diff --git a/ShadowEditor.Web/src/ui/Config.css b/ShadowEditor.Web/src/ui/Config.css
new file mode 100644
index 00000000..09748e85
--- /dev/null
+++ b/ShadowEditor.Web/src/ui/Config.css
@@ -0,0 +1,34 @@
+:root {
+ --theme-blue: #3399FF;
+ --theme-green: #64CF40;
+ --theme-orange: #F6A623;
+ --theme-black: #FFFFFF;
+ user-select: none;
+}
+
+:focus {
+ outline: none;
+}
+
+::-webkit-scrollbar {
+ width: 0.2em;
+}
+
+::-webkit-scrollbar:horizontal {
+ height: 0.5em;
+}
+
+::-webkit-scrollbar-track {
+ box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.3);
+ border-radius: 10px;
+}
+
+::-webkit-scrollbar-thumb {
+ border-radius: 10px;
+ background: rgba(0, 0, 0, 0.3);
+ box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.5);
+}
+
+::-webkit-scrollbar-thumb:window-inactive {
+ background: rgba(169, 169, 169, 0.4);
+}
\ No newline at end of file
diff --git a/ShadowEditor.Web/src/ui/canvas/Canvas.jsx b/ShadowEditor.Web/src/ui/canvas/Canvas.jsx
new file mode 100644
index 00000000..9438183f
--- /dev/null
+++ b/ShadowEditor.Web/src/ui/canvas/Canvas.jsx
@@ -0,0 +1,36 @@
+import './css/Canvas.css';
+import classNames from 'classnames/bind';
+import PropTypes from 'prop-types';
+
+/**
+ * 画布
+ * @author tengge / https://github.com/tengge1
+ */
+class Canvas extends React.Component {
+ constructor(props) {
+ super(props);
+ this.dom = React.createRef();
+ }
+
+ render() {
+ const { className, style, ...others } = this.props;
+
+ return ;
+ }
+}
+
+Canvas.propTypes = {
+ className: PropTypes.string,
+ style: PropTypes.object,
+};
+
+Canvas.defaultProps = {
+ className: null,
+ style: null,
+};
+
+export default Canvas;
\ No newline at end of file
diff --git a/ShadowEditor.Web/src/ui/canvas/css/Canvas.css b/ShadowEditor.Web/src/ui/canvas/css/Canvas.css
new file mode 100644
index 00000000..e69de29b
diff --git a/ShadowEditor.Web/src/ui/common/Accordion.jsx b/ShadowEditor.Web/src/ui/common/Accordion.jsx
new file mode 100644
index 00000000..06ce1b03
--- /dev/null
+++ b/ShadowEditor.Web/src/ui/common/Accordion.jsx
@@ -0,0 +1,11 @@
+/**
+ * 折叠面板
+ * @author tengge / https://github.com/tengge1
+ */
+class Accordion extends React.Component {
+ render() {
+ return null;
+ }
+}
+
+export default Accordion;
\ No newline at end of file
diff --git a/ShadowEditor.Web/src/ui/common/Buttons.jsx b/ShadowEditor.Web/src/ui/common/Buttons.jsx
new file mode 100644
index 00000000..87873177
--- /dev/null
+++ b/ShadowEditor.Web/src/ui/common/Buttons.jsx
@@ -0,0 +1,11 @@
+/**
+ * 很多按钮
+ * @author tengge / https://github.com/tengge1
+ */
+class Buttons extends React.Component {
+ render() {
+ return null;
+ }
+}
+
+export default Buttons;
\ No newline at end of file
diff --git a/ShadowEditor.Web/src/ui/common/Column.jsx b/ShadowEditor.Web/src/ui/common/Column.jsx
new file mode 100644
index 00000000..9dfc91fb
--- /dev/null
+++ b/ShadowEditor.Web/src/ui/common/Column.jsx
@@ -0,0 +1,11 @@
+/**
+ * 列
+ * @author tengge / https://github.com/tengge1
+ */
+class Column extends React.Component {
+ render() {
+ return null;
+ }
+}
+
+export default Column;
\ No newline at end of file
diff --git a/ShadowEditor.Web/src/ui/common/Columns.jsx b/ShadowEditor.Web/src/ui/common/Columns.jsx
new file mode 100644
index 00000000..bf0b82d9
--- /dev/null
+++ b/ShadowEditor.Web/src/ui/common/Columns.jsx
@@ -0,0 +1,11 @@
+/**
+ * 很多列
+ * @author tengge / https://github.com/tengge1
+ */
+class Columns extends React.Component {
+ render() {
+ return null;
+ }
+}
+
+export default Columns;
\ No newline at end of file
diff --git a/ShadowEditor.Web/src/ui/common/Content.jsx b/ShadowEditor.Web/src/ui/common/Content.jsx
new file mode 100644
index 00000000..5b676576
--- /dev/null
+++ b/ShadowEditor.Web/src/ui/common/Content.jsx
@@ -0,0 +1,11 @@
+/**
+ * 内容
+ * @author tengge / https://github.com/tengge1
+ */
+class Content extends React.Component {
+ render() {
+ return null;
+ }
+}
+
+export default Content;
\ No newline at end of file
diff --git a/ShadowEditor.Web/src/ui/common/Row.jsx b/ShadowEditor.Web/src/ui/common/Row.jsx
new file mode 100644
index 00000000..692d145b
--- /dev/null
+++ b/ShadowEditor.Web/src/ui/common/Row.jsx
@@ -0,0 +1,11 @@
+/**
+ * 行
+ * @author tengge / https://github.com/tengge1
+ */
+class Row extends React.Component {
+ render() {
+ return null;
+ }
+}
+
+export default Row;
\ No newline at end of file
diff --git a/ShadowEditor.Web/src/ui/common/Rows.jsx b/ShadowEditor.Web/src/ui/common/Rows.jsx
new file mode 100644
index 00000000..f002459e
--- /dev/null
+++ b/ShadowEditor.Web/src/ui/common/Rows.jsx
@@ -0,0 +1,11 @@
+/**
+ * 很多行
+ * @author tengge / https://github.com/tengge1
+ */
+class Rows extends React.Component {
+ render() {
+ return null;
+ }
+}
+
+export default Rows;
\ No newline at end of file
diff --git a/ShadowEditor.Web/src/ui/form/Button.jsx b/ShadowEditor.Web/src/ui/form/Button.jsx
new file mode 100644
index 00000000..f02b5658
--- /dev/null
+++ b/ShadowEditor.Web/src/ui/form/Button.jsx
@@ -0,0 +1,38 @@
+import './css/Button.css';
+import classNames from 'classnames/bind';
+import PropTypes from 'prop-types';
+
+/**
+ * 按钮
+ * @author tengge / https://github.com/tengge1
+ */
+class Button extends React.Component {
+ render() {
+ const { className, style, children, color, disabled, ...others } = this.props;
+ return ;
+ }
+}
+
+Button.propTypes = {
+ className: PropTypes.string,
+ style: PropTypes.object,
+ children: PropTypes.node,
+ color: PropTypes.oneOf(['primary', 'success', 'warn', 'danger']),
+ disabled: PropTypes.bool,
+};
+
+Button.defaultProps = {
+ className: null,
+ style: null,
+ children: null,
+ color: null,
+ disabled: false,
+};
+
+export default Button;
\ No newline at end of file
diff --git a/ShadowEditor.Web/src/ui/form/CheckBox.jsx b/ShadowEditor.Web/src/ui/form/CheckBox.jsx
new file mode 100644
index 00000000..fefcf930
--- /dev/null
+++ b/ShadowEditor.Web/src/ui/form/CheckBox.jsx
@@ -0,0 +1,61 @@
+import './css/CheckBox.css';
+import classNames from 'classnames/bind';
+import PropTypes from 'prop-types';
+
+/**
+ * 复选框
+ * @author tengge / https://github.com/tengge1
+ */
+class CheckBox extends React.Component {
+ constructor(props) {
+ super(props);
+
+ this.state = {
+ checked: props.checked,
+ };
+
+ this.handleChange = this.handleChange.bind(this, props.onChange);
+ }
+
+ handleChange(onChange, event) {
+ const target = event.target;
+ const name = target.getAttribute('name');
+ const checked = target.checked;
+
+ this.setState({ checked });
+
+ onChange && onChange(name, checked, event);
+ }
+
+ render() {
+ const { className, style, name, checked, disabled, onChange } = this.props;
+ return ;
+ }
+}
+
+CheckBox.propTypes = {
+ className: PropTypes.string,
+ style: PropTypes.object,
+ name: PropTypes.string,
+ checked: PropTypes.bool,
+ disabled: PropTypes.bool,
+ onChange: PropTypes.func,
+};
+
+CheckBox.defaultProps = {
+ className: null,
+ style: null,
+ name: undefined,
+ checked: false,
+ disabled: false,
+ onChange: null,
+};
+
+export default CheckBox;
\ No newline at end of file
diff --git a/ShadowEditor.Web/src/ui/form/Form.jsx b/ShadowEditor.Web/src/ui/form/Form.jsx
new file mode 100644
index 00000000..94f5d992
--- /dev/null
+++ b/ShadowEditor.Web/src/ui/form/Form.jsx
@@ -0,0 +1,51 @@
+import './css/Form.css';
+import classNames from 'classnames/bind';
+import PropTypes from 'prop-types';
+
+/**
+ * 表单
+ * @author tengge / https://github.com/tengge1
+ */
+class Form extends React.Component {
+ constructor(props) {
+ super(props);
+
+ this.handleSubmit = this.handleSubmit.bind(this, props.onSubmit);
+ }
+
+ handleSubmit(onSubmit) {
+ event.preventDefault();
+ onSubmit && onSubmit();
+ }
+
+ render() {
+ const { className, style, children, direction, onSubmit, ...others } = this.props;
+ return
;
+ }
+}
+
+Form.propTypes = {
+ onSubmit: PropTypes.func,
+
+ className: PropTypes.string,
+ style: PropTypes.object,
+ children: PropTypes.node,
+ direction: PropTypes.oneOf(['horizontal', 'vertical']),
+};
+
+Form.defaultProps = {
+ onSubmit: null,
+
+ className: null,
+ style: null,
+ children: null,
+ direction: 'horizontal',
+};
+
+export default Form;
\ No newline at end of file
diff --git a/ShadowEditor.Web/src/ui/form/FormControl.jsx b/ShadowEditor.Web/src/ui/form/FormControl.jsx
new file mode 100644
index 00000000..68d27cd7
--- /dev/null
+++ b/ShadowEditor.Web/src/ui/form/FormControl.jsx
@@ -0,0 +1,31 @@
+import './css/FormControl.css';
+import classNames from 'classnames/bind';
+import PropTypes from 'prop-types';
+
+/**
+ * 表单项
+ * @author tengge / https://github.com/tengge1
+ */
+class FormControl extends React.Component {
+ render() {
+ const { className, style, children, ...others } = this.props;
+
+ return
+ {children}
+
;
+ }
+}
+
+FormControl.propTypes = {
+ className: PropTypes.string,
+ style: PropTypes.object,
+ children: PropTypes.node,
+};
+
+FormControl.defaultProps = {
+ className: null,
+ style: null,
+ children: null,
+};
+
+export default FormControl;
\ No newline at end of file
diff --git a/ShadowEditor.Web/src/ui/form/IconButton.jsx b/ShadowEditor.Web/src/ui/form/IconButton.jsx
new file mode 100644
index 00000000..e40ac063
--- /dev/null
+++ b/ShadowEditor.Web/src/ui/form/IconButton.jsx
@@ -0,0 +1,41 @@
+import './css/IconButton.css';
+import classNames from 'classnames/bind';
+import PropTypes from 'prop-types';
+
+/**
+ * 图标按钮
+ * @author tengge / https://github.com/tengge1
+ */
+class IconButton extends React.Component {
+ render() {
+ const { className, style, icon, title, selected, onClick, ...others } = this.props;
+ return ;
+ }
+}
+
+IconButton.propTypes = {
+ className: PropTypes.string,
+ style: PropTypes.object,
+ icon: PropTypes.string,
+ title: PropTypes.string,
+ selected: PropTypes.bool,
+ onClick: PropTypes.func,
+};
+
+IconButton.defaultProps = {
+ className: null,
+ style: null,
+ icon: null,
+ title: null,
+ selected: false,
+ onClick: null,
+};
+
+export default IconButton;
\ No newline at end of file
diff --git a/ShadowEditor.Web/src/ui/form/Input.jsx b/ShadowEditor.Web/src/ui/form/Input.jsx
new file mode 100644
index 00000000..13059b76
--- /dev/null
+++ b/ShadowEditor.Web/src/ui/form/Input.jsx
@@ -0,0 +1,56 @@
+import './css/Input.css';
+import classNames from 'classnames/bind';
+import PropTypes from 'prop-types';
+
+/**
+ * 输入框
+ * @author tengge / https://github.com/tengge1
+ */
+class Input extends React.Component {
+ constructor(props) {
+ super(props);
+
+ this.handleChange = this.handleChange.bind(this, props.onChange);
+ this.handleInput = this.handleInput.bind(this, props.onInput);
+ }
+
+ handleChange(onChange, event) {
+ onChange && onChange(event.target.value, event);
+ }
+
+ handleInput(onInput, event) {
+ onInput && onInput(event.target.value, event);
+ }
+
+ render() {
+ const { className, style, value, disabled, onChange, onInput } = this.props;
+
+ return ;
+ }
+}
+
+Input.propTypes = {
+ className: PropTypes.string,
+ style: PropTypes.object,
+ value: PropTypes.string,
+ disabled: PropTypes.bool,
+ onChange: PropTypes.func,
+ onInput: PropTypes.func,
+};
+
+Input.defaultProps = {
+ className: null,
+ style: null,
+ value: '',
+ disabled: false,
+ onChange: null,
+ onInput: null,
+};
+
+export default Input;
\ No newline at end of file
diff --git a/ShadowEditor.Web/src/ui/form/Label.jsx b/ShadowEditor.Web/src/ui/form/Label.jsx
new file mode 100644
index 00000000..c8eb80a5
--- /dev/null
+++ b/ShadowEditor.Web/src/ui/form/Label.jsx
@@ -0,0 +1,31 @@
+import './css/Label.css';
+import classNames from 'classnames/bind';
+import PropTypes from 'prop-types';
+
+/**
+ * 标签
+ * @author tengge / https://github.com/tengge1
+ */
+class Label extends React.Component {
+ render() {
+ const { className, style, children, ...others } = this.props;
+
+ return ;
+ }
+}
+
+Label.propTypes = {
+ className: PropTypes.string,
+ style: PropTypes.object,
+ children: PropTypes.node,
+};
+
+Label.defaultProps = {
+ className: null,
+ style: null,
+ children: null,
+};
+
+export default Label;
\ No newline at end of file
diff --git a/ShadowEditor.Web/src/ui/form/Radio.jsx b/ShadowEditor.Web/src/ui/form/Radio.jsx
new file mode 100644
index 00000000..902ea2fe
--- /dev/null
+++ b/ShadowEditor.Web/src/ui/form/Radio.jsx
@@ -0,0 +1,59 @@
+import './css/Radio.css';
+import classNames from 'classnames/bind';
+import PropTypes from 'prop-types';
+
+/**
+ * 单选框
+ * @author tengge / https://github.com/tengge1
+ */
+class Radio extends React.Component {
+ constructor(props) {
+ super(props);
+
+ this.state = {
+ selected: props.selected,
+ };
+
+ this.handleChange = this.handleChange.bind(this, props.onChange);
+ }
+
+ handleChange(onChange, event) {
+ this.setState({
+ selected: event.target.checked,
+ });
+ onChange && onChange(event.target.checked, event);
+ }
+
+ render() {
+ const { className, style, selected, disabled, onChange, ...others } = this.props;
+ return ;
+ }
+}
+
+Radio.propTypes = {
+ className: PropTypes.string,
+ style: PropTypes.object,
+ selected: PropTypes.bool,
+ disabled: PropTypes.bool,
+ onChange: PropTypes.func,
+};
+
+Radio.defaultProps = {
+ className: null,
+ style: null,
+ selected: false,
+ disabled: false,
+ onChange: null,
+};
+
+export default Radio;
\ No newline at end of file
diff --git a/ShadowEditor.Web/src/ui/form/SearchField.jsx b/ShadowEditor.Web/src/ui/form/SearchField.jsx
new file mode 100644
index 00000000..736da8c9
--- /dev/null
+++ b/ShadowEditor.Web/src/ui/form/SearchField.jsx
@@ -0,0 +1,156 @@
+import './css/SearchField.css';
+import classNames from 'classnames/bind';
+import PropTypes from 'prop-types';
+
+import IconButton from './IconButton.jsx';
+import CheckBox from './CheckBox.jsx';
+
+/**
+ * 搜索框
+ * @author tengge / https://github.com/tengge1
+ */
+class SearchField extends React.Component {
+ constructor(props) {
+ super(props);
+
+ this.state = {
+ value: props.value,
+ categories: [],
+ filterShow: false,
+ };
+
+ this.handleAdd = this.handleAdd.bind(this, props.onAdd);
+ this.handleChange = this.handleChange.bind(this, props.onChange);
+ this.handleInput = this.handleInput.bind(this, props.onInput);
+ this.handleReset = this.handleReset.bind(this, props.onInput, props.onChange);
+ this.handleShowFilter = this.handleShowFilter.bind(this);
+ this.handleCheckBoxChange = this.handleCheckBoxChange.bind(this, props.onInput, props.onChange);
+ }
+
+ render() {
+ const { className, style, data, placeholder, addHidden } = this.props;
+ const { value, categories, filterShow } = this.state;
+
+ return
+
+
+
+
+
+ {data.map(n => {
+ return
+ -1}
+ onChange={this.handleCheckBoxChange}>
+
+
;
+ })}
+
+
;
+ }
+
+ handleAdd(onAdd, event) {
+ onAdd && onAdd(event);
+ }
+
+ handleChange(onChange, event) {
+ event.stopPropagation();
+
+ const value = event.target.value;
+
+ this.setState({ value });
+
+ onChange && onChange(value, this.state.categories, event);
+ }
+
+ handleInput(onInput, event) {
+ event.stopPropagation();
+
+ const value = event.target.value;
+
+ this.setState({ value });
+
+ onInput && onInput(value, this.state.categories, event);
+ }
+
+ handleReset(onInput, onChange, event) {
+ const value = '';
+
+ this.setState({ value });
+
+ onInput && onInput(value, this.state.categories, event);
+ onChange && onChange(value, this.state.categories, event);
+ }
+
+ handleShowFilter() {
+ this.setState({
+ filterShow: !this.state.filterShow,
+ });
+ }
+
+ handleCheckBoxChange(onInput, onChange, name, checked, event) {
+ let categories = this.state.categories;
+ let index = categories.indexOf(name);
+
+ if (checked && index === -1) {
+ categories.push(name);
+ } else if (!checked && index > -1) {
+ categories.splice(index, 1);
+ } else {
+ console.warn(`SearchField: handleCheckBoxChange error.`);
+ return;
+ }
+
+ const value = this.state.value;
+
+ this.setState({ categories }, () => {
+ onInput && onInput(value, categories, event);
+ onChange && onChange(value, categories, event);
+ });
+ }
+}
+
+SearchField.propTypes = {
+ className: PropTypes.string,
+ style: PropTypes.object,
+ value: PropTypes.string,
+ data: PropTypes.array,
+ placeholder: PropTypes.string,
+ onAdd: PropTypes.func,
+ onChange: PropTypes.func,
+ onInput: PropTypes.func,
+ handleShowFilter: PropTypes.func,
+ addHidden: PropTypes.bool,
+};
+
+SearchField.defaultProps = {
+ className: null,
+ style: null,
+ value: '',
+ data: [],
+ placeholder: 'Enter a keyword',
+ onAdd: null,
+ onChange: null,
+ onInput: null,
+ handleShowFilter: null,
+ addHidden: false,
+};
+
+export default SearchField;
\ No newline at end of file
diff --git a/ShadowEditor.Web/src/ui/form/Select.jsx b/ShadowEditor.Web/src/ui/form/Select.jsx
new file mode 100644
index 00000000..d9e9db64
--- /dev/null
+++ b/ShadowEditor.Web/src/ui/form/Select.jsx
@@ -0,0 +1,63 @@
+import './css/Select.css';
+import classNames from 'classnames/bind';
+import PropTypes from 'prop-types';
+
+/**
+ * 输入框
+ * @author tengge / https://github.com/tengge1
+ */
+class Select extends React.Component {
+ constructor(props) {
+ super(props);
+
+ this.handleChange = this.handleChange.bind(this, props.onChange);
+ }
+
+ handleChange(onChange, event) {
+ const selectedIndex = event.target.selectedIndex;
+
+ if (selectedIndex === -1) {
+ onChange && onChange(null, event);
+ return;
+ }
+
+ const value = event.target.options[selectedIndex].value;
+
+ onChange && onChange(value, event);
+ }
+
+ render() {
+ const { className, style, options, value, disabled, onChange } = this.props;
+
+ return ;
+ }
+}
+
+Select.propTypes = {
+ className: PropTypes.string,
+ style: PropTypes.object,
+ options: PropTypes.object,
+ value: PropTypes.string,
+ disabled: PropTypes.bool,
+ onChange: PropTypes.func,
+};
+
+Select.defaultProps = {
+ className: null,
+ style: null,
+ options: null,
+ value: null,
+ disabled: false,
+ onChange: null,
+};
+
+export default Select;
\ No newline at end of file
diff --git a/ShadowEditor.Web/src/ui/form/TextArea.jsx b/ShadowEditor.Web/src/ui/form/TextArea.jsx
new file mode 100644
index 00000000..de84578d
--- /dev/null
+++ b/ShadowEditor.Web/src/ui/form/TextArea.jsx
@@ -0,0 +1,64 @@
+import './css/TextArea.css';
+import classNames from 'classnames/bind';
+import PropTypes from 'prop-types';
+
+/**
+ * 文本域
+ * @author tengge / https://github.com/tengge1
+ */
+class TextArea extends React.Component {
+ constructor(props) {
+ super(props);
+
+ this.state = {
+ value: props.value,
+ };
+
+ this.handleChange = this.handleChange.bind(this, props.onChange);
+ this.handleInput = this.handleInput.bind(this, props.onInput);
+ }
+
+ handleChange(onChange, event) {
+ this.setState({
+ value: event.target.value,
+ });
+ onChange && onChange(event.target.value, event);
+ }
+
+ handleInput(onInput, event) {
+ this.setState({
+ value: event.target.value,
+ });
+ onInput && onInput(event.target.value, event);
+ }
+
+ render() {
+ const { className, style, value, onChange, onInput, ...others } = this.props;
+
+ return ;
+ }
+}
+
+TextArea.propTypes = {
+ className: PropTypes.string,
+ style: PropTypes.object,
+ value: PropTypes.string,
+ onChange: PropTypes.func,
+ onInput: PropTypes.func,
+};
+
+TextArea.defaultProps = {
+ className: null,
+ style: null,
+ value: '',
+ onChange: null,
+ onInput: null,
+};
+
+export default TextArea;
\ No newline at end of file
diff --git a/ShadowEditor.Web/src/ui/form/Toggle.jsx b/ShadowEditor.Web/src/ui/form/Toggle.jsx
new file mode 100644
index 00000000..8a93ebdd
--- /dev/null
+++ b/ShadowEditor.Web/src/ui/form/Toggle.jsx
@@ -0,0 +1,58 @@
+import './css/Toggle.css';
+import classNames from 'classnames/bind';
+import PropTypes from 'prop-types';
+
+/**
+ * 开关
+ * @author tengge / https://github.com/tengge1
+ */
+class Toggle extends React.Component {
+ constructor(props) {
+ super(props);
+
+ this.state = {
+ selected: props.selected,
+ };
+
+ this.handleChange = this.handleChange.bind(this, props.onChange);
+ }
+
+ handleChange(onChange, event) {
+ var selected = event.target.classList.contains('selected');
+
+ this.setState({
+ selected: !selected,
+ });
+ onChange && onChange(!selected, event);
+ }
+
+ render() {
+ const { className, style, selected, disabled, onChange, ...others } = this.props;
+
+ return ;
+ }
+}
+
+Toggle.propTypes = {
+ className: PropTypes.string,
+ style: PropTypes.object,
+ selected: PropTypes.bool,
+ disabled: PropTypes.bool,
+ onChange: PropTypes.func,
+};
+
+Toggle.defaultProps = {
+ className: null,
+ style: null,
+ selected: false,
+ disabled: false,
+ onChange: null,
+};
+
+export default Toggle;
\ No newline at end of file
diff --git a/ShadowEditor.Web/src/ui/form/css/Button.css b/ShadowEditor.Web/src/ui/form/css/Button.css
new file mode 100644
index 00000000..08a588f9
--- /dev/null
+++ b/ShadowEditor.Web/src/ui/form/css/Button.css
@@ -0,0 +1,31 @@
+.Button {
+ height: 24px;
+ margin: 0 4px;
+ padding: 0 8px;
+ color: #fff;
+ background-color: #e74c3c;
+ border: none;
+ display: inline-block;
+ box-sizing: border-box;
+ cursor: pointer;
+}
+
+.Button.primary {
+ color: #fff;
+ background-color: #3399ff;
+}
+
+.Button.success {
+ color: #fff;
+ background-color: #64cf40;
+}
+
+.Button.warn {
+ color: #fff;
+ background-color: #f6a623;
+}
+
+.Button.disabled {
+ color: #fff;
+ background-color: #ebebeb;
+}
\ No newline at end of file
diff --git a/ShadowEditor.Web/src/ui/form/css/CheckBox.css b/ShadowEditor.Web/src/ui/form/css/CheckBox.css
new file mode 100644
index 00000000..ec2aa5f4
--- /dev/null
+++ b/ShadowEditor.Web/src/ui/form/css/CheckBox.css
@@ -0,0 +1,21 @@
+.CheckBox {
+ width: 20px;
+ height: 20px;
+ background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAYAAACNiR0NAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAvSURBVDhPYxg6oKur6z8lGGoMAoAEyQWjBkLAqIEkgVEDIWCEGkgJhhoz6AEDAwCX46nq5LTHtAAAAABJRU5ErkJggg==);
+ display: inline-block;
+ -webkit-appearance: none;
+ cursor: pointer;
+}
+
+.CheckBox.checked {
+ background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAYAAACNiR0NAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAACYSURBVDhP1YxRCoMwEERzp9ZL1RO156i306QuTJg1jpHFH/vgQXSHl/6Hx7uUKyJD1CgiMkSNIiJD1CgiMkSNznx9+UaG+OGZw6eUedn+Q4b4Y08VM5Eh7UD5XF2yviFD6mGc9uNqPoiZyBB/NPy32YuZyJB24KP+fSQyRI0q6taKDFGjiMgQNYqIDFGjiMgQNYqIzO1J6Qc9ntav8Xl7ewAAAABJRU5ErkJggg==);
+}
+
+.CheckBox.disabled {
+ background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAYAAACNiR0NAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAvSURBVDhPYxg64PXr1/8pwVBjEAAkSC4YNRACRg0kCYwaCAEj1EBKMNSYQQ8YGACTLe4e+yPbzQAAAABJRU5ErkJggg==);
+ cursor: default;
+}
+
+.CheckBox.checked.disabled {
+ background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAYAAACNiR0NAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAABkSURBVDhP5dBBCsAgDADB/P+rviCXWFYsLaGNYi6VLgREdARln0opNTOduWJztZ+AZlZVta3TINj9fAr0GC2DTxiF4PkvvjeMQpD8xQijIUjRA74pkNgbYTQNzrYxmJnOfD6RAzbN65BiZNB7AAAAAElFTkSuQmCC);
+}
\ No newline at end of file
diff --git a/ShadowEditor.Web/src/ui/form/css/Form.css b/ShadowEditor.Web/src/ui/form/css/Form.css
new file mode 100644
index 00000000..b1c7a853
--- /dev/null
+++ b/ShadowEditor.Web/src/ui/form/css/Form.css
@@ -0,0 +1,3 @@
+.Form {
+ box-sizing: border-box;
+}
\ No newline at end of file
diff --git a/ShadowEditor.Web/src/ui/form/css/FormControl.css b/ShadowEditor.Web/src/ui/form/css/FormControl.css
new file mode 100644
index 00000000..e26a5f0d
--- /dev/null
+++ b/ShadowEditor.Web/src/ui/form/css/FormControl.css
@@ -0,0 +1,13 @@
+.FormControl {
+ min-height: 20px;
+ margin: 4px;
+ box-sizing: border-box;
+}
+
+.FormControl>* {
+ vertical-align: middle;
+}
+
+.FormControl>.Label {
+ width: 60px;
+}
\ No newline at end of file
diff --git a/ShadowEditor.Web/src/ui/form/css/IconButton.css b/ShadowEditor.Web/src/ui/form/css/IconButton.css
new file mode 100644
index 00000000..29aa4d84
--- /dev/null
+++ b/ShadowEditor.Web/src/ui/form/css/IconButton.css
@@ -0,0 +1,35 @@
+.IconButton {
+ width: 32px;
+ height: 32px;
+ color: #555;
+ background: none;
+ margin: 4px;
+ padding: 0;
+ border: 1px solid #ccc;
+ border-radius: 2px;
+ box-sizing: border-box;
+ cursor: pointer;
+}
+
+.IconButton.selected {
+ color: #fff;
+ background: #3399ff;
+ border: 1px solid #3399ff;
+}
+
+.IconButton:hover {
+ color: #3399ff;
+ background: none;
+ border: 1px solid #3399ff;
+}
+
+.IconButton.selected:hover {
+ color: #fff;
+ background: #3399ff;
+ border: 2px solid #3399ff;
+}
+
+.IconButton .iconfont {
+ font-size: 20px;
+ pointer-events: none;
+}
\ No newline at end of file
diff --git a/ShadowEditor.Web/src/ui/form/css/Input.css b/ShadowEditor.Web/src/ui/form/css/Input.css
new file mode 100644
index 00000000..8b378228
--- /dev/null
+++ b/ShadowEditor.Web/src/ui/form/css/Input.css
@@ -0,0 +1,9 @@
+.Input {
+ width: 160px;
+ font: 12px 'Microsoft YaHei';
+ margin: 1px 0;
+ padding: 0 2px;
+ border: 1px solid rgb(217, 217, 217);
+ box-sizing: border-box;
+ vertical-align: top;
+}
\ No newline at end of file
diff --git a/ShadowEditor.Web/src/ui/form/css/Label.css b/ShadowEditor.Web/src/ui/form/css/Label.css
new file mode 100644
index 00000000..c083c72a
--- /dev/null
+++ b/ShadowEditor.Web/src/ui/form/css/Label.css
@@ -0,0 +1,6 @@
+.Label {
+ height: 20px;
+ font: 12px 'Microsoft YaHei';
+ line-height: 20px;
+ display: inline-block;
+}
\ No newline at end of file
diff --git a/ShadowEditor.Web/src/ui/form/css/Radio.css b/ShadowEditor.Web/src/ui/form/css/Radio.css
new file mode 100644
index 00000000..bb57f091
--- /dev/null
+++ b/ShadowEditor.Web/src/ui/form/css/Radio.css
@@ -0,0 +1,21 @@
+.Radio {
+ width: 20px;
+ height: 20px;
+ background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAYAAACNiR0NAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAEiSURBVDhP1ZPLaoNAGIVdttC+TUveqHmAEkp3br0gKCq60bfwti40BNF9kodIusrOzIFTxNYxE3d+8MOg/znM/Bdt2RiG8WxZ1krEmrHSdf2Jv+/Ddd3POI4PSZKci6K4IHCOomjved4H09TwfX9XVdWpkyDMz0EQfDN9Gpi1bUupnKZpOpG7pWwcPHPqZn/BTcXzN5QPQQNQM+YqI2p6dBznkTY96CCKzjxloLFt+5U2PRgLdJJ5yuR5fjFN8402PXMNoZEZzn6yqP8LbXqwAaIpe+Ypg6akafpAmyHYgLIslW+ZZdmP0LxTPg42AEN7i7quuzAMvyibBhuAoaX2H7iZstkv2ADUB0VHJxE449vNZ8rABmBoMRYIdFPagIWgaVdKsJshjm46QwAAAABJRU5ErkJggg==);
+ display: inline-block;
+ -webkit-appearance: none;
+ cursor: pointer;
+}
+
+.Radio.selected {
+ background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAYAAACNiR0NAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAFlSURBVDhP1ZO/SsNQFMYzKuimm4OPoFVxlKoFoTrqI+gDiIhro4KLm9C61ERaseDiE2gRlOqmiLRd62w61enzftcTcluTNHbrDw5cknM+zl9ruJk4wfhcAYupAnZofE+eYUx+/4/0BQ7WS2hsVeDZd+jQ+N4oo77sYF/ckrHqonZcxRciOKzCyzh4FPd4KHbzLpExXL8BGRdPEhYOy+zNrP0NvHz+Gt8m9j28tIM9Ce+GA2DPxFdz+wEsFQE1DG1885tJtozm1ClGRSaAE2TTxU9nY4qZomammypm5hwLIhPAtVAldMQPz62/Yr7xn09OTT+Vx7bIBAwqyJhQQeU4cMmzecyLTAAvgEsrfpqkQ5kuYkRkuuEFHKmlFV8Ns2GJNDMzovrXXnGxK+Hh8AIqamn7cfUKrF3iQcLi4QXYPZmaMLPEYj68gGwJTTadk6TxzZ71LTMKXgCXlmtB4zQjBzAkWNYPuWx6Vj2oQrgAAAAASUVORK5CYII=);
+}
+
+.Radio.disabled {
+ background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAYAAACNiR0NAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAEFSURBVDhP1ZNdCsIwEIT7qGBvo3gjPYCU4v0ExSOoN+ifTUvpW52hI0FJY/TNDxZLsjNsdtfovxmGIc7zfJ1l2ZbBb5wtdP0dEO/Lsrwi6qZpega/i6K44C5VWhgQnWFwRzVOjDE1TI9K90OzrusknYY5yD1J5obP9FX2DitFGxLJX8F9zJ6NqeFAc8PPXDYWTpBNH9PCoQbalWwsXIu2bXvlBcPpQ7uRjeVXQ2qchr8+uaqqGoZL2Vhwt8DlZUwLR0OZyeYVVJmiJ8FVYm0MdnEnuRuYHr9Y7INkfvgP8FWqysLMnqA3CfvDQXGSDA6AZx+fOQWKmXNpuRaKJc7cA/gTougBp8flNLlkjQ0AAAAASUVORK5CYII=);
+ cursor: default;
+}
+
+.Radio.selected.disabled {
+ background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAYAAACNiR0NAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAE1SURBVDhP1ZNJDoJAEEVZaiK30XgjPYAhxvuZaDwCegMmGUIIC9r/6UIaRBR3vqRip4ZPdVVr/TdKKdv3/bXneVsaz/AtJDwNFB/CMLzC4jRNCxrPQRC4iO0l7TtQdIHAHd0MkiRJDNGTpI9DsTzPpfQ9zEHuWcqG4TX7nVVVpcqyrI1nE3aKMThS3gVxmzPTqZqiKBQ+orCQ2nimzwQ1N/zMRaYFyWsOXafpzkwxU9TslDXwrUSmBcnbLMuen+cV+2KNMdbA7cO3EZkWOH8SZA18r4K/XjmKohj+pci0ILZA0NVpmglLmYlMFxTsMZNnl4Td8Io0szOCZ5PgLe6kfBiIniY87KOUjcN/QL9TE+nsO7EGzMbhfLgobpLGBdD38ZrvQDNzjGCFhWzElvANL+BPsKwHYaPc6BXkh3gAAAAASUVORK5CYII=);
+}
\ No newline at end of file
diff --git a/ShadowEditor.Web/src/ui/form/css/SearchField.css b/ShadowEditor.Web/src/ui/form/css/SearchField.css
new file mode 100644
index 00000000..254daf6e
--- /dev/null
+++ b/ShadowEditor.Web/src/ui/form/css/SearchField.css
@@ -0,0 +1,58 @@
+.SearchField {
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+}
+
+.SearchField>.input {
+ width: 0;
+ font: 12px 'Microsoft YaHei';
+ margin: 1px 0;
+ padding: 2px;
+ border: 1px solid rgb(217, 217, 217);
+ box-sizing: border-box;
+ flex: 1;
+}
+
+.SearchField>.IconButton {
+ width: 22px;
+ height: 22px;
+}
+
+.SearchField>.IconButton.hidden {
+ display: none;
+}
+
+.SearchField>.IconButton>.iconfont {
+ font-size: 14px;
+}
+
+.SearchField>.category {
+ position: absolute;
+ right: 8px;
+ top: 28px;
+ height: 160px;
+ background: #fff;
+ border: 1px solid #ccc;
+ display: inline-block;
+ z-index: 10;
+ overflow-y: auto;
+}
+
+.SearchField>.category.hidden {
+ display: none;
+}
+
+.SearchField>.category>.item {
+ padding: 0 8px 0 2px;
+}
+
+.SearchField>.category>.item>.CheckBox {
+ vertical-align: middle;
+}
+
+.SearchField>.category>.item>.title {
+ font-size: 14px;
+ line-height: 26px;
+ vertical-align: middle;
+}
\ No newline at end of file
diff --git a/ShadowEditor.Web/src/ui/form/css/Select.css b/ShadowEditor.Web/src/ui/form/css/Select.css
new file mode 100644
index 00000000..82f091d5
--- /dev/null
+++ b/ShadowEditor.Web/src/ui/form/css/Select.css
@@ -0,0 +1,3 @@
+.Select {
+ box-sizing: border-box;
+}
\ No newline at end of file
diff --git a/ShadowEditor.Web/src/ui/form/css/TextArea.css b/ShadowEditor.Web/src/ui/form/css/TextArea.css
new file mode 100644
index 00000000..146f65db
--- /dev/null
+++ b/ShadowEditor.Web/src/ui/form/css/TextArea.css
@@ -0,0 +1,10 @@
+.TextArea {
+ width: 160px;
+ height: 200px;
+ font: 12px 'Microsoft YaHei';
+ margin: 0px;
+ padding: 0px 2px;
+ border: 1px solid rgb(217, 217, 217);
+ box-sizing: border-box;
+ vertical-align: top;
+}
\ No newline at end of file
diff --git a/ShadowEditor.Web/src/ui/form/css/Toggle.css b/ShadowEditor.Web/src/ui/form/css/Toggle.css
new file mode 100644
index 00000000..01e8a1b0
--- /dev/null
+++ b/ShadowEditor.Web/src/ui/form/css/Toggle.css
@@ -0,0 +1,21 @@
+.Toggle {
+ width: 34px;
+ height: 20px;
+ margin: 0 4px;
+ background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACIAAAAUCAYAAADoZO9yAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAABYSURBVEhL7da9CQAgDIRR918gC2SBzJGhImlEzsofMMUJrz2+dDYRiQrqhZhZuPsxVV3Gd4yQHLt5ecg8vIshiCGIIYghiCGIIaheSA5lzKln34DfioRIdJ5veQfy/zjLAAAAAElFTkSuQmCC);
+ display: inline-block;
+ cursor: pointer;
+}
+
+.Toggle.selected {
+ background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACIAAAAUCAYAAADoZO9yAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAABRSURBVEhLYzCe+f//YMDDxyEzz/z/P4NMXL8fYQ7FDqEEnHmGMGfUISAw6hB0MOoQdDDqEHQw6hB0MOoQdDA8HQJqBpCLqdoMoBYeJA75/x8AV8uDZSB9PMIAAAAASUVORK5CYII=);
+}
+
+.Toggle.disabled {
+ background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACIAAAAUCAYAAADoZO9yAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAABVSURBVEhL7dY7CgAgDERB73/VEEiTOpJGZK38gClWmHZ56WwiEhXUCzGzcPdjqrqM7xghOXbz8pB5eBdDEEMQQxBDEEMQQ1C9kBzKmFPPvgG/FQmR6IJTLfO1daX2AAAAAElFTkSuQmCC);
+ cursor: default;
+}
+
+.Toggle.selected.disabled {
+ background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACIAAAAUCAYAAADoZO9yAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAABQSURBVEhL7dahCgAgDIRh3/9Vrw1WVhTLwmEQNci4H66Or2kD0H9YHYi7H8/M8s415KaIyDuCzAThBOEE4QThBOFqQlbP++6efgNe7RMI+gDSby3zpGs0DgAAAABJRU5ErkJggg==);
+}
\ No newline at end of file
diff --git a/ShadowEditor.Web/src/ui/icon/Icon.jsx b/ShadowEditor.Web/src/ui/icon/Icon.jsx
new file mode 100644
index 00000000..b7cf8cd1
--- /dev/null
+++ b/ShadowEditor.Web/src/ui/icon/Icon.jsx
@@ -0,0 +1,52 @@
+import './css/Icon.css';
+import classNames from 'classnames/bind';
+import PropTypes from 'prop-types';
+
+/**
+ * 图标
+ * @author tengge / https://github.com/tengge1
+ */
+class Icon extends React.Component {
+ constructor(props) {
+ super(props);
+
+ this.handleClick = this.handleClick.bind(this, props.onClick);
+ }
+
+ render() {
+ const { className, style, name, icon, title } = this.props;
+
+ return ;
+ }
+
+ handleClick(onClick, event) {
+ const name = event.target.getAttribute('name');
+ onClick && onClick(name, event);
+ }
+}
+
+Icon.propTypes = {
+ className: PropTypes.string,
+ style: PropTypes.object,
+ name: PropTypes.string,
+ icon: PropTypes.string,
+ title: PropTypes.string,
+ onClick: PropTypes.func,
+};
+
+Icon.defaultProps = {
+ className: null,
+ style: null,
+ name: null,
+ icon: null,
+ title: null,
+ onClick: null,
+};
+
+export default Icon;
\ No newline at end of file
diff --git a/ShadowEditor.Web/src/ui/icon/css/Icon.css b/ShadowEditor.Web/src/ui/icon/css/Icon.css
new file mode 100644
index 00000000..78b8fcad
--- /dev/null
+++ b/ShadowEditor.Web/src/ui/icon/css/Icon.css
@@ -0,0 +1,5 @@
+.Icon {
+ font-size: 20px;
+ margin: 2px;
+ display: inline-block;
+}
\ No newline at end of file
diff --git a/ShadowEditor.Web/src/ui/image/Image.jsx b/ShadowEditor.Web/src/ui/image/Image.jsx
new file mode 100644
index 00000000..7d8dddeb
--- /dev/null
+++ b/ShadowEditor.Web/src/ui/image/Image.jsx
@@ -0,0 +1,35 @@
+import './css/Image.css';
+import classNames from 'classnames/bind';
+import PropTypes from 'prop-types';
+
+/**
+ * 图片
+ * @author tengge / https://github.com/tengge1
+ */
+class Image extends React.Component {
+ render() {
+ const { className, style, src, title } = this.props;
+
+ return
;
+ }
+}
+
+Image.propTypes = {
+ className: PropTypes.string,
+ style: PropTypes.object,
+ src: PropTypes.string,
+ title: PropTypes.string,
+};
+
+Image.defaultProps = {
+ className: null,
+ style: null,
+ src: null,
+ title: null,
+};
+
+export default Image;
\ No newline at end of file
diff --git a/ShadowEditor.Web/src/ui/image/ImageList.jsx b/ShadowEditor.Web/src/ui/image/ImageList.jsx
new file mode 100644
index 00000000..b36b7d40
--- /dev/null
+++ b/ShadowEditor.Web/src/ui/image/ImageList.jsx
@@ -0,0 +1,167 @@
+import './css/ImageList.css';
+import classNames from 'classnames/bind';
+import PropTypes from 'prop-types';
+
+import Icon from '../icon/Icon.jsx';
+import IconButton from '../form/IconButton.jsx';
+import Input from '../form/Input.jsx';
+
+/**
+ * 图片列表
+ * @author tengge / https://github.com/tengge1
+ */
+class ImageList extends React.Component {
+ constructor(props) {
+ super(props);
+
+ const { onClick, onEdit, onDelete } = props;
+
+ this.state = {
+ pageSize: 6,
+ pageNum: 0,
+ };
+
+ this.handleFirstPage = this.handleFirstPage.bind(this);
+ this.handleLastPage = this.handleLastPage.bind(this);
+ this.handlePreviousPage = this.handlePreviousPage.bind(this);
+ this.handleNextPage = this.handleNextPage.bind(this);
+
+ this.handleClick = this.handleClick.bind(this, onClick);
+ this.handleEdit = this.handleEdit.bind(this, onEdit);
+ this.handleDelete = this.handleDelete.bind(this, onDelete);
+ }
+
+ render() {
+ const { className, style, data, firstPageText, lastPageText, currentPageText, previousPageText, nextPageText } = this.props;
+ const { pageSize, pageNum } = this.state;
+
+ const totalPage = this.getTotalPage();
+
+ const current = data.filter((n, i) => {
+ return i >= pageSize * pageNum && i < pageSize * (pageNum + 1);
+ });
+
+ return
+
+ {current.map(n => {
+ return
+ {n.src ?
+

:
+
+
+
}
+
{n.title}
+ {n.cornerText &&
{n.cornerText}
}
+
+
+
;
+ })}
+
+
+
+
+
+
+
+
+ 共{totalPage}页
+
+
+
;
+ }
+
+ handleFirstPage() {
+ this.setState({
+ pageNum: 0,
+ });
+ }
+
+ handleLastPage() {
+ const totalPage = this.getTotalPage();
+
+ this.setState({
+ pageNum: totalPage < 1 ? 0 : totalPage - 1,
+ });
+ }
+
+ handleNextPage() {
+ this.setState(state => {
+ const totalPage = this.getTotalPage();
+
+ return {
+ pageNum: state.pageNum < totalPage - 1 ? state.pageNum + 1 : totalPage - 1,
+ };
+ });
+ }
+
+ handlePreviousPage() {
+ this.setState(state => {
+ return {
+ pageNum: state.pageNum > 0 ? state.pageNum - 1 : 0,
+ };
+ });
+ }
+
+ handleClick(onClick, event) {
+ event.stopPropagation();
+
+ const id = event.target.getAttribute('data-id');
+ const data = this.props.data.filter(n => n.id === id)[0];
+
+ onClick && onClick(data, event);
+ }
+
+ handleEdit(onEdit, event) {
+ event.stopPropagation();
+
+ const id = event.target.getAttribute('data-id');
+ const data = this.props.data.filter(n => n.id === id)[0];
+
+ onEdit && onEdit(data, event);
+ }
+
+ handleDelete(onDelete, event) {
+ event.stopPropagation();
+
+ const id = event.target.getAttribute('data-id');
+ const data = this.props.data.filter(n => n.id === id)[0];
+
+ onDelete && onDelete(data, event);
+ }
+
+ getTotalPage() {
+ const total = this.props.data.length;
+ const pageSize = this.state.pageSize;
+ return total % pageSize === 0 ? total / pageSize : parseInt(total / pageSize) + 1;
+ }
+}
+
+ImageList.propTypes = {
+ className: PropTypes.string,
+ style: PropTypes.object,
+ data: PropTypes.array,
+ onClick: PropTypes.func,
+ onEdit: PropTypes.func,
+ onDelete: PropTypes.func,
+ firstPageText: PropTypes.string,
+ lastPageText: PropTypes.string,
+ currentPageText: PropTypes.string,
+ previousPageText: PropTypes.string,
+ nextPageText: PropTypes.string,
+};
+
+ImageList.defaultProps = {
+ className: null,
+ style: null,
+ data: [],
+ onClick: null,
+ onEdit: null,
+ onDelete: null,
+ firstPageText: 'First Page',
+ lastPageText: 'Last Page',
+ currentPageText: 'Current Page',
+ previousPageText: 'Previous Page',
+ nextPageText: 'Next Page',
+};
+
+export default ImageList;
\ No newline at end of file
diff --git a/ShadowEditor.Web/src/ui/image/ImageUploader.jsx b/ShadowEditor.Web/src/ui/image/ImageUploader.jsx
new file mode 100644
index 00000000..204e63c1
--- /dev/null
+++ b/ShadowEditor.Web/src/ui/image/ImageUploader.jsx
@@ -0,0 +1,81 @@
+import './css/ImageUploader.css';
+import classNames from 'classnames/bind';
+import PropTypes from 'prop-types';
+
+/**
+ * 图片上传控件
+ * @author tengge / https://github.com/tengge1
+ */
+class ImageUploader extends React.Component {
+ constructor(props) {
+ super(props);
+
+ this.handleSelect = this.handleSelect.bind(this);
+ this.handleChange = this.handleChange.bind(this, props.onChange);
+ }
+
+ render() {
+ const { className, style, url, server, noImageText } = this.props;
+
+ if (url && url != 'null') {
+ return
;
+ } else {
+ return
+ {noImageText}
+
;
+ }
+ }
+
+ componentDidMount() {
+ var input = document.createElement('input');
+ input.type = 'file';
+ input.style.display = 'none';
+ input.addEventListener('change', this.handleChange);
+
+ document.body.appendChild(input);
+
+ this.input = input;
+ }
+
+ componentWillUnmount() {
+ var input = this.input;
+ input.removeEventListener('change', this.handleChange);
+
+ document.body.removeChild(input);
+
+ this.input = null;
+ }
+
+ handleSelect() {
+ this.input.click();
+ }
+
+ handleChange(onChange, event) {
+ onChange && onChange(event.target.files[0], event);
+ }
+}
+
+ImageUploader.propTypes = {
+ className: PropTypes.string,
+ style: PropTypes.object,
+ url: PropTypes.string,
+ server: PropTypes.string,
+ noImageText: PropTypes.string,
+ onChange: PropTypes.func,
+};
+
+ImageUploader.defaultProps = {
+ className: null,
+ style: null,
+ url: null,
+ server: '',
+ noImageText: 'No Image',
+ onChange: null,
+};
+
+export default ImageUploader;
\ No newline at end of file
diff --git a/ShadowEditor.Web/src/ui/image/css/Image.css b/ShadowEditor.Web/src/ui/image/css/Image.css
new file mode 100644
index 00000000..e69de29b
diff --git a/ShadowEditor.Web/src/ui/image/css/ImageList.css b/ShadowEditor.Web/src/ui/image/css/ImageList.css
new file mode 100644
index 00000000..36eb822c
--- /dev/null
+++ b/ShadowEditor.Web/src/ui/image/css/ImageList.css
@@ -0,0 +1,133 @@
+.ImageList {
+ position: relative;
+ width: 100%;
+ height: calc(100% - 30px);
+}
+
+.ImageList>.content {
+ position: absolute;
+ width: 100%;
+ height: calc(100% - 24px);
+ display: flex;
+ flex-direction: row;
+ flex-wrap: wrap;
+ align-content: flex-start;
+ justify-content: flex-start;
+ box-sizing: border-box;
+ overflow-x: hidden;
+ overflow-y: auto;
+}
+
+.ImageList>.content>.item {
+ position: relative;
+ width: 104px;
+ height: 104px;
+ margin: 4px;
+ display: inline-block;
+ border: 1px solid #ddd;
+ box-sizing: border-box;
+ cursor: pointer;
+}
+
+.ImageList>.content>.item>.img {
+ width: 100%;
+ height: 100%;
+ border-radius: 3px;
+ pointer-events: none;
+}
+
+.ImageList>.content>.item>.no-img {
+ width: 100%;
+ height: 100px;
+ border-radius: 3px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ pointer-events: none;
+}
+
+.ImageList>.content>.item>.title {
+ position: absolute;
+ left: 2px;
+ bottom: 2px;
+ right: 2px;
+ background-color: rgba(0, 0, 0, 0.5);
+ font-size: 12px;
+ color: #fff;
+ padding: 1px 4px;
+ pointer-events: none;
+ word-break: break-word;
+}
+
+.ImageList>.content>.item>.cornerText {
+ position: absolute;
+ left: 0;
+ top: 0;
+ font-size: 12px;
+ font-weight: bold;
+ padding: 0 2px;
+ color: #555;
+ background: rgba(255, 255, 255, 0.8);
+ box-sizing: border-box;
+}
+
+.ImageList>.content>.item>.IconButton {
+ width: 16px;
+ height: 16px;
+ margin: 2px;
+ padding: 0;
+ background: rgba(255, 255, 255, 0.8);
+ border: none;
+ cursor: pointer;
+ pointer-events: all;
+}
+
+.ImageList>.content>.item>.edit {
+ position: absolute;
+ right: 24px;
+ top: 0;
+}
+
+.ImageList>.content>.item>.delete {
+ position: absolute;
+ right: 2px;
+ top: 0;
+}
+
+.ImageList>.content>.item>.IconButton i {
+ font-size: 14px;
+}
+
+.ImageList>.page {
+ position: absolute;
+ left: 0;
+ bottom: 0;
+ width: 100%;
+ font-size: 12px;
+ padding-top: 3px;
+ border-top: 1px solid #ddd;
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+}
+
+.ImageList>.page>.IconButton {
+ width: 20px;
+ height: 20px;
+ margin: 0 2px;
+}
+
+.ImageList>.page>.IconButton>.iconfont {
+ font-size: 14px;
+}
+
+.ImageList>.page>.current {
+ width: 0;
+ flex: 1;
+ margin: 0 2px;
+ padding: 1px 2px;
+}
+
+.ImageList>.page>.info {
+ margin: 0 2px;
+}
\ No newline at end of file
diff --git a/ShadowEditor.Web/src/ui/image/css/ImageUploader.css b/ShadowEditor.Web/src/ui/image/css/ImageUploader.css
new file mode 100644
index 00000000..b9f7b565
--- /dev/null
+++ b/ShadowEditor.Web/src/ui/image/css/ImageUploader.css
@@ -0,0 +1,15 @@
+.ImageUploader {
+ max-width: 160px;
+ max-height: 120px;
+ border-radius: 2px;
+ cursor: pointer;
+}
+
+.ImageUploader.empty {
+ width: 160px;
+ height: 120px;
+ border: 1px solid #ccc;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+}
\ No newline at end of file
diff --git a/ShadowEditor.Web/src/ui/index.js b/ShadowEditor.Web/src/ui/index.js
new file mode 100644
index 00000000..c008efb7
--- /dev/null
+++ b/ShadowEditor.Web/src/ui/index.js
@@ -0,0 +1,88 @@
+import './Config.css';
+import '../css/icon/iconfont.css';
+
+export { default as classNames } from 'classnames/bind';
+export { default as PropTypes } from 'prop-types';
+
+// canvas
+export { default as Canvas } from './canvas/Canvas.jsx';
+
+// common
+export { default as Accordion } from './common/Accordion.jsx';
+export { default as Buttons } from './common/Buttons.jsx';
+export { default as Column } from './common/Column.jsx';
+export { default as Columns } from './common/Columns.jsx';
+export { default as Content } from './common/Content.jsx';
+export { default as Row } from './common/Row.jsx';
+export { default as Rows } from './common/Rows.jsx';
+
+// form
+export { default as Button } from './form/Button.jsx';
+export { default as CheckBox } from './form/CheckBox.jsx';
+export { default as Form } from './form/Form.jsx';
+export { default as FormControl } from './form/FormControl.jsx';
+export { default as IconButton } from './form/IconButton.jsx';
+export { default as Input } from './form/Input.jsx';
+export { default as Label } from './form/Label.jsx';
+export { default as Radio } from './form/Radio.jsx';
+export { default as SearchField } from './form/SearchField.jsx';
+export { default as Select } from './form/Select.jsx';
+export { default as TextArea } from './form/TextArea.jsx';
+export { default as Toggle } from './form/Toggle.jsx';
+
+// icon
+export { default as Icon } from './icon/Icon.jsx';
+
+// image
+export { default as Image } from './image/Image.jsx';
+export { default as ImageList } from './image/ImageList.jsx';
+export { default as ImageUploader } from './image/ImageUploader.jsx';
+
+// layout
+export { default as AbsoluteLayout } from './layout/AbsoluteLayout.jsx';
+export { default as AccordionLayout } from './layout/AccordionLayout.jsx';
+export { default as BorderLayout } from './layout/BorderLayout.jsx';
+export { default as HBoxLayout } from './layout/HBoxLayout.jsx';
+export { default as TabLayout } from './layout/TabLayout.jsx';
+export { default as VBoxLayout } from './layout/VBoxLayout.jsx';
+
+// menu
+export { default as MenuBar } from './menu/MenuBar.jsx';
+export { default as MenuBarFiller } from './menu/MenuBarFiller.jsx';
+export { default as MenuItem } from './menu/MenuItem.jsx';
+export { default as MenuItemSeparator } from './menu/MenuItemSeparator.jsx';
+
+// panel
+export { default as Panel } from './panel/Panel.jsx';
+
+// property
+export { default as PropertyGrid } from './property/PropertyGrid.jsx';
+
+// svg
+export { default as SVG } from './svg/SVG.jsx';
+
+// table
+export { default as DataGrid } from './table/DataGrid.jsx';
+export { default as Table } from './table/Table.jsx';
+export { default as TableBody } from './table/TableBody.jsx';
+export { default as TableCell } from './table/TableCell.jsx';
+export { default as TableHead } from './table/TableHead.jsx';
+export { default as TableRow } from './table/TableRow.jsx';
+
+// timeline
+export { default as Timeline } from './timeline/Timeline.jsx';
+
+// toolbar
+export { default as Toolbar } from './toolbar/Toolbar.jsx';
+export { default as ToolbarFiller } from './toolbar/ToolbarFiller.jsx';
+export { default as ToolbarSeparator } from './toolbar/ToolbarSeparator.jsx';
+
+// tree
+export { default as Tree } from './tree/Tree.jsx';
+
+// window
+export { default as Alert } from './window/Alert.jsx';
+export { default as Confirm } from './window/Confirm.jsx';
+export { default as Prompt } from './window/Prompt.jsx';
+export { default as Toast } from './window/Toast.jsx';
+export { default as Window } from './window/Window.jsx';
\ No newline at end of file
diff --git a/ShadowEditor.Web/src/ui/layout/AbsoluteLayout.jsx b/ShadowEditor.Web/src/ui/layout/AbsoluteLayout.jsx
new file mode 100644
index 00000000..201bfeb1
--- /dev/null
+++ b/ShadowEditor.Web/src/ui/layout/AbsoluteLayout.jsx
@@ -0,0 +1,41 @@
+import './css/AbsoluteLayout.css';
+import classNames from 'classnames/bind';
+import PropTypes from 'prop-types';
+
+/**
+ * 绝对定位布局
+ * @author tengge / https://github.com/tengge1
+ */
+class AbsoluteLayout extends React.Component {
+ render() {
+ const { className, style, children, left, top, ...others } = this.props;
+
+ const position = {
+ left: left || 0,
+ top: top || 0,
+ };
+
+ return {children}
;
+ }
+}
+
+AbsoluteLayout.propTypes = {
+ className: PropTypes.string,
+ style: PropTypes.object,
+ children: PropTypes.node,
+ left: PropTypes.string,
+ top: PropTypes.string,
+};
+
+AbsoluteLayout.defaultProps = {
+ className: null,
+ style: null,
+ children: null,
+ left: '0',
+ top: '0',
+};
+
+export default AbsoluteLayout;
\ No newline at end of file
diff --git a/ShadowEditor.Web/src/ui/layout/AccordionLayout.jsx b/ShadowEditor.Web/src/ui/layout/AccordionLayout.jsx
new file mode 100644
index 00000000..b619e98f
--- /dev/null
+++ b/ShadowEditor.Web/src/ui/layout/AccordionLayout.jsx
@@ -0,0 +1,67 @@
+import './css/AccordionLayout.css';
+import classNames from 'classnames/bind';
+import PropTypes from 'prop-types';
+
+import AccordionPanel from './private/AccordionPanel.jsx';
+
+/**
+ * 折叠布局
+ * @author tengge / https://github.com/tengge1
+ */
+class AccordionLayout extends React.Component {
+ constructor(props) {
+ super(props);
+
+ this.state = {
+ activeIndex: props.activeIndex,
+ };
+
+ this.handleClick = this.handleClick.bind(this, props.onActive);
+ }
+
+ handleClick(onActive, index, name, event) {
+ onActive && onActive(index, name, event);
+ this.setState({
+ activeIndex: index,
+ });
+ }
+
+ render() {
+ const { className, style, children } = this.props;
+
+ const content = Array.isArray(children) ? children : [children];
+
+ return
+ {content.map((n, i) => {
+ return
{n.props.children};
+ })}
+
;
+ }
+}
+
+AccordionLayout.propTypes = {
+ className: PropTypes.string,
+ style: PropTypes.object,
+ children: PropTypes.node,
+ activeIndex: PropTypes.number,
+ onActive: PropTypes.func,
+};
+
+AccordionLayout.defaultProps = {
+ className: null,
+ style: null,
+ children: null,
+ activeIndex: 0,
+ onActive: null,
+};
+
+export default AccordionLayout;
\ No newline at end of file
diff --git a/ShadowEditor.Web/src/ui/layout/BorderLayout.jsx b/ShadowEditor.Web/src/ui/layout/BorderLayout.jsx
new file mode 100644
index 00000000..f045847a
--- /dev/null
+++ b/ShadowEditor.Web/src/ui/layout/BorderLayout.jsx
@@ -0,0 +1,291 @@
+import './css/BorderLayout.css';
+import classNames from 'classnames/bind';
+import PropTypes from 'prop-types';
+
+/**
+ * 边框布局
+ * @author tengge / https://github.com/tengge1
+ */
+class BorderLayout extends React.Component {
+ constructor(props) {
+ super(props);
+
+ const children = this.props.children;
+ const north = children && children.filter(n => n.props.region === 'north')[0];
+ const south = children && children.filter(n => n.props.region === 'south')[0];
+ const west = children && children.filter(n => n.props.region === 'west')[0];
+ const east = children && children.filter(n => n.props.region === 'east')[0];
+ const center = children && children.filter(n => n.props.region === 'center')[0];
+
+ const northSplit = north && north.props.split || false;
+ const southSplit = south && south.props.split || false;
+ const westSplit = west && west.props.split || false;
+ const eastSplit = east && east.props.split || false;
+
+ const northCollapsed = north && north.props.collapsed || false;
+ const southCollapsed = south && south.props.collapsed || false;
+ const westCollapsed = west && west.props.collapsed || false;
+ const eastCollapsed = east && east.props.collapsed || false;
+
+ const onNorthToggle = north && north.props.onToggle || null;
+ const onSouthToggle = south && south.props.onToggle || null;
+ const onWestToggle = west && west.props.onToggle || null;
+ const onEastToggle = east && east.props.onToggle || null;
+
+ this.northRef = React.createRef();
+ this.southRef = React.createRef();
+ this.westRef = React.createRef();
+ this.eastRef = React.createRef();
+
+ this.state = {
+ northSplit, southSplit, westSplit, eastSplit,
+ northCollapsed, southCollapsed, westCollapsed, eastCollapsed,
+ };
+
+ this.handleNorthClick = this.handleNorthClick.bind(this, onNorthToggle);
+ this.handleSouthClick = this.handleSouthClick.bind(this, onSouthToggle);
+ this.handleWestClick = this.handleWestClick.bind(this, onWestToggle);
+ this.handleEastClick = this.handleEastClick.bind(this, onEastToggle);
+
+ this.handleTransitionEnd = this.handleTransitionEnd.bind(this, onNorthToggle, onSouthToggle, onWestToggle, onEastToggle);
+ }
+
+ handleNorthClick() {
+ if (!this.state.northSplit) {
+ return;
+ }
+
+ this.setState((state, props) => {
+ const collapsed = !state.northCollapsed;
+
+ const dom = this.northRef.current;
+ const height = dom.clientHeight;
+
+ if (collapsed) {
+ dom.style.marginTop = `-${height - 8}px`;
+ } else {
+ dom.style.marginTop = null;
+ }
+
+ return {
+ northCollapsed: collapsed,
+ };
+ });
+ }
+
+ handleSouthClick() {
+ if (!this.state.southSplit) {
+ return;
+ }
+
+ this.setState((state, props) => {
+ const collapsed = !state.southCollapsed;
+
+ const dom = this.southRef.current;
+ const height = dom.clientHeight;
+
+ if (collapsed) {
+ dom.style.marginBottom = `-${height - 8}px`;
+ } else {
+ dom.style.marginBottom = null;
+ }
+
+ return {
+ southCollapsed: collapsed,
+ };
+ });
+ }
+
+ handleWestClick() {
+ if (!this.state.westSplit) {
+ return;
+ }
+
+ const dom = this.westRef.current;
+
+ this.setState((state, props) => {
+ const collapsed = !state.westCollapsed;
+
+ const width = dom.clientWidth;
+
+ if (collapsed) {
+ dom.style.marginLeft = `-${width - 8}px`;
+ } else {
+ dom.style.marginLeft = null;
+ }
+
+ return {
+ westCollapsed: collapsed,
+ };
+ });
+ }
+
+ handleEastClick() {
+ if (!this.state.eastSplit) {
+ return;
+ }
+
+ this.setState((state, props) => {
+ const collapsed = !state.eastCollapsed;
+
+ const dom = this.eastRef.current;
+ const width = dom.clientWidth;
+
+ if (collapsed) {
+ dom.style.marginRight = `-${width - 8}px`;
+ } else {
+ dom.style.marginRight = null;
+ }
+
+ return {
+ eastCollapsed: collapsed,
+ };
+ });
+ }
+
+ handleTransitionEnd(onNorthToggle, onSouthToggle, onWestToggle, onEastToggle, event) {
+ const region = event.target.getAttribute('region');
+
+ switch (region) {
+ case 'north':
+ onNorthToggle && onNorthToggle(!this.state.northCollapsed);
+ break;
+ case 'south':
+ onSouthToggle && onSouthToggle(!this.state.southCollapsed);
+ break;
+ case 'west':
+ onWestToggle && onWestToggle(!this.state.westCollapsed);
+ break;
+ case 'east':
+ onEastToggle && onEastToggle(!this.state.eastCollapsed);
+ break;
+ }
+ }
+
+ render() {
+ const { className, style, children } = this.props;
+
+ let north = [], south = [], west = [], east = [], center = [], others = [];
+
+ children && children.forEach(n => {
+ switch (n.props.region) {
+ case 'north':
+ north.push(n);
+ break;
+ case 'south':
+ south.push(n);
+ break;
+ case 'west':
+ west.push(n);
+ break;
+ case 'east':
+ east.push(n);
+ break;
+ case 'center':
+ center.push(n);
+ break;
+ default:
+ others.push(n);
+ break;
+ }
+ });
+
+ if (center.length === 0) {
+ console.warn(`BorderLayout: center region is not defined.`);
+ }
+
+ // north region
+ const northRegion = north.length > 0 && (
+
+ {north}
+
+ {this.state.northSplit &&
}
+
);
+
+ // south region
+ const southRegion = south.length > 0 && (
+ {this.state.southSplit &&
}
+
+ {south}
+
+
);
+
+ // west region
+ const westRegion = west.length > 0 && (
+
+ {west}
+
+ {this.state.westSplit &&
}
+
);
+
+ // east region
+ const eastRegion = east.length > 0 && ();
+
+ // center region
+ const centerRegion = center.length > 0 && (
+ {center}
+
);
+
+ const otherRegion = others.length > 0 && others;
+
+ return
+ {northRegion}
+
+ {westRegion}
+ {centerRegion}
+ {eastRegion}
+
+ {southRegion}
+ {otherRegion}
+
;
+ }
+}
+
+BorderLayout.propTypes = {
+ className: PropTypes.string,
+ style: PropTypes.object,
+ children: PropTypes.node,
+};
+
+BorderLayout.defaultProps = {
+ className: null,
+ style: null,
+ children: null,
+};
+
+export default BorderLayout;
\ No newline at end of file
diff --git a/ShadowEditor.Web/src/ui/layout/HBoxLayout.jsx b/ShadowEditor.Web/src/ui/layout/HBoxLayout.jsx
new file mode 100644
index 00000000..3fdbb0c5
--- /dev/null
+++ b/ShadowEditor.Web/src/ui/layout/HBoxLayout.jsx
@@ -0,0 +1,32 @@
+import './css/HBoxLayout.css';
+import classNames from 'classnames/bind';
+import PropTypes from 'prop-types';
+
+/**
+ * 水平布局
+ * @author tengge / https://github.com/tengge1
+ */
+class HBoxLayout extends React.Component {
+ render() {
+ const { className, style, children, ...others } = this.props;
+
+ return {children}
;
+ }
+}
+
+HBoxLayout.propTypes = {
+ className: PropTypes.string,
+ style: PropTypes.object,
+ children: PropTypes.node,
+};
+
+HBoxLayout.defaultProps = {
+ className: null,
+ style: null,
+ children: null,
+};
+
+export default HBoxLayout;
\ No newline at end of file
diff --git a/ShadowEditor.Web/src/ui/layout/TabLayout.jsx b/ShadowEditor.Web/src/ui/layout/TabLayout.jsx
new file mode 100644
index 00000000..730b947a
--- /dev/null
+++ b/ShadowEditor.Web/src/ui/layout/TabLayout.jsx
@@ -0,0 +1,64 @@
+import './css/TabLayout.css';
+import classNames from 'classnames/bind';
+import PropTypes from 'prop-types';
+
+/**
+ * 选项卡布局
+ * @author tengge / https://github.com/tengge1
+ */
+class TabLayout extends React.Component {
+ constructor(props) {
+ super(props);
+
+ this.state = {
+ activeTab: props.activeTab,
+ };
+
+ this.handleClick = this.handleClick.bind(this);
+ }
+
+ handleClick(event) {
+ var tabIndex = event.target.tabIndex;
+ this.setState({
+ activeTab: tabIndex,
+ });
+ }
+
+ render() {
+ const { className, style, children, activeTab, ...others } = this.props;
+
+ return
+
+ {children.map((n, i) => {
+ return
{n.props.title}
;
+ })}
+
+
+ {children.map((n, i) => {
+ return
{n}
;
+ })}
+
+
;
+ }
+}
+
+TabLayout.propTypes = {
+ className: PropTypes.string,
+ style: PropTypes.object,
+ children: PropTypes.node,
+ activeTab: PropTypes.number,
+};
+
+TabLayout.defaultProps = {
+ className: null,
+ style: null,
+ children: null,
+ activeTab: 0,
+};
+
+export default TabLayout;
\ No newline at end of file
diff --git a/ShadowEditor.Web/src/ui/layout/VBoxLayout.jsx b/ShadowEditor.Web/src/ui/layout/VBoxLayout.jsx
new file mode 100644
index 00000000..fade0d3e
--- /dev/null
+++ b/ShadowEditor.Web/src/ui/layout/VBoxLayout.jsx
@@ -0,0 +1,32 @@
+import './css/VBoxLayout.css';
+import classNames from 'classnames/bind';
+import PropTypes from 'prop-types';
+
+/**
+ * 竖直布局
+ * @author tengge / https://github.com/tengge1
+ */
+class VBoxLayout extends React.Component {
+ render() {
+ const { className, style, children, ...others } = this.props;
+
+ return {children}
;
+ }
+}
+
+VBoxLayout.propTypes = {
+ className: PropTypes.string,
+ style: PropTypes.object,
+ children: PropTypes.node,
+};
+
+VBoxLayout.defaultProps = {
+ className: null,
+ style: null,
+ children: null,
+};
+
+export default VBoxLayout;
\ No newline at end of file
diff --git a/ShadowEditor.Web/src/ui/layout/css/AbsoluteLayout.css b/ShadowEditor.Web/src/ui/layout/css/AbsoluteLayout.css
new file mode 100644
index 00000000..9ddbf4cc
--- /dev/null
+++ b/ShadowEditor.Web/src/ui/layout/css/AbsoluteLayout.css
@@ -0,0 +1,4 @@
+.AbsoluteLayout {
+ position: absolute;
+ display: block;
+}
\ No newline at end of file
diff --git a/ShadowEditor.Web/src/ui/layout/css/AccordionLayout.css b/ShadowEditor.Web/src/ui/layout/css/AccordionLayout.css
new file mode 100644
index 00000000..f2974af1
--- /dev/null
+++ b/ShadowEditor.Web/src/ui/layout/css/AccordionLayout.css
@@ -0,0 +1,5 @@
+.AccordionLayout {
+ width: 100%;
+ height: 100%;
+ display: block;
+}
\ No newline at end of file
diff --git a/ShadowEditor.Web/src/ui/layout/css/BorderLayout.css b/ShadowEditor.Web/src/ui/layout/css/BorderLayout.css
new file mode 100644
index 00000000..3dd8bb77
--- /dev/null
+++ b/ShadowEditor.Web/src/ui/layout/css/BorderLayout.css
@@ -0,0 +1,198 @@
+.BorderLayout {
+ width: 100%;
+ height: 100%;
+ display: flex;
+ flex-direction: column;
+ --up-arrow: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACgAAAAJCAMAAAB30J7MAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyJpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMy1jMDExIDY2LjE0NTY2MSwgMjAxMi8wMi8wNi0xNDo1NjoyNyAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iIHhtbG5zOnN0UmVmPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VSZWYjIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6ODU5RUQ4NzY3RTI2MTFFOUE4RjBBODU0OThFNTczRkMiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6ODU5RUQ4NzU3RTI2MTFFOUE4RjBBODU0OThFNTczRkMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENTNiAoV2luZG93cykiPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmRpZDo3ODlBMTI3RDI2N0VFOTExOTFBREVBQjM5NUM3ODkwMiIgc3RSZWY6ZG9jdW1lbnRJRD0ieG1wLmRpZDo3ODlBMTI3RDI2N0VFOTExOTFBREVBQjM5NUM3ODkwMiIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/PlbUzZ8AAAMAUExURfb29tra2tnZ2fX19YuLi4qKivf39wcHBwgICAkJCQoKCgsLCwwMDA0NDQ4ODg8PDxAQEBERERISEhMTExQUFBUVFRYWFhcXFxgYGBkZGRoaGhsbGxwcHB0dHR4eHh8fHyAgICEhISIiIiMjIyQkJCUlJSYmJicnJygoKCkpKSoqKisrKywsLC0tLS4uLi8vLzAwMDExMTIyMjMzMzQ0NDU1NTY2Njc3Nzg4ODk5OTo6Ojs7Ozw8PD09PT4+Pj8/P0BAQEFBQUJCQkNDQ0REREVFRUZGRkdHR0hISElJSUpKSktLS0xMTE1NTU5OTk9PT1BQUFFRUVJSUlNTU1RUVFVVVVZWVldXV1hYWFlZWVpaWltbW1xcXF1dXV5eXl9fX2BgYGFhYWJiYmNjY2RkZGVlZWZmZmdnZ2hoaGlpaWpqamtra2xsbG1tbW5ubm9vb3BwcHFxcXJycnNzc3R0dHV1dXZ2dnd3d3h4eHl5eXp6ent7e3x8fH19fX5+fn9/f4CAgIGBgYKCgoODg4SEhIWFhYaGhoeHh4iIiImJiYqKiouLi4yMjI2NjY6Ojo+Pj5CQkJGRkZKSkpOTk5SUlJWVlZaWlpeXl5iYmJmZmZqampubm5ycnJ2dnZ6enp+fn6CgoKGhoaKioqOjo6SkpKWlpaampqenp6ioqKmpqaqqqqurq6ysrK2tra6urq+vr7CwsLGxsbKysrOzs7S0tLW1tba2tre3t7i4uLm5ubq6uru7u7y8vL29vb6+vr+/v8DAwMHBwcLCwsPDw8TExMXFxcbGxsfHx8jIyMnJycrKysvLy8zMzM3Nzc7Ozs/Pz9DQ0NHR0dLS0tPT09TU1NXV1dbW1tfX19jY2NnZ2dra2tvb29zc3N3d3d7e3t/f3+Dg4OHh4eLi4uPj4+Tk5OXl5ebm5ufn5+jo6Onp6erq6uvr6+zs7O3t7e7u7u/v7/Dw8PHx8fLy8vPz8/T09PX19fb29vf39/j4+Pn5+fr6+vv7+/z8/P39/f7+/v///9oQUYcAAAA3SURBVHjaYvhOJGCgRGFX1zeiFHYB0TciFHaBiW8EFXZBqa8EFHbBGN3f8Cq8hWDeJDN4AAIMAEFsVuZN2iPeAAAAAElFTkSuQmCC);
+ --down-arrow: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACgAAAAJCAMAAAB30J7MAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyJpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMy1jMDExIDY2LjE0NTY2MSwgMjAxMi8wMi8wNi0xNDo1NjoyNyAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iIHhtbG5zOnN0UmVmPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VSZWYjIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6OEZDOUYxQTk3RTI2MTFFOTlBRTE4RTE1RkRCMUI3MEYiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6OEZDOUYxQTg3RTI2MTFFOTlBRTE4RTE1RkRCMUI3MEYiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENTNiAoV2luZG93cykiPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmRpZDo3ODlBMTI3RDI2N0VFOTExOTFBREVBQjM5NUM3ODkwMiIgc3RSZWY6ZG9jdW1lbnRJRD0ieG1wLmRpZDo3ODlBMTI3RDI2N0VFOTExOTFBREVBQjM5NUM3ODkwMiIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/PqOucMsAAAMAUExURfb29tra2tnZ2fX19YuLi4qKivf39wcHBwgICAkJCQoKCgsLCwwMDA0NDQ4ODg8PDxAQEBERERISEhMTExQUFBUVFRYWFhcXFxgYGBkZGRoaGhsbGxwcHB0dHR4eHh8fHyAgICEhISIiIiMjIyQkJCUlJSYmJicnJygoKCkpKSoqKisrKywsLC0tLS4uLi8vLzAwMDExMTIyMjMzMzQ0NDU1NTY2Njc3Nzg4ODk5OTo6Ojs7Ozw8PD09PT4+Pj8/P0BAQEFBQUJCQkNDQ0REREVFRUZGRkdHR0hISElJSUpKSktLS0xMTE1NTU5OTk9PT1BQUFFRUVJSUlNTU1RUVFVVVVZWVldXV1hYWFlZWVpaWltbW1xcXF1dXV5eXl9fX2BgYGFhYWJiYmNjY2RkZGVlZWZmZmdnZ2hoaGlpaWpqamtra2xsbG1tbW5ubm9vb3BwcHFxcXJycnNzc3R0dHV1dXZ2dnd3d3h4eHl5eXp6ent7e3x8fH19fX5+fn9/f4CAgIGBgYKCgoODg4SEhIWFhYaGhoeHh4iIiImJiYqKiouLi4yMjI2NjY6Ojo+Pj5CQkJGRkZKSkpOTk5SUlJWVlZaWlpeXl5iYmJmZmZqampubm5ycnJ2dnZ6enp+fn6CgoKGhoaKioqOjo6SkpKWlpaampqenp6ioqKmpqaqqqqurq6ysrK2tra6urq+vr7CwsLGxsbKysrOzs7S0tLW1tba2tre3t7i4uLm5ubq6uru7u7y8vL29vb6+vr+/v8DAwMHBwcLCwsPDw8TExMXFxcbGxsfHx8jIyMnJycrKysvLy8zMzM3Nzc7Ozs/Pz9DQ0NHR0dLS0tPT09TU1NXV1dbW1tfX19jY2NnZ2dra2tvb29zc3N3d3d7e3t/f3+Dg4OHh4eLi4uPj4+Tk5OXl5ebm5ufn5+jo6Onp6erq6uvr6+zs7O3t7e7u7u/v7/Dw8PHx8fLy8vPz8/T09PX19fb29vf39/j4+Pn5+fr6+vv7+/z8/P39/f7+/v///9oQUYcAAAA/SURBVHjaYvhOJGBA5d5EMG/hVfitG8bqwm/i969d2NVhKPz+rQurOkyFYJVd34lQCFTZ9Z0ohUQFD24AEGAACN5W5nESKn0AAAAASUVORK5CYII=);
+ --left-arrow: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAkAAAAoCAMAAAAbvyCxAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyJpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMy1jMDExIDY2LjE0NTY2MSwgMjAxMi8wMi8wNi0xNDo1NjoyNyAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENTNiAoV2luZG93cykiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6REE4QkI1QUU3OTZEMTFFOTk0NzNEQjBGRjI0QzU2NzQiIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6REE4QkI1QUY3OTZEMTFFOTk0NzNEQjBGRjI0QzU2NzQiPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDpEQThCQjVBQzc5NkQxMUU5OTQ3M0RCMEZGMjRDNTY3NCIgc3RSZWY6ZG9jdW1lbnRJRD0ieG1wLmRpZDpEQThCQjVBRDc5NkQxMUU5OTQ3M0RCMEZGMjRDNTY3NCIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/PuJET8oAAAAVUExURfb29tra2tnZ2fX19YuLi4qKivf39wWvvrYAAAA7SURBVHjaYmCDAYbBwmJggrKYWaBiDKxQWTADxIIwQCxWNkwWGyvCPFaEyawIO1gR9jIOHp8DAUCAAQBxQwhCpBDaBAAAAABJRU5ErkJggg==);
+ --right-arrow: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAkAAAAoCAMAAAAbvyCxAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyJpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMy1jMDExIDY2LjE0NTY2MSwgMjAxMi8wMi8wNi0xNDo1NjoyNyAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENTNiAoV2luZG93cykiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6MEI0QUI4OEY3OTZFMTFFOTlGQjFGRTU4QTJGRTA0QTgiIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6MEI0QUI4OTA3OTZFMTFFOTlGQjFGRTU4QTJGRTA0QTgiPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDowQjRBQjg4RDc5NkUxMUU5OUZCMUZFNThBMkZFMDRBOCIgc3RSZWY6ZG9jdW1lbnRJRD0ieG1wLmRpZDowQjRBQjg4RTc5NkUxMUU5OUZCMUZFNThBMkZFMDRBOCIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/PtHSBrsAAAAVUExURfb29tnZ2YuLi/X19djY2IqKivf3956cLrcAAAA3SURBVHjazI/JDQAwDMLcg+4/cl8WK4SXFQISPMUAur2lbvqXJtJsJCR0MXFsYdu8Bi1XX4ABAHgwCELn0SAjAAAAAElFTkSuQmCC);
+}
+
+/* north */
+
+.BorderLayout>.north {
+ border-bottom: 1px solid #eee;
+ display: flex;
+ flex-direction: column;
+ align-items: stretch;
+ z-index: 100;
+ transition: all 0.4s;
+}
+
+.BorderLayout>.north>.content {
+ width: 100%;
+ height: 100%;
+ display: block;
+}
+
+.BorderLayout>.north.split>.content {
+ overflow-y: hidden;
+}
+
+.BorderLayout>.north>.control {
+ height: 8px;
+ border-top: 1px solid #eee;
+ box-sizing: border-box;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+.BorderLayout>.north>.control>.button {
+ width: 40px;
+ height: 9px;
+ background: var(--up-arrow);
+ cursor: pointer;
+}
+
+.BorderLayout>.north.collapsed>.control>.button {
+ background: var(--down-arrow);
+}
+
+/* south */
+
+.BorderLayout>.south {
+ border-top: 1px solid #eee;
+ display: flex;
+ flex-direction: column;
+ align-items: stretch;
+ z-index: 100;
+ transition: all 0.4s;
+}
+
+.BorderLayout>.south>.content {
+ width: 100%;
+ height: 100%;
+ display: block;
+}
+
+.BorderLayout>.south.split>.content {
+ overflow-y: hidden;
+}
+
+.BorderLayout>.south>.control {
+ height: 8px;
+ border-bottom: 1px solid #eee;
+ box-sizing: border-box;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+.BorderLayout>.south>.control>.button {
+ width: 40px;
+ height: 9px;
+ cursor: pointer;
+}
+
+.BorderLayout>.south>.control>.button {
+ background: var(--down-arrow);
+}
+
+.BorderLayout>.south.collapsed>.control>.button {
+ background: var(--up-arrow);
+}
+
+/* middle */
+
+.BorderLayout>.middle {
+ flex: 1;
+ display: flex;
+ flex-direction: row;
+ overflow: hidden;
+}
+
+/* west */
+
+.BorderLayout>.middle>.west {
+ border-right: 1px solid #eee;
+ display: flex;
+ flex-direction: row;
+ align-items: stretch;
+ transition: all 0.4s;
+}
+
+.BorderLayout>.middle>.west>.content {
+ width: 100%;
+ height: 100%;
+ display: block;
+}
+
+.BorderLayout>.middle>.west.split>.content {
+ overflow-x: hidden;
+}
+
+.BorderLayout>.middle>.west>.control {
+ width: 8px;
+ border-left: 1px solid #eee;
+ box-sizing: border-box;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+.BorderLayout>.middle>.west>.control>.button {
+ width: 9px;
+ height: 40px;
+ cursor: pointer;
+}
+
+.BorderLayout>.middle>.west>.control>.button {
+ background: var(--left-arrow);
+}
+
+.BorderLayout>.middle>.west.collapsed>.control>.button {
+ background: var(--right-arrow);
+}
+
+/* center */
+
+.BorderLayout>.middle>.center {
+ position: relative;
+ flex: 1;
+ display: block;
+}
+
+/* east */
+
+.BorderLayout>.middle>.east {
+ border-left: 1px solid #eee;
+ display: flex;
+ flex-direction: row;
+ align-items: stretch;
+ transition: all 0.4s;
+}
+
+.BorderLayout>.middle>.east>.content {
+ width: 100%;
+ height: 100%;
+ display: block;
+}
+
+.BorderLayout>.middle>.east.split>.content {
+ overflow-x: hidden;
+}
+
+.BorderLayout>.middle>.east>.control {
+ width: 8px;
+ border-right: 1px solid #eee;
+ box-sizing: border-box;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+.BorderLayout>.middle>.east>.control>.button {
+ width: 9px;
+ height: 40px;
+ cursor: pointer;
+}
+
+.BorderLayout>.middle>.east>.control>.button {
+ background: var(--right-arrow);
+}
+
+.BorderLayout>.middle>.east.collapsed>.control>.button {
+ background: var(--left-arrow);
+}
\ No newline at end of file
diff --git a/ShadowEditor.Web/src/ui/layout/css/Content.css b/ShadowEditor.Web/src/ui/layout/css/Content.css
new file mode 100644
index 00000000..dea508ad
--- /dev/null
+++ b/ShadowEditor.Web/src/ui/layout/css/Content.css
@@ -0,0 +1,3 @@
+.Content {
+ display: block-inline;
+}
\ No newline at end of file
diff --git a/ShadowEditor.Web/src/ui/layout/css/HBoxLayout.css b/ShadowEditor.Web/src/ui/layout/css/HBoxLayout.css
new file mode 100644
index 00000000..2cf31bfc
--- /dev/null
+++ b/ShadowEditor.Web/src/ui/layout/css/HBoxLayout.css
@@ -0,0 +1,6 @@
+.HBoxLayout {
+ display: flex;
+ flex-direction: row;
+ overflow-x: auto;
+ overflow-y: hidden;
+}
\ No newline at end of file
diff --git a/ShadowEditor.Web/src/ui/layout/css/TabLayout.css b/ShadowEditor.Web/src/ui/layout/css/TabLayout.css
new file mode 100644
index 00000000..9db391da
--- /dev/null
+++ b/ShadowEditor.Web/src/ui/layout/css/TabLayout.css
@@ -0,0 +1,46 @@
+.TabLayout {
+ position: relative;
+}
+
+.TabLayout>.tabs {
+ height: 32px;
+ box-sizing: border-box;
+}
+
+.TabLayout>.tabs>.tab {
+ height: 32px;
+ font: 12px 'Microsoft YaHei';
+ line-height: 32px;
+ padding: 0 8px;
+ color: #737373;
+ display: inline-block;
+ box-sizing: border-box;
+ cursor: pointer;
+}
+
+.TabLayout>.tabs>.tab.selected {
+ color: #3399ff;
+ border-bottom: 1px solid #3399ff;
+ cursor: default;
+}
+
+.TabLayout>.contents {
+ position: relative;
+ width: 100%;
+ height: calc(100% - 32px);
+}
+
+.TabLayout>.contents>.content {
+ position: relative;
+ width: 100%;
+ height: 100%;
+ display: none;
+}
+
+.TabLayout>.contents>.content.show {
+ display: block;
+}
+
+.TabLayout>.contents>.content>* {
+ height: 100%;
+}
\ No newline at end of file
diff --git a/ShadowEditor.Web/src/ui/layout/css/VBoxLayout.css b/ShadowEditor.Web/src/ui/layout/css/VBoxLayout.css
new file mode 100644
index 00000000..50416d08
--- /dev/null
+++ b/ShadowEditor.Web/src/ui/layout/css/VBoxLayout.css
@@ -0,0 +1,6 @@
+.VBoxLayout {
+ display: flex;
+ flex-direction: column;
+ overflow-x: hidden;
+ overflow-y: auto;
+}
\ No newline at end of file
diff --git a/ShadowEditor.Web/src/ui/layout/private/AccordionPanel.jsx b/ShadowEditor.Web/src/ui/layout/private/AccordionPanel.jsx
new file mode 100644
index 00000000..7c4e14bc
--- /dev/null
+++ b/ShadowEditor.Web/src/ui/layout/private/AccordionPanel.jsx
@@ -0,0 +1,96 @@
+import './css/AccordionPanel.css';
+import classNames from 'classnames/bind';
+import PropTypes from 'prop-types';
+
+/**
+ * 单个折叠面板
+ * @private
+ * @author tengge / https://github.com/tengge1
+ */
+class AccordionPanel extends React.Component {
+ constructor(props) {
+ super(props);
+
+ this.state = {
+ maximized: props.maximized,
+ };
+
+ this.handleClick = this.handleClick.bind(this, props.onClick, props.index, props.name);
+ this.handleMaximize = this.handleMaximize.bind(this, props.onMaximize);
+ }
+
+ handleClick(onClick, index, name, event) {
+ onClick && onClick(index, name, event);
+ }
+
+ handleMaximize(onMaximize, event) {
+ this.setState(state => ({
+ maximized: !state.maximized,
+ }));
+
+ onMaximize && onMaximize(event);
+ }
+
+ render() {
+ const { title, className, style, children, show, total, index, collpased,
+ maximizable, maximized, onMaximize } = this.props;
+
+ const maximizeControl = maximizable &&
+ {this.state.maximized ? : }
+
;
+
+ const _style = collpased ? style : Object.assign({}, style, {
+ height: `calc(100% - ${26 * (total - 1)}px`,
+ });
+
+ return
+
+
{title}
+
+ {maximizeControl}
+
+
+
+ {children}
+
+
;
+ }
+}
+
+AccordionPanel.propTypes = {
+ name: PropTypes.string,
+ title: PropTypes.string,
+ className: PropTypes.string,
+ style: PropTypes.object,
+ children: PropTypes.node,
+ show: PropTypes.bool,
+ total: PropTypes.number,
+ index: PropTypes.number,
+ collpased: PropTypes.bool,
+ maximizable: PropTypes.bool,
+ maximized: PropTypes.bool,
+ onMaximize: PropTypes.bool,
+ onClick: PropTypes.func,
+};
+
+AccordionPanel.defaultProps = {
+ name: null,
+ title: null,
+ className: null,
+ style: null,
+ children: null,
+ show: true,
+ total: 1,
+ index: 0,
+ collpased: true,
+ maximizable: false,
+ maximized: false,
+ onMaximize: null,
+ onClick: null,
+};
+
+export default AccordionPanel;
\ No newline at end of file
diff --git a/ShadowEditor.Web/src/ui/layout/private/css/AccordionPanel.css b/ShadowEditor.Web/src/ui/layout/private/css/AccordionPanel.css
new file mode 100644
index 00000000..d1642144
--- /dev/null
+++ b/ShadowEditor.Web/src/ui/layout/private/css/AccordionPanel.css
@@ -0,0 +1,82 @@
+.AccordionPanel {
+ position: relative;
+ left: 0;
+ top: 0;
+ width: 240px;
+ height: 320px;
+ background: #fafafa;
+ box-sizing: border-box;
+ transition: all 0.4s;
+}
+
+.AccordionPanel.collpased {
+ height: 26px;
+ overflow: hidden;
+}
+
+.AccordionPanel.maximized {
+ position: fixed;
+ left: 0;
+ top: 0;
+ width: 100% !important;
+ height: 100% !important;
+ z-index: 9000;
+}
+
+.AccordionPanel.hidden {
+ display: none;
+}
+
+.AccordionPanel>.header {
+ position: relative;
+ height: 24px;
+ background-color: #2c3e50;
+ cursor: pointer;
+}
+
+.AccordionPanel>.header>.title {
+ height: 100%;
+ line-height: 24px;
+ font-size: 12px;
+ padding-left: 8px;
+ color: #fff;
+ display: inline-block;
+ box-sizing: border-box;
+}
+
+.AccordionPanel>.header>.controls {
+ position: absolute;
+ left: 0;
+ right: 4px;
+ top: 0;
+ bottom: 0;
+ text-align: right;
+ white-space: nowrap;
+}
+
+.AccordionPanel>.header>.controls>.control {
+ width: 24px;
+ height: 24px;
+ margin: 0 2px;
+ display: inline-block;
+ box-sizing: border-box;
+ cursor: pointer;
+}
+
+.AccordionPanel>.header>.controls>.control>.iconfont {
+ font-size: 16px;
+ line-height: 24px;
+ color: #fff;
+}
+
+.AccordionPanel>.body {
+ height: calc(100% - 24px);
+ padding: 4px;
+ box-sizing: border-box;
+}
+
+.AccordionPanel.collapsed>.body {
+ height: 0;
+ padding: 0;
+ overflow-y: hidden;
+}
\ No newline at end of file
diff --git a/ShadowEditor.Web/src/ui/menu/MenuBar.jsx b/ShadowEditor.Web/src/ui/menu/MenuBar.jsx
new file mode 100644
index 00000000..76a18916
--- /dev/null
+++ b/ShadowEditor.Web/src/ui/menu/MenuBar.jsx
@@ -0,0 +1,32 @@
+import './css/MenuBar.css';
+import classNames from 'classnames/bind';
+import PropTypes from 'prop-types';
+
+/**
+ * 菜单栏
+ * @author tengge / https://github.com/tengge1
+ */
+class MenuBar extends React.Component {
+ render() {
+ const { className, style, children, ...others } = this.props;
+
+ return ;
+ }
+}
+
+MenuBar.propTypes = {
+ className: PropTypes.string,
+ style: PropTypes.object,
+ children: PropTypes.node,
+};
+
+MenuBar.defaultProps = {
+ className: null,
+ style: null,
+ children: null,
+};
+
+export default MenuBar;
\ No newline at end of file
diff --git a/ShadowEditor.Web/src/ui/menu/MenuBarFiller.jsx b/ShadowEditor.Web/src/ui/menu/MenuBarFiller.jsx
new file mode 100644
index 00000000..11483645
--- /dev/null
+++ b/ShadowEditor.Web/src/ui/menu/MenuBarFiller.jsx
@@ -0,0 +1,27 @@
+import './css/MenuBarFiller.css';
+import classNames from 'classnames/bind';
+import PropTypes from 'prop-types';
+
+/**
+ * 菜单栏填充
+ * @author tengge / https://github.com/tengge1
+ */
+class MenuBarFiller extends React.Component {
+ render() {
+ const { className, style } = this.props;
+
+ return ;
+ }
+}
+
+MenuBarFiller.propTypes = {
+ className: PropTypes.string,
+ style: PropTypes.object,
+};
+
+MenuBarFiller.defaultProps = {
+ className: null,
+ style: null,
+};
+
+export default MenuBarFiller;
\ No newline at end of file
diff --git a/ShadowEditor.Web/src/ui/menu/MenuItem.jsx b/ShadowEditor.Web/src/ui/menu/MenuItem.jsx
new file mode 100644
index 00000000..0ac05c00
--- /dev/null
+++ b/ShadowEditor.Web/src/ui/menu/MenuItem.jsx
@@ -0,0 +1,59 @@
+import './css/MenuItem.css';
+import classNames from 'classnames/bind';
+import PropTypes from 'prop-types';
+
+/**
+ * 菜单项
+ * @author tengge / https://github.com/tengge1
+ */
+class MenuItem extends React.Component {
+ constructor(props) {
+ super(props);
+ this.handleClick = this.handleClick.bind(this, props.onClick);
+ }
+
+ handleClick(onClick, event) {
+ event.stopPropagation();
+ onClick && onClick(event);
+ }
+
+ render() {
+ const { title, className, style, children, show, onClick, ...others } = this.props;
+
+ const subMenu = children && children.length && <>
+
+
+ >;
+
+ return
+ {title}
+ {subMenu}
+ ;
+ }
+}
+
+MenuItem.propTypes = {
+ title: PropTypes.string,
+ className: PropTypes.string,
+ style: PropTypes.object,
+ children: PropTypes.node,
+ show: PropTypes.bool,
+ onClick: PropTypes.func,
+};
+
+MenuItem.defaultProps = {
+ title: null,
+ className: null,
+ style: null,
+ children: null,
+ show: true,
+ onClick: null,
+};
+
+export default MenuItem;
\ No newline at end of file
diff --git a/ShadowEditor.Web/src/ui/menu/MenuItemSeparator.jsx b/ShadowEditor.Web/src/ui/menu/MenuItemSeparator.jsx
new file mode 100644
index 00000000..63cd8df4
--- /dev/null
+++ b/ShadowEditor.Web/src/ui/menu/MenuItemSeparator.jsx
@@ -0,0 +1,32 @@
+import './css/MenuItemSeparator.css';
+import classNames from 'classnames/bind';
+import PropTypes from 'prop-types';
+
+/**
+ * 菜单项分隔符
+ * @author tengge / https://github.com/tengge1
+ */
+class MenuItemSeparator extends React.Component {
+ render() {
+ const { className, style, ...others } = this.props;
+
+ return
+
+ ;
+ }
+}
+
+MenuItemSeparator.propTypes = {
+ className: PropTypes.string,
+ style: PropTypes.object,
+};
+
+MenuItemSeparator.defaultProps = {
+ className: null,
+ style: null,
+};
+
+export default MenuItemSeparator;
\ No newline at end of file
diff --git a/ShadowEditor.Web/src/ui/menu/css/MenuBar.css b/ShadowEditor.Web/src/ui/menu/css/MenuBar.css
new file mode 100644
index 00000000..134757b6
--- /dev/null
+++ b/ShadowEditor.Web/src/ui/menu/css/MenuBar.css
@@ -0,0 +1,38 @@
+.MenuBar {
+ font: 13px 'Microsoft YaHei';
+ color: rgb(0, 0, 0);
+ margin: 0;
+ padding: 0;
+ background: linear-gradient(to bottom, rgb(250, 252, 253), rgb(232, 241, 251) 40%, rgb(220, 230, 243) 40%, rgb(220, 231, 245));
+ border-bottom: 1px solid #bbb;
+ display: flex;
+ box-sizing: border-box;
+ list-style: none;
+}
+
+.MenuBar>.MenuItem {
+ position: relative;
+ min-width: auto;
+ margin: 0;
+ padding: 0 16px;
+ display: inline-block;
+ cursor: default;
+}
+
+.MenuBar>.MenuItem:hover {
+ background: initial;
+ color: initial;
+ margin: 0;
+ padding: 0 16px;
+ border: initial;
+}
+
+.MenuBar>.MenuItem>.suffix {
+ display: none;
+}
+
+.MenuBar>.MenuItem>.sub {
+ position: absolute;
+ left: 0;
+ top: auto;
+}
\ No newline at end of file
diff --git a/ShadowEditor.Web/src/ui/menu/css/MenuBarFiller.css b/ShadowEditor.Web/src/ui/menu/css/MenuBarFiller.css
new file mode 100644
index 00000000..2143fd1f
--- /dev/null
+++ b/ShadowEditor.Web/src/ui/menu/css/MenuBarFiller.css
@@ -0,0 +1,3 @@
+.MenuBarFiller {
+ flex: 1;
+}
\ No newline at end of file
diff --git a/ShadowEditor.Web/src/ui/menu/css/MenuItem.css b/ShadowEditor.Web/src/ui/menu/css/MenuItem.css
new file mode 100644
index 00000000..8498e3c6
--- /dev/null
+++ b/ShadowEditor.Web/src/ui/menu/css/MenuItem.css
@@ -0,0 +1,69 @@
+.MenuItem {
+ position: relative;
+ min-width: 120px;
+ line-height: 24px;
+ margin: 3px;
+ padding: 2px 14px 2px 30px;
+ vertical-align: top;
+ white-space: nowrap;
+ box-sizing: border-box;
+ cursor: pointer;
+ user-select: none;
+ --right-arrow: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAIAAAABAQMAAADO7O3JAAAAA3NCSVQICAjb4U/gAAAABlBMVEXi4+P///9V63aNAAAACXBIWXMAAAsSAAALEgHS3X78AAAAHHRFWHRTb2Z0d2FyZQBBZG9iZSBGaXJld29ya3MgQ1M1cbXjNgAAABZ0RVh0Q3JlYXRpb24gVGltZQAwNy8wNi8xMZANh+UAAAAKSURBVAiZY3AAAABCAEGV6TQ4AAAAAElFTkSuQmCC') repeat-y rgb(240, 240, 240);
+}
+
+.MenuItem:hover {
+ background: linear-gradient(to bottom, rgba(193, 222, 255, 0.2), rgba(193, 222, 255, 0.4));
+ color: black;
+ padding: 1px 13px 1px 29px;
+ border: 1px solid rgb(183, 212, 246);
+}
+
+.MenuItem>span {
+ pointer-events: none;
+}
+
+.MenuItem>.suffix {
+ width: 40px;
+ margin-left: 48px;
+ margin-right: -4px;
+ text-align: right;
+ display: inline-block;
+ pointer-events: none;
+}
+
+.MenuItem>.sub {
+ position: absolute;
+ left: calc(100% + 4px);
+ top: -4px;
+ box-sizing: border-box;
+ cursor: default;
+ display: none;
+ z-index: 300;
+}
+
+.MenuItem>.sub::before {
+ position: absolute;
+ left: -6px;
+ height: 100%;
+ width: 8px;
+ content: '';
+}
+
+.MenuItem:hover>.sub {
+ display: block;
+}
+
+.MenuItem>.sub>.wrap {
+ position: relative;
+ list-style: none;
+ background: var(--right-arrow);
+ background-position: 24px 0;
+ margin: 0;
+ padding: 0;
+ border: 1px solid rgb(195, 195, 195);
+ border-radius: 5px;
+ box-shadow: rgba(128, 128, 128, 0.5) 0px 0px 16px 1px;
+ box-sizing: border-box;
+ vertical-align: text-bottom;
+}
\ No newline at end of file
diff --git a/ShadowEditor.Web/src/ui/menu/css/MenuItemSeparator.css b/ShadowEditor.Web/src/ui/menu/css/MenuItemSeparator.css
new file mode 100644
index 00000000..0cf7a024
--- /dev/null
+++ b/ShadowEditor.Web/src/ui/menu/css/MenuItemSeparator.css
@@ -0,0 +1,11 @@
+.MenuItemSeparator {
+ white-space: nowrap;
+ display: block;
+}
+
+.MenuItemSeparator .separator {
+ height: 2px;
+ margin-left: 24px;
+ background: rgb(229, 229, 229);
+ background-image: linear-gradient(to bottom, rgb(226, 226, 226), rgb(226, 226, 226) 50%, rgb(252, 252, 252) 50%, rgb(252, 252, 252));
+}
\ No newline at end of file
diff --git a/ShadowEditor.Web/src/ui/panel/Panel.jsx b/ShadowEditor.Web/src/ui/panel/Panel.jsx
new file mode 100644
index 00000000..45e63b6e
--- /dev/null
+++ b/ShadowEditor.Web/src/ui/panel/Panel.jsx
@@ -0,0 +1,123 @@
+import './css/Panel.css';
+import classNames from 'classnames/bind';
+import PropTypes from 'prop-types';
+
+/**
+ * 面板
+ * @author tengge / https://github.com/tengge1
+ */
+class Panel extends React.Component {
+ constructor(props) {
+ super(props);
+
+ this.state = {
+ collapsed: props.collapsed,
+ maximized: props.maximized,
+ closed: props.closed,
+ };
+
+ this.handleCollapse = this.handleCollapse.bind(this, props.onCollapse);
+ this.handleMaximize = this.handleMaximize.bind(this, props.onMaximize);
+ this.handleClose = this.handleClose.bind(this, props.onClose);
+ }
+
+ handleCollapse(onCollapse, event) {
+ this.setState(state => ({
+ collapsed: !state.collapsed,
+ }));
+
+ onCollapse && onCollapse(event);
+ }
+
+ handleMaximize(onMaximize, event) {
+ this.setState(state => ({
+ maximized: !state.maximized,
+ }));
+
+ onMaximize && onMaximize(event);
+ }
+
+ handleClose(onClose, event) {
+ this.setState(state => ({
+ closed: !state.closed,
+ }));
+
+ onClose && onClose(event);
+ }
+
+ render() {
+ const { title, className, style, children, show, header,
+ collapsible, collapsed, onCollapse,
+ maximizable, maximized, onMaximize,
+ closable, closed, onClose } = this.props;
+
+ const collapseControl = collapsible &&
+ {this.state.collapsed ? : }
+
;
+
+ const maximizeControl = maximizable &&
+ {this.state.maximized ? : }
+
;
+
+ const closeControl = closable &&
+
+
;
+
+ return
+
+
{title}
+
+ {collapseControl}
+ {maximizeControl}
+ {closeControl}
+
+
+
+ {children}
+
+
;
+ }
+}
+
+Panel.propTypes = {
+ title: PropTypes.string,
+ className: PropTypes.string,
+ style: PropTypes.object,
+ children: PropTypes.node,
+ show: PropTypes.bool,
+ header: PropTypes.bool,
+ collapsible: PropTypes.bool,
+ collapsed: PropTypes.bool,
+ onCollapse: PropTypes.func,
+ maximizable: PropTypes.bool,
+ maximized: PropTypes.bool,
+ onMaximize: PropTypes.bool,
+ closable: PropTypes.bool,
+ closed: PropTypes.bool,
+ onClose: PropTypes.func,
+};
+
+Panel.defaultProps = {
+ title: null,
+ className: null,
+ style: null,
+ children: null,
+ show: true,
+ header: true,
+ collapsible: false,
+ collapsed: false,
+ onCollapse: null,
+ maximizable: false,
+ maximized: false,
+ onMaximize: null,
+ closable: false,
+ closed: false,
+ onClose: null,
+};
+
+export default Panel;
\ No newline at end of file
diff --git a/ShadowEditor.Web/src/ui/panel/css/Panel.css b/ShadowEditor.Web/src/ui/panel/css/Panel.css
new file mode 100644
index 00000000..51a47618
--- /dev/null
+++ b/ShadowEditor.Web/src/ui/panel/css/Panel.css
@@ -0,0 +1,88 @@
+.Panel {
+ position: relative;
+ left: 0;
+ top: 0;
+ width: 200px;
+ height: 320px;
+ background: #fafafa;
+ border: 1px solid #2c3e50;
+ box-sizing: border-box;
+}
+
+.Panel.maximized {
+ position: fixed;
+ left: 0;
+ top: 0;
+ width: 100%;
+ height: 100%;
+ z-index: 9000;
+}
+
+.Panel.collapsed {
+ height: auto;
+}
+
+.Panel.hidden {
+ display: none;
+}
+
+.Panel>.header {
+ position: relative;
+ height: 24px;
+ background-color: #2c3e50;
+}
+
+.Panel>.header.hidden {
+ display: none;
+}
+
+.Panel>.header>.title {
+ height: 100%;
+ line-height: 24px;
+ font-size: 12px;
+ padding-left: 8px;
+ color: #fff;
+ display: inline-block;
+ box-sizing: border-box;
+}
+
+.Panel>.header>.controls {
+ position: absolute;
+ left: 0;
+ right: 4px;
+ top: 0;
+ bottom: 0;
+ text-align: right;
+ white-space: nowrap;
+}
+
+.Panel>.header>.controls>.control {
+ width: 32px;
+ height: 32px;
+ margin: 0 4px;
+ display: inline-block;
+ box-sizing: border-box;
+ cursor: pointer;
+}
+
+.Panel>.header>.controls>.control>.iconfont {
+ font-size: 16px;
+ line-height: 24px;
+ color: #fff;
+}
+
+.Panel>.body {
+ height: calc(100% - 24px);
+ padding: 4px;
+ box-sizing: border-box;
+}
+
+.Panel.collapsed>.body {
+ height: 0;
+ padding: 0;
+ overflow-y: hidden;
+}
+
+.Panel>.header.hidden+div.body {
+ height: 100%;
+}
\ No newline at end of file
diff --git a/ShadowEditor.Web/src/ui/property/ButtonField.jsx b/ShadowEditor.Web/src/ui/property/ButtonField.jsx
new file mode 100644
index 00000000..e69de29b
diff --git a/ShadowEditor.Web/src/ui/property/ColorField.jsx b/ShadowEditor.Web/src/ui/property/ColorField.jsx
new file mode 100644
index 00000000..e69de29b
diff --git a/ShadowEditor.Web/src/ui/property/IntegerField.jsx b/ShadowEditor.Web/src/ui/property/IntegerField.jsx
new file mode 100644
index 00000000..e69de29b
diff --git a/ShadowEditor.Web/src/ui/property/MapField.jsx b/ShadowEditor.Web/src/ui/property/MapField.jsx
new file mode 100644
index 00000000..e69de29b
diff --git a/ShadowEditor.Web/src/ui/property/NumberField.jsx b/ShadowEditor.Web/src/ui/property/NumberField.jsx
new file mode 100644
index 00000000..e69de29b
diff --git a/ShadowEditor.Web/src/ui/property/PropertyGrid.jsx b/ShadowEditor.Web/src/ui/property/PropertyGrid.jsx
new file mode 100644
index 00000000..fad43784
--- /dev/null
+++ b/ShadowEditor.Web/src/ui/property/PropertyGrid.jsx
@@ -0,0 +1,60 @@
+import './css/PropertyGrid.css';
+import classNames from 'classnames/bind';
+import PropTypes from 'prop-types';
+
+/**
+ * 属性表格
+ * @author tengge / https://github.com/tengge1
+ */
+class PropertyGrid extends React.Component {
+ constructor(props) {
+ super(props);
+
+ this.state = {
+
+ };
+ }
+
+ handleCollapse() {
+
+ }
+
+ render() {
+ const { className, style, data } = this.props;
+
+ return
+ {data.map((group, i) => {
+ return
+
+
+ {group.children.map((item, j) => {
+ return
+
{item.label}
+
{item.value}
+
;
+ })}
+
+
;
+ })}
+
;
+ }
+}
+
+PropertyGrid.propTypes = {
+ className: PropTypes.string,
+ style: PropTypes.object,
+ data: PropTypes.array,
+};
+
+PropertyGrid.defaultProps = {
+ className: null,
+ style: null,
+ data: [],
+};
+
+export default PropertyGrid;
\ No newline at end of file
diff --git a/ShadowEditor.Web/src/ui/property/TextField.jsx b/ShadowEditor.Web/src/ui/property/TextField.jsx
new file mode 100644
index 00000000..e69de29b
diff --git a/ShadowEditor.Web/src/ui/property/css/PropertyGrid.css b/ShadowEditor.Web/src/ui/property/css/PropertyGrid.css
new file mode 100644
index 00000000..f9e6eac7
--- /dev/null
+++ b/ShadowEditor.Web/src/ui/property/css/PropertyGrid.css
@@ -0,0 +1,84 @@
+.PropertyGrid {
+ position: relative;
+ color: black;
+ font: 12px 'Microsoft YaHei';
+ box-sizing: border-box;
+ overflow-x: hidden;
+ overflow-y: auto;
+ cursor: default;
+ user-select: none;
+ --bg-expand: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAICAMAAAD3JJ6EAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyJpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMy1jMDExIDY2LjE0NTY2MSwgMjAxMi8wMi8wNi0xNDo1NjoyNyAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENTNiAoV2luZG93cykiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6QzBCN0FCRUU3NzExMTFFOTlFQjFFMDA4RkM3NzE5OTIiIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6QzBCN0FCRUY3NzExMTFFOTlFQjFFMDA4RkM3NzE5OTIiPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDpDMEI3QUJFQzc3MTExMUU5OUVCMUUwMDhGQzc3MTk5MiIgc3RSZWY6ZG9jdW1lbnRJRD0ieG1wLmRpZDpDMEI3QUJFRDc3MTExMUU5OUVCMUUwMDhGQzc3MTk5MiIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/PrAM5dcAAABFUExURb/BwfT3977BwamqqsnMzZOUlNPX19/j5ImKioeHh/f5+d7h4snLzNTY2Pr7/IiIiJOTk6mrq56foJ6fn7S1toaGhv///4ulXXIAAAAXdFJOU/////////////////////////////8A5kDmXgAAAD1JREFUeNokxkkSgCAAA8GguADukvz/qVRgDlONmUx0L8rdwX1DxuAkSL/1BfM0V5mqZL0GtTx+Zzz8JsAA9ZAFxigsqFUAAAAASUVORK5CYII=) no-repeat center;
+ --bg-collapse: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAKCAMAAAC+Ge+yAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyJpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMy1jMDExIDY2LjE0NTY2MSwgMjAxMi8wMi8wNi0xNDo1NjoyNyAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENTNiAoV2luZG93cykiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6MTlEMEIzNDQ3NzEyMTFFOUE2RDFDMzI1OUNGODkzQjEiIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6MTlEMEIzNDU3NzEyMTFFOUE2RDFDMzI1OUNGODkzQjEiPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDoxOUQwQjM0Mjc3MTIxMUU5QTZEMUMzMjU5Q0Y4OTNCMSIgc3RSZWY6ZG9jdW1lbnRJRD0ieG1wLmRpZDoxOUQwQjM0Mzc3MTIxMUU5QTZEMUMzMjU5Q0Y4OTNCMSIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/Ps1N6eUAAAA/UExURYiJiZ6en6mrq8nMzamqqtTY2NPX14eHh77AwPr7+4iIiN/j5N/i45+foL/CwsnLzPj6+rS1tpOTk4aGhv///0pgFIkAAAAVdFJOU///////////////////////////ACvZfeoAAAA3SURBVHjaYmDiFgEDBmEGPgFOMENYmJENyhAWFoQxhIX4YQxmFCleVpBidi4OiDksPBADAQIMAOvOBU7M/UDuAAAAAElFTkSuQmCC) no-repeat center;
+}
+
+.PropertyGrid .group {
+ background: #ecf0f1;
+}
+
+.PropertyGrid .group .head {
+ height: 20px;
+ box-sizing: border-box;
+}
+
+.PropertyGrid .group .head .icon {
+ width: 20px;
+ height: 20px;
+ float: left;
+}
+
+.PropertyGrid .group .head .icon .icon-expand {
+ width: 20px;
+ height: 20px;
+ background: var(--bg-expand);
+ display: inline-block;
+}
+
+.PropertyGrid .group .head .icon .icon-collapse {
+ width: 20px;
+ height: 20px;
+ background: var(--bg-collapse);
+ display: inline-block;
+}
+
+.PropertyGrid .group .head .title {
+ width: calc(100% - 20px);
+ height: 20px;
+ line-height: 20px;
+ padding-left: 4px;
+ display: inline-block;
+ border-bottom: 1px solid #d9d9d9;
+ box-sizing: border-box;
+}
+
+.PropertyGrid .group .property {
+ position: relative;
+ background: #fff;
+ margin-left: 20px;
+}
+
+.PropertyGrid .group .property.hide {
+ display: none;
+}
+
+.PropertyGrid .group .property .item {
+ height: 20px;
+ line-height: 20px;
+ border-bottom: 1px solid #d9d9d9;
+ box-sizing: border-box;
+ vertical-align: middle;
+}
+
+.PropertyGrid .group .property .item .label {
+ width: 120px;
+ padding: 0 4px;
+ display: inline-block;
+ box-sizing: border-box;
+}
+
+.PropertyGrid .group .property .item .value {
+ width: calc(100% - 120px);
+ padding: 0 4px;
+ display: inline-block;
+ border-left: 1px solid #d9d9d9;
+ box-sizing: border-box;
+}
\ No newline at end of file
diff --git a/ShadowEditor.Web/src/ui/svg/SVG.jsx b/ShadowEditor.Web/src/ui/svg/SVG.jsx
new file mode 100644
index 00000000..bd72cb82
--- /dev/null
+++ b/ShadowEditor.Web/src/ui/svg/SVG.jsx
@@ -0,0 +1,30 @@
+import './css/SVG.css';
+import classNames from 'classnames/bind';
+import PropTypes from 'prop-types';
+
+/**
+ * SVG
+ * @author tengge / https://github.com/tengge1
+ */
+class SVG extends React.Component {
+ render() {
+ const { className, style, ...others } = this.props;
+
+ return ;
+ }
+}
+
+SVG.propTypes = {
+ className: PropTypes.string,
+ style: PropTypes.object,
+};
+
+SVG.defaultProps = {
+ className: null,
+ style: null,
+};
+
+export default SVG;
\ No newline at end of file
diff --git a/ShadowEditor.Web/src/ui/svg/css/SVG.css b/ShadowEditor.Web/src/ui/svg/css/SVG.css
new file mode 100644
index 00000000..e69de29b
diff --git a/ShadowEditor.Web/src/ui/table/DataGrid.jsx b/ShadowEditor.Web/src/ui/table/DataGrid.jsx
new file mode 100644
index 00000000..673ba45f
--- /dev/null
+++ b/ShadowEditor.Web/src/ui/table/DataGrid.jsx
@@ -0,0 +1,91 @@
+import './css/DataGrid.css';
+import classNames from 'classnames/bind';
+import PropTypes from 'prop-types';
+import Column from '../common/Column.jsx';
+import Columns from '../common/Columns.jsx';
+
+/**
+ * 数据表格
+ * @author tengge / https://github.com/tengge1
+ */
+class DataGrid extends React.Component {
+ constructor(props) {
+ super(props);
+
+ this.state = {
+ data: props.data,
+ selected: null,
+ };
+
+ this.handleClick = this.handleClick.bind(this, props.onSelect);
+ }
+
+ handleClick(onSelect, event) {
+ const id = event.currentTarget.getAttribute('data-id');
+ const record = this.state.data.filter(n => n.id === id)[0];
+
+ this.setState({
+ selected: id,
+ });
+
+ onSelect && onSelect(record);
+ }
+
+ render() {
+ const { className, style, children } = this.props;
+ const { data, selected } = this.state;
+
+ const columns = children.props.children.map(n => {
+ return {
+ field: n.props.field,
+ title: n.props.title,
+ };
+ });
+
+ const header =
+
+ {columns.map(n => {
+ return | {n.title} | ;
+ })}
+
+ ;
+
+ const body =
+ {data.map(n => {
+ return
+ {columns.map(m => {
+ return | {n[m.field]} | ;
+ })}
+
;
+ })}
+ ;
+
+ return ;
+ }
+}
+
+DataGrid.propTypes = {
+ className: PropTypes.string,
+ style: PropTypes.object,
+ children: (props, propName, componentName) => {
+ const children = props[propName];
+ if (children.type !== Columns) {
+ return new TypeError(`Invalid prop \`${propName}\` of type \`${children.type.name}\` supplied to \`${componentName}\`, expected \`Columns\`.`);
+ }
+ },
+ data: PropTypes.array,
+ onSelect: PropTypes.func,
+};
+
+DataGrid.defaultProps = {
+ className: null,
+ style: null,
+ children: null,
+ data: [],
+ onSelect: null,
+};
+
+export default DataGrid;
\ No newline at end of file
diff --git a/ShadowEditor.Web/src/ui/table/Pager.jsx b/ShadowEditor.Web/src/ui/table/Pager.jsx
new file mode 100644
index 00000000..e69de29b
diff --git a/ShadowEditor.Web/src/ui/table/Table.jsx b/ShadowEditor.Web/src/ui/table/Table.jsx
new file mode 100644
index 00000000..b9e164b1
--- /dev/null
+++ b/ShadowEditor.Web/src/ui/table/Table.jsx
@@ -0,0 +1,34 @@
+import './css/Table.css';
+import classNames from 'classnames/bind';
+import PropTypes from 'prop-types';
+
+/**
+ * 表格
+ * @author tengge / https://github.com/tengge1
+ */
+class Table extends React.Component {
+ render() {
+ const { className, style, children, ...others } = this.props;
+
+ return ;
+ }
+}
+
+Table.propTypes = {
+ className: PropTypes.string,
+ style: PropTypes.object,
+ children: PropTypes.node,
+};
+
+Table.defaultProps = {
+ className: null,
+ style: null,
+ children: null,
+};
+
+export default Table;
\ No newline at end of file
diff --git a/ShadowEditor.Web/src/ui/table/TableBody.jsx b/ShadowEditor.Web/src/ui/table/TableBody.jsx
new file mode 100644
index 00000000..97ae9021
--- /dev/null
+++ b/ShadowEditor.Web/src/ui/table/TableBody.jsx
@@ -0,0 +1,34 @@
+import './css/TableBody.css';
+import classNames from 'classnames/bind';
+import PropTypes from 'prop-types';
+
+/**
+ * 表格内容
+ * @author tengge / https://github.com/tengge1
+ */
+class TableBody extends React.Component {
+ render() {
+ const { className, style, children, ...others } = this.props;
+
+ return
+ {children}
+ ;
+ }
+}
+
+TableBody.propTypes = {
+ className: PropTypes.string,
+ style: PropTypes.object,
+ children: PropTypes.node,
+};
+
+TableBody.defaultProps = {
+ className: null,
+ style: null,
+ children: null,
+};
+
+export default TableBody;
\ No newline at end of file
diff --git a/ShadowEditor.Web/src/ui/table/TableCell.jsx b/ShadowEditor.Web/src/ui/table/TableCell.jsx
new file mode 100644
index 00000000..0f1f351f
--- /dev/null
+++ b/ShadowEditor.Web/src/ui/table/TableCell.jsx
@@ -0,0 +1,34 @@
+import './css/TableCell.css';
+import classNames from 'classnames/bind';
+import PropTypes from 'prop-types';
+
+/**
+ * 表格单元格
+ * @author tengge / https://github.com/tengge1
+ */
+class TableCell extends React.Component {
+ render() {
+ const { className, style, children, ...others } = this.props;
+
+ return
+ {children}
+ | ;
+ }
+}
+
+TableCell.propTypes = {
+ className: PropTypes.string,
+ style: PropTypes.object,
+ children: PropTypes.node,
+};
+
+TableCell.defaultProps = {
+ className: null,
+ style: null,
+ children: null,
+};
+
+export default TableCell;
\ No newline at end of file
diff --git a/ShadowEditor.Web/src/ui/table/TableHead.jsx b/ShadowEditor.Web/src/ui/table/TableHead.jsx
new file mode 100644
index 00000000..434e6f0e
--- /dev/null
+++ b/ShadowEditor.Web/src/ui/table/TableHead.jsx
@@ -0,0 +1,34 @@
+import './css/TableHead.css';
+import classNames from 'classnames/bind';
+import PropTypes from 'prop-types';
+
+/**
+ * 表格头部
+ * @author tengge / https://github.com/tengge1
+ */
+class TableHead extends React.Component {
+ render() {
+ const { className, style, children, ...others } = this.props;
+
+ return
+ {children}
+ ;
+ }
+}
+
+TableHead.propTypes = {
+ className: PropTypes.string,
+ style: PropTypes.object,
+ children: PropTypes.node,
+};
+
+TableHead.defaultProps = {
+ className: null,
+ style: null,
+ children: null,
+};
+
+export default TableHead;
\ No newline at end of file
diff --git a/ShadowEditor.Web/src/ui/table/TableRow.jsx b/ShadowEditor.Web/src/ui/table/TableRow.jsx
new file mode 100644
index 00000000..d30ed4e8
--- /dev/null
+++ b/ShadowEditor.Web/src/ui/table/TableRow.jsx
@@ -0,0 +1,34 @@
+import './css/TableRow.css';
+import classNames from 'classnames/bind';
+import PropTypes from 'prop-types';
+
+/**
+ * 表格行
+ * @author tengge / https://github.com/tengge1
+ */
+class TableRow extends React.Component {
+ render() {
+ const { className, style, children, ...others } = this.props;
+
+ return
+ {children}
+
;
+ }
+}
+
+TableRow.propTypes = {
+ className: PropTypes.string,
+ style: PropTypes.object,
+ children: PropTypes.node,
+};
+
+TableRow.defaultProps = {
+ className: null,
+ style: null,
+ children: null,
+};
+
+export default TableRow;
\ No newline at end of file
diff --git a/ShadowEditor.Web/src/ui/table/css/DataGrid.css b/ShadowEditor.Web/src/ui/table/css/DataGrid.css
new file mode 100644
index 00000000..76020123
--- /dev/null
+++ b/ShadowEditor.Web/src/ui/table/css/DataGrid.css
@@ -0,0 +1,37 @@
+.DataGrid {
+ display: inline-block;
+ border-collapse: collapse;
+ user-select: none;
+ cursor: default;
+}
+
+.DataGrid>thead {
+ background-color: #f7f7f7;
+}
+
+.DataGrid>thead>tr,
+.DataGrid>tbody>tr {
+ height: 24px;
+ line-height: 24px;
+}
+
+.DataGrid>tbody>tr:nth-child(even) {
+ background-color: #f7f7f7;
+}
+
+.DataGrid>tbody>tr.selected {
+ color: #fff;
+ background-color: #3399ff;
+}
+
+.DataGrid>tbody>tr:hover {
+ color: #fff;
+ background-color: #3399ff;
+}
+
+.DataGrid>thead>tr>td,
+.DataGrid>tbody>tr>td {
+ padding: 0 16px;
+ border: 1px solid #ebebeb;
+ box-sizing: border-box;
+}
\ No newline at end of file
diff --git a/ShadowEditor.Web/src/ui/table/css/Table.css b/ShadowEditor.Web/src/ui/table/css/Table.css
new file mode 100644
index 00000000..6e4f2345
--- /dev/null
+++ b/ShadowEditor.Web/src/ui/table/css/Table.css
@@ -0,0 +1,6 @@
+.Table {
+ display: inline-block;
+ border-collapse: collapse;
+ user-select: none;
+ cursor: default;
+}
\ No newline at end of file
diff --git a/ShadowEditor.Web/src/ui/table/css/TableBody.css b/ShadowEditor.Web/src/ui/table/css/TableBody.css
new file mode 100644
index 00000000..5c0bfd81
--- /dev/null
+++ b/ShadowEditor.Web/src/ui/table/css/TableBody.css
@@ -0,0 +1 @@
+.TableBody {}
\ No newline at end of file
diff --git a/ShadowEditor.Web/src/ui/table/css/TableCell.css b/ShadowEditor.Web/src/ui/table/css/TableCell.css
new file mode 100644
index 00000000..3dd7d48c
--- /dev/null
+++ b/ShadowEditor.Web/src/ui/table/css/TableCell.css
@@ -0,0 +1,5 @@
+.TableCell {
+ padding: 0 16px;
+ border: 1px solid #ebebeb;
+ box-sizing: border-box;
+}
\ No newline at end of file
diff --git a/ShadowEditor.Web/src/ui/table/css/TableHead.css b/ShadowEditor.Web/src/ui/table/css/TableHead.css
new file mode 100644
index 00000000..2f0cdecf
--- /dev/null
+++ b/ShadowEditor.Web/src/ui/table/css/TableHead.css
@@ -0,0 +1,3 @@
+.TableHead {
+ background-color: #f7f7f7;
+}
\ No newline at end of file
diff --git a/ShadowEditor.Web/src/ui/table/css/TableRow.css b/ShadowEditor.Web/src/ui/table/css/TableRow.css
new file mode 100644
index 00000000..c57f5ffe
--- /dev/null
+++ b/ShadowEditor.Web/src/ui/table/css/TableRow.css
@@ -0,0 +1,13 @@
+.TableRow {
+ height: 24px;
+ line-height: 24px;
+}
+
+.TableRow:nth-child(even) {
+ background-color: #f7f7f7;
+}
+
+.TableBody .TableRow:hover {
+ color: #fff;
+ background-color: #3399ff;
+}
\ No newline at end of file
diff --git a/ShadowEditor.Web/src/ui/table/datagrid/CheckBoxColumn.jsx b/ShadowEditor.Web/src/ui/table/datagrid/CheckBoxColumn.jsx
new file mode 100644
index 00000000..e69de29b
diff --git a/ShadowEditor.Web/src/ui/table/datagrid/RowNumberColumn.jsx b/ShadowEditor.Web/src/ui/table/datagrid/RowNumberColumn.jsx
new file mode 100644
index 00000000..e69de29b
diff --git a/ShadowEditor.Web/src/ui/timeline/Timeline.jsx b/ShadowEditor.Web/src/ui/timeline/Timeline.jsx
new file mode 100644
index 00000000..39566b84
--- /dev/null
+++ b/ShadowEditor.Web/src/ui/timeline/Timeline.jsx
@@ -0,0 +1,146 @@
+import './css/Timeline.css';
+import classNames from 'classnames/bind';
+import PropTypes from 'prop-types';
+
+import TimelineControl from './private/TimelineControl.jsx';
+import CheckBox from '../form/CheckBox.jsx';
+import Label from '../form/Label.jsx';
+
+/**
+ * 时间轴
+ * @author tengge / https://github.com/tengge1
+ */
+class Timeline extends React.Component {
+ constructor(props) {
+ super(props);
+
+ this.canvas = React.createRef();
+ this.scale = 30; // 尺寸,1秒=30像素
+ }
+
+ render() {
+ const { className, style, animations, tip } = this.props;
+
+ const infos = animations.map(layer => {
+ return
+
+
+
;
+ });
+
+ const layers = animations.map(layer => {
+ return
+ {layer.animations.map(animation => {
+ const style = {
+ left: animation.beginTime * this.scale + 'px',
+ width: (animation.endTime - animation.beginTime) * this.scale + 'px',
+ };
+
+ return
+ {animation.name}
+
;
+ })}
+
;
+ });
+
+ return ;
+ }
+
+ componentDidMount() {
+ var duration = 120; // 持续时长(秒)
+ var scale = this.scale;
+
+ var width = duration * scale; // 画布宽度
+ var scale5 = scale / 5; // 0.2秒像素数
+ var margin = 0; // 时间轴前后间距
+
+ var canvas = this.canvas.current;
+ canvas.style.width = (width + margin * 2) + 'px';
+ canvas.width = canvas.clientWidth;
+ canvas.height = 32;
+
+ var context = canvas.getContext('2d');
+
+ // 时间轴背景
+ context.fillStyle = '#fafafa';
+ context.fillRect(0, 0, canvas.width, canvas.height);
+
+ // 时间轴刻度
+ context.strokeStyle = '#555';
+ context.beginPath();
+
+ for (var i = margin; i <= width + margin; i += scale) { // 绘制每一秒
+ for (var j = 0; j < 5; j++) { // 绘制每个小格
+ if (j === 0) { // 长刻度
+ context.moveTo(i + scale5 * j, 22);
+ context.lineTo(i + scale5 * j, 30);
+ } else { // 短刻度
+ context.moveTo(i + scale5 * j, 26);
+ context.lineTo(i + scale5 * j, 30);
+ }
+ }
+ }
+
+ context.stroke();
+
+ // 时间轴文字
+ context.font = '12px Arial';
+ context.fillStyle = '#888'
+
+ for (var i = 0; i <= duration; i += 2) { // 对于每两秒
+ var minute = Math.floor(i / 60);
+ var second = Math.floor(i % 60);
+
+ var text = (minute > 0 ? minute + ':' : '') + ('0' + second).slice(-2);
+
+ if (i === 0) {
+ context.textAlign = 'left';
+ } else if (i === duration) {
+ context.textAlign = 'right';
+ } else {
+ context.textAlign = 'center';
+ }
+
+ context.fillText(text, margin + i * scale, 16);
+ }
+ }
+
+ componentWillUnmount() {
+
+ }
+}
+
+Timeline.propTypes = {
+ className: PropTypes.string,
+ style: PropTypes.object,
+ animations: PropTypes.array,
+ tip: PropTypes.string,
+};
+
+Timeline.defaultProps = {
+ className: null,
+ style: null,
+ animations: [],
+ tip: undefined,
+};
+
+export default Timeline;
\ No newline at end of file
diff --git a/ShadowEditor.Web/src/ui/timeline/css/Timeline.css b/ShadowEditor.Web/src/ui/timeline/css/Timeline.css
new file mode 100644
index 00000000..ba7960c8
--- /dev/null
+++ b/ShadowEditor.Web/src/ui/timeline/css/Timeline.css
@@ -0,0 +1,102 @@
+.Timeline {
+ width: 100%;
+ height: 150px;
+ font-size: 12px;
+ display: flex;
+ flex-direction: column;
+}
+
+.Timeline>.box {
+ position: relative;
+ display: flex;
+ flex: 1;
+ flex-direction: row;
+ overflow: auto;
+}
+
+/* left */
+
+.Timeline>.box>.left {
+ position: sticky;
+ left: 0;
+ width: 100px;
+ background-color: #fff;
+ border-right: 1px solid #ddd;
+ box-sizing: border-box;
+ padding-top: 33px;
+ z-index: 10;
+}
+
+.Timeline>.box>.left>.info {
+ position: relative;
+ height: 24px;
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ justify-content: flex-start;
+ border-bottom: 1px solid #ddd;
+ box-sizing: border-box;
+}
+
+.Timeline>.box>.left>.info:first-child {
+ border-top: 1px solid #ddd;
+}
+
+/* right */
+
+.Timeline>.box>.right {
+ position: relative;
+ display: flex;
+ flex: 1;
+ flex-direction: column;
+}
+
+.Timeline>.box>.right>.timeline {
+ position: absolute;
+ height: 32px;
+}
+
+.Timeline>.box>.right>.layers {
+ position: absolute;
+ top: 32px;
+ width: 100%;
+ flex: 1;
+ border-top: 1px solid #ddd;
+ box-sizing: border-box;
+}
+
+.Timeline>.box>.right>.layers>.layer {
+ position: relative;
+ width: 100%;
+ height: 24px;
+ border-bottom: 1px solid #ddd;
+ box-sizing: border-box;
+}
+
+.Timeline>.box>.right>.layers>.layer>.item {
+ position: absolute;
+ left: 0;
+ width: 80px;
+ height: 100%;
+ color: #fff;
+ background: #2c3e50;
+ box-sizing: border-box;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ border-radius: 2px;
+}
+
+.Timeline>.box>.right>.layers>.layer>.item>.smaller {
+ transform: scale(0.9);
+}
+
+.Timeline>.box>.right>.slider {
+ position: absolute;
+ left: 0;
+ top: 0;
+ width: 0;
+ height: 100%;
+ border: 1px solid red;
+ box-sizing: border-box;
+}
\ No newline at end of file
diff --git a/ShadowEditor.Web/src/ui/timeline/private/TimelineControl.jsx b/ShadowEditor.Web/src/ui/timeline/private/TimelineControl.jsx
new file mode 100644
index 00000000..7b98a3c0
--- /dev/null
+++ b/ShadowEditor.Web/src/ui/timeline/private/TimelineControl.jsx
@@ -0,0 +1,53 @@
+import './css/TimelineControl.css';
+import classNames from 'classnames/bind';
+import PropTypes from 'prop-types';
+
+import Toolbar from '../../toolbar/Toolbar.jsx';
+import ToolbarFiller from '../../toolbar/ToolbarFiller.jsx';
+import ToolbarSeparator from '../../toolbar/ToolbarSeparator.jsx';
+import IconButton from '../../form/IconButton.jsx';
+import Label from '../../form/Label.jsx';
+
+/**
+ * 时间轴控制
+ * @author tengge / https://github.com/tengge1
+ */
+class TimelineControl extends React.Component {
+ constructor(props) {
+ super(props);
+ }
+
+ render() {
+ const { className, style, tip } = this.props;
+
+ return
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ;
+ }
+}
+
+TimelineControl.propTypes = {
+ className: PropTypes.string,
+ style: PropTypes.object,
+ tip: PropTypes.string,
+};
+
+TimelineControl.defaultProps = {
+ className: null,
+ style: null,
+ tip: 'Double click the area below to add animation.',
+};
+
+export default TimelineControl;
\ No newline at end of file
diff --git a/ShadowEditor.Web/src/ui/timeline/private/css/TimelineControl.css b/ShadowEditor.Web/src/ui/timeline/private/css/TimelineControl.css
new file mode 100644
index 00000000..0ef2f562
--- /dev/null
+++ b/ShadowEditor.Web/src/ui/timeline/private/css/TimelineControl.css
@@ -0,0 +1,7 @@
+.TimelineControl>.IconButton {
+ border: none;
+ width: 24px;
+ height: 24px;
+ margin: 0 4px;
+ padding: 0;
+}
\ No newline at end of file
diff --git a/ShadowEditor.Web/src/ui/toolbar/Toolbar.jsx b/ShadowEditor.Web/src/ui/toolbar/Toolbar.jsx
new file mode 100644
index 00000000..1ce6598c
--- /dev/null
+++ b/ShadowEditor.Web/src/ui/toolbar/Toolbar.jsx
@@ -0,0 +1,33 @@
+import './css/Toolbar.css';
+import classNames from 'classnames/bind';
+import PropTypes from 'prop-types';
+
+/**
+ * 工具栏
+ * @author tengge / https://github.com/tengge1
+ */
+class Toolbar extends React.Component {
+ render() {
+ const { className, style, children, direction } = this.props;
+
+ return {children}
;
+ }
+}
+
+Toolbar.propTypes = {
+ className: PropTypes.string,
+ style: PropTypes.object,
+ direction: PropTypes.oneOf(['horizontal', 'vertical']),
+ children: PropTypes.node,
+};
+
+Toolbar.defaultProps = {
+ className: null,
+ style: null,
+ direction: 'horizontal',
+ children: null,
+};
+
+export default Toolbar;
\ No newline at end of file
diff --git a/ShadowEditor.Web/src/ui/toolbar/ToolbarFiller.jsx b/ShadowEditor.Web/src/ui/toolbar/ToolbarFiller.jsx
new file mode 100644
index 00000000..05e67754
--- /dev/null
+++ b/ShadowEditor.Web/src/ui/toolbar/ToolbarFiller.jsx
@@ -0,0 +1,27 @@
+import './css/ToolbarFiller.css';
+import classNames from 'classnames/bind';
+import PropTypes from 'prop-types';
+
+/**
+ * 工具栏填充
+ * @author tengge / https://github.com/tengge1
+ */
+class ToolbarFiller extends React.Component {
+ render() {
+ const { className, style } = this.props;
+
+ return ;
+ }
+}
+
+ToolbarFiller.propTypes = {
+ className: PropTypes.string,
+ style: PropTypes.object,
+};
+
+ToolbarFiller.defaultProps = {
+ className: null,
+ style: null,
+};
+
+export default ToolbarFiller;
\ No newline at end of file
diff --git a/ShadowEditor.Web/src/ui/toolbar/ToolbarSeparator.jsx b/ShadowEditor.Web/src/ui/toolbar/ToolbarSeparator.jsx
new file mode 100644
index 00000000..76b153a4
--- /dev/null
+++ b/ShadowEditor.Web/src/ui/toolbar/ToolbarSeparator.jsx
@@ -0,0 +1,28 @@
+import classNames from 'classnames/bind';
+import PropTypes from 'prop-types';
+
+/**
+ * 工具栏分割线
+ * @author tengge / https://github.com/tengge1
+ */
+class ToolbarSeparator extends React.Component {
+ render() {
+ const { className, style } = this.props;
+
+ return ;
+ }
+}
+
+ToolbarSeparator.propTypes = {
+ className: PropTypes.string,
+ style: PropTypes.object,
+};
+
+ToolbarSeparator.defaultProps = {
+ className: null,
+ style: null,
+};
+
+export default ToolbarSeparator;
\ No newline at end of file
diff --git a/ShadowEditor.Web/src/ui/toolbar/css/Toolbar.css b/ShadowEditor.Web/src/ui/toolbar/css/Toolbar.css
new file mode 100644
index 00000000..b17353bc
--- /dev/null
+++ b/ShadowEditor.Web/src/ui/toolbar/css/Toolbar.css
@@ -0,0 +1,52 @@
+.Toolbar {
+ background: #fafafa;
+ display: flex !important;
+ align-items: center;
+ box-sizing: border-box;
+ user-select: none;
+ overflow: hidden;
+}
+
+.Toolbar.horizontal {
+ width: 100%;
+ height: 25px;
+ flex-direction: row;
+ background: linear-gradient(to bottom, rgb(250, 252, 253), rgb(232, 241, 251) 40%, rgb(220, 230, 243) 40%, rgb(220, 231, 245));
+}
+
+.Toolbar.vertical {
+ width: 32px;
+ height: 100%;
+ flex-direction: column;
+ background: #fafafa;
+}
+
+.Toolbar .Icon {
+ width: 32px;
+ height: 32px;
+ line-height: 32px;
+ text-align: center;
+}
+
+/* ToolbarSeparator */
+
+.ToolbarSeparator {
+ white-space: nowrap;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+}
+
+.ToolbarSeparator>.separator {
+ width: 2px;
+ height: 24px;
+ background: rgb(229, 229, 229);
+ background-image: linear-gradient(to right, rgb(226, 226, 226), rgb(226, 226, 226) 50%, rgb(252, 252, 252) 50%, rgb(252, 252, 252));
+}
+
+.Toolbar.vertical>.ToolbarSeparator>.separator {
+ width: 24px;
+ height: 2px;
+ background: rgb(229, 229, 229);
+ background-image: linear-gradient(to bottom, rgb(226, 226, 226), rgb(226, 226, 226) 50%, rgb(252, 252, 252) 50%, rgb(252, 252, 252));
+}
\ No newline at end of file
diff --git a/ShadowEditor.Web/src/ui/toolbar/css/ToolbarFiller.css b/ShadowEditor.Web/src/ui/toolbar/css/ToolbarFiller.css
new file mode 100644
index 00000000..50519b5d
--- /dev/null
+++ b/ShadowEditor.Web/src/ui/toolbar/css/ToolbarFiller.css
@@ -0,0 +1,3 @@
+.ToolbarFiller {
+ flex: 1;
+}
\ No newline at end of file
diff --git a/ShadowEditor.Web/src/ui/tree/Tree.jsx b/ShadowEditor.Web/src/ui/tree/Tree.jsx
new file mode 100644
index 00000000..d0519c1a
--- /dev/null
+++ b/ShadowEditor.Web/src/ui/tree/Tree.jsx
@@ -0,0 +1,208 @@
+import './css/Tree.css';
+import classNames from 'classnames/bind';
+import PropTypes from 'prop-types';
+
+import CheckBox from '../form/CheckBox.jsx';
+
+/**
+ * 树
+ * @author tengge / https://github.com/tengge1
+ */
+class Tree extends React.Component {
+ constructor(props) {
+ super(props);
+
+ const { onExpand, onSelect, onDoubleClick, onDrop } = this.props;
+
+ this.handleExpandNode = this.handleExpandNode.bind(this, onExpand);
+ this.handleClick = this.handleClick.bind(this, onSelect);
+ this.handleDoubleClick = this.handleDoubleClick.bind(this, onDoubleClick);
+
+ this.handleDrag = this.handleDrag.bind(this);
+ this.handleDragStart = this.handleDragStart.bind(this);
+ this.handleDragOver = this.handleDragOver.bind(this);
+ this.handleDragLeave = this.handleDragLeave.bind(this);
+ this.handleDrop = this.handleDrop.bind(this, onDrop);
+ }
+
+ render() {
+ const { data, className, style } = this.props;
+
+ // 创建节点
+ var list = [];
+
+ Array.isArray(data) && data.forEach(n => {
+ list.push(this.createNode(n));
+ });
+
+ return ;
+ }
+
+ createNode(data) {
+ const leaf = !data.children || data.children.length === 0;
+
+ const children = leaf ? null : ({data.children.map(n => {
+ return this.createNode(n);
+ })}
);
+
+ let checkbox = null;
+
+ if (data.checked === true || data.checked === false) {
+ checkbox = ;
+ }
+
+ return
+
+ {checkbox}
+
+ {data.text}
+ {leaf ? null : children}
+ ;
+ }
+
+ handleExpandNode(onExpand, event) {
+ event.stopPropagation();
+ const value = event.target.getAttribute('value');
+
+ onExpand && onExpand(value, event);
+ }
+
+ handleClick(onSelect, event) {
+ var value = event.target.getAttribute('value');
+ if (value) {
+ onSelect && onSelect(value, event);
+ }
+ }
+
+ handleDoubleClick(onDoubleClick, event) {
+ var value = event.target.getAttribute('value');
+ if (value) {
+ onDoubleClick && onDoubleClick(value, event);
+ }
+ }
+
+ // --------------------- 拖拽事件 ---------------------------
+
+ handleDrag(event) {
+ event.stopPropagation();
+ this.currentDrag = event.currentTarget;
+ }
+
+ handleDragStart(event) {
+ event.stopPropagation();
+ event.dataTransfer.setData('text', 'foo');
+ }
+
+ handleDragOver(event) {
+ event.preventDefault();
+ event.stopPropagation();
+
+ var target = event.currentTarget;
+
+ if (target === this.currentDrag) {
+ return;
+ }
+
+ var area = event.nativeEvent.offsetY / target.clientHeight;
+
+ if (area < 0.25) {
+ target.classList.add('dragTop');
+ } else if (area > 0.75) {
+ target.classList.add('dragBottom');
+ } else {
+ target.classList.add('drag');
+ }
+ }
+
+ handleDragLeave(event) {
+ event.preventDefault();
+ event.stopPropagation();
+
+ var target = event.currentTarget;
+
+ if (target === this.currentDrag) {
+ return;
+ }
+
+ target.classList.remove('dragTop');
+ target.classList.remove('dragBottom');
+ target.classList.remove('drag');
+ }
+
+ handleDrop(onDrop, event) {
+ event.preventDefault();
+ event.stopPropagation();
+
+ var target = event.currentTarget;
+
+ if (target === this.currentDrag) {
+ return;
+ }
+
+ target.classList.remove('dragTop');
+ target.classList.remove('dragBottom');
+ target.classList.remove('drag');
+
+ if (typeof (onDrop) === 'function') {
+ const area = event.nativeEvent.offsetY / target.clientHeight;
+
+ const currentValue = this.currentDrag.getAttribute('value');
+
+ if (area < 0.25) { // 放在当前元素前面
+ onDrop(
+ currentValue, // 拖动要素
+ target.parentNode.parentNode.getAttribute('value'), // 新位置父级
+ target.getAttribute('value'), // 新位置索引
+ ); // 拖动, 父级, 索引
+ } else if (area > 0.75) { // 放在当前元素后面
+ onDrop(
+ currentValue,
+ target.parentNode.parentNode.getAttribute('value'),
+ target.nextSibling == null ? null : target.nextSibling.getAttribute('value'), // target.nextSibling为null,说明是最后一个位置
+ );
+ } else { // 成为该元素子级
+ onDrop(
+ currentValue,
+ target.getAttribute('value'),
+ null,
+ );
+ }
+ }
+ }
+}
+
+Tree.propTypes = {
+ className: PropTypes.string,
+ style: PropTypes.object,
+ data: PropTypes.array,
+ selected: PropTypes.string,
+ onExpand: PropTypes.func,
+ onSelect: PropTypes.func,
+ onDoubleClick: PropTypes.func,
+ onDrop: PropTypes.func,
+};
+
+Tree.defaultProps = {
+ className: null,
+ style: null,
+ data: [],
+ selected: null,
+ onExpand: null,
+ onSelect: null,
+ onDoubleClick: null,
+ onDrop: null,
+};
+
+export default Tree;
\ No newline at end of file
diff --git a/ShadowEditor.Web/src/ui/tree/css/Tree.css b/ShadowEditor.Web/src/ui/tree/css/Tree.css
new file mode 100644
index 00000000..5360945e
--- /dev/null
+++ b/ShadowEditor.Web/src/ui/tree/css/Tree.css
@@ -0,0 +1,103 @@
+.Tree {
+ list-style: none;
+ margin: 0;
+ padding: 0;
+ font: 12px 'Microsoft YaHei';
+ line-height: 18px;
+ --icon-plus: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAkAAAAJCAMAAADXT/YiAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAA2ZpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMy1jMDExIDY2LjE0NTY2MSwgMjAxMi8wMi8wNi0xNDo1NjoyNyAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iIHhtbG5zOnN0UmVmPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VSZWYjIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtcE1NOk9yaWdpbmFsRG9jdW1lbnRJRD0ieG1wLmRpZDpCNzY5QkUzNTgzNzVFOTExOEU2NkEzOTNDMkUxQ0UzNiIgeG1wTU06RG9jdW1lbnRJRD0ieG1wLmRpZDpGNjVEQzExQzc2NDYxMUU5OEMxN0UxQ0QyRDMwMjk0NyIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDpGNjVEQzExQjc2NDYxMUU5OEMxN0UxQ0QyRDMwMjk0NyIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgQ1M2IChXaW5kb3dzKSI+IDx4bXBNTTpEZXJpdmVkRnJvbSBzdFJlZjppbnN0YW5jZUlEPSJ4bXAuaWlkOkI4NjlCRTM1ODM3NUU5MTE4RTY2QTM5M0MyRTFDRTM2IiBzdFJlZjpkb2N1bWVudElEPSJ4bXAuZGlkOkI3NjlCRTM1ODM3NUU5MTE4RTY2QTM5M0MyRTFDRTM2Ii8+IDwvcmRmOkRlc2NyaXB0aW9uPiA8L3JkZjpSREY+IDwveDp4bXBtZXRhPiA8P3hwYWNrZXQgZW5kPSJyIj8+GSEyxQAAAI1QTFRFCQsNxr6u39vSDhIWExgca4ehM0FNIyw1Q1Rla4egZYCZTmN11tHG2NPJKzdBZH6WPU5cX3iPSl5wWHCF9fXxFBofDREUUmd7z8i7VWyBRlhp6unjHCQqGSAmLTpF7e3nHygw5eHaXXaMJzE5CAoM0sy/5OHZ3NjP8PDswrio/f379/f1////AAAA////NGgXgAAAAC90Uk5T/////////////////////////////////////////////////////////////wBapTj3AAAAWUlEQVR42iTBBxKCQBAEwAExIYiIBFGScIFd9/7/POqKbrhjnBf18+Jw/e9OyIhIiPiOHzML2+mM90u88IubtYN8tCnRapNKopoDHipYPcD189hF1eI2AQYAwn4J7uCjPfoAAAAASUVORK5CYII=);
+ --icon-minus: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAkAAAAJCAMAAADXT/YiAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAA2ZpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMy1jMDExIDY2LjE0NTY2MSwgMjAxMi8wMi8wNi0xNDo1NjoyNyAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iIHhtbG5zOnN0UmVmPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VSZWYjIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtcE1NOk9yaWdpbmFsRG9jdW1lbnRJRD0ieG1wLmRpZDpCNzY5QkUzNTgzNzVFOTExOEU2NkEzOTNDMkUxQ0UzNiIgeG1wTU06RG9jdW1lbnRJRD0ieG1wLmRpZDowRTM3OTM2RDc2NDcxMUU5OUYyREZCNzdBMzZGQTU0QSIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDowRTM3OTM2Qzc2NDcxMUU5OUYyREZCNzdBMzZGQTU0QSIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgQ1M2IChXaW5kb3dzKSI+IDx4bXBNTTpEZXJpdmVkRnJvbSBzdFJlZjppbnN0YW5jZUlEPSJ4bXAuaWlkOkI4NjlCRTM1ODM3NUU5MTE4RTY2QTM5M0MyRTFDRTM2IiBzdFJlZjpkb2N1bWVudElEPSJ4bXAuZGlkOkI3NjlCRTM1ODM3NUU5MTE4RTY2QTM5M0MyRTFDRTM2Ii8+IDwvcmRmOkRlc2NyaXB0aW9uPiA8L3JkZjpSREY+IDwveDp4bXBtZXRhPiA8P3hwYWNrZXQgZW5kPSJyIj8+7URHdgAAAHtQTFRFDxMW9fXxJC01KzdBJC42XHWLTWFzXHSLb4yoTWF0DhIWdJSw6unjVWt/KzhBNEFOVGt/xr6uz8i7dJOvNEJO5eHab42n39vSFRof7e3nHCMqY32W0sy/5OHZ3NjPPExaaoaf8PDs2NPJwrio/f379/f1AAAA////////MX4KXQAAACl0Uk5T/////////////////////////////////////////////////////wBS9CCHAAAATUlEQVR42iTBBRKAIAAEwLO7i1ZQlP+/0EF34ar84ElfOEzPb8d6feyMw3p6icCD29tCZFpTWkvF0EhljBEjQSqG0yvh9q6NGYF7BRgAle0Iqns528wAAAAASUVORK5CYII=);
+ --icon-node: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA4AAAAPCAMAAADjyg5GAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAA2ZpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMy1jMDExIDY2LjE0NTY2MSwgMjAxMi8wMi8wNi0xNDo1NjoyNyAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iIHhtbG5zOnN0UmVmPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VSZWYjIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtcE1NOk9yaWdpbmFsRG9jdW1lbnRJRD0ieG1wLmRpZDpCNzY5QkUzNTgzNzVFOTExOEU2NkEzOTNDMkUxQ0UzNiIgeG1wTU06RG9jdW1lbnRJRD0ieG1wLmRpZDo2MTQyNjRFRDc2NDYxMUU5QTdGRjlBOUM1MTgxQUEyNCIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDo2MTQyNjRFQzc2NDYxMUU5QTdGRjlBOUM1MTgxQUEyNCIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgQ1M2IChXaW5kb3dzKSI+IDx4bXBNTTpEZXJpdmVkRnJvbSBzdFJlZjppbnN0YW5jZUlEPSJ4bXAuaWlkOkI4NjlCRTM1ODM3NUU5MTE4RTY2QTM5M0MyRTFDRTM2IiBzdFJlZjpkb2N1bWVudElEPSJ4bXAuZGlkOkI3NjlCRTM1ODM3NUU5MTE4RTY2QTM5M0MyRTFDRTM2Ii8+IDwvcmRmOkRlc2NyaXB0aW9uPiA8L3JkZjpSREY+IDwveDp4bXBtZXRhPiA8P3hwYWNrZXQgZW5kPSJyIj8+2JwdnQAAAUdQTFRF2tra39/f1tbWysrK49zf8vLy3d3dxsbG7OzstbCyz8/P4+Hiz8nMzc3NvL282NjYiIeI+/v78Ors4dvd4dvempaXyMjI1tPV8vDxwry/t7K0xMLD3Nzci4eJycnJ3NXY4uLi8/Pzd3d31tHTcW5vz87Pzs7O0s7QsbGx2dPW6OLl6efo9PPz8ertpqam493gu7u7x8fH4tvegn6A3dfZxcXFpqKjycPG29TX5N3gwcHB1M7R1c/R2dnZ9fT08PDwxsDDvr290NDQwcDA6eLl2tPW19fX0dDQ9u/yxb/C4eHh8Ons19HU7ufq1dXV9Ozvwbu+u7a4ysTG8/Hy29zb/Pz839jbsq2vgX1/5+bnx8HEpqGj0MrMrqiqvb293NbZmJiY8/LztrO0jIyM+ff4zs/O7u7u9O3w7ezt/f39/v7+////////roPGVgAAAG10Uk5T////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////AC221EsAAADFSURBVHjaYsgBgqwcbmEhQSCVwwDkZeZwi3j46ORkg7lZOdpG6WHiIEkgNytH1dPfWspcAqw4KycjQiUg0MFGPgWoGshNNIvmjIpxddGyy8lmABoULBnEo2npyxOZkwUymdVd2YLFO14aZFR2GoMfn6O+rkmSEz9HNgMvKwO7RrIoS3iCgZu9HAObKSMDM6exrIAeF6MVE9AoNeYQ9VilOAVeQ0WQRTkyYqnOTBzsTEDngh2ZY8vGwBWK8EKOFyNYOAcgwADsYjo583VUugAAAABJRU5ErkJggg==);
+ --folder-open: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAANCAMAAACXZR4WAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAA2ZpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMy1jMDExIDY2LjE0NTY2MSwgMjAxMi8wMi8wNi0xNDo1NjoyNyAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iIHhtbG5zOnN0UmVmPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VSZWYjIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtcE1NOk9yaWdpbmFsRG9jdW1lbnRJRD0ieG1wLmRpZDpCNzY5QkUzNTgzNzVFOTExOEU2NkEzOTNDMkUxQ0UzNiIgeG1wTU06RG9jdW1lbnRJRD0ieG1wLmRpZDpGN0MzMTAyMDc2NDUxMUU5OTAzM0U1RjFGODU1RURCQSIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDpGN0MzMTAxRjc2NDUxMUU5OTAzM0U1RjFGODU1RURCQSIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgQ1M2IChXaW5kb3dzKSI+IDx4bXBNTTpEZXJpdmVkRnJvbSBzdFJlZjppbnN0YW5jZUlEPSJ4bXAuaWlkOkI4NjlCRTM1ODM3NUU5MTE4RTY2QTM5M0MyRTFDRTM2IiBzdFJlZjpkb2N1bWVudElEPSJ4bXAuZGlkOkI3NjlCRTM1ODM3NUU5MTE4RTY2QTM5M0MyRTFDRTM2Ii8+IDwvcmRmOkRlc2NyaXB0aW9uPiA8L3JkZjpSREY+IDwveDp4bXBtZXRhPiA8P3hwYWNrZXQgZW5kPSJyIj8+orCXfgAAAJZQTFRFzJk0zc3NoG4Iy5gz+vv8yZYx//+lxcXFmmgCnGoEnmwG2r9spHIMvYol/dVwp3UPmWcByaAhtYIdsH4Yt7e3yJUw9/CJt4Qfxpoa/9eE4rNDo3EL0dHRrHoUs4Ebwo8qonAK4sFQ8OR6uoci17hA/+uEz6ImpXMN/+R//9t1/++JxMTE//iTxpoZ//+c//+Z////////7K5NWgAAADJ0Uk5T/////////////////////////////////////////////////////////////////wANUJjvAAAAgElEQVR42kzLVxLCMAxFUTk9oYXeW+gY2dL+N4ecGA9n9HXnCZgZIE7n7IEcESXJMQQg49DgspwcphLiLkgS+x1wSp/AjHLAP+fbVgNa/2FsD9dRG377BQ4rDYz27WXjTdkG8+xk13shQcrLa1Z17sLD6Z+iqixmSgIrHSj+CjAAb/seguUxx1gAAAAASUVORK5CYII=);
+ --folder-close: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA8AAAANCAMAAABBwMRzAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAA2ZpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMy1jMDExIDY2LjE0NTY2MSwgMjAxMi8wMi8wNi0xNDo1NjoyNyAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iIHhtbG5zOnN0UmVmPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VSZWYjIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtcE1NOk9yaWdpbmFsRG9jdW1lbnRJRD0ieG1wLmRpZDpCNzY5QkUzNTgzNzVFOTExOEU2NkEzOTNDMkUxQ0UzNiIgeG1wTU06RG9jdW1lbnRJRD0ieG1wLmRpZDpEQTA0NkVFNDc2NDUxMUU5ODVCQ0E0NDExMUZBOTUzQiIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDpEQTA0NkVFMzc2NDUxMUU5ODVCQ0E0NDExMUZBOTUzQiIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgQ1M2IChXaW5kb3dzKSI+IDx4bXBNTTpEZXJpdmVkRnJvbSBzdFJlZjppbnN0YW5jZUlEPSJ4bXAuaWlkOkI4NjlCRTM1ODM3NUU5MTE4RTY2QTM5M0MyRTFDRTM2IiBzdFJlZjpkb2N1bWVudElEPSJ4bXAuZGlkOkI3NjlCRTM1ODM3NUU5MTE4RTY2QTM5M0MyRTFDRTM2Ii8+IDwvcmRmOkRlc2NyaXB0aW9uPiA8L3JkZjpSREY+IDwveDp4bXBtZXRhPiA8P3hwYWNrZXQgZW5kPSJyIj8+eRolvgAAAIFQTFRF09PTmWcBy5gzwo8q27dxnmwGs4EbuocitYIdvYol//+6zMzMnGoEsH4YyZYxoG4Io3ELp3UPqHYQrHoUt4QfwI0opXMNq3kTx5QvyJUw1tbWrnwWwMDAmmgCxZItzJk027dS/9Rv//iT//+c/9t1/+R//++Jy8vL//////+Z////MJO0LgAAACt0Uk5T////////////////////////////////////////////////////////ACPJp9AAAABxSURBVHjaTM5XEoJAEEXRR44CYk4MmHpe73+BYpV2cf7u34Wqc2Fz0h+o896XZfRvF49fcZFt83SlCP3C5oyGD8N1hSMXOsHAu2EriPg2TAQ7vgxrQcGn4U2QcTIMKuh8cTn0+65N6uAKzItioB8BBgD8YxgHc9UPOwAAAABJRU5ErkJggg==);
+ overflow-y: auto;
+}
+
+.Tree .node {
+ background: #fff;
+}
+
+.Tree .node.selected {
+ background: #34495e;
+}
+
+.Tree .node.drag {
+ border: 1px dashed #999;
+}
+
+.Tree .node.dragTop {
+ border-top: 1px dashed #999;
+}
+
+.Tree .node.dragBottom {
+ border-bottom: 1px dashed #999;
+}
+
+.Tree .node>i {
+ display: inline-block;
+ vertical-align: middle;
+}
+
+.Tree .node>i.expand {
+ width: 9px;
+ height: 9px;
+}
+
+.Tree .node>i.expand.plus {
+ background: var(--icon-plus);
+}
+
+.Tree .node>i.expand.minus {
+ background: var(--icon-minus);
+}
+
+.Tree .node>i.type {
+ margin-left: 4px;
+}
+
+.Tree .node>i.type.node {
+ width: 14px;
+ height: 15px;
+ margin-right: 2px;
+ background: var(--icon-node);
+}
+
+.Tree .node>i.type.open {
+ width: 16px;
+ height: 13px;
+ background: var(--folder-open);
+}
+
+.Tree .node>i.type.close {
+ width: 15px;
+ height: 13px;
+ margin-right: 1px;
+ background: var(--folder-close);
+}
+
+.Tree .node>.CheckBox {
+ margin: 2px -2px 2px 4px;
+ vertical-align: middle;
+}
+
+.Tree .node>a {
+ color: #555;
+ margin-left: 4px;
+ text-decoration: none;
+ vertical-align: middle;
+ pointer-events: none;
+}
+
+.Tree .node.selected>a {
+ color: #fff;
+}
+
+.Tree .node>.sub {
+ margin: 0;
+ padding: 0 0 0 16px;
+ background: #fff;
+ list-style: none;
+}
+
+.Tree .node>.sub.collpase {
+ display: none;
+}
\ No newline at end of file
diff --git a/ShadowEditor.Web/src/ui/window/Alert.jsx b/ShadowEditor.Web/src/ui/window/Alert.jsx
new file mode 100644
index 00000000..fe56e4c6
--- /dev/null
+++ b/ShadowEditor.Web/src/ui/window/Alert.jsx
@@ -0,0 +1,71 @@
+import './css/Alert.css';
+import classNames from 'classnames/bind';
+import PropTypes from 'prop-types';
+
+import Window from './Window.jsx';
+import Content from '../common/Content.jsx';
+import Buttons from '../common/Buttons.jsx';
+import Button from '../form/Button.jsx';
+
+/**
+ * 提示框
+ */
+class Alert extends React.Component {
+ constructor(props) {
+ super(props);
+
+ this.handleOK = this.handleOK.bind(this, props.onOK);
+ this.handleClose = this.handleClose.bind(this, props.onClose);
+ }
+
+ render() {
+ const { className, style, title, children, hidden, mask, okText } = this.props;
+
+ return
+ {children}
+
+
+
+ ;
+ }
+
+ handleOK(onOK, event) {
+ onOK && onOK(event);
+ }
+
+ handleClose(onClose, event) {
+ onClose && onClose(event);
+ }
+}
+
+Alert.propTypes = {
+ className: PropTypes.string,
+ style: PropTypes.object,
+ title: PropTypes.string,
+ children: PropTypes.node,
+ hidden: PropTypes.bool,
+ mask: PropTypes.bool,
+ okText: PropTypes.string,
+ onOK: PropTypes.func,
+ onClose: PropTypes.func,
+};
+
+Alert.defaultProps = {
+ className: null,
+ style: null,
+ title: 'Message',
+ children: null,
+ hidden: false,
+ mask: false,
+ okText: 'OK',
+ onOK: null,
+ onClose: null,
+};
+
+export default Alert;
\ No newline at end of file
diff --git a/ShadowEditor.Web/src/ui/window/Confirm.jsx b/ShadowEditor.Web/src/ui/window/Confirm.jsx
new file mode 100644
index 00000000..d6d40ca2
--- /dev/null
+++ b/ShadowEditor.Web/src/ui/window/Confirm.jsx
@@ -0,0 +1,81 @@
+import './css/Confirm.css';
+import classNames from 'classnames/bind';
+import PropTypes from 'prop-types';
+
+import Window from './Window.jsx';
+import Content from '../common/Content.jsx';
+import Buttons from '../common/Buttons.jsx';
+import Button from '../form/Button.jsx';
+
+/**
+ * 询问框
+ */
+class Confirm extends React.Component {
+ constructor(props) {
+ super(props);
+
+ this.handleOK = this.handleOK.bind(this, props.onOK);
+ this.handleCancel = this.handleCancel.bind(this, props.onCancel);
+ this.handleClose = this.handleClose.bind(this, props.onClose);
+ }
+
+ render() {
+ const { className, style, title, children, hidden, mask, okText, cancelText } = this.props;
+
+ return
+ {children}
+
+
+
+
+ ;
+ }
+
+ handleOK(onOK, event) {
+ onOK && onOK(event);
+ }
+
+ handleCancel(onCancel, event) {
+ onCancel && onCancel(event);
+ }
+
+ handleClose(onClose, event) {
+ onClose && onClose(event);
+ }
+}
+
+Confirm.propTypes = {
+ className: PropTypes.string,
+ style: PropTypes.object,
+ title: PropTypes.string,
+ children: PropTypes.node,
+ hidden: PropTypes.bool,
+ mask: PropTypes.bool,
+ okText: PropTypes.string,
+ cancelText: PropTypes.string,
+ onOK: PropTypes.func,
+ onCancel: PropTypes.func,
+ onClose: PropTypes.func,
+};
+
+Confirm.defaultProps = {
+ className: null,
+ style: null,
+ title: 'Confirm',
+ children: null,
+ hidden: false,
+ mask: false,
+ okText: 'OK',
+ cancelText: 'Cancel',
+ onOK: null,
+ onCancel: null,
+ onClose: null,
+};
+
+export default Confirm;
\ No newline at end of file
diff --git a/ShadowEditor.Web/src/ui/window/Prompt.jsx b/ShadowEditor.Web/src/ui/window/Prompt.jsx
new file mode 100644
index 00000000..e5e71ccc
--- /dev/null
+++ b/ShadowEditor.Web/src/ui/window/Prompt.jsx
@@ -0,0 +1,86 @@
+import './css/Prompt.css';
+import classNames from 'classnames/bind';
+import PropTypes from 'prop-types';
+
+import Window from './Window.jsx';
+import Content from '../common/Content.jsx';
+import Input from '../form/Input.jsx';
+import Buttons from '../common/Buttons.jsx';
+import Button from '../form/Button.jsx';
+
+/**
+ * 弹窗输入框
+ */
+class Prompt extends React.Component {
+ constructor(props) {
+ super(props);
+
+ this.state = {
+ value: props.value,
+ };
+
+ this.handleOK = this.handleOK.bind(this, props.onOK);
+ this.handleClose = this.handleClose.bind(this, props.onClose);
+ this.handleChange = this.handleChange.bind(this);
+ }
+
+ render() {
+ const { className, style, title, content, hidden, mask, okText } = this.props;
+
+ return
+
+ {content}
+
+
+
+
+
+ ;
+ }
+
+ handleOK(onOK, event) {
+ onOK && onOK(this.state.value, event);
+ }
+
+ handleClose(onClose, event) {
+ onClose && onClose(event);
+ }
+
+ handleChange(value) {
+ this.setState({ value });
+ }
+}
+
+Prompt.propTypes = {
+ className: PropTypes.string,
+ style: PropTypes.object,
+ title: PropTypes.string,
+ content: PropTypes.node,
+ value: PropTypes.string,
+ hidden: PropTypes.bool,
+ mask: PropTypes.bool,
+ okText: PropTypes.string,
+ onOK: PropTypes.func,
+ onClose: PropTypes.func,
+};
+
+Prompt.defaultProps = {
+ className: null,
+ style: null,
+ title: 'Prompt',
+ content: null,
+ value: '',
+ hidden: false,
+ mask: false,
+ okText: 'OK',
+ onOK: null,
+ onClose: null,
+};
+
+export default Prompt;
\ No newline at end of file
diff --git a/ShadowEditor.Web/src/ui/window/Toast.jsx b/ShadowEditor.Web/src/ui/window/Toast.jsx
new file mode 100644
index 00000000..a549e19a
--- /dev/null
+++ b/ShadowEditor.Web/src/ui/window/Toast.jsx
@@ -0,0 +1,36 @@
+import './css/Toast.css';
+import classNames from 'classnames/bind';
+import PropTypes from 'prop-types';
+
+/**
+ * 提示窗
+ */
+class Toast extends React.Component {
+ constructor(props) {
+ super(props);
+ }
+
+ render() {
+ const { className, style, children } = this.props;
+
+ return ;
+ }
+}
+
+Toast.propTypes = {
+ className: PropTypes.string,
+ style: PropTypes.object,
+ children: PropTypes.node,
+};
+
+Toast.defaultProps = {
+ className: null,
+ style: null,
+ children: null,
+};
+
+export default Toast;
\ No newline at end of file
diff --git a/ShadowEditor.Web/src/ui/window/Window.jsx b/ShadowEditor.Web/src/ui/window/Window.jsx
new file mode 100644
index 00000000..77eef58c
--- /dev/null
+++ b/ShadowEditor.Web/src/ui/window/Window.jsx
@@ -0,0 +1,138 @@
+import './css/Window.css';
+import classNames from 'classnames/bind';
+import PropTypes from 'prop-types';
+
+import Content from '../common/Content.jsx';
+import Buttons from '../common/Buttons.jsx';
+
+/**
+ * 窗口
+ */
+class Window extends React.Component {
+ constructor(props) {
+ super(props);
+
+ this.dom = React.createRef();
+
+ this.isDown = false;
+ this.offsetX = 0;
+ this.offsetY = 0;
+
+ this.handleMouseDown = this.handleMouseDown.bind(this);
+ this.handleMouseMove = this.handleMouseMove.bind(this);
+ this.handleMouseUp = this.handleMouseUp.bind(this);
+ this.handleClose = this.handleClose.bind(this, props.onClose);
+ }
+
+ render() {
+ const { className, style, title, children, hidden, mask } = this.props;
+
+ let _children = null;
+
+ if (children && Array.isArray(children)) {
+ _children = children;
+ } else if (children) {
+ _children = [children];
+ }
+
+ const content = _children.filter(n => {
+ return n.type === Content;
+ })[0];
+
+ const buttons = _children.filter(n => {
+ return n.type === Buttons;
+ })[0];
+
+ return
+
+
+
+
{content && content.props.children}
+
+
+ {buttons && buttons.props.children}
+
+
+
+
+
;
+ }
+
+ handleMouseDown(event) {
+ this.isDown = true;
+
+ var dom = this.dom.current;
+ var left = dom.style.left === '' ? 0 : parseInt(dom.style.left.replace('px', ''));
+ var top = dom.style.top === '' ? 0 : parseInt(dom.style.top.replace('px', ''));
+
+ this.offsetX = event.clientX - left;
+ this.offsetY = event.clientY - top;
+ }
+
+ handleMouseMove(event) {
+ if (!this.isDown) {
+ return;
+ }
+
+ var dx = event.clientX - this.offsetX;
+ var dy = event.clientY - this.offsetY;
+
+ var dom = this.dom.current;
+ dom.style.left = `${dx}px`;
+ dom.style.top = `${dy}px`;
+ }
+
+ handleMouseUp(event) {
+ this.isDown = false;
+ this.offsetX = 0;
+ this.offsetY = 0;
+ }
+
+ handleClose(onClose, event) {
+ onClose && onClose(event);
+ }
+
+ componentDidMount() {
+ document.body.addEventListener('mousemove', this.handleMouseMove);
+ document.body.addEventListener('mouseup', this.handleMouseUp);
+ }
+
+ componentWillUnmount() {
+ document.body.removeEventListener('mousemove', this.handleMouseMove);
+ document.body.removeEventListener('mouseup', this.handleMouseUp);
+ }
+}
+
+Window.show = function () {
+
+}
+
+Window.propTypes = {
+ className: PropTypes.string,
+ style: PropTypes.object,
+ title: PropTypes.string,
+ children: PropTypes.node,
+ hidden: PropTypes.bool,
+ mask: PropTypes.bool,
+ onClose: PropTypes.func,
+};
+
+Window.defaultProps = {
+ className: null,
+ style: null,
+ title: 'Window',
+ children: null,
+ hidden: false,
+ mask: true,
+ onClose: null,
+};
+
+export default Window;
\ No newline at end of file
diff --git a/ShadowEditor.Web/src/ui/window/css/Alert.css b/ShadowEditor.Web/src/ui/window/css/Alert.css
new file mode 100644
index 00000000..c9bbad98
--- /dev/null
+++ b/ShadowEditor.Web/src/ui/window/css/Alert.css
@@ -0,0 +1,4 @@
+.Window.Alert {
+ width: 320px;
+ height: 180px;
+}
\ No newline at end of file
diff --git a/ShadowEditor.Web/src/ui/window/css/Confirm.css b/ShadowEditor.Web/src/ui/window/css/Confirm.css
new file mode 100644
index 00000000..fc3ba3e4
--- /dev/null
+++ b/ShadowEditor.Web/src/ui/window/css/Confirm.css
@@ -0,0 +1,4 @@
+.Window.Confirm {
+ width: 320px;
+ height: 180px;
+}
\ No newline at end of file
diff --git a/ShadowEditor.Web/src/ui/window/css/Prompt.css b/ShadowEditor.Web/src/ui/window/css/Prompt.css
new file mode 100644
index 00000000..9d974e14
--- /dev/null
+++ b/ShadowEditor.Web/src/ui/window/css/Prompt.css
@@ -0,0 +1,9 @@
+.Window.Prompt {
+ width: 320px;
+ height: 180px;
+}
+
+.Window.Prompt>.wrap>.content>.Input {
+ margin-left: 6px;
+ vertical-align: middle;
+}
\ No newline at end of file
diff --git a/ShadowEditor.Web/src/ui/window/css/Toast.css b/ShadowEditor.Web/src/ui/window/css/Toast.css
new file mode 100644
index 00000000..85b7c40a
--- /dev/null
+++ b/ShadowEditor.Web/src/ui/window/css/Toast.css
@@ -0,0 +1,22 @@
+.ToastMark {
+ position: fixed;
+ left: 0;
+ top: 0;
+ width: 100%;
+ height: 100%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ z-index: 1300;
+ pointer-events: none;
+}
+
+.Toast {
+ color: #fff;
+ background-color: rgba(0, 0, 0, 0.5);
+ padding: 16px 24px;
+ border-radius: 3px;
+ box-shadow: 1px 1px 1px rgba(0, 0, 0, 0.14);
+ display: inline-block;
+ pointer-events: none;
+}
\ No newline at end of file
diff --git a/ShadowEditor.Web/src/ui/window/css/Window.css b/ShadowEditor.Web/src/ui/window/css/Window.css
new file mode 100644
index 00000000..71909b7c
--- /dev/null
+++ b/ShadowEditor.Web/src/ui/window/css/Window.css
@@ -0,0 +1,105 @@
+.WindowMask {
+ position: fixed;
+ left: 0;
+ top: 0;
+ width: 100%;
+ height: 100%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ pointer-events: none;
+ z-index: 900;
+}
+
+.WindowMask.hidden {
+ display: none;
+}
+
+.WindowMask.mask {
+ background-color: rgba(0, 0, 0, 0.6);
+ pointer-events: all;
+ z-index: 1000;
+}
+
+.Window {
+ position: relative;
+ width: 600px;
+ height: 400px;
+ padding: 0px 5px 5px;
+ background: rgb(7, 97, 134);
+ box-shadow: rgba(0, 0, 0, 0.2) 0px 5px 10px 0px;
+ box-sizing: border-box;
+ pointer-events: all;
+ z-index: 200;
+}
+
+.Window .wrap {
+ position: relative;
+ width: 100%;
+ height: 100%;
+ background: #fff;
+}
+
+.Window .wrap .title {
+ font-size: 14px;
+ line-height: 24px;
+ color: #fff;
+ background: rgb(7, 97, 134);
+ text-align: left;
+ display: block;
+ cursor: move;
+ white-space: nowrap;
+ overflow: hidden;
+}
+
+.Window .wrap .title .controls {
+ position: absolute;
+ left: 0;
+ top: 0;
+ right: 4px;
+ text-align: right;
+ white-space: nowrap;
+ user-select: none;
+}
+
+.Window .wrap .title .controls .icon {
+ width: 16px;
+ height: 24px;
+ margin-right: 4px;
+ vertical-align: top;
+ display: inline-block;
+ cursor: pointer;
+}
+
+.Window .wrap .content {
+ position: absolute;
+ left: 20px;
+ right: 20px;
+ top: 44px;
+ bottom: 52px;
+ font-size: 12px;
+ overflow: hidden;
+}
+
+.Window .wrap .buttons {
+ position: absolute;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ background: rgb(218, 236, 244);
+ text-align: right;
+ white-space: nowrap;
+ overflow: hidden;
+}
+
+.Window .wrap .buttons .button-wrap {
+ position: relative;
+ height: 24px;
+ margin: 4px;
+ padding: 0;
+ border: none;
+ display: inline-block;
+ text-align: left;
+ vertical-align: middle;
+ box-sizing: border-box;
+}
\ No newline at end of file