init from https://github.com/Hufe921/canvas-editor
No files matched your search
@@ -0,0 +1,21 @@
|
||||
.ce-block-item {
|
||||
position: absolute;
|
||||
z-index: 0;
|
||||
background-color: #ffffff;
|
||||
border: 1px solid rgb(235 236 240);
|
||||
}
|
||||
|
||||
.ce-block-item .ce-resizer-selection {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.ce-block-item .ce-resizer-mask {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 1;
|
||||
background-color: transparent;
|
||||
}
|
||||
@@ -0,0 +1,196 @@
|
||||
.ce-contextmenu-container {
|
||||
z-index: 9;
|
||||
position: fixed;
|
||||
display: none;
|
||||
padding: 4px;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
background: #fff;
|
||||
box-shadow: 0 2px 12px 0 rgb(56 56 56 / 20%);
|
||||
border: 1px solid #e2e6ed;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.ce-contextmenu-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.ce-contextmenu-content .ce-contextmenu-sub-item::after {
|
||||
position: absolute;
|
||||
content: "";
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
right: 12px;
|
||||
background: url(../../images/submenu-dropdown.svg);
|
||||
}
|
||||
|
||||
.ce-contextmenu-content .ce-contextmenu-item {
|
||||
min-width: 140px;
|
||||
padding: 0 32px 0 16px;
|
||||
height: 30px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
white-space: nowrap;
|
||||
box-sizing: border-box;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.ce-contextmenu-content .ce-contextmenu-item.hover {
|
||||
background: rgba(25, 55, 88, .04);
|
||||
}
|
||||
|
||||
.ce-contextmenu-content .ce-contextmenu-item span {
|
||||
max-width: 300px;
|
||||
font-size: 12px;
|
||||
color: #3d4757;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.ce-contextmenu-content .ce-contextmenu-item span.ce-shortcut {
|
||||
color: #767c85;
|
||||
height: 30px;
|
||||
flex: 1;
|
||||
text-align: right;
|
||||
line-height: 30px;
|
||||
margin-left: 20px;
|
||||
}
|
||||
|
||||
.ce-contextmenu-content .ce-contextmenu-item i {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
vertical-align: middle;
|
||||
display: inline-block;
|
||||
background-repeat: no-repeat;
|
||||
background-size: 100% 100%;
|
||||
flex-shrink: 0;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.ce-contextmenu-divider {
|
||||
background-color: #e2e6ed;
|
||||
margin: 4px 16px;
|
||||
height: 1px;
|
||||
}
|
||||
|
||||
.ce-contextmenu-print {
|
||||
background-image: url(../../../assets/images/print.svg);
|
||||
}
|
||||
|
||||
.ce-contextmenu-image {
|
||||
background-image: url(../../../assets/images/image.svg);
|
||||
}
|
||||
|
||||
.ce-contextmenu-image-change {
|
||||
background-image: url(../../../assets/images/image-change.svg);
|
||||
}
|
||||
|
||||
.ce-contextmenu-insert-row-col {
|
||||
background-image: url(../../../assets/images/insert-row-col.svg);
|
||||
}
|
||||
|
||||
.ce-contextmenu-insert-top-row {
|
||||
background-image: url(../../../assets/images/insert-top-row.svg);
|
||||
}
|
||||
|
||||
.ce-contextmenu-insert-bottom-row {
|
||||
background-image: url(../../../assets/images/insert-bottom-row.svg);
|
||||
}
|
||||
|
||||
.ce-contextmenu-insert-left-col {
|
||||
background-image: url(../../../assets/images/insert-left-col.svg);
|
||||
}
|
||||
|
||||
.ce-contextmenu-insert-right-col {
|
||||
background-image: url(../../../assets/images/insert-right-col.svg);
|
||||
}
|
||||
|
||||
.ce-contextmenu-delete-row-col {
|
||||
background-image: url(../../../assets/images/delete-row-col.svg);
|
||||
}
|
||||
|
||||
.ce-contextmenu-delete-row {
|
||||
background-image: url(../../../assets/images/delete-row.svg);
|
||||
}
|
||||
|
||||
.ce-contextmenu-delete-col {
|
||||
background-image: url(../../../assets/images/delete-col.svg);
|
||||
}
|
||||
|
||||
.ce-contextmenu-delete-table {
|
||||
background-image: url(../../../assets/images/delete-table.svg);
|
||||
}
|
||||
|
||||
.ce-contextmenu-merge-cell {
|
||||
background-image: url(../../../assets/images/merge-cell.svg);
|
||||
}
|
||||
|
||||
.ce-contextmenu-merge-cancel-cell {
|
||||
background-image: url(../../../assets/images/merge-cancel-cell.svg);
|
||||
}
|
||||
|
||||
.ce-contextmenu-vertical-align {
|
||||
background-image: url(../../../assets/images/vertical-align.svg);
|
||||
}
|
||||
|
||||
.ce-contextmenu-vertical-align-top {
|
||||
background-image: url(../../../assets/images/vertical-align-top.svg);
|
||||
}
|
||||
|
||||
.ce-contextmenu-vertical-align-middle {
|
||||
background-image: url(../../../assets/images/vertical-align-middle.svg);
|
||||
}
|
||||
|
||||
.ce-contextmenu-vertical-align-bottom {
|
||||
background-image: url(../../../assets/images/vertical-align-bottom.svg);
|
||||
}
|
||||
|
||||
.ce-contextmenu-border-all {
|
||||
background-image: url(../../../assets/images/table-border-all.svg);
|
||||
}
|
||||
|
||||
.ce-contextmenu-border-empty {
|
||||
background-image: url(../../../assets/images/table-border-empty.svg);
|
||||
}
|
||||
|
||||
.ce-contextmenu-border-dash {
|
||||
background-image: url(../../../assets/images/table-border-dash.svg);
|
||||
}
|
||||
|
||||
.ce-contextmenu-border-external {
|
||||
background-image: url(../../../assets/images/table-border-external.svg);
|
||||
}
|
||||
|
||||
.ce-contextmenu-border-internal {
|
||||
background-image: url(../../../assets/images/table-border-internal.svg);
|
||||
}
|
||||
|
||||
.ce-contextmenu-border-td {
|
||||
background-image: url(../../../assets/images/table-border-td.svg);
|
||||
}
|
||||
|
||||
.ce-contextmenu-border-td-top {
|
||||
background-image: url(../../../assets/images/table-border-td-top.svg);
|
||||
}
|
||||
|
||||
.ce-contextmenu-border-td-left {
|
||||
background-image: url(../../../assets/images/table-border-td-left.svg);
|
||||
}
|
||||
|
||||
.ce-contextmenu-border-td-bottom {
|
||||
background-image: url(../../../assets/images/table-border-td-bottom.svg);
|
||||
}
|
||||
|
||||
.ce-contextmenu-border-td-right {
|
||||
background-image: url(../../../assets/images/table-border-td-right.svg);
|
||||
}
|
||||
|
||||
.ce-contextmenu-border-td-forward {
|
||||
background-image: url(../../../assets/images/table-border-td-forward.svg);
|
||||
}
|
||||
|
||||
.ce-contextmenu-border-td-back {
|
||||
background-image: url(../../../assets/images/table-border-td-back.svg);
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
.ce-select-control-popup {
|
||||
max-width: 160px;
|
||||
min-width: 69px;
|
||||
max-height: 225px;
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
border: 1px solid #e4e7ed;
|
||||
border-radius: 4px;
|
||||
background-color: #fff;
|
||||
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, .1);
|
||||
box-sizing: border-box;
|
||||
margin: 5px 0;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.ce-select-control-popup ul {
|
||||
list-style: none;
|
||||
padding: 3px 0;
|
||||
margin: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.ce-select-control-popup ul li {
|
||||
font-size: 13px;
|
||||
padding: 0 20px;
|
||||
position: relative;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
color: #666;
|
||||
height: 36px;
|
||||
line-height: 36px;
|
||||
box-sizing: border-box;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.ce-select-control-popup ul li:hover {
|
||||
background-color: #EEF2FD;
|
||||
}
|
||||
|
||||
.ce-select-control-popup ul li.active {
|
||||
color: var(--COLOR-HOVER, #5175f4);
|
||||
font-weight: 700;
|
||||
}
|
||||
@@ -0,0 +1,233 @@
|
||||
.ce-date-container {
|
||||
display: none;
|
||||
width: 300px;
|
||||
overflow: hidden;
|
||||
left: 0;
|
||||
right: 0;
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
color: #606266;
|
||||
background: #ffffff;
|
||||
border-radius: 4px;
|
||||
padding: 10px;
|
||||
user-select: none;
|
||||
border: 1px solid #e4e7ed;
|
||||
box-shadow: 0 2px 12px 0 rgb(0 0 0 / 10%);
|
||||
}
|
||||
|
||||
.ce-date-container.active {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.ce-date-wrap {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.ce-date-wrap.active {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.ce-date-title {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
color: #606266;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.ce-date-title>span {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.ce-date-title>span:not(.ce-date-title__now) {
|
||||
font-family: cursive;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.ce-date-title>span:not(.ce-date-title__now):hover {
|
||||
color: #5175F4;
|
||||
}
|
||||
|
||||
.ce-date-title .ce-date-title__pre-year {
|
||||
width: 15%;
|
||||
}
|
||||
|
||||
.ce-date-title .ce-date-title__pre-month {
|
||||
width: 15%;
|
||||
}
|
||||
|
||||
.ce-date-title .ce-date-title__now {
|
||||
width: 40%;
|
||||
}
|
||||
|
||||
.ce-date-title .ce-date-title__next-year {
|
||||
width: 15%;
|
||||
}
|
||||
|
||||
.ce-date-title .ce-date-title__next-month {
|
||||
width: 15%;
|
||||
}
|
||||
|
||||
.ce-date-week {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin-top: 15px;
|
||||
padding-bottom: 5px;
|
||||
border-bottom: 1px solid #e4e7ed;
|
||||
}
|
||||
|
||||
.ce-date-week>span {
|
||||
list-style: none;
|
||||
width: calc(100%/7);
|
||||
text-align: center;
|
||||
color: #606266;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.ce-date-day {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
.ce-date-day>div {
|
||||
width: calc(100%/7);
|
||||
height: 40px;
|
||||
text-align: center;
|
||||
color: #606266;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
line-height: 40px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.ce-date-day>div:hover {
|
||||
color: #5175F4;
|
||||
opacity: .8;
|
||||
}
|
||||
|
||||
.ce-date-day>div.active {
|
||||
color: #5175F4;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.ce-date-day>div.disable {
|
||||
color: #c0c4cc;
|
||||
}
|
||||
|
||||
.ce-date-day>div.select {
|
||||
color: #fff;
|
||||
background-color: #5175F4;
|
||||
}
|
||||
|
||||
.ce-time-wrap {
|
||||
display: none;
|
||||
padding: 10px;
|
||||
height: 286px;
|
||||
}
|
||||
|
||||
.ce-time-wrap ::-webkit-scrollbar {
|
||||
width: 0;
|
||||
}
|
||||
|
||||
.ce-time-wrap.active {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.ce-time-wrap li {
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.ce-time-wrap>li {
|
||||
width: 33.3%;
|
||||
height: 100%;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.ce-time-wrap>li>span {
|
||||
transform: translateY(-5px);
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.ce-time-wrap>li>ol {
|
||||
height: calc(100% - 20px);
|
||||
overflow-y: auto;
|
||||
border: 1px solid #e2e2e2;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.ce-time-wrap>li:first-child>ol {
|
||||
border-right: 0;
|
||||
}
|
||||
|
||||
.ce-time-wrap>li:last-child>ol {
|
||||
border-left: 0;
|
||||
}
|
||||
|
||||
.ce-time-wrap>li>ol>li {
|
||||
line-height: 30px;
|
||||
cursor: pointer;
|
||||
transition: all .3s;
|
||||
}
|
||||
|
||||
.ce-time-wrap>li>ol>li:hover {
|
||||
background-color: #eaeaea;
|
||||
}
|
||||
|
||||
.ce-time-wrap>li>ol>li.active {
|
||||
color: #ffffff;
|
||||
background: #5175F4;
|
||||
}
|
||||
|
||||
.ce-date-menu {
|
||||
width: 100%;
|
||||
height: 28px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
padding-top: 10px;
|
||||
position: relative;
|
||||
border-top: 1px solid #e4e7ed;
|
||||
}
|
||||
|
||||
.ce-date-menu button {
|
||||
display: inline-block;
|
||||
line-height: 1;
|
||||
white-space: nowrap;
|
||||
cursor: pointer;
|
||||
background: #fff;
|
||||
border: 1px solid #dcdfe6;
|
||||
color: #606266;
|
||||
appearance: none;
|
||||
text-align: center;
|
||||
box-sizing: border-box;
|
||||
outline: none;
|
||||
margin: 0;
|
||||
transition: .1s;
|
||||
font-weight: 500;
|
||||
user-select: none;
|
||||
padding: 7px 15px;
|
||||
font-size: 12px;
|
||||
border-radius: 3px;
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.ce-date-menu button:hover {
|
||||
color: #5175F4;
|
||||
border-color: #5175F4;
|
||||
}
|
||||
|
||||
.ce-date-menu button.ce-date-menu__time {
|
||||
border: 1px solid transparent;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.ce-date-menu button.ce-date-menu__time:hover {
|
||||
color: #5175F4;
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
.ce-hyperlink-popup {
|
||||
background: #fff;
|
||||
box-shadow: 0 2px 12px 0 rgb(98 107 132 / 20%);
|
||||
border-radius: 2px;
|
||||
color: #3d4757;
|
||||
padding: 12px 16px;
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
text-align: center;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.ce-hyperlink-popup a {
|
||||
min-width: 100px;
|
||||
max-width: 300px;
|
||||
font-size: 12px;
|
||||
display: inline-block;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
border-bottom-width: 1px;
|
||||
border-bottom-style: solid;
|
||||
color: #0000ff;
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
@import './control/select.css';
|
||||
@import './date/datePicker.css';
|
||||
@import './block/block.css';
|
||||
@import './table/table.css';
|
||||
@import './resizer/resizer.css';
|
||||
@import './previewer/previewer.css';
|
||||
@import './contextmenu/contextmenu.css';
|
||||
@import './hyperlink/hyperlink.css';
|
||||
@import './zone/zone.css';
|
||||
|
||||
.ce-inputarea {
|
||||
width: 100px;
|
||||
height: 30px;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
left: 0;
|
||||
top: 0;
|
||||
letter-spacing: 0;
|
||||
font-size: 12px;
|
||||
position: absolute;
|
||||
z-index: -1;
|
||||
outline: none;
|
||||
resize: none;
|
||||
border: none;
|
||||
overflow: hidden;
|
||||
color: transparent;
|
||||
user-select: none;
|
||||
caret-color: transparent;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.ce-cursor {
|
||||
width: 1px;
|
||||
height: 20px;
|
||||
left: 0;
|
||||
right: 0;
|
||||
position: absolute;
|
||||
outline: none;
|
||||
background-color: #000000;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.ce-cursor.ce-cursor--animation {
|
||||
animation-duration: 1s;
|
||||
animation-iteration-count: infinite;
|
||||
animation-name: cursorAnimation;
|
||||
}
|
||||
|
||||
@keyframes cursorAnimation {
|
||||
from {
|
||||
opacity: 1
|
||||
}
|
||||
|
||||
13% {
|
||||
opacity: 0
|
||||
}
|
||||
|
||||
50% {
|
||||
opacity: 0
|
||||
}
|
||||
|
||||
63% {
|
||||
opacity: 1
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1
|
||||
}
|
||||
}
|
||||
|
||||
.ce-float-image {
|
||||
position: absolute;
|
||||
opacity: 0.5;
|
||||
pointer-events: none;
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
.ce-image-previewer {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
top: 0;
|
||||
z-index: 1000;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
background: #f2f4f7;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
animation: previewerAnimation .3s;
|
||||
}
|
||||
|
||||
@keyframes previewerAnimation {
|
||||
0% {
|
||||
opacity: 0.1;
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.ce-image-previewer .image-close {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
display: inline-block;
|
||||
position: absolute;
|
||||
right: 50px;
|
||||
top: 30px;
|
||||
z-index: 99;
|
||||
cursor: pointer;
|
||||
background: url(../../images/close.svg) no-repeat;
|
||||
background-size: 100% 100%;
|
||||
transition: all .3s;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.ce-image-previewer .image-close:hover {
|
||||
background-color: #e2e6ed;
|
||||
}
|
||||
|
||||
.ce-image-previewer .ce-image-container {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.ce-image-previewer .ce-image-container img {
|
||||
cursor: move;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.ce-image-previewer .ce-image-menu {
|
||||
height: 50px;
|
||||
position: absolute;
|
||||
bottom: 50px;
|
||||
z-index: 99;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.ce-image-previewer .ce-image-menu i {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
margin: 0 8px;
|
||||
cursor: pointer;
|
||||
display: inline-block;
|
||||
background-repeat: no-repeat;
|
||||
background-size: 100% 100%;
|
||||
transition: all .3s;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.ce-image-previewer .ce-image-menu i:hover {
|
||||
background-color: #e2e6ed;
|
||||
}
|
||||
|
||||
.ce-image-previewer .ce-image-menu i.zoom-in {
|
||||
background-image: url(../../images/zoom-in.svg);
|
||||
}
|
||||
|
||||
.ce-image-previewer .ce-image-menu i.zoom-out {
|
||||
background-image: url(../../images/zoom-out.svg);
|
||||
}
|
||||
|
||||
.ce-image-previewer .ce-image-menu i.rotate {
|
||||
background-image: url(../../images/rotate.svg);
|
||||
}
|
||||
|
||||
.ce-image-previewer .ce-image-menu i.original-size {
|
||||
background-image: url(../../images/original-size.svg);
|
||||
}
|
||||
|
||||
.ce-image-previewer .ce-image-menu i.image-download {
|
||||
background-image: url(../../images/image-download.svg);
|
||||
}
|
||||
|
||||
.ce-image-previewer .ce-image-menu .image-navigate {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.ce-image-previewer .ce-image-menu i.image-pre {
|
||||
background-image: url(../../images/image-pre.svg);
|
||||
}
|
||||
|
||||
.ce-image-previewer .ce-image-menu i.image-next {
|
||||
background-image: url(../../images/image-next.svg);
|
||||
}
|
||||
|
||||
.ce-image-previewer .ce-image-menu .image-count {
|
||||
color: #000000;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.ce-image-previewer .ce-image-menu i.disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.5;
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
.ce-resizer-selection {
|
||||
position: absolute;
|
||||
border: 1px solid;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.ce-resizer-selection .resizer-handle {
|
||||
position: absolute;
|
||||
z-index: 9;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
box-shadow: 0 1px 4px 0 rgb(0 0 0 / 30%);
|
||||
border-radius: 5px;
|
||||
border: 2px solid #ffffff;
|
||||
box-sizing: border-box;
|
||||
pointer-events: initial;
|
||||
}
|
||||
|
||||
.ce-resizer-selection .handle-0 {
|
||||
cursor: nw-resize;
|
||||
}
|
||||
|
||||
.ce-resizer-selection .handle-1 {
|
||||
cursor: n-resize;
|
||||
}
|
||||
|
||||
.ce-resizer-selection .handle-2 {
|
||||
cursor: ne-resize;
|
||||
}
|
||||
|
||||
.ce-resizer-selection .handle-3 {
|
||||
cursor: e-resize;
|
||||
}
|
||||
|
||||
.ce-resizer-selection .handle-4 {
|
||||
cursor: se-resize;
|
||||
}
|
||||
|
||||
.ce-resizer-selection .handle-5 {
|
||||
cursor: s-resize;
|
||||
}
|
||||
|
||||
.ce-resizer-selection .handle-6 {
|
||||
cursor: sw-resize;
|
||||
}
|
||||
|
||||
.ce-resizer-selection .handle-7 {
|
||||
cursor: w-resize;
|
||||
}
|
||||
|
||||
.ce-resizer-size-view {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 20px;
|
||||
white-space: nowrap;
|
||||
position: absolute;
|
||||
z-index: 9;
|
||||
top: -30px;
|
||||
left: 0;
|
||||
opacity: .9;
|
||||
background-color: #000000;
|
||||
padding: 0 5px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.ce-resizer-size-view span {
|
||||
color: #ffffff;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.ce-resizer-image {
|
||||
position: absolute;
|
||||
opacity: 0.5;
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
.ce-table-tool__row {
|
||||
position: absolute;
|
||||
width: 12px;
|
||||
border-radius: 6.5px;
|
||||
overflow: hidden;
|
||||
background-color: #E2E6ED;
|
||||
}
|
||||
|
||||
.ce-table-tool__row .ce-table-tool__row__item {
|
||||
width: 100%;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
transition: all .3s;
|
||||
}
|
||||
|
||||
.ce-table-tool__row .ce-table-tool__row__item::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 2px;
|
||||
width: 8px;
|
||||
height: 1px;
|
||||
background-color: #C0C6CF;
|
||||
}
|
||||
|
||||
.ce-table-tool__row .ce-table-tool__row__item:hover {
|
||||
background-color: #dadce0;
|
||||
}
|
||||
|
||||
.ce-table-tool__row .ce-table-tool__row__item:last-child:after {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.ce-table-tool__quick__add {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
position: absolute;
|
||||
border-radius: 50%;
|
||||
background-color: #E2E6ED;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.ce-table-tool__quick__add::after {
|
||||
content: '+';
|
||||
color: #ffffff;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -55%);
|
||||
}
|
||||
|
||||
.ce-table-tool__select {
|
||||
width: 16px;
|
||||
height: 18px;
|
||||
position: absolute;
|
||||
border-radius: 3px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.ce-table-tool__select:hover {
|
||||
background-color: #E2E6ED;
|
||||
}
|
||||
|
||||
.ce-table-tool__select::after {
|
||||
content: ':::';
|
||||
color: #AAAAAB;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-75%, -50%) rotate(-90deg);
|
||||
}
|
||||
|
||||
.ce-table-tool__col {
|
||||
position: absolute;
|
||||
height: 12px;
|
||||
border-radius: 6.5px;
|
||||
overflow: hidden;
|
||||
background-color: #E2E6ED;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.ce-table-tool__col .ce-table-tool__col__item {
|
||||
height: 100%;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
transition: all .3s;
|
||||
}
|
||||
|
||||
.ce-table-tool__col .ce-table-tool__col__item::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 2px;
|
||||
left: -1px;
|
||||
width: 1px;
|
||||
height: 8px;
|
||||
z-index: 1;
|
||||
background-color: #C0C6CF;
|
||||
}
|
||||
|
||||
.ce-table-tool__col .ce-table-tool__col__item:hover {
|
||||
background-color: #dadce0;
|
||||
}
|
||||
|
||||
.ce-table-tool__col .ce-table-tool__col__item:first-child:after {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.ce-table-tool__row .ce-table-tool__row__item.active,
|
||||
.ce-table-tool__col .ce-table-tool__col__item.active {
|
||||
background-color: #C4D7FA;
|
||||
}
|
||||
|
||||
.ce-table-tool__col .ce-table-tool__anchor {
|
||||
right: -5px;
|
||||
width: 10px;
|
||||
height: 12px;
|
||||
z-index: 9;
|
||||
position: absolute;
|
||||
cursor: col-resize;
|
||||
}
|
||||
|
||||
.ce-table-tool__row .ce-table-tool__anchor {
|
||||
bottom: -5px;
|
||||
left: 0;
|
||||
width: 12px;
|
||||
height: 10px;
|
||||
z-index: 9;
|
||||
position: absolute;
|
||||
cursor: row-resize;
|
||||
}
|
||||
|
||||
.ce-table-anchor__line {
|
||||
z-index: 9;
|
||||
position: absolute;
|
||||
border: 1px dotted #000000;
|
||||
}
|
||||
|
||||
.ce-table-tool__border {
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
background: transparent;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.ce-table-tool__border__row {
|
||||
position: absolute;
|
||||
cursor: row-resize;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.ce-table-tool__border__col {
|
||||
position: absolute;
|
||||
cursor: col-resize;
|
||||
pointer-events: auto;
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
.ce-zone-indicator>div {
|
||||
padding: 3px 6px;
|
||||
color: #000000;
|
||||
font-size: 12px;
|
||||
background: rgb(218 231 252);
|
||||
position: absolute;
|
||||
transform-origin: 0 0;
|
||||
}
|
||||
|
||||
.ce-zone-indicator-border__top,
|
||||
.ce-zone-indicator-border__bottom,
|
||||
.ce-zone-indicator-border__left,
|
||||
.ce-zone-indicator-border__right {
|
||||
display: block;
|
||||
position: absolute;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.ce-zone-indicator-border__top {
|
||||
border-top: 2px dashed rgb(238, 238, 238);
|
||||
}
|
||||
|
||||
.ce-zone-indicator-border__bottom {
|
||||
border-top: 2px dashed rgb(238, 238, 238);
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.ce-zone-indicator-border__left {
|
||||
border-left: 2px dashed rgb(238, 238, 238);
|
||||
}
|
||||
|
||||
.ce-zone-indicator-border__right {
|
||||
border-right: 2px dashed rgb(238, 238, 238);
|
||||
}
|
||||
|
||||
.ce-zone-tip {
|
||||
display: none;
|
||||
align-items: center;
|
||||
height: 30px;
|
||||
white-space: nowrap;
|
||||
position: fixed;
|
||||
opacity: .9;
|
||||
background-color: #000000;
|
||||
padding: 0 5px;
|
||||
border-radius: 4px;
|
||||
z-index: 9;
|
||||
transition: all .3s;
|
||||
outline: none;
|
||||
user-select: none;
|
||||
pointer-events: none;
|
||||
transform: translate(10px, 10px);
|
||||
}
|
||||
|
||||
.ce-zone-tip.show {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.ce-zone-tip span {
|
||||
color: #ffffff;
|
||||
font-size: 12px;
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
<svg width="32" height="32" xmlns="http://www.w3.org/2000/svg"><path d="M23.97 7l1.415 1.414-7.779 7.778 7.779 7.779-1.414 1.414-7.779-7.779-7.778 7.779L7 23.97l7.778-7.779L7 8.414 8.414 7l7.778 7.778L23.971 7z" fill="#3D4757" fill-rule="evenodd"/></svg>
|
||||
|
After Width: | Height: | Size: 254 B |
@@ -0,0 +1 @@
|
||||
<svg width="16" height="16" xmlns="http://www.w3.org/2000/svg"><g fill="none" fill-rule="evenodd"><path stroke="#929AA8" d="M5.5 7.5v-6h4v6z"/><path fill="#3D4757" d="M13 15V3h1v12zM1 15V3h1v12zm4 0v-1h1v1zm1-1v-1h1v1zm1-1v-1h1v1zm-1-1v-1h1v1zm-1-1v-1h1v1zm3 1v-1h1v1zm0 2v-1h1v1zm1-3v-1h1v1zm0 4v-1h1v1z"/></g></svg>
|
||||
|
After Width: | Height: | Size: 317 B |
@@ -0,0 +1 @@
|
||||
<svg width="16" height="16" xmlns="http://www.w3.org/2000/svg"><g fill="none" fill-rule="evenodd"><path stroke="#929AA8" d="M8.5 6.5h6v2h-6z"/><path fill="#3D4757" d="M2 12h11v1H2zM2 2h11v1H2zm.63 3L7 9.35l-.635.65L2 5.63z"/><path fill="#3D4757" d="M2 9.363L6.355 5 7 5.707 2.695 10z"/></g></svg>
|
||||
|
After Width: | Height: | Size: 296 B |
@@ -0,0 +1 @@
|
||||
<svg width="16" height="16" xmlns="http://www.w3.org/2000/svg"><g fill="none" fill-rule="evenodd"><path stroke="#929AA8" d="M8.5 5.5h6v4h-6z"/><path fill="#3D4757" d="M1 13h12v1H1zM1 1h12v1H1zm0 4h1v1H1zm1 1h1v1H2zm1 1h1v1H3zm1-1h1v1H4zm1-1h1v1H5zM4 8h1v1H4zM2 8h1v1H2zm3 1h1v1H5zM1 9h1v1H1z"/></g></svg>
|
||||
|
After Width: | Height: | Size: 304 B |
@@ -0,0 +1 @@
|
||||
<svg width="16" height="16" xmlns="http://www.w3.org/2000/svg"><g fill="#3D4757" fill-rule="evenodd"><path d="M14 13h-1v-3H2v3H1v-3a1 1 0 011-1h11a1 1 0 011 1v3z" fill-rule="nonzero"/><path d="M5.625 2L10 6.375 9.375 7 5 2.625z"/><path d="M5 6.375L9.375 2l.625.625L5.625 7z"/></g></svg>
|
||||
|
After Width: | Height: | Size: 286 B |
@@ -0,0 +1 @@
|
||||
<svg width="16" height="16" xmlns="http://www.w3.org/2000/svg"><g fill="none" fill-rule="evenodd"><g transform="translate(2 4)" fill="#3D4757"><circle fill-rule="nonzero" cx="3" cy="1" r="1"/><path d="M7.473 8.223L3.47 4.107 0 7.667v-1.5C1.715 4.6 2.707 3.664 2.975 3.358c.402-.457.651-.39 1.042 0L7.473 7 9.96 4.349c.414-.462.62-.462 1.011-.071L13 7.06v1.5l-2.51-3.41-3.017 3.072z"/></g><path d="M6 1.5H1.5v12h13v-4V13a.5.5 0 01-.5.5H2a.5.5 0 01-.5-.5V2a.5.5 0 01.5-.5h4zm8.5 8V6l-.5.5h1l-.5-.5v3.5zM6 1.5h4L9.5 1v1l.5-.5H6z" stroke="#3D4757"/><path d="M13.085 1.316l-3.814 4a1 1 0 001.458 1.368l3.815-4a1 1 0 10-1.459-1.368z" fill="#3D4757" fill-rule="nonzero"/></g></svg>
|
||||
|
After Width: | Height: | Size: 674 B |
@@ -0,0 +1 @@
|
||||
<svg width="24" height="24" xmlns="http://www.w3.org/2000/svg"><path d="M4.5 15v3.5h15V15H21v5H3v-5h1.5zm8.232-11.226v9.196l4.05-4.05 1.06 1.06-5.834 5.834-5.833-5.833 1.06-1.06 3.998 3.996V3.774h1.5z" fill="#3D4757"/></svg>
|
||||
|
After Width: | Height: | Size: 224 B |
@@ -0,0 +1 @@
|
||||
<svg width="20" height="20" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M7.5 4.063L13.125 10 7.5 15.938" stroke="#3D4757" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>
|
||||
|
After Width: | Height: | Size: 207 B |
@@ -0,0 +1 @@
|
||||
<svg width="20" height="20" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M12.5 4.063L6.875 10l5.625 5.938" stroke="#3D4757" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>
|
||||
|
After Width: | Height: | Size: 208 B |
@@ -0,0 +1 @@
|
||||
<svg version="1.1" id="图层_1" xmlns="http://www.w3.org/2000/svg" x="0" y="0" viewBox="0 0 16 16" xml:space="preserve"><style>.st0{fill:#3d4757}</style><g id="_x30_0-公共_x2F_02工具栏_x2F_插入图片-16px-"><g id="Group-19" transform="translate(1 1)"><path id="Combined-Shape" class="st0" d="M1 0h12c.6 0 1 .4 1 1v11c0 .6-.4 1-1 1H1c-.6 0-1-.4-1-1V1c0-.6.4-1 1-1zm0 1v11h12V1H1z"/><circle id="椭圆形" class="st0" cx="10" cy="4" r="1"/><path id="Path" class="st0" d="M8.5 11.2l-4-4.1L1 10.7V9.2c1.7-1.6 2.7-2.5 3-2.8.4-.5.7-.4 1 0L8.5 10 11 7.3c.4-.5.6-.5 1-.1l2 2.8v1.5l-2.5-3.4-3 3.1z"/></g></g></svg>
|
||||
|
After Width: | Height: | Size: 613 B |
@@ -0,0 +1 @@
|
||||
<svg width="16" height="16" xmlns="http://www.w3.org/2000/svg"><g fill="none" fill-rule="evenodd"><path fill="#3D4757" d="M7 11h1V8H7zm.5 3L5 11h5z"/><rect stroke="#3D4757" x="1.5" y="2.5" width="12" height="3" rx="1"/></g></svg>
|
||||
|
After Width: | Height: | Size: 229 B |
@@ -0,0 +1 @@
|
||||
<svg width="16" height="16" xmlns="http://www.w3.org/2000/svg"><g fill="none" fill-rule="evenodd"><path fill="#3D4757" d="M11 7v1h3V7zm-3 .5L11 5v5z"/><rect stroke="#3D4757" transform="rotate(90 4 7.5)" x="-2" y="6" width="12" height="3" rx="1"/></g></svg>
|
||||
|
After Width: | Height: | Size: 256 B |
@@ -0,0 +1 @@
|
||||
<svg width="16" height="16" xmlns="http://www.w3.org/2000/svg"><g fill="none" fill-rule="evenodd"><path fill="#3D4757" d="M5 8V7H2v1zm3-.5L5 10V5z"/><rect stroke="#3D4757" transform="rotate(90 12 7.5)" x="6" y="6" width="12" height="3" rx="1"/></g></svg>
|
||||
|
After Width: | Height: | Size: 254 B |
@@ -0,0 +1 @@
|
||||
<svg width="16" height="16" xmlns="http://www.w3.org/2000/svg"><g fill="none" fill-rule="evenodd"><path stroke="#3D4757" d="M8.5 5.5h6v4h-6z"/><path fill="#3D4757" d="M4 7v1h2V7zm-3 .5L4 5v5zM1 1h12v1H1zm0 12h12v1H1z"/></g></svg>
|
||||
|
After Width: | Height: | Size: 229 B |
@@ -0,0 +1 @@
|
||||
<svg width="16" height="16" xmlns="http://www.w3.org/2000/svg"><g fill="none" fill-rule="evenodd"><path fill="#3D4757" d="M8 5H7v3h1zm-.5-3L10 5H5z"/><rect stroke="#3D4757" x="1.5" y="10.5" width="12" height="3" rx="1"/></g></svg>
|
||||
|
After Width: | Height: | Size: 230 B |
@@ -0,0 +1 @@
|
||||
<svg width="16" height="16" xmlns="http://www.w3.org/2000/svg"><g fill="#3D4757" fill-rule="evenodd"><path d="M6 1v1H2v11h4v1H2a1 1 0 01-1-1V2a1 1 0 011-1h4zm3 0h4a1 1 0 011 1v11a1 1 0 01-1 1H9v-1h4V2H9V1z"/><path fill-rule="nonzero" d="M6 1h1v4H6zm2 0h1v4H8z"/><path d="M3 7.5L5 6v3zm9 0L10 6v3z"/><path d="M4 7h3v1H4zm4 0h3v1H8z"/><path fill-rule="nonzero" d="M8 10h1v4H8zm-2 0h1v4H6z"/></g></svg>
|
||||
|
After Width: | Height: | Size: 399 B |
@@ -0,0 +1 @@
|
||||
<svg width="16" height="16" xmlns="http://www.w3.org/2000/svg"><g fill="#3D4757" fill-rule="evenodd"><path d="M6 1v1H2v11h4v1H2a1 1 0 01-1-1V2a1 1 0 011-1h4zm3 0h4a1 1 0 011 1v11a1 1 0 01-1 1H9v-1h4V2H9V1z"/><path fill-rule="nonzero" d="M6 1h1v4H6zm2 0h1v4H8z"/><path d="M8 7.5L10 6v3zm-1 0L5 6v3z"/><path d="M9 7h3v1H9zM3 7h3v1H3z"/><path fill-rule="nonzero" d="M8 10h1v4H8zm-2 0h1v4H6z"/></g></svg>
|
||||
|
After Width: | Height: | Size: 400 B |
@@ -0,0 +1 @@
|
||||
<svg width="32" height="32" xmlns="http://www.w3.org/2000/svg"><path d="M4 4h24v24H4V4zm2 2v20h20V6H6zm4 5h2v10h-2V11zm5 2h2v2h-2v-2zm0 4h2v2h-2v-2zm5-6h2v10h-2V11z" fill="#3D4757"/></svg>
|
||||
|
After Width: | Height: | Size: 188 B |
@@ -0,0 +1 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><g fill="#3D4757" fill-rule="evenodd"><path d="M12 4h-1V2H5v2H4V2a1 1 0 011-1h6a1 1 0 011 1v2zm0 5v4a1 1 0 01-1 1H5a1 1 0 01-1-1V9h1v4h6V9h1z"/><path d="M12 12v-1h2V5H2v6h2v1H2a1 1 0 01-1-1V5a1 1 0 011-1h12a1 1 0 011 1v6a1 1 0 01-1 1h-2z"/><path d="M3 8h10v1H3zm8-2h2v1h-2z"/></g></svg>
|
||||
|
After Width: | Height: | Size: 369 B |
@@ -0,0 +1 @@
|
||||
<svg width="32" height="32" xmlns="http://www.w3.org/2000/svg"><g fill="#3D4757" fill-rule="evenodd"><path d="M16 4c6.627 0 12 5.373 12 12a11.97 11.97 0 01-4 8.944V23h-.86A9.968 9.968 0 0026 16c0-5.523-4.477-10-10-10S6 10.477 6 16c0 5.185 3.947 9.449 9 9.95v2.009C8.84 27.451 4 22.291 4 16 4 9.373 9.373 4 16 4z" fill-rule="nonzero"/><path d="M19.879 27.328l1.767-6.717 4.95 4.95z"/></g></svg>
|
||||
|
After Width: | Height: | Size: 393 B |
@@ -0,0 +1 @@
|
||||
<svg width="16" height="16" xmlns="http://www.w3.org/2000/svg"><g fill="none" fill-rule="evenodd"><path d="M0 0h16v16H0z"/><g fill="#767C85"><path d="M7 12.243l-.707-.707 4.243-4.243.707.707z"/><path d="M6.293 4.464L7 3.757 11.243 8l-.707.707z"/></g></g></svg>
|
||||
|
After Width: | Height: | Size: 260 B |
@@ -0,0 +1 @@
|
||||
<svg width="16" height="16" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M2.5 3a.5.5 0 01.5-.5h11a.5.5 0 01.5.5v11a.5.5 0 01-.5.5H3a.5.5 0 01-.5-.5V3z" stroke="#3D4757"/><path fill="#3D4757" d="M3 8h11v1H3z"/><path fill="#3D4757" d="M9 3v11H8V3z"/></svg>
|
||||
|
After Width: | Height: | Size: 265 B |
@@ -0,0 +1 @@
|
||||
<svg width="16" height="16" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M13 3h-1V2h1a1 1 0 011 1v1h-1V3zm-3-1v1H8.5v2h-1V3H6V2h4zM4 2v1H3v1H2V3a1 1 0 011-1h1zM2 6h1v1.5h2v1H3V10H2V6zm0 6h1v1h1v1H3a1 1 0 01-1-1v-1zm4 2v-1h1.5v-2h1v2H10v1H6zm6 0v-1h1v-1h1v1a1 1 0 01-1 1h-1zm2-4h-1V8.5h-2v-1h2V6h1v4zM8.5 7.5v-1h-1v1h-1v1h1v1h1v-1h1v-1h-1z" fill="#000000"/></svg>
|
||||
|
After Width: | Height: | Size: 413 B |
@@ -0,0 +1 @@
|
||||
<svg width="16" height="16" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M13 3h-1V2h1a1 1 0 011 1v1h-1V3zm-3-1v1H8.5v2h-1V3H6V2h4zM4 2v1H3v1H2V3a1 1 0 011-1h1zM2 6h1v1.5h2v1H3V10H2V6zm0 6h1v1h1v1H3a1 1 0 01-1-1v-1zm4 2v-1h1.5v-2h1v2H10v1H6zm6 0v-1h1v-1h1v1a1 1 0 01-1 1h-1zm2-4h-1V8.5h-2v-1h2V6h1v4zM8.5 7.5v-1h-1v1h-1v1h1v1h1v-1h1v-1h-1z" fill="#AAACB0"/></svg>
|
||||
|
After Width: | Height: | Size: 413 B |
@@ -0,0 +1 @@
|
||||
<svg width="16" height="16" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M2.5 3a.5.5 0 01.5-.5h11a.5.5 0 01.5.5v11a.5.5 0 01-.5.5H3a.5.5 0 01-.5-.5V3z" stroke="#3D4757"/><path fill-rule="evenodd" clip-rule="evenodd" d="M9 5V3H8v2h1zm0 9v-2H8v2h1zM5 8H3v1h2V8zm9 0h-2v1h2V8zM9 7v1h1v1H9v1H8V9H7V8h1V7h1z" fill="#AAACB0"/></svg>
|
||||
|
After Width: | Height: | Size: 337 B |
@@ -0,0 +1 @@
|
||||
<svg width="16" height="16" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M2.5 3a.5.5 0 01.5-.5h11a.5.5 0 01.5.5v11a.5.5 0 01-.5.5H3a.5.5 0 01-.5-.5V3z" stroke="#AAACB0"/><path fill-rule="evenodd" clip-rule="evenodd" d="M9 5V3H8v2h1zm0 9v-2H8v2h1zM5 8H3v1h2V8zm9 0h-2v1h2V8zM9 7v1h1v1H9v1H8V9H7V8h1V7h1z" fill="#3D4757"/></svg>
|
||||
|
After Width: | Height: | Size: 337 B |
@@ -0,0 +1 @@
|
||||
<svg width="16" height="16" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M2.5 3a.5.5 0 01.5-.5h11a.5.5 0 01.5.5v11a.5.5 0 01-.5.5H3a.5.5 0 01-.5-.5V3z" stroke="#AAACB0" /><path stroke="#3D4757" d="M3 3 l11 11" /></svg>
|
||||
|
After Width: | Height: | Size: 229 B |
@@ -0,0 +1 @@
|
||||
<svg width="16" height="16" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M2.5 3a.5.5 0 01.5-.5h11a.5.5 0 01.5.5v11a.5.5 0 01-.5.5H3a.5.5 0 01-.5-.5V3z" stroke="#AAACB0"/><path stroke="#3D4757" stroke-width="2" d="M2.5 14 h12"/></svg>
|
||||
|
After Width: | Height: | Size: 244 B |
@@ -0,0 +1 @@
|
||||
<svg width="16" height="16" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M2.5 3a.5.5 0 01.5-.5h11a.5.5 0 01.5.5v11a.5.5 0 01-.5.5H3a.5.5 0 01-.5-.5V3z" stroke="#AAACB0" /><path stroke="#3D4757" d="M14 3 l-11 11" /></svg>
|
||||
|
After Width: | Height: | Size: 231 B |
@@ -0,0 +1 @@
|
||||
<svg width="16" height="16" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M2.5 3a.5.5 0 01.5-.5h11a.5.5 0 01.5.5v11a.5.5 0 01-.5.5H3a.5.5 0 01-.5-.5V3z" stroke="#AAACB0"/><path stroke="#3D4757" stroke-width="2" d="M3 3 v11"/></svg>
|
||||
|
After Width: | Height: | Size: 241 B |
@@ -0,0 +1 @@
|
||||
<svg width="16" height="16" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M2.5 3a.5.5 0 01.5-.5h11a.5.5 0 01.5.5v11a.5.5 0 01-.5.5H3a.5.5 0 01-.5-.5V3z" stroke="#AAACB0"/><path stroke="#3D4757" stroke-width="2" d="M14 3 v11"/></svg>
|
||||
|
After Width: | Height: | Size: 242 B |
@@ -0,0 +1 @@
|
||||
<svg width="16" height="16" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M2.5 3a.5.5 0 01.5-.5h11a.5.5 0 01.5.5v11a.5.5 0 01-.5.5H3a.5.5 0 01-.5-.5V3z" stroke="#AAACB0"/><path stroke="#3D4757" stroke-width="2" d="M2.5 3 h12"/></svg>
|
||||
|
After Width: | Height: | Size: 243 B |
@@ -0,0 +1 @@
|
||||
<svg width="16" height="16" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M2.5 3a.5.5 0 01.5-.5h11a.5.5 0 01.5.5v11a.5.5 0 01-.5.5H3a.5.5 0 01-.5-.5V3z" stroke="#AAACB0"/><path stroke="#3D4757" d="M8.5 2.5 v6 h-6"/></svg>
|
||||
|
After Width: | Height: | Size: 231 B |
@@ -0,0 +1 @@
|
||||
<svg width="16" height="16" xmlns="http://www.w3.org/2000/svg"><path d="M7 9h1V3H7zm.5 3L5 9h5zM2 13h11v1H2z" fill="#3D4757" fill-rule="evenodd"/></svg>
|
||||
|
After Width: | Height: | Size: 152 B |
@@ -0,0 +1 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><path d="M8 12H7v3h1zm-.5-3l2.5 3H5zM7 3h1V0H7zm.5 3L5 3h5zM2 7h11v1H2z" fill="#3D4757" fill-rule="evenodd"/></svg>
|
||||
|
After Width: | Height: | Size: 198 B |
@@ -0,0 +1 @@
|
||||
<svg width="16" height="16" xmlns="http://www.w3.org/2000/svg"><path d="M8 8H7v6h1zm-.5-3L10 8H5zM2 3h11v1H2z" fill="#3D4757" fill-rule="evenodd"/></svg>
|
||||
|
After Width: | Height: | Size: 153 B |
@@ -0,0 +1 @@
|
||||
<svg height="16" viewBox="0 0 16 16" width="16" xmlns="http://www.w3.org/2000/svg"><path d="M2 13h12v1H2zm0-3h8v1H2zm0-3h12v1H2zm0-6h12v1H2zm0 3h8v1H2z" fill="#3d4757" fill-rule="evenodd"/></svg>
|
||||
|
After Width: | Height: | Size: 195 B |
@@ -0,0 +1 @@
|
||||
<svg width="32" height="32" xmlns="http://www.w3.org/2000/svg"><path d="M14 14v-4h2v4h4v2h-4v4h-2v-4h-4v-2h4zm8.749 10.163A11.952 11.952 0 0115 27C8.373 27 3 21.627 3 15S8.373 3 15 3s12 5.373 12 12c0 2.954-1.067 5.658-2.837 7.749l4.908 4.908-1.414 1.414-4.908-4.908zM15 25c5.523 0 10-4.477 10-10S20.523 5 15 5 5 9.477 5 15s4.477 10 10 10z" fill="#3D4757"/></svg>
|
||||
|
After Width: | Height: | Size: 362 B |
@@ -0,0 +1 @@
|
||||
<svg width="32" height="32" xmlns="http://www.w3.org/2000/svg"><path d="M22.749 24.163A11.952 11.952 0 0115 27C8.373 27 3 21.627 3 15S8.373 3 15 3s12 5.373 12 12c0 2.954-1.067 5.658-2.837 7.749l4.908 4.908-1.414 1.414-4.908-4.908zM15 25c5.523 0 10-4.477 10-10S20.523 5 15 5 5 9.477 5 15s4.477 10 10 10zm-5-11h10v2H10v-2z" fill="#3D4757"/></svg>
|
||||
|
After Width: | Height: | Size: 344 B |
@@ -0,0 +1,21 @@
|
||||
import { EventBusMap } from '../../interface/EventBus'
|
||||
import { Draw } from '../draw/Draw'
|
||||
import { EventBus } from '../event/eventbus/EventBus'
|
||||
import { positionContextChange } from './handlers/positionContextChange'
|
||||
|
||||
export class Actuator {
|
||||
private draw: Draw
|
||||
private eventBus: EventBus<EventBusMap>
|
||||
|
||||
constructor(draw: Draw) {
|
||||
this.draw = draw
|
||||
this.eventBus = draw.getEventBus()
|
||||
this.execute()
|
||||
}
|
||||
|
||||
private execute() {
|
||||
this.eventBus.on('positionContextChange', payload => {
|
||||
positionContextChange(this.draw, payload)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { IPositionContextChangePayload } from '../../../interface/Listener'
|
||||
import { Draw } from '../../draw/Draw'
|
||||
|
||||
export function positionContextChange(
|
||||
draw: Draw,
|
||||
payload: IPositionContextChangePayload
|
||||
) {
|
||||
const { value, oldValue } = payload
|
||||
// 表格工具移除
|
||||
if (oldValue.isTable && !value.isTable) {
|
||||
draw.getTableTool().dispose()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,296 @@
|
||||
import { CommandAdapt } from './CommandAdapt'
|
||||
|
||||
// 通过CommandAdapt中转避免直接暴露编辑器上下文
|
||||
export class Command {
|
||||
public executeMode: CommandAdapt['mode']
|
||||
public executeCut: CommandAdapt['cut']
|
||||
public executeCopy: CommandAdapt['copy']
|
||||
public executePaste: CommandAdapt['paste']
|
||||
public executeSelectAll: CommandAdapt['selectAll']
|
||||
public executeBackspace: CommandAdapt['backspace']
|
||||
public executeSetRange: CommandAdapt['setRange']
|
||||
public executeReplaceRange: CommandAdapt['replaceRange']
|
||||
public executeSetPositionContext: CommandAdapt['setPositionContext']
|
||||
public executeForceUpdate: CommandAdapt['forceUpdate']
|
||||
public executeBlur: CommandAdapt['blur']
|
||||
public executeUndo: CommandAdapt['undo']
|
||||
public executeRedo: CommandAdapt['redo']
|
||||
public executePainter: CommandAdapt['painter']
|
||||
public executeApplyPainterStyle: CommandAdapt['applyPainterStyle']
|
||||
public executeFormat: CommandAdapt['format']
|
||||
public executeFont: CommandAdapt['font']
|
||||
public executeSize: CommandAdapt['size']
|
||||
public executeSizeAdd: CommandAdapt['sizeAdd']
|
||||
public executeSizeMinus: CommandAdapt['sizeMinus']
|
||||
public executeBold: CommandAdapt['bold']
|
||||
public executeItalic: CommandAdapt['italic']
|
||||
public executeUnderline: CommandAdapt['underline']
|
||||
public executeStrikeout: CommandAdapt['strikeout']
|
||||
public executeSuperscript: CommandAdapt['superscript']
|
||||
public executeSubscript: CommandAdapt['subscript']
|
||||
public executeColor: CommandAdapt['color']
|
||||
public executeHighlight: CommandAdapt['highlight']
|
||||
public executeTitle: CommandAdapt['title']
|
||||
public executeList: CommandAdapt['list']
|
||||
public executeRowFlex: CommandAdapt['rowFlex']
|
||||
public executeRowMargin: CommandAdapt['rowMargin']
|
||||
public executeInsertTable: CommandAdapt['insertTable']
|
||||
public executeInsertTableTopRow: CommandAdapt['insertTableTopRow']
|
||||
public executeInsertTableBottomRow: CommandAdapt['insertTableBottomRow']
|
||||
public executeInsertTableLeftCol: CommandAdapt['insertTableLeftCol']
|
||||
public executeInsertTableRightCol: CommandAdapt['insertTableRightCol']
|
||||
public executeDeleteTableRow: CommandAdapt['deleteTableRow']
|
||||
public executeDeleteTableCol: CommandAdapt['deleteTableCol']
|
||||
public executeDeleteTable: CommandAdapt['deleteTable']
|
||||
public executeMergeTableCell: CommandAdapt['mergeTableCell']
|
||||
public executeCancelMergeTableCell: CommandAdapt['cancelMergeTableCell']
|
||||
public executeSplitVerticalTableCell: CommandAdapt['splitVerticalTableCell']
|
||||
public executeSplitHorizontalTableCell: CommandAdapt['splitHorizontalTableCell']
|
||||
public executeTableTdVerticalAlign: CommandAdapt['tableTdVerticalAlign']
|
||||
public executeTableBorderType: CommandAdapt['tableBorderType']
|
||||
public executeTableBorderColor: CommandAdapt['tableBorderColor']
|
||||
public executeTableTdBorderType: CommandAdapt['tableTdBorderType']
|
||||
public executeTableTdSlashType: CommandAdapt['tableTdSlashType']
|
||||
public executeTableTdBackgroundColor: CommandAdapt['tableTdBackgroundColor']
|
||||
public executeTableSelectAll: CommandAdapt['tableSelectAll']
|
||||
public executeImage: CommandAdapt['image']
|
||||
public executeHyperlink: CommandAdapt['hyperlink']
|
||||
public executeDeleteHyperlink: CommandAdapt['deleteHyperlink']
|
||||
public executeCancelHyperlink: CommandAdapt['cancelHyperlink']
|
||||
public executeEditHyperlink: CommandAdapt['editHyperlink']
|
||||
public executeSeparator: CommandAdapt['separator']
|
||||
public executePageBreak: CommandAdapt['pageBreak']
|
||||
public executeAddWatermark: CommandAdapt['addWatermark']
|
||||
public executeDeleteWatermark: CommandAdapt['deleteWatermark']
|
||||
public executeSearch: CommandAdapt['search']
|
||||
public executeSearchNavigatePre: CommandAdapt['searchNavigatePre']
|
||||
public executeSearchNavigateNext: CommandAdapt['searchNavigateNext']
|
||||
public executeReplace: CommandAdapt['replace']
|
||||
public executePrint: CommandAdapt['print']
|
||||
public executeReplaceImageElement: CommandAdapt['replaceImageElement']
|
||||
public executeSaveAsImageElement: CommandAdapt['saveAsImageElement']
|
||||
public executeChangeImageDisplay: CommandAdapt['changeImageDisplay']
|
||||
public executePageMode: CommandAdapt['pageMode']
|
||||
public executePageScale: CommandAdapt['pageScale']
|
||||
public executePageScaleRecovery: CommandAdapt['pageScaleRecovery']
|
||||
public executePageScaleMinus: CommandAdapt['pageScaleMinus']
|
||||
public executePageScaleAdd: CommandAdapt['pageScaleAdd']
|
||||
public executePaperSize: CommandAdapt['paperSize']
|
||||
public executePaperDirection: CommandAdapt['paperDirection']
|
||||
public executeSetPaperMargin: CommandAdapt['setPaperMargin']
|
||||
public executeSetMainBadge: CommandAdapt['setMainBadge']
|
||||
public executeSetAreaBadge: CommandAdapt['setAreaBadge']
|
||||
public executeInsertElementList: CommandAdapt['insertElementList']
|
||||
public executeInsertArea: CommandAdapt['insertArea']
|
||||
public executeSetAreaValue: CommandAdapt['setAreaValue']
|
||||
public executeSetAreaProperties: CommandAdapt['setAreaProperties']
|
||||
public executeLocationArea: CommandAdapt['locationArea']
|
||||
public executeAppendElementList: CommandAdapt['appendElementList']
|
||||
public executeUpdateElementById: CommandAdapt['updateElementById']
|
||||
public executeDeleteElementById: CommandAdapt['deleteElementById']
|
||||
public executeSetValue: CommandAdapt['setValue']
|
||||
public executeRemoveControl: CommandAdapt['removeControl']
|
||||
public executeTranslate: CommandAdapt['translate']
|
||||
public executeSetLocale: CommandAdapt['setLocale']
|
||||
public executeLocationCatalog: CommandAdapt['locationCatalog']
|
||||
public executeWordTool: CommandAdapt['wordTool']
|
||||
public executeSetHTML: CommandAdapt['setHTML']
|
||||
public executeSetGroup: CommandAdapt['setGroup']
|
||||
public executeDeleteGroup: CommandAdapt['deleteGroup']
|
||||
public executeLocationGroup: CommandAdapt['locationGroup']
|
||||
public executeSetZone: CommandAdapt['setZone']
|
||||
public executeSetControlValue: CommandAdapt['setControlValue']
|
||||
public executeSetControlValueList: CommandAdapt['setControlValueList']
|
||||
public executeSetControlExtension: CommandAdapt['setControlExtension']
|
||||
public executeSetControlExtensionList: CommandAdapt['setControlExtensionList']
|
||||
public executeSetControlProperties: CommandAdapt['setControlProperties']
|
||||
public executeSetControlPropertiesList: CommandAdapt['setControlPropertiesList']
|
||||
public executeSetControlHighlight: CommandAdapt['setControlHighlight']
|
||||
public executeLocationControl: CommandAdapt['locationControl']
|
||||
public executeInsertControl: CommandAdapt['insertControl']
|
||||
public executeUpdateOptions: CommandAdapt['updateOptions']
|
||||
public executeInsertTitle: CommandAdapt['insertTitle']
|
||||
public executeFocus: CommandAdapt['focus']
|
||||
public getCatalog: CommandAdapt['getCatalog']
|
||||
public getImage: CommandAdapt['getImage']
|
||||
public getOptions: CommandAdapt['getOptions']
|
||||
public getValue: CommandAdapt['getValue']
|
||||
public getValueAsync: CommandAdapt['getValueAsync']
|
||||
public getAreaValue: CommandAdapt['getAreaValue']
|
||||
public getHTML: CommandAdapt['getHTML']
|
||||
public getText: CommandAdapt['getText']
|
||||
public getWordCount: CommandAdapt['getWordCount']
|
||||
public getCursorPosition: CommandAdapt['getCursorPosition']
|
||||
public getRange: CommandAdapt['getRange']
|
||||
public getRangeText: CommandAdapt['getRangeText']
|
||||
public getRangeContext: CommandAdapt['getRangeContext']
|
||||
public getRangeRow: CommandAdapt['getRangeRow']
|
||||
public getRangeParagraph: CommandAdapt['getRangeParagraph']
|
||||
public getKeywordRangeList: CommandAdapt['getKeywordRangeList']
|
||||
public getKeywordContext: CommandAdapt['getKeywordContext']
|
||||
public getPaperMargin: CommandAdapt['getPaperMargin']
|
||||
public getSearchNavigateInfo: CommandAdapt['getSearchNavigateInfo']
|
||||
public getLocale: CommandAdapt['getLocale']
|
||||
public getGroupIds: CommandAdapt['getGroupIds']
|
||||
public getControlValue: CommandAdapt['getControlValue']
|
||||
public getControlList: CommandAdapt['getControlList']
|
||||
public getContainer: CommandAdapt['getContainer']
|
||||
public getTitleValue: CommandAdapt['getTitleValue']
|
||||
public getPositionContextByEvent: CommandAdapt['getPositionContextByEvent']
|
||||
public getElementById: CommandAdapt['getElementById']
|
||||
|
||||
constructor(adapt: CommandAdapt) {
|
||||
// 全局命令
|
||||
this.executeMode = adapt.mode.bind(adapt)
|
||||
this.executeCut = adapt.cut.bind(adapt)
|
||||
this.executeCopy = adapt.copy.bind(adapt)
|
||||
this.executePaste = adapt.paste.bind(adapt)
|
||||
this.executeSelectAll = adapt.selectAll.bind(adapt)
|
||||
this.executeBackspace = adapt.backspace.bind(adapt)
|
||||
this.executeSetRange = adapt.setRange.bind(adapt)
|
||||
this.executeReplaceRange = adapt.replaceRange.bind(adapt)
|
||||
this.executeSetPositionContext = adapt.setPositionContext.bind(adapt)
|
||||
this.executeForceUpdate = adapt.forceUpdate.bind(adapt)
|
||||
this.executeBlur = adapt.blur.bind(adapt)
|
||||
// 撤销、重做、格式刷、清除格式
|
||||
this.executeUndo = adapt.undo.bind(adapt)
|
||||
this.executeRedo = adapt.redo.bind(adapt)
|
||||
this.executePainter = adapt.painter.bind(adapt)
|
||||
this.executeApplyPainterStyle = adapt.applyPainterStyle.bind(adapt)
|
||||
this.executeFormat = adapt.format.bind(adapt)
|
||||
// 字体、字体大小、字体变大、字体变小、加粗、斜体、下划线、删除线、字体颜色、背景色
|
||||
this.executeFont = adapt.font.bind(adapt)
|
||||
this.executeSize = adapt.size.bind(adapt)
|
||||
this.executeSizeAdd = adapt.sizeAdd.bind(adapt)
|
||||
this.executeSizeMinus = adapt.sizeMinus.bind(adapt)
|
||||
this.executeBold = adapt.bold.bind(adapt)
|
||||
this.executeItalic = adapt.italic.bind(adapt)
|
||||
this.executeUnderline = adapt.underline.bind(adapt)
|
||||
this.executeStrikeout = adapt.strikeout.bind(adapt)
|
||||
this.executeSuperscript = adapt.superscript.bind(adapt)
|
||||
this.executeSubscript = adapt.subscript.bind(adapt)
|
||||
this.executeColor = adapt.color.bind(adapt)
|
||||
this.executeHighlight = adapt.highlight.bind(adapt)
|
||||
// 标题、对齐方式、列表
|
||||
this.executeTitle = adapt.title.bind(adapt)
|
||||
this.executeList = adapt.list.bind(adapt)
|
||||
this.executeRowFlex = adapt.rowFlex.bind(adapt)
|
||||
this.executeRowMargin = adapt.rowMargin.bind(adapt)
|
||||
// 表格、图片上传、超链接、搜索、打印、图片操作
|
||||
this.executeInsertTable = adapt.insertTable.bind(adapt)
|
||||
this.executeInsertTableTopRow = adapt.insertTableTopRow.bind(adapt)
|
||||
this.executeInsertTableBottomRow = adapt.insertTableBottomRow.bind(adapt)
|
||||
this.executeInsertTableLeftCol = adapt.insertTableLeftCol.bind(adapt)
|
||||
this.executeInsertTableRightCol = adapt.insertTableRightCol.bind(adapt)
|
||||
this.executeDeleteTableRow = adapt.deleteTableRow.bind(adapt)
|
||||
this.executeDeleteTableCol = adapt.deleteTableCol.bind(adapt)
|
||||
this.executeDeleteTable = adapt.deleteTable.bind(adapt)
|
||||
this.executeMergeTableCell = adapt.mergeTableCell.bind(adapt)
|
||||
this.executeCancelMergeTableCell = adapt.cancelMergeTableCell.bind(adapt)
|
||||
this.executeSplitVerticalTableCell =
|
||||
adapt.splitVerticalTableCell.bind(adapt)
|
||||
this.executeSplitHorizontalTableCell =
|
||||
adapt.splitHorizontalTableCell.bind(adapt)
|
||||
this.executeTableTdVerticalAlign = adapt.tableTdVerticalAlign.bind(adapt)
|
||||
this.executeTableBorderType = adapt.tableBorderType.bind(adapt)
|
||||
this.executeTableBorderColor = adapt.tableBorderColor.bind(adapt)
|
||||
this.executeTableTdBorderType = adapt.tableTdBorderType.bind(adapt)
|
||||
this.executeTableTdSlashType = adapt.tableTdSlashType.bind(adapt)
|
||||
this.executeTableTdBackgroundColor =
|
||||
adapt.tableTdBackgroundColor.bind(adapt)
|
||||
this.executeTableSelectAll = adapt.tableSelectAll.bind(adapt)
|
||||
this.executeImage = adapt.image.bind(adapt)
|
||||
this.executeHyperlink = adapt.hyperlink.bind(adapt)
|
||||
this.executeDeleteHyperlink = adapt.deleteHyperlink.bind(adapt)
|
||||
this.executeCancelHyperlink = adapt.cancelHyperlink.bind(adapt)
|
||||
this.executeEditHyperlink = adapt.editHyperlink.bind(adapt)
|
||||
this.executeSeparator = adapt.separator.bind(adapt)
|
||||
this.executePageBreak = adapt.pageBreak.bind(adapt)
|
||||
this.executeAddWatermark = adapt.addWatermark.bind(adapt)
|
||||
this.executeDeleteWatermark = adapt.deleteWatermark.bind(adapt)
|
||||
this.executeSearch = adapt.search.bind(adapt)
|
||||
this.executeSearchNavigatePre = adapt.searchNavigatePre.bind(adapt)
|
||||
this.executeSearchNavigateNext = adapt.searchNavigateNext.bind(adapt)
|
||||
this.executeReplace = adapt.replace.bind(adapt)
|
||||
this.executePrint = adapt.print.bind(adapt)
|
||||
this.executeReplaceImageElement = adapt.replaceImageElement.bind(adapt)
|
||||
this.executeSaveAsImageElement = adapt.saveAsImageElement.bind(adapt)
|
||||
this.executeChangeImageDisplay = adapt.changeImageDisplay.bind(adapt)
|
||||
// 页面模式、页面缩放、纸张大小、纸张方向、页边距
|
||||
this.executePageMode = adapt.pageMode.bind(adapt)
|
||||
this.executePageScale = adapt.pageScale.bind(adapt)
|
||||
this.executePageScaleRecovery = adapt.pageScaleRecovery.bind(adapt)
|
||||
this.executePageScaleMinus = adapt.pageScaleMinus.bind(adapt)
|
||||
this.executePageScaleAdd = adapt.pageScaleAdd.bind(adapt)
|
||||
this.executePaperSize = adapt.paperSize.bind(adapt)
|
||||
this.executePaperDirection = adapt.paperDirection.bind(adapt)
|
||||
this.executeSetPaperMargin = adapt.setPaperMargin.bind(adapt)
|
||||
// 签章
|
||||
this.executeSetMainBadge = adapt.setMainBadge.bind(adapt)
|
||||
this.executeSetAreaBadge = adapt.setAreaBadge.bind(adapt)
|
||||
// 区域
|
||||
this.getAreaValue = adapt.getAreaValue.bind(adapt)
|
||||
this.executeInsertArea = adapt.insertArea.bind(adapt)
|
||||
this.executeSetAreaValue = adapt.setAreaValue.bind(adapt)
|
||||
this.executeSetAreaProperties = adapt.setAreaProperties.bind(adapt)
|
||||
this.executeLocationArea = adapt.locationArea.bind(adapt)
|
||||
// 通用
|
||||
this.executeInsertElementList = adapt.insertElementList.bind(adapt)
|
||||
this.executeAppendElementList = adapt.appendElementList.bind(adapt)
|
||||
this.executeUpdateElementById = adapt.updateElementById.bind(adapt)
|
||||
this.executeDeleteElementById = adapt.deleteElementById.bind(adapt)
|
||||
this.executeSetValue = adapt.setValue.bind(adapt)
|
||||
this.executeRemoveControl = adapt.removeControl.bind(adapt)
|
||||
this.executeTranslate = adapt.translate.bind(adapt)
|
||||
this.executeSetLocale = adapt.setLocale.bind(adapt)
|
||||
this.executeLocationCatalog = adapt.locationCatalog.bind(adapt)
|
||||
this.executeWordTool = adapt.wordTool.bind(adapt)
|
||||
this.executeSetHTML = adapt.setHTML.bind(adapt)
|
||||
this.executeSetGroup = adapt.setGroup.bind(adapt)
|
||||
this.executeDeleteGroup = adapt.deleteGroup.bind(adapt)
|
||||
this.executeLocationGroup = adapt.locationGroup.bind(adapt)
|
||||
this.executeSetZone = adapt.setZone.bind(adapt)
|
||||
this.executeUpdateOptions = adapt.updateOptions.bind(adapt)
|
||||
this.executeInsertTitle = adapt.insertTitle.bind(adapt)
|
||||
this.executeFocus = adapt.focus.bind(adapt)
|
||||
// 获取
|
||||
this.getImage = adapt.getImage.bind(adapt)
|
||||
this.getOptions = adapt.getOptions.bind(adapt)
|
||||
this.getValue = adapt.getValue.bind(adapt)
|
||||
this.getValueAsync = adapt.getValueAsync.bind(adapt)
|
||||
this.getHTML = adapt.getHTML.bind(adapt)
|
||||
this.getText = adapt.getText.bind(adapt)
|
||||
this.getWordCount = adapt.getWordCount.bind(adapt)
|
||||
this.getCursorPosition = adapt.getCursorPosition.bind(adapt)
|
||||
this.getRange = adapt.getRange.bind(adapt)
|
||||
this.getRangeText = adapt.getRangeText.bind(adapt)
|
||||
this.getRangeContext = adapt.getRangeContext.bind(adapt)
|
||||
this.getRangeRow = adapt.getRangeRow.bind(adapt)
|
||||
this.getRangeParagraph = adapt.getRangeParagraph.bind(adapt)
|
||||
this.getKeywordRangeList = adapt.getKeywordRangeList.bind(adapt)
|
||||
this.getKeywordContext = adapt.getKeywordContext.bind(adapt)
|
||||
this.getCatalog = adapt.getCatalog.bind(adapt)
|
||||
this.getPaperMargin = adapt.getPaperMargin.bind(adapt)
|
||||
this.getSearchNavigateInfo = adapt.getSearchNavigateInfo.bind(adapt)
|
||||
this.getLocale = adapt.getLocale.bind(adapt)
|
||||
this.getGroupIds = adapt.getGroupIds.bind(adapt)
|
||||
this.getContainer = adapt.getContainer.bind(adapt)
|
||||
this.getTitleValue = adapt.getTitleValue.bind(adapt)
|
||||
this.getPositionContextByEvent = adapt.getPositionContextByEvent.bind(adapt)
|
||||
this.getElementById = adapt.getElementById.bind(adapt)
|
||||
// 控件
|
||||
this.executeSetControlValue = adapt.setControlValue.bind(adapt)
|
||||
this.executeSetControlValueList = adapt.setControlValueList.bind(adapt)
|
||||
this.executeSetControlExtension = adapt.setControlExtension.bind(adapt)
|
||||
this.executeSetControlExtensionList =
|
||||
adapt.setControlExtensionList.bind(adapt)
|
||||
this.executeSetControlProperties = adapt.setControlProperties.bind(adapt)
|
||||
this.executeSetControlPropertiesList =
|
||||
adapt.setControlPropertiesList.bind(adapt)
|
||||
this.executeSetControlHighlight = adapt.setControlHighlight.bind(adapt)
|
||||
this.getControlValue = adapt.getControlValue.bind(adapt)
|
||||
this.getControlList = adapt.getControlList.bind(adapt)
|
||||
this.executeLocationControl = adapt.locationControl.bind(adapt)
|
||||
this.executeInsertControl = adapt.insertControl.bind(adapt)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,363 @@
|
||||
import { NAME_PLACEHOLDER } from '../../dataset/constant/ContextMenu'
|
||||
import { EDITOR_COMPONENT, EDITOR_PREFIX } from '../../dataset/constant/Editor'
|
||||
import { EditorComponent } from '../../dataset/enum/Editor'
|
||||
import { DeepRequired } from '../../interface/Common'
|
||||
import { IEditorOption } from '../../interface/Editor'
|
||||
import { IElement } from '../../interface/Element'
|
||||
import {
|
||||
IContextMenuContext,
|
||||
IRegisterContextMenu
|
||||
} from '../../interface/contextmenu/ContextMenu'
|
||||
import { findParent } from '../../utils'
|
||||
import { zipElementList } from '../../utils/element'
|
||||
import { Command } from '../command/Command'
|
||||
import { Draw } from '../draw/Draw'
|
||||
import { I18n } from '../i18n/I18n'
|
||||
import { Position } from '../position/Position'
|
||||
import { RangeManager } from '../range/RangeManager'
|
||||
import { controlMenus } from './menus/controlMenus'
|
||||
import { globalMenus } from './menus/globalMenus'
|
||||
import { hyperlinkMenus } from './menus/hyperlinkMenus'
|
||||
import { imageMenus } from './menus/imageMenus'
|
||||
import { tableMenus } from './menus/tableMenus'
|
||||
|
||||
interface IRenderPayload {
|
||||
contextMenuList: IRegisterContextMenu[]
|
||||
left: number
|
||||
top: number
|
||||
parentMenuContainer?: HTMLDivElement
|
||||
}
|
||||
|
||||
export class ContextMenu {
|
||||
private options: DeepRequired<IEditorOption>
|
||||
private draw: Draw
|
||||
private command: Command
|
||||
private range: RangeManager
|
||||
private position: Position
|
||||
private i18n: I18n
|
||||
private container: HTMLDivElement
|
||||
private contextMenuList: IRegisterContextMenu[]
|
||||
private contextMenuContainerList: HTMLDivElement[]
|
||||
private contextMenuRelationShip: Map<HTMLDivElement, HTMLDivElement>
|
||||
private context: IContextMenuContext | null
|
||||
|
||||
constructor(draw: Draw, command: Command) {
|
||||
this.options = draw.getOptions()
|
||||
this.draw = draw
|
||||
this.command = command
|
||||
this.range = draw.getRange()
|
||||
this.position = draw.getPosition()
|
||||
this.i18n = draw.getI18n()
|
||||
this.container = draw.getContainer()
|
||||
this.context = null
|
||||
// 内部菜单
|
||||
this.contextMenuList = [
|
||||
...globalMenus,
|
||||
...tableMenus,
|
||||
...imageMenus,
|
||||
...controlMenus,
|
||||
...hyperlinkMenus
|
||||
]
|
||||
this.contextMenuContainerList = []
|
||||
this.contextMenuRelationShip = new Map()
|
||||
this._addEvent()
|
||||
}
|
||||
|
||||
public getContextMenuList(): IRegisterContextMenu[] {
|
||||
return this.contextMenuList
|
||||
}
|
||||
|
||||
private _addEvent() {
|
||||
// 菜单权限
|
||||
this.container.addEventListener('contextmenu', this._proxyContextMenuEvent)
|
||||
// 副作用处理
|
||||
document.addEventListener('mousedown', this._handleSideEffect)
|
||||
}
|
||||
|
||||
public removeEvent() {
|
||||
this.container.removeEventListener(
|
||||
'contextmenu',
|
||||
this._proxyContextMenuEvent
|
||||
)
|
||||
document.removeEventListener('mousedown', this._handleSideEffect)
|
||||
}
|
||||
|
||||
private _filterMenuList(
|
||||
menuList: IRegisterContextMenu[]
|
||||
): IRegisterContextMenu[] {
|
||||
const { contextMenuDisableKeys } = this.options
|
||||
const renderList: IRegisterContextMenu[] = []
|
||||
for (let m = 0; m < menuList.length; m++) {
|
||||
const menu = menuList[m]
|
||||
if (
|
||||
menu.disable ||
|
||||
(menu.key && contextMenuDisableKeys.includes(menu.key))
|
||||
) {
|
||||
continue
|
||||
}
|
||||
if (menu.isDivider) {
|
||||
renderList.push(menu)
|
||||
} else {
|
||||
if (menu.when?.(this.context!)) {
|
||||
renderList.push(menu)
|
||||
}
|
||||
}
|
||||
}
|
||||
return renderList
|
||||
}
|
||||
|
||||
private _proxyContextMenuEvent = (evt: MouseEvent) => {
|
||||
this.context = this._getContext()
|
||||
const renderList = this._filterMenuList(this.contextMenuList)
|
||||
const isRegisterContextMenu = renderList.some(menu => !menu.isDivider)
|
||||
if (isRegisterContextMenu) {
|
||||
this.dispose()
|
||||
this._render({
|
||||
contextMenuList: renderList,
|
||||
left: evt.x,
|
||||
top: evt.y
|
||||
})
|
||||
}
|
||||
evt.preventDefault()
|
||||
}
|
||||
|
||||
private _handleSideEffect = (evt: MouseEvent) => {
|
||||
if (this.contextMenuContainerList.length) {
|
||||
// 点击非右键菜单内
|
||||
const target = <Element>(evt?.composedPath()[0] || evt.target)
|
||||
const contextMenuDom = findParent(
|
||||
target,
|
||||
(node: Node & Element) =>
|
||||
!!node &&
|
||||
node.nodeType === 1 &&
|
||||
node.getAttribute(EDITOR_COMPONENT) === EditorComponent.CONTEXTMENU,
|
||||
true
|
||||
)
|
||||
if (!contextMenuDom) {
|
||||
this.dispose()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private _getContext(): IContextMenuContext {
|
||||
// 是否是只读模式
|
||||
const isReadonly = this.draw.isReadonly()
|
||||
const {
|
||||
isCrossRowCol: crossRowCol,
|
||||
startIndex,
|
||||
endIndex
|
||||
} = this.range.getRange()
|
||||
// 是否存在焦点
|
||||
const editorTextFocus = !!(~startIndex || ~endIndex)
|
||||
// 是否存在选区
|
||||
const editorHasSelection = editorTextFocus && startIndex !== endIndex
|
||||
// 是否在表格内
|
||||
const { isTable, trIndex, tdIndex, index } =
|
||||
this.position.getPositionContext()
|
||||
let tableElement: IElement | null = null
|
||||
if (isTable) {
|
||||
const originalElementList = this.draw.getOriginalElementList()
|
||||
const originTableElement = originalElementList[index!] || null
|
||||
if (originTableElement) {
|
||||
tableElement = zipElementList([originTableElement], {
|
||||
extraPickAttrs: ['id']
|
||||
})[0]
|
||||
}
|
||||
}
|
||||
// 是否存在跨行/列
|
||||
const isCrossRowCol = isTable && !!crossRowCol
|
||||
// 当前元素
|
||||
const elementList = this.draw.getElementList()
|
||||
const startElement = elementList[startIndex] || null
|
||||
const endElement = elementList[endIndex] || null
|
||||
// 当前区域
|
||||
const zone = this.draw.getZone().getZone()
|
||||
return {
|
||||
startElement,
|
||||
endElement,
|
||||
isReadonly,
|
||||
editorHasSelection,
|
||||
editorTextFocus,
|
||||
isCrossRowCol,
|
||||
zone,
|
||||
isInTable: isTable,
|
||||
trIndex: trIndex ?? null,
|
||||
tdIndex: tdIndex ?? null,
|
||||
tableElement,
|
||||
options: this.options
|
||||
}
|
||||
}
|
||||
|
||||
private _createContextMenuContainer(): HTMLDivElement {
|
||||
const contextMenuContainer = document.createElement('div')
|
||||
contextMenuContainer.classList.add(`${EDITOR_PREFIX}-contextmenu-container`)
|
||||
contextMenuContainer.setAttribute(
|
||||
EDITOR_COMPONENT,
|
||||
EditorComponent.CONTEXTMENU
|
||||
)
|
||||
this.container.append(contextMenuContainer)
|
||||
return contextMenuContainer
|
||||
}
|
||||
|
||||
private _render(payload: IRenderPayload): HTMLDivElement {
|
||||
const { contextMenuList, left, top, parentMenuContainer } = payload
|
||||
const contextMenuContainer = this._createContextMenuContainer()
|
||||
const contextMenuContent = document.createElement('div')
|
||||
contextMenuContent.classList.add(`${EDITOR_PREFIX}-contextmenu-content`)
|
||||
// 直接子菜单
|
||||
let childMenuContainer: HTMLDivElement | null = null
|
||||
// 父菜单添加子菜单映射关系
|
||||
if (parentMenuContainer) {
|
||||
this.contextMenuRelationShip.set(
|
||||
parentMenuContainer,
|
||||
contextMenuContainer
|
||||
)
|
||||
}
|
||||
for (let c = 0; c < contextMenuList.length; c++) {
|
||||
const menu = contextMenuList[c]
|
||||
if (menu.isDivider) {
|
||||
// 分割线相邻 || 首尾分隔符时不渲染
|
||||
if (
|
||||
c !== 0 &&
|
||||
c !== contextMenuList.length - 1 &&
|
||||
!contextMenuList[c - 1]?.isDivider
|
||||
) {
|
||||
const divider = document.createElement('div')
|
||||
divider.classList.add(`${EDITOR_PREFIX}-contextmenu-divider`)
|
||||
contextMenuContent.append(divider)
|
||||
}
|
||||
} else {
|
||||
const menuItem = document.createElement('div')
|
||||
menuItem.classList.add(`${EDITOR_PREFIX}-contextmenu-item`)
|
||||
// 菜单事件
|
||||
if (menu.childMenus) {
|
||||
const childMenus = this._filterMenuList(menu.childMenus)
|
||||
const isRegisterContextMenu = childMenus.some(menu => !menu.isDivider)
|
||||
if (isRegisterContextMenu) {
|
||||
menuItem.classList.add(`${EDITOR_PREFIX}-contextmenu-sub-item`)
|
||||
menuItem.onmouseenter = () => {
|
||||
this._setHoverStatus(menuItem, true)
|
||||
this._removeSubMenu(contextMenuContainer)
|
||||
// 子菜单
|
||||
const subMenuRect = menuItem.getBoundingClientRect()
|
||||
const left = subMenuRect.left + subMenuRect.width
|
||||
const top = subMenuRect.top
|
||||
childMenuContainer = this._render({
|
||||
contextMenuList: childMenus,
|
||||
left,
|
||||
top,
|
||||
parentMenuContainer: contextMenuContainer
|
||||
})
|
||||
}
|
||||
menuItem.onmouseleave = evt => {
|
||||
// 移动到子菜单选项选中状态不变化
|
||||
if (
|
||||
!childMenuContainer ||
|
||||
!childMenuContainer.contains(evt.relatedTarget as Node)
|
||||
) {
|
||||
this._setHoverStatus(menuItem, false)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
menuItem.onmouseenter = () => {
|
||||
this._setHoverStatus(menuItem, true)
|
||||
this._removeSubMenu(contextMenuContainer)
|
||||
}
|
||||
menuItem.onmouseleave = () => {
|
||||
this._setHoverStatus(menuItem, false)
|
||||
}
|
||||
menuItem.onclick = () => {
|
||||
if (menu.callback && this.context) {
|
||||
menu.callback(this.command, this.context)
|
||||
}
|
||||
this.dispose()
|
||||
}
|
||||
}
|
||||
// 图标
|
||||
const icon = document.createElement('i')
|
||||
menuItem.append(icon)
|
||||
if (menu.icon) {
|
||||
icon.classList.add(`${EDITOR_PREFIX}-contextmenu-${menu.icon}`)
|
||||
}
|
||||
// 文本
|
||||
const span = document.createElement('span')
|
||||
const name = menu.i18nPath
|
||||
? this._formatName(this.i18n.t(menu.i18nPath))
|
||||
: this._formatName(menu.name || '')
|
||||
span.append(document.createTextNode(name))
|
||||
menuItem.append(span)
|
||||
// 快捷方式提示
|
||||
if (menu.shortCut) {
|
||||
const span = document.createElement('span')
|
||||
span.classList.add(`${EDITOR_PREFIX}-shortcut`)
|
||||
span.append(document.createTextNode(menu.shortCut))
|
||||
menuItem.append(span)
|
||||
}
|
||||
contextMenuContent.append(menuItem)
|
||||
}
|
||||
}
|
||||
contextMenuContainer.append(contextMenuContent)
|
||||
contextMenuContainer.style.display = 'block'
|
||||
// 右侧空间不足时,以菜单右上角作为起始点
|
||||
const innerWidth = window.innerWidth
|
||||
const contextmenuRect = contextMenuContainer.getBoundingClientRect()
|
||||
const contextMenuWidth = contextmenuRect.width
|
||||
const adjustLeft =
|
||||
left + contextMenuWidth > innerWidth ? left - contextMenuWidth : left
|
||||
contextMenuContainer.style.left = `${adjustLeft}px`
|
||||
// 下侧空间不足时,以菜单底部作为起始点
|
||||
const innerHeight = window.innerHeight
|
||||
const contextMenuHeight = contextmenuRect.height
|
||||
const adjustTop =
|
||||
top + contextMenuHeight > innerHeight ? top - contextMenuHeight : top
|
||||
contextMenuContainer.style.top = `${adjustTop}px`
|
||||
this.contextMenuContainerList.push(contextMenuContainer)
|
||||
return contextMenuContainer
|
||||
}
|
||||
|
||||
private _removeSubMenu(payload: HTMLDivElement) {
|
||||
const childMenu = this.contextMenuRelationShip.get(payload)
|
||||
if (childMenu) {
|
||||
this._removeSubMenu(childMenu)
|
||||
childMenu.remove()
|
||||
this.contextMenuRelationShip.delete(payload)
|
||||
}
|
||||
}
|
||||
|
||||
private _setHoverStatus(payload: HTMLDivElement, status: boolean) {
|
||||
if (status) {
|
||||
payload.parentNode
|
||||
?.querySelectorAll(`${EDITOR_PREFIX}-contextmenu-item`)
|
||||
.forEach(child => child.classList.remove('hover'))
|
||||
payload.classList.add('hover')
|
||||
} else {
|
||||
payload.classList.remove('hover')
|
||||
}
|
||||
}
|
||||
|
||||
private _formatName(name: string): string {
|
||||
const placeholderValues = Object.values(NAME_PLACEHOLDER)
|
||||
const placeholderReg = new RegExp(`${placeholderValues.join('|')}`)
|
||||
let formatName = name
|
||||
if (placeholderReg.test(formatName)) {
|
||||
// 选区名称
|
||||
const selectedReg = new RegExp(NAME_PLACEHOLDER.SELECTED_TEXT, 'g')
|
||||
if (selectedReg.test(formatName)) {
|
||||
const selectedText = this.range.toString()
|
||||
formatName = formatName.replace(selectedReg, selectedText)
|
||||
}
|
||||
}
|
||||
return formatName
|
||||
}
|
||||
|
||||
public registerContextMenuList(payload: IRegisterContextMenu[]) {
|
||||
this.contextMenuList.push(...payload)
|
||||
}
|
||||
|
||||
public dispose() {
|
||||
this.contextMenuContainerList.forEach(child => child.remove())
|
||||
this.contextMenuContainerList = []
|
||||
this.contextMenuRelationShip.clear()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import { INTERNAL_CONTEXT_MENU_KEY } from '../../../dataset/constant/ContextMenu'
|
||||
import { EditorMode } from '../../../dataset/enum/Editor'
|
||||
import { IRegisterContextMenu } from '../../../interface/contextmenu/ContextMenu'
|
||||
import { Command } from '../../command/Command'
|
||||
const {
|
||||
CONTROL: { DELETE }
|
||||
} = INTERNAL_CONTEXT_MENU_KEY
|
||||
|
||||
export const controlMenus: IRegisterContextMenu[] = [
|
||||
{
|
||||
key: DELETE,
|
||||
i18nPath: 'contextmenu.control.delete',
|
||||
when: payload => {
|
||||
return (
|
||||
!payload.isReadonly &&
|
||||
!payload.editorHasSelection &&
|
||||
!!payload.startElement?.controlId &&
|
||||
payload.options.mode !== EditorMode.FORM
|
||||
)
|
||||
},
|
||||
callback: (command: Command) => {
|
||||
command.executeRemoveControl()
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,66 @@
|
||||
import { INTERNAL_CONTEXT_MENU_KEY } from '../../../dataset/constant/ContextMenu'
|
||||
import { IRegisterContextMenu } from '../../../interface/contextmenu/ContextMenu'
|
||||
import { isApple } from '../../../utils/ua'
|
||||
import { Command } from '../../command/Command'
|
||||
const {
|
||||
GLOBAL: { CUT, COPY, PASTE, SELECT_ALL, PRINT }
|
||||
} = INTERNAL_CONTEXT_MENU_KEY
|
||||
|
||||
export const globalMenus: IRegisterContextMenu[] = [
|
||||
{
|
||||
key: CUT,
|
||||
i18nPath: 'contextmenu.global.cut',
|
||||
shortCut: `${isApple ? '⌘' : 'Ctrl'} + X`,
|
||||
when: payload => {
|
||||
return !payload.isReadonly
|
||||
},
|
||||
callback: (command: Command) => {
|
||||
command.executeCut()
|
||||
}
|
||||
},
|
||||
{
|
||||
key: COPY,
|
||||
i18nPath: 'contextmenu.global.copy',
|
||||
shortCut: `${isApple ? '⌘' : 'Ctrl'} + C`,
|
||||
when: payload => {
|
||||
return payload.editorHasSelection || payload.isCrossRowCol
|
||||
},
|
||||
callback: (command: Command) => {
|
||||
command.executeCopy()
|
||||
}
|
||||
},
|
||||
{
|
||||
key: PASTE,
|
||||
i18nPath: 'contextmenu.global.paste',
|
||||
shortCut: `${isApple ? '⌘' : 'Ctrl'} + V`,
|
||||
when: payload => {
|
||||
return !payload.isReadonly && payload.editorTextFocus
|
||||
},
|
||||
callback: (command: Command) => {
|
||||
command.executePaste()
|
||||
}
|
||||
},
|
||||
{
|
||||
key: SELECT_ALL,
|
||||
i18nPath: 'contextmenu.global.selectAll',
|
||||
shortCut: `${isApple ? '⌘' : 'Ctrl'} + A`,
|
||||
when: payload => {
|
||||
return payload.editorTextFocus
|
||||
},
|
||||
callback: (command: Command) => {
|
||||
command.executeSelectAll()
|
||||
}
|
||||
},
|
||||
{
|
||||
isDivider: true
|
||||
},
|
||||
{
|
||||
key: PRINT,
|
||||
i18nPath: 'contextmenu.global.print',
|
||||
icon: 'print',
|
||||
when: () => true,
|
||||
callback: (command: Command) => {
|
||||
command.executePrint()
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,58 @@
|
||||
import { INTERNAL_CONTEXT_MENU_KEY } from '../../../dataset/constant/ContextMenu'
|
||||
import { ElementType } from '../../../dataset/enum/Element'
|
||||
import {
|
||||
IContextMenuContext,
|
||||
IRegisterContextMenu
|
||||
} from '../../../interface/contextmenu/ContextMenu'
|
||||
import { Command } from '../../command/Command'
|
||||
const {
|
||||
HYPERLINK: { DELETE, CANCEL, EDIT }
|
||||
} = INTERNAL_CONTEXT_MENU_KEY
|
||||
|
||||
export const hyperlinkMenus: IRegisterContextMenu[] = [
|
||||
{
|
||||
key: DELETE,
|
||||
i18nPath: 'contextmenu.hyperlink.delete',
|
||||
when: payload => {
|
||||
return (
|
||||
!payload.isReadonly &&
|
||||
payload.startElement?.type === ElementType.HYPERLINK
|
||||
)
|
||||
},
|
||||
callback: (command: Command) => {
|
||||
command.executeDeleteHyperlink()
|
||||
}
|
||||
},
|
||||
{
|
||||
key: CANCEL,
|
||||
i18nPath: 'contextmenu.hyperlink.cancel',
|
||||
when: payload => {
|
||||
return (
|
||||
!payload.isReadonly &&
|
||||
payload.startElement?.type === ElementType.HYPERLINK
|
||||
)
|
||||
},
|
||||
callback: (command: Command) => {
|
||||
command.executeCancelHyperlink()
|
||||
}
|
||||
},
|
||||
{
|
||||
key: EDIT,
|
||||
i18nPath: 'contextmenu.hyperlink.edit',
|
||||
when: payload => {
|
||||
return (
|
||||
!payload.isReadonly &&
|
||||
payload.startElement?.type === ElementType.HYPERLINK
|
||||
)
|
||||
},
|
||||
callback: (command: Command, context: IContextMenuContext) => {
|
||||
const url = window.prompt(
|
||||
command.executeTranslate('contextmenu.hyperlink.edit'),
|
||||
context.startElement?.url
|
||||
)
|
||||
if (url) {
|
||||
command.executeEditHyperlink(url)
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,134 @@
|
||||
import { INTERNAL_CONTEXT_MENU_KEY } from '../../../dataset/constant/ContextMenu'
|
||||
import { ImageDisplay } from '../../../dataset/enum/Common'
|
||||
import { ElementType } from '../../../dataset/enum/Element'
|
||||
import {
|
||||
IContextMenuContext,
|
||||
IRegisterContextMenu
|
||||
} from '../../../interface/contextmenu/ContextMenu'
|
||||
import { Command } from '../../command/Command'
|
||||
const {
|
||||
IMAGE: {
|
||||
CHANGE,
|
||||
SAVE_AS,
|
||||
TEXT_WRAP,
|
||||
TEXT_WRAP_EMBED,
|
||||
TEXT_WRAP_UP_DOWN,
|
||||
TEXT_WRAP_SURROUND,
|
||||
TEXT_WRAP_FLOAT_TOP,
|
||||
TEXT_WRAP_FLOAT_BOTTOM
|
||||
}
|
||||
} = INTERNAL_CONTEXT_MENU_KEY
|
||||
|
||||
export const imageMenus: IRegisterContextMenu[] = [
|
||||
{
|
||||
key: CHANGE,
|
||||
i18nPath: 'contextmenu.image.change',
|
||||
icon: 'image-change',
|
||||
when: payload => {
|
||||
return (
|
||||
!payload.isReadonly &&
|
||||
!payload.editorHasSelection &&
|
||||
payload.startElement?.type === ElementType.IMAGE
|
||||
)
|
||||
},
|
||||
callback: (command: Command) => {
|
||||
// 创建代理元素
|
||||
const proxyInputFile = document.createElement('input')
|
||||
proxyInputFile.type = 'file'
|
||||
proxyInputFile.accept = '.png, .jpg, .jpeg'
|
||||
// 监听上传
|
||||
proxyInputFile.onchange = () => {
|
||||
const file = proxyInputFile.files![0]!
|
||||
const fileReader = new FileReader()
|
||||
fileReader.readAsDataURL(file)
|
||||
fileReader.onload = () => {
|
||||
const value = fileReader.result as string
|
||||
command.executeReplaceImageElement(value)
|
||||
}
|
||||
}
|
||||
proxyInputFile.click()
|
||||
}
|
||||
},
|
||||
{
|
||||
key: SAVE_AS,
|
||||
i18nPath: 'contextmenu.image.saveAs',
|
||||
icon: 'image',
|
||||
when: payload => {
|
||||
return (
|
||||
!payload.editorHasSelection &&
|
||||
payload.startElement?.type === ElementType.IMAGE
|
||||
)
|
||||
},
|
||||
callback: (command: Command) => {
|
||||
command.executeSaveAsImageElement()
|
||||
}
|
||||
},
|
||||
{
|
||||
key: TEXT_WRAP,
|
||||
i18nPath: 'contextmenu.image.textWrap',
|
||||
when: payload => {
|
||||
return (
|
||||
!payload.isReadonly &&
|
||||
!payload.editorHasSelection &&
|
||||
payload.startElement?.type === ElementType.IMAGE
|
||||
)
|
||||
},
|
||||
childMenus: [
|
||||
{
|
||||
key: TEXT_WRAP_EMBED,
|
||||
i18nPath: 'contextmenu.image.textWrapType.embed',
|
||||
when: () => true,
|
||||
callback: (command: Command, context: IContextMenuContext) => {
|
||||
command.executeChangeImageDisplay(
|
||||
context.startElement!,
|
||||
ImageDisplay.BLOCK
|
||||
)
|
||||
}
|
||||
},
|
||||
{
|
||||
key: TEXT_WRAP_UP_DOWN,
|
||||
i18nPath: 'contextmenu.image.textWrapType.upDown',
|
||||
when: () => true,
|
||||
callback: (command: Command, context: IContextMenuContext) => {
|
||||
command.executeChangeImageDisplay(
|
||||
context.startElement!,
|
||||
ImageDisplay.INLINE
|
||||
)
|
||||
}
|
||||
},
|
||||
{
|
||||
key: TEXT_WRAP_SURROUND,
|
||||
i18nPath: 'contextmenu.image.textWrapType.surround',
|
||||
when: () => true,
|
||||
callback: (command: Command, context: IContextMenuContext) => {
|
||||
command.executeChangeImageDisplay(
|
||||
context.startElement!,
|
||||
ImageDisplay.SURROUND
|
||||
)
|
||||
}
|
||||
},
|
||||
{
|
||||
key: TEXT_WRAP_FLOAT_TOP,
|
||||
i18nPath: 'contextmenu.image.textWrapType.floatTop',
|
||||
when: () => true,
|
||||
callback: (command: Command, context: IContextMenuContext) => {
|
||||
command.executeChangeImageDisplay(
|
||||
context.startElement!,
|
||||
ImageDisplay.FLOAT_TOP
|
||||
)
|
||||
}
|
||||
},
|
||||
{
|
||||
key: TEXT_WRAP_FLOAT_BOTTOM,
|
||||
i18nPath: 'contextmenu.image.textWrapType.floatBottom',
|
||||
when: () => true,
|
||||
callback: (command: Command, context: IContextMenuContext) => {
|
||||
command.executeChangeImageDisplay(
|
||||
context.startElement!,
|
||||
ImageDisplay.FLOAT_BOTTOM
|
||||
)
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,331 @@
|
||||
import { INTERNAL_CONTEXT_MENU_KEY } from '../../../dataset/constant/ContextMenu'
|
||||
import { EditorMode } from '../../../dataset/enum/Editor'
|
||||
import { VerticalAlign } from '../../../dataset/enum/VerticalAlign'
|
||||
import {
|
||||
TableBorder,
|
||||
TdBorder,
|
||||
TdSlash
|
||||
} from '../../../dataset/enum/table/Table'
|
||||
import { IRegisterContextMenu } from '../../../interface/contextmenu/ContextMenu'
|
||||
import { Command } from '../../command/Command'
|
||||
const {
|
||||
TABLE: {
|
||||
BORDER,
|
||||
BORDER_ALL,
|
||||
BORDER_EMPTY,
|
||||
BORDER_DASH,
|
||||
BORDER_EXTERNAL,
|
||||
BORDER_INTERNAL,
|
||||
BORDER_TD,
|
||||
BORDER_TD_TOP,
|
||||
BORDER_TD_LEFT,
|
||||
BORDER_TD_BOTTOM,
|
||||
BORDER_TD_RIGHT,
|
||||
BORDER_TD_BACK,
|
||||
BORDER_TD_FORWARD,
|
||||
VERTICAL_ALIGN,
|
||||
VERTICAL_ALIGN_TOP,
|
||||
VERTICAL_ALIGN_MIDDLE,
|
||||
VERTICAL_ALIGN_BOTTOM,
|
||||
INSERT_ROW_COL,
|
||||
INSERT_TOP_ROW,
|
||||
INSERT_BOTTOM_ROW,
|
||||
INSERT_LEFT_COL,
|
||||
INSERT_RIGHT_COL,
|
||||
DELETE_ROW_COL,
|
||||
DELETE_ROW,
|
||||
DELETE_COL,
|
||||
DELETE_TABLE,
|
||||
MERGE_CELL,
|
||||
CANCEL_MERGE_CELL
|
||||
}
|
||||
} = INTERNAL_CONTEXT_MENU_KEY
|
||||
|
||||
export const tableMenus: IRegisterContextMenu[] = [
|
||||
{
|
||||
isDivider: true
|
||||
},
|
||||
{
|
||||
key: BORDER,
|
||||
i18nPath: 'contextmenu.table.border',
|
||||
icon: 'border-all',
|
||||
when: payload => {
|
||||
return (
|
||||
!payload.isReadonly &&
|
||||
payload.isInTable &&
|
||||
payload.options.mode !== EditorMode.FORM
|
||||
)
|
||||
},
|
||||
childMenus: [
|
||||
{
|
||||
key: BORDER_ALL,
|
||||
i18nPath: 'contextmenu.table.borderAll',
|
||||
icon: 'border-all',
|
||||
when: () => true,
|
||||
callback: (command: Command) => {
|
||||
command.executeTableBorderType(TableBorder.ALL)
|
||||
}
|
||||
},
|
||||
{
|
||||
key: BORDER_EMPTY,
|
||||
i18nPath: 'contextmenu.table.borderEmpty',
|
||||
icon: 'border-empty',
|
||||
when: () => true,
|
||||
callback: (command: Command) => {
|
||||
command.executeTableBorderType(TableBorder.EMPTY)
|
||||
}
|
||||
},
|
||||
{
|
||||
key: BORDER_DASH,
|
||||
i18nPath: 'contextmenu.table.borderDash',
|
||||
icon: 'border-dash',
|
||||
when: () => true,
|
||||
callback: (command: Command) => {
|
||||
command.executeTableBorderType(TableBorder.DASH)
|
||||
}
|
||||
},
|
||||
{
|
||||
key: BORDER_EXTERNAL,
|
||||
i18nPath: 'contextmenu.table.borderExternal',
|
||||
icon: 'border-external',
|
||||
when: () => true,
|
||||
callback: (command: Command) => {
|
||||
command.executeTableBorderType(TableBorder.EXTERNAL)
|
||||
}
|
||||
},
|
||||
{
|
||||
key: BORDER_INTERNAL,
|
||||
i18nPath: 'contextmenu.table.borderInternal',
|
||||
icon: 'border-internal',
|
||||
when: () => true,
|
||||
callback: (command: Command) => {
|
||||
command.executeTableBorderType(TableBorder.INTERNAL)
|
||||
}
|
||||
},
|
||||
{
|
||||
key: BORDER_TD,
|
||||
i18nPath: 'contextmenu.table.borderTd',
|
||||
icon: 'border-td',
|
||||
when: () => true,
|
||||
childMenus: [
|
||||
{
|
||||
key: BORDER_TD_TOP,
|
||||
i18nPath: 'contextmenu.table.borderTdTop',
|
||||
icon: 'border-td-top',
|
||||
when: () => true,
|
||||
callback: (command: Command) => {
|
||||
command.executeTableTdBorderType(TdBorder.TOP)
|
||||
}
|
||||
},
|
||||
{
|
||||
key: BORDER_TD_RIGHT,
|
||||
i18nPath: 'contextmenu.table.borderTdRight',
|
||||
icon: 'border-td-right',
|
||||
when: () => true,
|
||||
callback: (command: Command) => {
|
||||
command.executeTableTdBorderType(TdBorder.RIGHT)
|
||||
}
|
||||
},
|
||||
{
|
||||
key: BORDER_TD_BOTTOM,
|
||||
i18nPath: 'contextmenu.table.borderTdBottom',
|
||||
icon: 'border-td-bottom',
|
||||
when: () => true,
|
||||
callback: (command: Command) => {
|
||||
command.executeTableTdBorderType(TdBorder.BOTTOM)
|
||||
}
|
||||
},
|
||||
{
|
||||
key: BORDER_TD_LEFT,
|
||||
i18nPath: 'contextmenu.table.borderTdLeft',
|
||||
icon: 'border-td-left',
|
||||
when: () => true,
|
||||
callback: (command: Command) => {
|
||||
command.executeTableTdBorderType(TdBorder.LEFT)
|
||||
}
|
||||
},
|
||||
{
|
||||
key: BORDER_TD_FORWARD,
|
||||
i18nPath: 'contextmenu.table.borderTdForward',
|
||||
icon: 'border-td-forward',
|
||||
when: () => true,
|
||||
callback: (command: Command) => {
|
||||
command.executeTableTdSlashType(TdSlash.FORWARD)
|
||||
}
|
||||
},
|
||||
{
|
||||
key: BORDER_TD_BACK,
|
||||
i18nPath: 'contextmenu.table.borderTdBack',
|
||||
icon: 'border-td-back',
|
||||
when: () => true,
|
||||
callback: (command: Command) => {
|
||||
command.executeTableTdSlashType(TdSlash.BACK)
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
key: VERTICAL_ALIGN,
|
||||
i18nPath: 'contextmenu.table.verticalAlign',
|
||||
icon: 'vertical-align',
|
||||
when: payload => {
|
||||
return (
|
||||
!payload.isReadonly &&
|
||||
payload.isInTable &&
|
||||
payload.options.mode !== EditorMode.FORM
|
||||
)
|
||||
},
|
||||
childMenus: [
|
||||
{
|
||||
key: VERTICAL_ALIGN_TOP,
|
||||
i18nPath: 'contextmenu.table.verticalAlignTop',
|
||||
icon: 'vertical-align-top',
|
||||
when: () => true,
|
||||
callback: (command: Command) => {
|
||||
command.executeTableTdVerticalAlign(VerticalAlign.TOP)
|
||||
}
|
||||
},
|
||||
{
|
||||
key: VERTICAL_ALIGN_MIDDLE,
|
||||
i18nPath: 'contextmenu.table.verticalAlignMiddle',
|
||||
icon: 'vertical-align-middle',
|
||||
when: () => true,
|
||||
callback: (command: Command) => {
|
||||
command.executeTableTdVerticalAlign(VerticalAlign.MIDDLE)
|
||||
}
|
||||
},
|
||||
{
|
||||
key: VERTICAL_ALIGN_BOTTOM,
|
||||
i18nPath: 'contextmenu.table.verticalAlignBottom',
|
||||
icon: 'vertical-align-bottom',
|
||||
when: () => true,
|
||||
callback: (command: Command) => {
|
||||
command.executeTableTdVerticalAlign(VerticalAlign.BOTTOM)
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
key: INSERT_ROW_COL,
|
||||
i18nPath: 'contextmenu.table.insertRowCol',
|
||||
icon: 'insert-row-col',
|
||||
when: payload => {
|
||||
return (
|
||||
!payload.isReadonly &&
|
||||
payload.isInTable &&
|
||||
payload.options.mode !== EditorMode.FORM
|
||||
)
|
||||
},
|
||||
childMenus: [
|
||||
{
|
||||
key: INSERT_TOP_ROW,
|
||||
i18nPath: 'contextmenu.table.insertTopRow',
|
||||
icon: 'insert-top-row',
|
||||
when: () => true,
|
||||
callback: (command: Command) => {
|
||||
command.executeInsertTableTopRow()
|
||||
}
|
||||
},
|
||||
{
|
||||
key: INSERT_BOTTOM_ROW,
|
||||
i18nPath: 'contextmenu.table.insertBottomRow',
|
||||
icon: 'insert-bottom-row',
|
||||
when: () => true,
|
||||
callback: (command: Command) => {
|
||||
command.executeInsertTableBottomRow()
|
||||
}
|
||||
},
|
||||
{
|
||||
key: INSERT_LEFT_COL,
|
||||
i18nPath: 'contextmenu.table.insertLeftCol',
|
||||
icon: 'insert-left-col',
|
||||
when: () => true,
|
||||
callback: (command: Command) => {
|
||||
command.executeInsertTableLeftCol()
|
||||
}
|
||||
},
|
||||
{
|
||||
key: INSERT_RIGHT_COL,
|
||||
i18nPath: 'contextmenu.table.insertRightCol',
|
||||
icon: 'insert-right-col',
|
||||
when: () => true,
|
||||
callback: (command: Command) => {
|
||||
command.executeInsertTableRightCol()
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
key: DELETE_ROW_COL,
|
||||
i18nPath: 'contextmenu.table.deleteRowCol',
|
||||
icon: 'delete-row-col',
|
||||
when: payload => {
|
||||
return (
|
||||
!payload.isReadonly &&
|
||||
payload.isInTable &&
|
||||
payload.options.mode !== EditorMode.FORM
|
||||
)
|
||||
},
|
||||
childMenus: [
|
||||
{
|
||||
key: DELETE_ROW,
|
||||
i18nPath: 'contextmenu.table.deleteRow',
|
||||
icon: 'delete-row',
|
||||
when: () => true,
|
||||
callback: (command: Command) => {
|
||||
command.executeDeleteTableRow()
|
||||
}
|
||||
},
|
||||
{
|
||||
key: DELETE_COL,
|
||||
i18nPath: 'contextmenu.table.deleteCol',
|
||||
icon: 'delete-col',
|
||||
when: () => true,
|
||||
callback: (command: Command) => {
|
||||
command.executeDeleteTableCol()
|
||||
}
|
||||
},
|
||||
{
|
||||
key: DELETE_TABLE,
|
||||
i18nPath: 'contextmenu.table.deleteTable',
|
||||
icon: 'delete-table',
|
||||
when: () => true,
|
||||
callback: (command: Command) => {
|
||||
command.executeDeleteTable()
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
key: MERGE_CELL,
|
||||
i18nPath: 'contextmenu.table.mergeCell',
|
||||
icon: 'merge-cell',
|
||||
when: payload => {
|
||||
return (
|
||||
!payload.isReadonly &&
|
||||
payload.isCrossRowCol &&
|
||||
payload.options.mode !== EditorMode.FORM
|
||||
)
|
||||
},
|
||||
callback: (command: Command) => {
|
||||
command.executeMergeTableCell()
|
||||
}
|
||||
},
|
||||
{
|
||||
key: CANCEL_MERGE_CELL,
|
||||
i18nPath: 'contextmenu.table.mergeCancelCell',
|
||||
icon: 'merge-cancel-cell',
|
||||
when: payload => {
|
||||
return (
|
||||
!payload.isReadonly &&
|
||||
payload.isInTable &&
|
||||
payload.options.mode !== EditorMode.FORM
|
||||
)
|
||||
},
|
||||
callback: (command: Command) => {
|
||||
command.executeCancelMergeTableCell()
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,235 @@
|
||||
import { CURSOR_AGENT_OFFSET_HEIGHT } from '../../dataset/constant/Cursor'
|
||||
import { EDITOR_PREFIX } from '../../dataset/constant/Editor'
|
||||
import { MoveDirection } from '../../dataset/enum/Observer'
|
||||
import { DeepRequired } from '../../interface/Common'
|
||||
import { ICursorOption } from '../../interface/Cursor'
|
||||
import { IEditorOption } from '../../interface/Editor'
|
||||
import { IElementPosition } from '../../interface/Element'
|
||||
import { findScrollContainer } from '../../utils'
|
||||
import { isMobile } from '../../utils/ua'
|
||||
import { Draw } from '../draw/Draw'
|
||||
import { CanvasEvent } from '../event/CanvasEvent'
|
||||
import { Position } from '../position/Position'
|
||||
import { CursorAgent } from './CursorAgent'
|
||||
|
||||
export type IDrawCursorOption = ICursorOption & {
|
||||
isShow?: boolean
|
||||
isBlink?: boolean
|
||||
isFocus?: boolean
|
||||
hitLineStartIndex?: number
|
||||
}
|
||||
|
||||
export interface IMoveCursorToVisibleOption {
|
||||
direction: MoveDirection
|
||||
cursorPosition: IElementPosition
|
||||
}
|
||||
|
||||
export class Cursor {
|
||||
private readonly ANIMATION_CLASS = `${EDITOR_PREFIX}-cursor--animation`
|
||||
|
||||
private draw: Draw
|
||||
private container: HTMLDivElement
|
||||
private options: DeepRequired<IEditorOption>
|
||||
private position: Position
|
||||
private cursorDom: HTMLDivElement
|
||||
private cursorAgent: CursorAgent
|
||||
private blinkTimeout: number | null
|
||||
private hitLineStartIndex: number | undefined
|
||||
|
||||
constructor(draw: Draw, canvasEvent: CanvasEvent) {
|
||||
this.draw = draw
|
||||
this.container = draw.getContainer()
|
||||
this.position = draw.getPosition()
|
||||
this.options = draw.getOptions()
|
||||
|
||||
this.cursorDom = document.createElement('div')
|
||||
this.cursorDom.classList.add(`${EDITOR_PREFIX}-cursor`)
|
||||
this.container.append(this.cursorDom)
|
||||
this.cursorAgent = new CursorAgent(draw, canvasEvent)
|
||||
this.blinkTimeout = null
|
||||
}
|
||||
|
||||
public getCursorDom(): HTMLDivElement {
|
||||
return this.cursorDom
|
||||
}
|
||||
|
||||
public getAgentDom(): HTMLTextAreaElement {
|
||||
return this.cursorAgent.getAgentCursorDom()
|
||||
}
|
||||
|
||||
public getAgentIsActive(): boolean {
|
||||
return this.getAgentDom() === document.activeElement
|
||||
}
|
||||
|
||||
public getAgentDomValue(): string {
|
||||
return this.getAgentDom().value
|
||||
}
|
||||
|
||||
public clearAgentDomValue() {
|
||||
this.getAgentDom().value = ''
|
||||
}
|
||||
|
||||
public getHitLineStartIndex() {
|
||||
return this.hitLineStartIndex
|
||||
}
|
||||
|
||||
private _blinkStart() {
|
||||
this.cursorDom.classList.add(this.ANIMATION_CLASS)
|
||||
}
|
||||
|
||||
private _blinkStop() {
|
||||
this.cursorDom.classList.remove(this.ANIMATION_CLASS)
|
||||
}
|
||||
|
||||
private _setBlinkTimeout() {
|
||||
this._clearBlinkTimeout()
|
||||
this.blinkTimeout = window.setTimeout(() => {
|
||||
this._blinkStart()
|
||||
}, 500)
|
||||
}
|
||||
|
||||
private _clearBlinkTimeout() {
|
||||
if (this.blinkTimeout) {
|
||||
this._blinkStop()
|
||||
window.clearTimeout(this.blinkTimeout)
|
||||
this.blinkTimeout = null
|
||||
}
|
||||
}
|
||||
|
||||
public focus() {
|
||||
// 移动端只读模式禁用聚焦避免唤起输入法,web端允许聚焦避免事件无法捕获
|
||||
if (isMobile && this.draw.isReadonly()) return
|
||||
const agentCursorDom = this.cursorAgent.getAgentCursorDom()
|
||||
// 光标不聚焦时重新定位
|
||||
if (document.activeElement !== agentCursorDom) {
|
||||
agentCursorDom.focus()
|
||||
agentCursorDom.setSelectionRange(0, 0)
|
||||
}
|
||||
}
|
||||
|
||||
public drawCursor(payload?: IDrawCursorOption) {
|
||||
let cursorPosition = this.position.getCursorPosition()
|
||||
if (!cursorPosition) return
|
||||
const { scale, cursor } = this.options
|
||||
const {
|
||||
color,
|
||||
width,
|
||||
isShow = true,
|
||||
isBlink = true,
|
||||
isFocus = true,
|
||||
hitLineStartIndex
|
||||
} = { ...cursor, ...payload }
|
||||
// 设置光标代理
|
||||
const height = this.draw.getHeight()
|
||||
const pageGap = this.draw.getPageGap()
|
||||
// 光标位置
|
||||
this.hitLineStartIndex = hitLineStartIndex
|
||||
if (hitLineStartIndex) {
|
||||
const positionList = this.position.getPositionList()
|
||||
cursorPosition = positionList[hitLineStartIndex]
|
||||
}
|
||||
const {
|
||||
metrics,
|
||||
coordinate: { leftTop, rightTop },
|
||||
ascent,
|
||||
pageNo
|
||||
} = cursorPosition
|
||||
const zoneManager = this.draw.getZone()
|
||||
const curPageNo = zoneManager.isMainActive()
|
||||
? pageNo
|
||||
: this.draw.getPageNo()
|
||||
const preY = curPageNo * (height + pageGap)
|
||||
// 默认偏移高度
|
||||
const defaultOffsetHeight = CURSOR_AGENT_OFFSET_HEIGHT * scale
|
||||
// 增加1/4字体大小(最小为defaultOffsetHeight即默认偏移高度)
|
||||
const increaseHeight = Math.min(metrics.height / 4, defaultOffsetHeight)
|
||||
const cursorHeight = metrics.height + increaseHeight * 2
|
||||
const agentCursorDom = this.cursorAgent.getAgentCursorDom()
|
||||
if (isFocus) {
|
||||
setTimeout(() => {
|
||||
this.focus()
|
||||
})
|
||||
}
|
||||
// fillText位置 + 文字基线到底部距离 - 模拟光标偏移量
|
||||
const descent =
|
||||
metrics.boundingBoxDescent < 0 ? 0 : metrics.boundingBoxDescent
|
||||
const cursorTop =
|
||||
leftTop[1] + ascent + descent - (cursorHeight - increaseHeight) + preY
|
||||
const cursorLeft = hitLineStartIndex ? leftTop[0] : rightTop[0]
|
||||
agentCursorDom.style.left = `${cursorLeft}px`
|
||||
agentCursorDom.style.top = `${
|
||||
cursorTop + cursorHeight - defaultOffsetHeight
|
||||
}px`
|
||||
// 模拟光标显示
|
||||
if (!isShow) {
|
||||
this.recoveryCursor()
|
||||
return
|
||||
}
|
||||
const isReadonly = this.draw.isReadonly()
|
||||
this.cursorDom.style.width = `${width * scale}px`
|
||||
this.cursorDom.style.backgroundColor = color
|
||||
this.cursorDom.style.left = `${cursorLeft}px`
|
||||
this.cursorDom.style.top = `${cursorTop}px`
|
||||
this.cursorDom.style.display = isReadonly ? 'none' : 'block'
|
||||
this.cursorDom.style.height = `${cursorHeight}px`
|
||||
if (isBlink) {
|
||||
this._setBlinkTimeout()
|
||||
} else {
|
||||
this._clearBlinkTimeout()
|
||||
}
|
||||
}
|
||||
|
||||
public recoveryCursor() {
|
||||
this.cursorDom.style.display = 'none'
|
||||
this._clearBlinkTimeout()
|
||||
}
|
||||
|
||||
public moveCursorToVisible(payload: IMoveCursorToVisibleOption) {
|
||||
const { cursorPosition, direction } = payload
|
||||
if (!cursorPosition || !direction) return
|
||||
const {
|
||||
pageNo,
|
||||
coordinate: { leftTop, leftBottom }
|
||||
} = cursorPosition
|
||||
// 当前页面距离滚动容器顶部距离
|
||||
const prePageY =
|
||||
pageNo * (this.draw.getHeight() + this.draw.getPageGap()) +
|
||||
this.container.getBoundingClientRect().top
|
||||
// 向上移动时:以顶部距离为准,向下移动时:以底部位置为准
|
||||
const isUp = direction === MoveDirection.UP
|
||||
const x = leftBottom[0]
|
||||
const y = isUp ? leftTop[1] + prePageY : leftBottom[1] + prePageY
|
||||
// 查找滚动容器,如果是滚动容器是document,则限制范围为当前窗口
|
||||
const scrollContainer = findScrollContainer(this.container)
|
||||
const rect = {
|
||||
left: 0,
|
||||
right: 0,
|
||||
top: 0,
|
||||
bottom: 0
|
||||
}
|
||||
if (scrollContainer === document.documentElement) {
|
||||
rect.right = window.innerWidth
|
||||
rect.bottom = window.innerHeight
|
||||
} else {
|
||||
const { left, right, top, bottom } =
|
||||
scrollContainer.getBoundingClientRect()
|
||||
rect.left = left
|
||||
rect.right = right
|
||||
rect.top = top
|
||||
rect.bottom = bottom
|
||||
}
|
||||
// 可视范围根据参数调整
|
||||
const { maskMargin } = this.options
|
||||
rect.top += maskMargin[0]
|
||||
rect.bottom -= maskMargin[2]
|
||||
// 不在可视范围时,移动滚动条到合适位置
|
||||
if (
|
||||
!(x >= rect.left && x <= rect.right && y >= rect.top && y <= rect.bottom)
|
||||
) {
|
||||
const { scrollLeft, scrollTop } = scrollContainer
|
||||
isUp
|
||||
? scrollContainer.scroll(scrollLeft, scrollTop - (rect.top - y))
|
||||
: scrollContainer.scroll(scrollLeft, scrollTop + y - rect.bottom)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
import { EDITOR_PREFIX } from '../../dataset/constant/Editor'
|
||||
import { EventBusMap } from '../../interface/EventBus'
|
||||
import { Draw } from '../draw/Draw'
|
||||
import { CanvasEvent } from '../event/CanvasEvent'
|
||||
import { EventBus } from '../event/eventbus/EventBus'
|
||||
import { pasteByEvent } from '../event/handlers/paste'
|
||||
|
||||
export class CursorAgent {
|
||||
private draw: Draw
|
||||
private container: HTMLDivElement
|
||||
private agentCursorDom: HTMLTextAreaElement
|
||||
private canvasEvent: CanvasEvent
|
||||
private eventBus: EventBus<EventBusMap>
|
||||
|
||||
constructor(draw: Draw, canvasEvent: CanvasEvent) {
|
||||
this.draw = draw
|
||||
this.container = draw.getContainer()
|
||||
this.canvasEvent = canvasEvent
|
||||
this.eventBus = draw.getEventBus()
|
||||
// 代理光标绘制
|
||||
const agentCursorDom = document.createElement('textarea')
|
||||
agentCursorDom.autocomplete = 'off'
|
||||
agentCursorDom.classList.add(`${EDITOR_PREFIX}-inputarea`)
|
||||
agentCursorDom.innerText = ''
|
||||
this.container.append(agentCursorDom)
|
||||
this.agentCursorDom = agentCursorDom
|
||||
// 事件
|
||||
agentCursorDom.onkeydown = (evt: KeyboardEvent) => this._keyDown(evt)
|
||||
agentCursorDom.oninput = this._input.bind(this)
|
||||
agentCursorDom.onpaste = (evt: ClipboardEvent) => this._paste(evt)
|
||||
agentCursorDom.addEventListener(
|
||||
'compositionstart',
|
||||
this._compositionstart.bind(this)
|
||||
)
|
||||
agentCursorDom.addEventListener(
|
||||
'compositionend',
|
||||
this._compositionend.bind(this)
|
||||
)
|
||||
}
|
||||
|
||||
public getAgentCursorDom(): HTMLTextAreaElement {
|
||||
return this.agentCursorDom
|
||||
}
|
||||
|
||||
private _keyDown(evt: KeyboardEvent) {
|
||||
this.canvasEvent.keydown(evt)
|
||||
}
|
||||
|
||||
private _input(evt: Event) {
|
||||
const data = (<InputEvent>evt).data
|
||||
if (data) {
|
||||
this.canvasEvent.input(data)
|
||||
}
|
||||
if (this.eventBus.isSubscribe('input')) {
|
||||
this.eventBus.emit('input', evt)
|
||||
}
|
||||
}
|
||||
|
||||
private _paste(evt: ClipboardEvent) {
|
||||
const isReadonly = this.draw.isReadonly()
|
||||
if (isReadonly) return
|
||||
const clipboardData = evt.clipboardData
|
||||
if (!clipboardData) return
|
||||
pasteByEvent(this.canvasEvent, evt)
|
||||
evt.preventDefault()
|
||||
}
|
||||
|
||||
private _compositionstart() {
|
||||
this.canvasEvent.compositionstart()
|
||||
}
|
||||
|
||||
private _compositionend(evt: CompositionEvent) {
|
||||
this.canvasEvent.compositionend(evt)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
import { ControlComponent } from '../../../../dataset/enum/Control'
|
||||
import { KeyMap } from '../../../../dataset/enum/KeyMap'
|
||||
import {
|
||||
IControlContext,
|
||||
IControlInstance,
|
||||
IControlRuleOption
|
||||
} from '../../../../interface/Control'
|
||||
import { IElement } from '../../../../interface/Element'
|
||||
import { Control } from '../Control'
|
||||
|
||||
export class CheckboxControl implements IControlInstance {
|
||||
protected element: IElement
|
||||
protected control: Control
|
||||
|
||||
constructor(element: IElement, control: Control) {
|
||||
this.element = element
|
||||
this.control = control
|
||||
}
|
||||
|
||||
public setElement(element: IElement) {
|
||||
this.element = element
|
||||
}
|
||||
|
||||
public getElement(): IElement {
|
||||
return this.element
|
||||
}
|
||||
|
||||
public getCode(): string | null {
|
||||
return this.element.control?.code || null
|
||||
}
|
||||
|
||||
public getValue(): IElement[] {
|
||||
const elementList = this.control.getElementList()
|
||||
const { startIndex } = this.control.getRange()
|
||||
const startElement = elementList[startIndex]
|
||||
const data: IElement[] = []
|
||||
// 向左查找
|
||||
let preIndex = startIndex
|
||||
while (preIndex > 0) {
|
||||
const preElement = elementList[preIndex]
|
||||
if (
|
||||
preElement.controlId !== startElement.controlId ||
|
||||
preElement.controlComponent === ControlComponent.PREFIX ||
|
||||
preElement.controlComponent === ControlComponent.PRE_TEXT
|
||||
) {
|
||||
break
|
||||
}
|
||||
if (preElement.controlComponent === ControlComponent.VALUE) {
|
||||
data.unshift(preElement)
|
||||
}
|
||||
preIndex--
|
||||
}
|
||||
// 向右查找
|
||||
let nextIndex = startIndex + 1
|
||||
while (nextIndex < elementList.length) {
|
||||
const nextElement = elementList[nextIndex]
|
||||
if (
|
||||
nextElement.controlId !== startElement.controlId ||
|
||||
nextElement.controlComponent === ControlComponent.POSTFIX ||
|
||||
nextElement.controlComponent === ControlComponent.POST_TEXT
|
||||
) {
|
||||
break
|
||||
}
|
||||
if (nextElement.controlComponent === ControlComponent.VALUE) {
|
||||
data.push(nextElement)
|
||||
}
|
||||
nextIndex++
|
||||
}
|
||||
return data
|
||||
}
|
||||
|
||||
public setValue(): number {
|
||||
return -1
|
||||
}
|
||||
|
||||
public setSelect(
|
||||
codes: string[],
|
||||
context: IControlContext = {},
|
||||
options: IControlRuleOption = {}
|
||||
) {
|
||||
// 校验是否可以设置
|
||||
if (
|
||||
!options.isIgnoreDisabledRule &&
|
||||
this.control.getIsDisabledControl(context)
|
||||
) {
|
||||
return
|
||||
}
|
||||
const { control } = this.element
|
||||
const elementList = context.elementList || this.control.getElementList()
|
||||
const { startIndex } = context.range || this.control.getRange()
|
||||
const startElement = elementList[startIndex]
|
||||
// 向左查找
|
||||
let preIndex = startIndex
|
||||
while (preIndex > 0) {
|
||||
const preElement = elementList[preIndex]
|
||||
if (
|
||||
preElement.controlId !== startElement.controlId ||
|
||||
preElement.controlComponent === ControlComponent.PREFIX ||
|
||||
preElement.controlComponent === ControlComponent.PRE_TEXT
|
||||
) {
|
||||
break
|
||||
}
|
||||
if (preElement.controlComponent === ControlComponent.CHECKBOX) {
|
||||
const checkbox = preElement.checkbox!
|
||||
checkbox.value = codes.includes(checkbox.code!)
|
||||
}
|
||||
preIndex--
|
||||
}
|
||||
// 向右查找
|
||||
let nextIndex = startIndex + 1
|
||||
while (nextIndex < elementList.length) {
|
||||
const nextElement = elementList[nextIndex]
|
||||
if (
|
||||
nextElement.controlId !== startElement.controlId ||
|
||||
nextElement.controlComponent === ControlComponent.POSTFIX ||
|
||||
nextElement.controlComponent === ControlComponent.POST_TEXT
|
||||
) {
|
||||
break
|
||||
}
|
||||
if (nextElement.controlComponent === ControlComponent.CHECKBOX) {
|
||||
const checkbox = nextElement.checkbox!
|
||||
checkbox.value = codes.includes(checkbox.code!)
|
||||
}
|
||||
nextIndex++
|
||||
}
|
||||
control!.code = codes.join(',')
|
||||
this.control.repaintControl({
|
||||
curIndex: startIndex,
|
||||
isSetCursor: false
|
||||
})
|
||||
this.control.emitControlContentChange({
|
||||
context
|
||||
})
|
||||
}
|
||||
|
||||
public keydown(evt: KeyboardEvent): number | null {
|
||||
if (this.control.getIsDisabledControl()) {
|
||||
return null
|
||||
}
|
||||
const range = this.control.getRange()
|
||||
// 收缩边界到Value内
|
||||
this.control.shrinkBoundary()
|
||||
const { startIndex, endIndex } = range
|
||||
// 删除
|
||||
if (evt.key === KeyMap.Backspace || evt.key === KeyMap.Delete) {
|
||||
return this.control.removeControl(startIndex)
|
||||
}
|
||||
return endIndex
|
||||
}
|
||||
|
||||
public cut(): number {
|
||||
return -1
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,397 @@
|
||||
import {
|
||||
CONTROL_STYLE_ATTR,
|
||||
EDITOR_ELEMENT_STYLE_ATTR,
|
||||
TEXTLIKE_ELEMENT_TYPE
|
||||
} from '../../../../dataset/constant/Element'
|
||||
import { ControlComponent } from '../../../../dataset/enum/Control'
|
||||
import { ElementType } from '../../../../dataset/enum/Element'
|
||||
import { KeyMap } from '../../../../dataset/enum/KeyMap'
|
||||
import { DeepRequired } from '../../../../interface/Common'
|
||||
import {
|
||||
IControlContext,
|
||||
IControlInstance,
|
||||
IControlRuleOption
|
||||
} from '../../../../interface/Control'
|
||||
import { IEditorOption } from '../../../../interface/Editor'
|
||||
import { IElement } from '../../../../interface/Element'
|
||||
import { omitObject, pickObject } from '../../../../utils'
|
||||
import { formatElementContext } from '../../../../utils/element'
|
||||
import { Draw } from '../../Draw'
|
||||
import { DatePicker } from '../../particle/date/DatePicker'
|
||||
import { Control } from '../Control'
|
||||
|
||||
export class DateControl implements IControlInstance {
|
||||
private draw: Draw
|
||||
private element: IElement
|
||||
private control: Control
|
||||
private isPopup: boolean
|
||||
private datePicker: DatePicker | null
|
||||
private options: DeepRequired<IEditorOption>
|
||||
|
||||
constructor(element: IElement, control: Control) {
|
||||
const draw = control.getDraw()
|
||||
this.draw = draw
|
||||
this.options = draw.getOptions()
|
||||
this.element = element
|
||||
this.control = control
|
||||
this.isPopup = false
|
||||
this.datePicker = null
|
||||
}
|
||||
|
||||
public setElement(element: IElement) {
|
||||
this.element = element
|
||||
}
|
||||
|
||||
public getElement(): IElement {
|
||||
return this.element
|
||||
}
|
||||
|
||||
public getIsPopup(): boolean {
|
||||
return this.isPopup
|
||||
}
|
||||
|
||||
public getValueRange(context: IControlContext = {}): [number, number] | null {
|
||||
const elementList = context.elementList || this.control.getElementList()
|
||||
const { startIndex } = context.range || this.control.getRange()
|
||||
const startElement = elementList[startIndex]
|
||||
// 向左查找
|
||||
let preIndex = startIndex
|
||||
while (preIndex > 0) {
|
||||
const preElement = elementList[preIndex]
|
||||
if (
|
||||
preElement.controlId !== startElement.controlId ||
|
||||
preElement.controlComponent === ControlComponent.PREFIX ||
|
||||
preElement.controlComponent === ControlComponent.PRE_TEXT
|
||||
) {
|
||||
break
|
||||
}
|
||||
preIndex--
|
||||
}
|
||||
// 向右查找
|
||||
let nextIndex = startIndex + 1
|
||||
while (nextIndex < elementList.length) {
|
||||
const nextElement = elementList[nextIndex]
|
||||
if (
|
||||
nextElement.controlId !== startElement.controlId ||
|
||||
nextElement.controlComponent === ControlComponent.POSTFIX ||
|
||||
nextElement.controlComponent === ControlComponent.POST_TEXT
|
||||
) {
|
||||
break
|
||||
}
|
||||
nextIndex++
|
||||
}
|
||||
if (preIndex === nextIndex) return null
|
||||
return [preIndex, nextIndex - 1]
|
||||
}
|
||||
|
||||
public getValue(context: IControlContext = {}): IElement[] {
|
||||
const elementList = context.elementList || this.control.getElementList()
|
||||
const range = this.getValueRange(context)
|
||||
if (!range) return []
|
||||
const data: IElement[] = []
|
||||
const [startIndex, endIndex] = range
|
||||
for (let i = startIndex; i <= endIndex; i++) {
|
||||
const element = elementList[i]
|
||||
if (element.controlComponent === ControlComponent.VALUE) {
|
||||
data.push(element)
|
||||
}
|
||||
}
|
||||
return data
|
||||
}
|
||||
|
||||
public setValue(
|
||||
data: IElement[],
|
||||
context: IControlContext = {},
|
||||
options: IControlRuleOption = {}
|
||||
): number {
|
||||
// 校验是否可以设置
|
||||
if (
|
||||
!options.isIgnoreDisabledRule &&
|
||||
this.control.getIsDisabledControl(context)
|
||||
) {
|
||||
return -1
|
||||
}
|
||||
const elementList = context.elementList || this.control.getElementList()
|
||||
const range = context.range || this.control.getRange()
|
||||
// 收缩边界到Value内
|
||||
this.control.shrinkBoundary(context)
|
||||
const { startIndex, endIndex } = range
|
||||
const draw = this.control.getDraw()
|
||||
// 移除选区元素
|
||||
if (startIndex !== endIndex) {
|
||||
draw.spliceElementList(elementList, startIndex + 1, endIndex - startIndex)
|
||||
} else {
|
||||
// 移除空白占位符
|
||||
this.control.removePlaceholder(startIndex, context)
|
||||
}
|
||||
// 非文本类元素或前缀过渡掉样式属性
|
||||
const startElement = elementList[startIndex]
|
||||
const anchorElement =
|
||||
(startElement.type &&
|
||||
!TEXTLIKE_ELEMENT_TYPE.includes(startElement.type)) ||
|
||||
startElement.controlComponent === ControlComponent.PREFIX ||
|
||||
startElement.controlComponent === ControlComponent.PRE_TEXT
|
||||
? pickObject(startElement, [
|
||||
'control',
|
||||
'controlId',
|
||||
...CONTROL_STYLE_ATTR
|
||||
])
|
||||
: omitObject(startElement, ['type'])
|
||||
// 插入起始位置
|
||||
const start = range.startIndex + 1
|
||||
for (let i = 0; i < data.length; i++) {
|
||||
const newElement: IElement = {
|
||||
...anchorElement,
|
||||
...data[i],
|
||||
controlComponent: ControlComponent.VALUE
|
||||
}
|
||||
formatElementContext(elementList, [newElement], startIndex, {
|
||||
editorOptions: this.options
|
||||
})
|
||||
draw.spliceElementList(elementList, start + i, 0, [newElement])
|
||||
}
|
||||
return start + data.length - 1
|
||||
}
|
||||
|
||||
public clearSelect(
|
||||
context: IControlContext = {},
|
||||
options: IControlRuleOption = {}
|
||||
): number {
|
||||
const { isIgnoreDisabledRule = false, isAddPlaceholder = true } = options
|
||||
// 校验是否可以设置
|
||||
if (!isIgnoreDisabledRule && this.control.getIsDisabledControl(context)) {
|
||||
return -1
|
||||
}
|
||||
const range = this.getValueRange(context)
|
||||
if (!range) return -1
|
||||
const [leftIndex, rightIndex] = range
|
||||
if (!~leftIndex || !~rightIndex) return -1
|
||||
const elementList = context.elementList || this.control.getElementList()
|
||||
// 删除元素
|
||||
const draw = this.control.getDraw()
|
||||
draw.spliceElementList(
|
||||
elementList,
|
||||
leftIndex + 1,
|
||||
rightIndex - leftIndex,
|
||||
[],
|
||||
{
|
||||
isIgnoreDeletedRule: options.isIgnoreDeletedRule
|
||||
}
|
||||
)
|
||||
// 增加占位符
|
||||
if (isAddPlaceholder) {
|
||||
this.control.addPlaceholder(leftIndex, context)
|
||||
}
|
||||
return leftIndex
|
||||
}
|
||||
|
||||
public setSelect(
|
||||
date: string,
|
||||
context: IControlContext = {},
|
||||
options: IControlRuleOption = {}
|
||||
) {
|
||||
// 校验是否可以设置
|
||||
if (
|
||||
!options.isIgnoreDisabledRule &&
|
||||
this.control.getIsDisabledControl(context)
|
||||
) {
|
||||
return
|
||||
}
|
||||
const elementList = context.elementList || this.control.getElementList()
|
||||
const range = context.range || this.control.getRange()
|
||||
// 样式赋值元素-默认值的第一个字符样式,否则取默认样式
|
||||
const valueElement = this.getValue(context)[0]
|
||||
const styleElement = valueElement
|
||||
? pickObject(valueElement, EDITOR_ELEMENT_STYLE_ATTR)
|
||||
: pickObject(elementList[range.startIndex], CONTROL_STYLE_ATTR)
|
||||
// 清空选项
|
||||
const prefixIndex = this.clearSelect(context, {
|
||||
isAddPlaceholder: false,
|
||||
isIgnoreDeletedRule: options.isIgnoreDeletedRule
|
||||
})
|
||||
if (!~prefixIndex) return
|
||||
// 属性赋值元素-默认为前缀属性
|
||||
const propertyElement = omitObject(
|
||||
elementList[prefixIndex],
|
||||
EDITOR_ELEMENT_STYLE_ATTR
|
||||
)
|
||||
const start = prefixIndex + 1
|
||||
const draw = this.control.getDraw()
|
||||
for (let i = 0; i < date.length; i++) {
|
||||
const newElement: IElement = {
|
||||
...styleElement,
|
||||
...propertyElement,
|
||||
type: ElementType.TEXT,
|
||||
value: date[i],
|
||||
controlComponent: ControlComponent.VALUE
|
||||
}
|
||||
formatElementContext(elementList, [newElement], prefixIndex, {
|
||||
editorOptions: this.options
|
||||
})
|
||||
draw.spliceElementList(elementList, start + i, 0, [newElement])
|
||||
}
|
||||
// 重新渲染控件
|
||||
if (!context.range) {
|
||||
const newIndex = start + date.length - 1
|
||||
this.control.repaintControl({
|
||||
curIndex: newIndex
|
||||
})
|
||||
this.control.emitControlContentChange({
|
||||
context
|
||||
})
|
||||
this.destroy()
|
||||
}
|
||||
}
|
||||
|
||||
public keydown(evt: KeyboardEvent): number | null {
|
||||
if (this.control.getIsDisabledControl()) {
|
||||
return null
|
||||
}
|
||||
const elementList = this.control.getElementList()
|
||||
const range = this.control.getRange()
|
||||
// 收缩边界到Value内
|
||||
this.control.shrinkBoundary()
|
||||
const { startIndex, endIndex } = range
|
||||
const startElement = elementList[startIndex]
|
||||
const endElement = elementList[endIndex]
|
||||
const draw = this.control.getDraw()
|
||||
// backspace
|
||||
if (evt.key === KeyMap.Backspace) {
|
||||
// 移除选区元素
|
||||
if (startIndex !== endIndex) {
|
||||
draw.spliceElementList(
|
||||
elementList,
|
||||
startIndex + 1,
|
||||
endIndex - startIndex
|
||||
)
|
||||
const value = this.getValue()
|
||||
if (!value.length) {
|
||||
this.control.addPlaceholder(startIndex)
|
||||
}
|
||||
return startIndex
|
||||
} else {
|
||||
if (
|
||||
startElement.controlComponent === ControlComponent.PREFIX ||
|
||||
startElement.controlComponent === ControlComponent.PRE_TEXT ||
|
||||
endElement.controlComponent === ControlComponent.POSTFIX ||
|
||||
endElement.controlComponent === ControlComponent.POST_TEXT ||
|
||||
startElement.controlComponent === ControlComponent.PLACEHOLDER
|
||||
) {
|
||||
// 前缀、后缀、占位符
|
||||
return this.control.removeControl(startIndex)
|
||||
} else {
|
||||
// 文本
|
||||
draw.spliceElementList(elementList, startIndex, 1)
|
||||
const value = this.getValue()
|
||||
if (!value.length) {
|
||||
this.control.addPlaceholder(startIndex - 1)
|
||||
}
|
||||
return startIndex - 1
|
||||
}
|
||||
}
|
||||
} else if (evt.key === KeyMap.Delete) {
|
||||
// 移除选区元素
|
||||
if (startIndex !== endIndex) {
|
||||
draw.spliceElementList(
|
||||
elementList,
|
||||
startIndex + 1,
|
||||
endIndex - startIndex
|
||||
)
|
||||
const value = this.getValue()
|
||||
if (!value.length) {
|
||||
this.control.addPlaceholder(startIndex)
|
||||
}
|
||||
return startIndex
|
||||
} else {
|
||||
const endNextElement = elementList[endIndex + 1]
|
||||
if (
|
||||
((startElement.controlComponent === ControlComponent.PREFIX ||
|
||||
startElement.controlComponent === ControlComponent.PRE_TEXT) &&
|
||||
endNextElement.controlComponent === ControlComponent.PLACEHOLDER) ||
|
||||
endNextElement.controlComponent === ControlComponent.POSTFIX ||
|
||||
endNextElement.controlComponent === ControlComponent.POST_TEXT ||
|
||||
startElement.controlComponent === ControlComponent.PLACEHOLDER
|
||||
) {
|
||||
// 前缀、后缀、占位符
|
||||
return this.control.removeControl(startIndex)
|
||||
} else {
|
||||
// 文本
|
||||
draw.spliceElementList(elementList, startIndex + 1, 1)
|
||||
const value = this.getValue()
|
||||
if (!value.length) {
|
||||
this.control.addPlaceholder(startIndex)
|
||||
}
|
||||
return startIndex
|
||||
}
|
||||
}
|
||||
}
|
||||
return endIndex
|
||||
}
|
||||
|
||||
public cut(): number {
|
||||
if (this.control.getIsDisabledControl()) {
|
||||
return -1
|
||||
}
|
||||
this.control.shrinkBoundary()
|
||||
const { startIndex, endIndex } = this.control.getRange()
|
||||
if (startIndex === endIndex) {
|
||||
return startIndex
|
||||
}
|
||||
const draw = this.control.getDraw()
|
||||
const elementList = this.control.getElementList()
|
||||
draw.spliceElementList(elementList, startIndex + 1, endIndex - startIndex)
|
||||
const value = this.getValue()
|
||||
if (!value.length) {
|
||||
this.control.addPlaceholder(startIndex)
|
||||
}
|
||||
return startIndex
|
||||
}
|
||||
|
||||
public awake() {
|
||||
if (
|
||||
this.isPopup ||
|
||||
this.control.getIsDisabledControl() ||
|
||||
!this.control.getIsRangeWithinControl()
|
||||
) {
|
||||
return
|
||||
}
|
||||
const position = this.control.getPosition()
|
||||
if (!position) return
|
||||
const elementList = this.draw.getElementList()
|
||||
const { startIndex } = this.control.getRange()
|
||||
if (elementList[startIndex + 1]?.controlId !== this.element.controlId) {
|
||||
return
|
||||
}
|
||||
// 渲染日期控件
|
||||
this.datePicker = new DatePicker(this.draw, {
|
||||
onSubmit: this._setDate.bind(this)
|
||||
})
|
||||
const value =
|
||||
this.getValue()
|
||||
.map(el => el.value)
|
||||
.join('') || ''
|
||||
const dateFormat = this.element.control?.dateFormat
|
||||
this.datePicker.render({
|
||||
value,
|
||||
position,
|
||||
dateFormat
|
||||
})
|
||||
// 弹窗状态
|
||||
this.isPopup = true
|
||||
}
|
||||
|
||||
public destroy() {
|
||||
if (!this.isPopup) return
|
||||
this.datePicker?.destroy()
|
||||
this.isPopup = false
|
||||
}
|
||||
|
||||
private _setDate(date: string) {
|
||||
if (!date) {
|
||||
this.clearSelect()
|
||||
} else {
|
||||
this.setSelect(date)
|
||||
}
|
||||
this.destroy()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,214 @@
|
||||
import { ZERO } from '../../../../dataset/constant/Common'
|
||||
import { ControlComponent } from '../../../../dataset/enum/Control'
|
||||
import { ElementType } from '../../../../dataset/enum/Element'
|
||||
import { DeepRequired } from '../../../../interface/Common'
|
||||
import {
|
||||
IControlHighlight,
|
||||
IControlHighlightRule
|
||||
} from '../../../../interface/Control'
|
||||
import { IEditorOption } from '../../../../interface/Editor'
|
||||
import { IElement, IElementPosition } from '../../../../interface/Element'
|
||||
import {
|
||||
ISearchResult,
|
||||
ISearchResultRestArgs
|
||||
} from '../../../../interface/Search'
|
||||
import { Draw } from '../../Draw'
|
||||
import { Control } from '../Control'
|
||||
|
||||
type IHighlightMatchResult = (ISearchResult & IControlHighlightRule)[]
|
||||
|
||||
export class ControlSearch {
|
||||
private draw: Draw
|
||||
private control: Control
|
||||
private options: DeepRequired<IEditorOption>
|
||||
private highlightList: IControlHighlight[]
|
||||
private highlightMatchResult: IHighlightMatchResult
|
||||
|
||||
constructor(control: Control) {
|
||||
this.draw = control.getDraw()
|
||||
this.control = control
|
||||
this.options = this.draw.getOptions()
|
||||
|
||||
this.highlightList = []
|
||||
this.highlightMatchResult = []
|
||||
}
|
||||
|
||||
// 获取控件设置高亮信息
|
||||
public getControlHighlight(elementList: IElement[], index: number) {
|
||||
const {
|
||||
control: {
|
||||
activeBackgroundColor,
|
||||
disabledBackgroundColor,
|
||||
existValueBackgroundColor,
|
||||
noValueBackgroundColor
|
||||
}
|
||||
} = this.options
|
||||
const element = elementList[index]
|
||||
const isPrintMode = this.draw.isPrintMode()
|
||||
const activeControlElement = this.control.getActiveControl()?.getElement()
|
||||
// 颜色配置:元素 > 控件激活 > 控件禁用 > 控件存在值 > 控件不存在值
|
||||
let isActiveControlHighlight = false
|
||||
let isDisabledControlHighlight = false
|
||||
let isExitsValueControlHighlight = false
|
||||
let isNoValueControlHighlight = false
|
||||
if (!element.highlight) {
|
||||
// 控件激活时高亮色
|
||||
isActiveControlHighlight =
|
||||
!isPrintMode &&
|
||||
!!activeBackgroundColor &&
|
||||
!!activeControlElement &&
|
||||
element.controlId === activeControlElement.controlId &&
|
||||
!this.control.getIsRangeInPostfix()
|
||||
}
|
||||
if (!isActiveControlHighlight) {
|
||||
// 控件禁用时高亮色
|
||||
isDisabledControlHighlight =
|
||||
!isPrintMode && !!disabledBackgroundColor && !!element.control?.disabled
|
||||
}
|
||||
if (!isDisabledControlHighlight) {
|
||||
// 控件存在值时高亮色
|
||||
isExitsValueControlHighlight =
|
||||
!isPrintMode &&
|
||||
!!existValueBackgroundColor &&
|
||||
!!element.controlId &&
|
||||
this.control.getIsExistValueByElementListIndex(elementList, index)
|
||||
}
|
||||
if (!isExitsValueControlHighlight) {
|
||||
// 控件不存在值时高亮色
|
||||
isNoValueControlHighlight =
|
||||
!isPrintMode &&
|
||||
!!noValueBackgroundColor &&
|
||||
!!element.controlId &&
|
||||
!this.control.getIsExistValueByElementListIndex(elementList, index)
|
||||
}
|
||||
return (
|
||||
(isActiveControlHighlight ? activeBackgroundColor : '') ||
|
||||
(isDisabledControlHighlight ? disabledBackgroundColor : '') ||
|
||||
(isExitsValueControlHighlight ? existValueBackgroundColor : '') ||
|
||||
(isNoValueControlHighlight ? noValueBackgroundColor : '')
|
||||
)
|
||||
}
|
||||
|
||||
public getHighlightMatchResult(): IHighlightMatchResult {
|
||||
return this.highlightMatchResult
|
||||
}
|
||||
|
||||
public getHighlightList(): IControlHighlight[] {
|
||||
return this.highlightList
|
||||
}
|
||||
|
||||
public setHighlightList(payload: IControlHighlight[]) {
|
||||
this.highlightList = payload
|
||||
}
|
||||
|
||||
public computeHighlightList() {
|
||||
const search = this.draw.getSearch()
|
||||
const computeHighlight = (
|
||||
elementList: IElement[],
|
||||
restArgs?: ISearchResultRestArgs
|
||||
) => {
|
||||
let i = 0
|
||||
while (i < elementList.length) {
|
||||
const element = elementList[i]
|
||||
i++
|
||||
// 表格下钻处理
|
||||
if (element.type === ElementType.TABLE) {
|
||||
const trList = element.trList!
|
||||
for (let r = 0; r < trList.length; r++) {
|
||||
const tr = trList[r]
|
||||
for (let d = 0; d < tr.tdList.length; d++) {
|
||||
const td = tr.tdList[d]
|
||||
const restArgs: ISearchResultRestArgs = {
|
||||
tableId: element.id,
|
||||
tableIndex: i - 1,
|
||||
trIndex: r,
|
||||
tdIndex: d,
|
||||
tdId: td.id
|
||||
}
|
||||
computeHighlight(td.value, restArgs)
|
||||
}
|
||||
}
|
||||
}
|
||||
const currentControl = element?.control
|
||||
if (!currentControl) continue
|
||||
const highlightIndex = this.highlightList.findIndex(
|
||||
highlight =>
|
||||
highlight.id === element.controlId ||
|
||||
(currentControl.conceptId &&
|
||||
currentControl.conceptId === highlight.conceptId)
|
||||
)
|
||||
if (!~highlightIndex) continue
|
||||
// 搜索后控件结束索引
|
||||
const startIndex = i
|
||||
let newEndIndex = i
|
||||
while (newEndIndex < elementList.length) {
|
||||
const nextElement = elementList[newEndIndex]
|
||||
if (nextElement.controlId !== element.controlId) break
|
||||
newEndIndex++
|
||||
}
|
||||
i = newEndIndex
|
||||
// 高亮信息
|
||||
const controlElementList = elementList
|
||||
.slice(startIndex, newEndIndex)
|
||||
.map(element =>
|
||||
element.controlComponent === ControlComponent.VALUE
|
||||
? element
|
||||
: { value: ZERO }
|
||||
)
|
||||
const highlight = this.highlightList[highlightIndex]
|
||||
const { ruleList } = highlight
|
||||
for (let r = 0; r < ruleList.length; r++) {
|
||||
const rule = ruleList[r]
|
||||
const searchResult = search.getMatchList(
|
||||
rule.keyword,
|
||||
controlElementList
|
||||
)
|
||||
this.highlightMatchResult.push(
|
||||
...searchResult.map(result => ({
|
||||
...result,
|
||||
...rule,
|
||||
...restArgs,
|
||||
index: result.index + startIndex // 实际索引
|
||||
}))
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
this.highlightMatchResult = []
|
||||
computeHighlight(this.draw.getOriginalMainElementList())
|
||||
}
|
||||
|
||||
public renderHighlightList(ctx: CanvasRenderingContext2D, pageIndex: number) {
|
||||
if (!this.highlightMatchResult?.length) return
|
||||
const { searchMatchAlpha, searchMatchColor } = this.options
|
||||
const positionList = this.draw.getPosition().getOriginalPositionList()
|
||||
const elementList = this.draw.getOriginalElementList()
|
||||
ctx.save()
|
||||
for (let s = 0; s < this.highlightMatchResult.length; s++) {
|
||||
const searchMatch = this.highlightMatchResult[s]
|
||||
let position: IElementPosition | null = null
|
||||
if (searchMatch.tableId) {
|
||||
const { tableIndex, trIndex, tdIndex, index } = searchMatch
|
||||
position =
|
||||
elementList[tableIndex!]?.trList![trIndex!].tdList[tdIndex!]
|
||||
?.positionList![index]
|
||||
} else {
|
||||
position = positionList[searchMatch.index]
|
||||
}
|
||||
if (!position) continue
|
||||
const {
|
||||
coordinate: { leftTop, leftBottom, rightTop },
|
||||
pageNo
|
||||
} = position
|
||||
if (pageNo !== pageIndex) continue
|
||||
ctx.fillStyle = searchMatch.backgroundColor || searchMatchColor
|
||||
ctx.globalAlpha = searchMatch.alpha || searchMatchAlpha
|
||||
const x = leftTop[0]
|
||||
const y = leftTop[1]
|
||||
const width = rightTop[0] - leftTop[0]
|
||||
const height = leftBottom[1] - leftTop[1]
|
||||
ctx.fillRect(x, y, width, height)
|
||||
}
|
||||
ctx.restore()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
import { TextControl } from '../text/TextControl'
|
||||
|
||||
export class NumberControl extends TextControl {}
|
||||
@@ -0,0 +1,68 @@
|
||||
import { ControlComponent } from '../../../../dataset/enum/Control'
|
||||
import {
|
||||
IControlContext,
|
||||
IControlRuleOption
|
||||
} from '../../../../interface/Control'
|
||||
import { CheckboxControl } from '../checkbox/CheckboxControl'
|
||||
|
||||
export class RadioControl extends CheckboxControl {
|
||||
public setSelect(
|
||||
codes: string[],
|
||||
context: IControlContext = {},
|
||||
options: IControlRuleOption = {}
|
||||
) {
|
||||
// 校验是否可以设置
|
||||
if (
|
||||
!options.isIgnoreDisabledRule &&
|
||||
this.control.getIsDisabledControl(context)
|
||||
) {
|
||||
return
|
||||
}
|
||||
const { control } = this.element
|
||||
const elementList = context.elementList || this.control.getElementList()
|
||||
const { startIndex } = context.range || this.control.getRange()
|
||||
const startElement = elementList[startIndex]
|
||||
// 向左查找
|
||||
let preIndex = startIndex
|
||||
while (preIndex > 0) {
|
||||
const preElement = elementList[preIndex]
|
||||
if (
|
||||
preElement.controlId !== startElement.controlId ||
|
||||
preElement.controlComponent === ControlComponent.PREFIX ||
|
||||
preElement.controlComponent === ControlComponent.PRE_TEXT
|
||||
) {
|
||||
break
|
||||
}
|
||||
if (preElement.controlComponent === ControlComponent.RADIO) {
|
||||
const radio = preElement.radio!
|
||||
radio.value = codes.includes(radio.code!)
|
||||
}
|
||||
preIndex--
|
||||
}
|
||||
// 向右查找
|
||||
let nextIndex = startIndex + 1
|
||||
while (nextIndex < elementList.length) {
|
||||
const nextElement = elementList[nextIndex]
|
||||
if (
|
||||
nextElement.controlId !== startElement.controlId ||
|
||||
nextElement.controlComponent === ControlComponent.POSTFIX ||
|
||||
nextElement.controlComponent === ControlComponent.POST_TEXT
|
||||
) {
|
||||
break
|
||||
}
|
||||
if (nextElement.controlComponent === ControlComponent.RADIO) {
|
||||
const radio = nextElement.radio!
|
||||
radio.value = codes.includes(radio.code!)
|
||||
}
|
||||
nextIndex++
|
||||
}
|
||||
control!.code = codes.join(',')
|
||||
this.control.repaintControl({
|
||||
curIndex: startIndex,
|
||||
isSetCursor: false
|
||||
})
|
||||
this.control.emitControlContentChange({
|
||||
context
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
import { DeepRequired } from '../../../../interface/Common'
|
||||
import { IEditorOption } from '../../../../interface/Editor'
|
||||
import { IElementFillRect } from '../../../../interface/Element'
|
||||
import { Draw } from '../../Draw'
|
||||
|
||||
export class ControlBorder {
|
||||
protected borderRect: IElementFillRect
|
||||
private options: DeepRequired<IEditorOption>
|
||||
|
||||
constructor(draw: Draw) {
|
||||
this.borderRect = this.clearBorderInfo()
|
||||
this.options = draw.getOptions()
|
||||
}
|
||||
|
||||
public clearBorderInfo() {
|
||||
this.borderRect = {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 0,
|
||||
height: 0
|
||||
}
|
||||
return this.borderRect
|
||||
}
|
||||
|
||||
public recordBorderInfo(x: number, y: number, width: number, height: number) {
|
||||
const isFirstRecord = !this.borderRect.width
|
||||
if (isFirstRecord) {
|
||||
this.borderRect.x = x
|
||||
this.borderRect.y = y
|
||||
this.borderRect.height = height
|
||||
}
|
||||
this.borderRect.width += width
|
||||
}
|
||||
|
||||
public render(ctx: CanvasRenderingContext2D) {
|
||||
if (!this.borderRect.width) return
|
||||
const {
|
||||
scale,
|
||||
control: { borderWidth, borderColor }
|
||||
} = this.options
|
||||
const { x, y, width, height } = this.borderRect
|
||||
ctx.save()
|
||||
ctx.translate(0, 1 * scale)
|
||||
ctx.lineWidth = borderWidth * scale
|
||||
ctx.strokeStyle = borderColor
|
||||
ctx.beginPath()
|
||||
ctx.rect(x, y, width, height)
|
||||
ctx.stroke()
|
||||
ctx.restore()
|
||||
this.clearBorderInfo()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,512 @@
|
||||
import {
|
||||
EDITOR_COMPONENT,
|
||||
EDITOR_PREFIX
|
||||
} from '../../../../dataset/constant/Editor'
|
||||
import {
|
||||
CONTROL_STYLE_ATTR,
|
||||
EDITOR_ELEMENT_STYLE_ATTR,
|
||||
TEXTLIKE_ELEMENT_TYPE
|
||||
} from '../../../../dataset/constant/Element'
|
||||
import { ControlComponent } from '../../../../dataset/enum/Control'
|
||||
import { EditorComponent } from '../../../../dataset/enum/Editor'
|
||||
import { ElementType } from '../../../../dataset/enum/Element'
|
||||
import { KeyMap } from '../../../../dataset/enum/KeyMap'
|
||||
import { DeepRequired } from '../../../../interface/Common'
|
||||
import {
|
||||
IControlContext,
|
||||
IControlInstance,
|
||||
IControlRuleOption
|
||||
} from '../../../../interface/Control'
|
||||
import { IEditorOption } from '../../../../interface/Editor'
|
||||
import { IElement } from '../../../../interface/Element'
|
||||
import {
|
||||
isArrayEqual,
|
||||
isNonValue,
|
||||
omitObject,
|
||||
pickObject,
|
||||
splitText
|
||||
} from '../../../../utils'
|
||||
import { formatElementContext } from '../../../../utils/element'
|
||||
import { Control } from '../Control'
|
||||
|
||||
export class SelectControl implements IControlInstance {
|
||||
private element: IElement
|
||||
private control: Control
|
||||
private isPopup: boolean
|
||||
private selectDom: HTMLDivElement | null
|
||||
private options: DeepRequired<IEditorOption>
|
||||
private VALUE_DELIMITER = ','
|
||||
private DEFAULT_MULTI_SELECT_DELIMITER = ','
|
||||
|
||||
constructor(element: IElement, control: Control) {
|
||||
const draw = control.getDraw()
|
||||
this.options = draw.getOptions()
|
||||
this.element = element
|
||||
this.control = control
|
||||
this.isPopup = false
|
||||
this.selectDom = null
|
||||
}
|
||||
|
||||
public setElement(element: IElement) {
|
||||
this.element = element
|
||||
}
|
||||
|
||||
public getElement(): IElement {
|
||||
return this.element
|
||||
}
|
||||
|
||||
public getIsPopup(): boolean {
|
||||
return this.isPopup
|
||||
}
|
||||
|
||||
public getCodes(): string[] {
|
||||
return this.element?.control?.code
|
||||
? this.element.control.code.split(',')
|
||||
: []
|
||||
}
|
||||
|
||||
public getText(codes: string[]): string | null {
|
||||
if (!this.element?.control) return null
|
||||
const control = this.element.control
|
||||
if (!control.valueSets?.length) return null
|
||||
const multiSelectDelimiter =
|
||||
control?.multiSelectDelimiter || this.DEFAULT_MULTI_SELECT_DELIMITER
|
||||
const valueSets = control.valueSets
|
||||
const valueList: string[] = []
|
||||
codes.forEach(code => {
|
||||
const valueSet = valueSets.find(v => v.code === code)
|
||||
if (valueSet && !isNonValue(valueSet.value)) {
|
||||
valueList.push(valueSet.value)
|
||||
}
|
||||
})
|
||||
return valueList.join(multiSelectDelimiter) || null
|
||||
}
|
||||
|
||||
public getValue(context: IControlContext = {}): IElement[] {
|
||||
const elementList = context.elementList || this.control.getElementList()
|
||||
const { startIndex } = context.range || this.control.getRange()
|
||||
const startElement = elementList[startIndex]
|
||||
const data: IElement[] = []
|
||||
// 向左查找
|
||||
let preIndex = startIndex
|
||||
while (preIndex > 0) {
|
||||
const preElement = elementList[preIndex]
|
||||
if (
|
||||
preElement.controlId !== startElement.controlId ||
|
||||
preElement.controlComponent === ControlComponent.PREFIX ||
|
||||
preElement.controlComponent === ControlComponent.PRE_TEXT
|
||||
) {
|
||||
break
|
||||
}
|
||||
if (preElement.controlComponent === ControlComponent.VALUE) {
|
||||
data.unshift(preElement)
|
||||
}
|
||||
preIndex--
|
||||
}
|
||||
// 向右查找
|
||||
let nextIndex = startIndex + 1
|
||||
while (nextIndex < elementList.length) {
|
||||
const nextElement = elementList[nextIndex]
|
||||
if (
|
||||
nextElement.controlId !== startElement.controlId ||
|
||||
nextElement.controlComponent === ControlComponent.POSTFIX ||
|
||||
nextElement.controlComponent === ControlComponent.POST_TEXT
|
||||
) {
|
||||
break
|
||||
}
|
||||
if (nextElement.controlComponent === ControlComponent.VALUE) {
|
||||
data.push(nextElement)
|
||||
}
|
||||
nextIndex++
|
||||
}
|
||||
return data
|
||||
}
|
||||
|
||||
public setValue(
|
||||
data: IElement[],
|
||||
context: IControlContext = {},
|
||||
options: IControlRuleOption = {}
|
||||
): number {
|
||||
// 校验是否可以设置
|
||||
if (
|
||||
!this.element.control?.selectExclusiveOptions?.inputAble ||
|
||||
(!options.isIgnoreDisabledRule &&
|
||||
this.control.getIsDisabledControl(context))
|
||||
) {
|
||||
return -1
|
||||
}
|
||||
const elementList = context.elementList || this.control.getElementList()
|
||||
const range = context.range || this.control.getRange()
|
||||
// 收缩边界到Value内
|
||||
this.control.shrinkBoundary(context)
|
||||
const { startIndex, endIndex } = range
|
||||
const draw = this.control.getDraw()
|
||||
// 移除选区元素
|
||||
if (startIndex !== endIndex) {
|
||||
draw.spliceElementList(elementList, startIndex + 1, endIndex - startIndex)
|
||||
} else {
|
||||
// 移除空白占位符
|
||||
this.control.removePlaceholder(startIndex, context)
|
||||
}
|
||||
// 非文本类元素或前缀过渡掉样式属性
|
||||
const startElement = elementList[startIndex]
|
||||
const anchorElement =
|
||||
(startElement.type &&
|
||||
!TEXTLIKE_ELEMENT_TYPE.includes(startElement.type)) ||
|
||||
startElement.controlComponent === ControlComponent.PREFIX ||
|
||||
startElement.controlComponent === ControlComponent.PRE_TEXT
|
||||
? pickObject(startElement, [
|
||||
'control',
|
||||
'controlId',
|
||||
...CONTROL_STYLE_ATTR
|
||||
])
|
||||
: omitObject(startElement, ['type'])
|
||||
// 插入起始位置
|
||||
const start = range.startIndex + 1
|
||||
for (let i = 0; i < data.length; i++) {
|
||||
const newElement: IElement = {
|
||||
...anchorElement,
|
||||
...data[i],
|
||||
controlComponent: ControlComponent.VALUE
|
||||
}
|
||||
formatElementContext(elementList, [newElement], startIndex, {
|
||||
editorOptions: this.options
|
||||
})
|
||||
draw.spliceElementList(elementList, start + i, 0, [newElement])
|
||||
}
|
||||
return start + data.length - 1
|
||||
}
|
||||
|
||||
public keydown(evt: KeyboardEvent): number | null {
|
||||
if (this.control.getIsDisabledControl()) {
|
||||
return null
|
||||
}
|
||||
const elementList = this.control.getElementList()
|
||||
const range = this.control.getRange()
|
||||
// 收缩边界到Value内
|
||||
this.control.shrinkBoundary()
|
||||
const { startIndex, endIndex } = range
|
||||
const startElement = elementList[startIndex]
|
||||
const endElement = elementList[endIndex]
|
||||
// backspace
|
||||
if (evt.key === KeyMap.Backspace) {
|
||||
// 清空选项
|
||||
if (startIndex !== endIndex) {
|
||||
return this.clearSelect()
|
||||
} else {
|
||||
if (
|
||||
startElement.controlComponent === ControlComponent.PREFIX ||
|
||||
startElement.controlComponent === ControlComponent.PRE_TEXT ||
|
||||
endElement.controlComponent === ControlComponent.POSTFIX ||
|
||||
endElement.controlComponent === ControlComponent.POST_TEXT ||
|
||||
startElement.controlComponent === ControlComponent.PLACEHOLDER
|
||||
) {
|
||||
// 前缀、后缀、占位符
|
||||
return this.control.removeControl(startIndex)
|
||||
} else {
|
||||
// 清空选项
|
||||
return this.clearSelect()
|
||||
}
|
||||
}
|
||||
} else if (evt.key === KeyMap.Delete) {
|
||||
// 移除选区元素
|
||||
if (startIndex !== endIndex) {
|
||||
// 清空选项
|
||||
return this.clearSelect()
|
||||
} else {
|
||||
const endNextElement = elementList[endIndex + 1]
|
||||
if (
|
||||
((startElement.controlComponent === ControlComponent.PREFIX ||
|
||||
startElement.controlComponent === ControlComponent.PRE_TEXT) &&
|
||||
endNextElement.controlComponent === ControlComponent.PLACEHOLDER) ||
|
||||
endNextElement.controlComponent === ControlComponent.POSTFIX ||
|
||||
endNextElement.controlComponent === ControlComponent.POST_TEXT ||
|
||||
startElement.controlComponent === ControlComponent.PLACEHOLDER
|
||||
) {
|
||||
// 前缀、后缀、占位符
|
||||
return this.control.removeControl(startIndex)
|
||||
} else {
|
||||
// 清空选项
|
||||
return this.clearSelect()
|
||||
}
|
||||
}
|
||||
}
|
||||
return endIndex
|
||||
}
|
||||
|
||||
public cut(): number {
|
||||
if (this.control.getIsDisabledControl()) {
|
||||
return -1
|
||||
}
|
||||
this.control.shrinkBoundary()
|
||||
const { startIndex, endIndex } = this.control.getRange()
|
||||
if (startIndex === endIndex) {
|
||||
return startIndex
|
||||
}
|
||||
// 清空选项
|
||||
return this.clearSelect()
|
||||
}
|
||||
|
||||
public clearSelect(
|
||||
context: IControlContext = {},
|
||||
options: IControlRuleOption = {}
|
||||
): number {
|
||||
const { isIgnoreDisabledRule = false, isAddPlaceholder = true } = options
|
||||
// 校验是否可以设置
|
||||
if (!isIgnoreDisabledRule && this.control.getIsDisabledControl(context)) {
|
||||
return -1
|
||||
}
|
||||
const elementList = context.elementList || this.control.getElementList()
|
||||
const { startIndex } = context.range || this.control.getRange()
|
||||
const startElement = elementList[startIndex]
|
||||
let leftIndex = -1
|
||||
let rightIndex = -1
|
||||
// 向左查找
|
||||
let preIndex = startIndex
|
||||
while (preIndex > 0) {
|
||||
const preElement = elementList[preIndex]
|
||||
if (
|
||||
preElement.controlId !== startElement.controlId ||
|
||||
preElement.controlComponent === ControlComponent.PREFIX ||
|
||||
preElement.controlComponent === ControlComponent.PRE_TEXT
|
||||
) {
|
||||
leftIndex = preIndex
|
||||
break
|
||||
}
|
||||
preIndex--
|
||||
}
|
||||
// 向右查找
|
||||
let nextIndex = startIndex + 1
|
||||
while (nextIndex < elementList.length) {
|
||||
const nextElement = elementList[nextIndex]
|
||||
if (
|
||||
nextElement.controlId !== startElement.controlId ||
|
||||
nextElement.controlComponent === ControlComponent.POSTFIX ||
|
||||
nextElement.controlComponent === ControlComponent.POST_TEXT
|
||||
) {
|
||||
rightIndex = nextIndex - 1
|
||||
break
|
||||
}
|
||||
nextIndex++
|
||||
}
|
||||
if (!~leftIndex || !~rightIndex) return -1
|
||||
// 删除元素
|
||||
const draw = this.control.getDraw()
|
||||
draw.spliceElementList(
|
||||
elementList,
|
||||
leftIndex + 1,
|
||||
rightIndex - leftIndex,
|
||||
[],
|
||||
{
|
||||
isIgnoreDeletedRule: options.isIgnoreDeletedRule
|
||||
}
|
||||
)
|
||||
// 增加占位符
|
||||
if (isAddPlaceholder) {
|
||||
this.control.addPlaceholder(preIndex, context)
|
||||
}
|
||||
this.control.setControlProperties(
|
||||
{
|
||||
code: null
|
||||
},
|
||||
{
|
||||
elementList,
|
||||
range: { startIndex: preIndex, endIndex: preIndex }
|
||||
}
|
||||
)
|
||||
return preIndex
|
||||
}
|
||||
|
||||
public setSelect(
|
||||
code: string,
|
||||
context: IControlContext = {},
|
||||
options: IControlRuleOption = {}
|
||||
) {
|
||||
// 校验是否可以设置
|
||||
if (
|
||||
!options.isIgnoreDisabledRule &&
|
||||
this.control.getIsDisabledControl(context)
|
||||
) {
|
||||
return
|
||||
}
|
||||
const elementList = context.elementList || this.control.getElementList()
|
||||
const range = context.range || this.control.getRange()
|
||||
const control = this.element.control!
|
||||
const newCodes = code?.split(this.VALUE_DELIMITER) || []
|
||||
// 缓存旧值
|
||||
const oldCode = control.code
|
||||
const oldCodes = control.code?.split(this.VALUE_DELIMITER) || []
|
||||
// 选项相同时无需重复渲染
|
||||
const isMultiSelect = control.isMultiSelect
|
||||
if (
|
||||
(!isMultiSelect && code === oldCode) ||
|
||||
(isMultiSelect && isArrayEqual(oldCodes, newCodes))
|
||||
) {
|
||||
this.control.repaintControl({
|
||||
curIndex: range.startIndex,
|
||||
isCompute: false,
|
||||
isSubmitHistory: false
|
||||
})
|
||||
this.destroy()
|
||||
return
|
||||
}
|
||||
const valueSets = control.valueSets
|
||||
if (!Array.isArray(valueSets) || !valueSets.length) return
|
||||
// 转换文本
|
||||
const text = this.getText(newCodes)
|
||||
if (!text) {
|
||||
// 之前存在内容时清空文本
|
||||
if (oldCode) {
|
||||
const prefixIndex = this.clearSelect(context, {
|
||||
isIgnoreDeletedRule: options.isIgnoreDeletedRule
|
||||
})
|
||||
if (~prefixIndex) {
|
||||
this.control.repaintControl({
|
||||
curIndex: prefixIndex
|
||||
})
|
||||
this.control.emitControlContentChange({
|
||||
controlValue: []
|
||||
})
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
// 样式赋值元素-默认值的第一个字符样式,否则取默认样式
|
||||
const valueElement = this.getValue(context)[0]
|
||||
const styleElement = valueElement
|
||||
? pickObject(valueElement, EDITOR_ELEMENT_STYLE_ATTR)
|
||||
: pickObject(elementList[range.startIndex], CONTROL_STYLE_ATTR)
|
||||
// 清空选项
|
||||
const prefixIndex = this.clearSelect(context, {
|
||||
isAddPlaceholder: false,
|
||||
isIgnoreDeletedRule: options.isIgnoreDeletedRule
|
||||
})
|
||||
if (!~prefixIndex) return
|
||||
// 当前无值时清空占位符
|
||||
if (!oldCode) {
|
||||
this.control.removePlaceholder(prefixIndex, context)
|
||||
}
|
||||
// 属性赋值元素-默认为前缀属性
|
||||
const propertyElement = omitObject(
|
||||
elementList[prefixIndex],
|
||||
EDITOR_ELEMENT_STYLE_ATTR
|
||||
)
|
||||
const start = prefixIndex + 1
|
||||
const data = splitText(text)
|
||||
const draw = this.control.getDraw()
|
||||
for (let i = 0; i < data.length; i++) {
|
||||
const newElement: IElement = {
|
||||
...styleElement,
|
||||
...propertyElement,
|
||||
type: ElementType.TEXT,
|
||||
value: data[i],
|
||||
controlComponent: ControlComponent.VALUE
|
||||
}
|
||||
formatElementContext(elementList, [newElement], prefixIndex, {
|
||||
editorOptions: this.options
|
||||
})
|
||||
draw.spliceElementList(elementList, start + i, 0, [newElement])
|
||||
}
|
||||
// 设置状态
|
||||
this.control.setControlProperties(
|
||||
{
|
||||
code
|
||||
},
|
||||
{
|
||||
elementList,
|
||||
range: { startIndex: prefixIndex, endIndex: prefixIndex }
|
||||
}
|
||||
)
|
||||
// 重新渲染控件
|
||||
if (!context.range) {
|
||||
const newIndex = start + data.length - 1
|
||||
this.control.repaintControl({
|
||||
curIndex: newIndex
|
||||
})
|
||||
this.control.emitControlContentChange({
|
||||
context
|
||||
})
|
||||
if (!isMultiSelect) {
|
||||
this.destroy()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private _createSelectPopupDom() {
|
||||
const control = this.element.control!
|
||||
const valueSets = control.valueSets
|
||||
if (!Array.isArray(valueSets) || !valueSets.length) return
|
||||
const position = this.control.getPosition()
|
||||
if (!position) return
|
||||
// dom树:<div><ul><li>item</li></ul></div>
|
||||
const selectPopupContainer = document.createElement('div')
|
||||
selectPopupContainer.classList.add(`${EDITOR_PREFIX}-select-control-popup`)
|
||||
selectPopupContainer.setAttribute(EDITOR_COMPONENT, EditorComponent.POPUP)
|
||||
const ul = document.createElement('ul')
|
||||
for (let v = 0; v < valueSets.length; v++) {
|
||||
const valueSet = valueSets[v]
|
||||
const li = document.createElement('li')
|
||||
let codes = this.getCodes()
|
||||
if (codes.includes(valueSet.code)) {
|
||||
li.classList.add('active')
|
||||
}
|
||||
li.onclick = () => {
|
||||
const codeIndex = codes.findIndex(code => code === valueSet.code)
|
||||
if (control.isMultiSelect) {
|
||||
if (~codeIndex) {
|
||||
codes.splice(codeIndex, 1)
|
||||
} else {
|
||||
codes.push(valueSet.code)
|
||||
}
|
||||
} else {
|
||||
if (~codeIndex) {
|
||||
codes = []
|
||||
} else {
|
||||
codes = [valueSet.code]
|
||||
}
|
||||
}
|
||||
this.setSelect(codes.join(this.VALUE_DELIMITER))
|
||||
}
|
||||
li.append(document.createTextNode(valueSet.value))
|
||||
ul.append(li)
|
||||
}
|
||||
selectPopupContainer.append(ul)
|
||||
// 定位
|
||||
const {
|
||||
coordinate: {
|
||||
leftTop: [left, top]
|
||||
},
|
||||
lineHeight
|
||||
} = position
|
||||
const preY = this.control.getPreY()
|
||||
selectPopupContainer.style.left = `${left}px`
|
||||
selectPopupContainer.style.top = `${top + preY + lineHeight}px`
|
||||
// 追加至container
|
||||
const container = this.control.getContainer()
|
||||
container.append(selectPopupContainer)
|
||||
this.selectDom = selectPopupContainer
|
||||
}
|
||||
|
||||
public awake() {
|
||||
if (
|
||||
this.isPopup ||
|
||||
this.control.getIsDisabledControl() ||
|
||||
!this.control.getIsRangeWithinControl()
|
||||
) {
|
||||
return
|
||||
}
|
||||
const { startIndex } = this.control.getRange()
|
||||
const elementList = this.control.getElementList()
|
||||
if (elementList[startIndex + 1]?.controlId !== this.element.controlId) {
|
||||
return
|
||||
}
|
||||
this._createSelectPopupDom()
|
||||
this.isPopup = true
|
||||
}
|
||||
|
||||
public destroy() {
|
||||
if (!this.isPopup) return
|
||||
this.selectDom?.remove()
|
||||
this.isPopup = false
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,276 @@
|
||||
import {
|
||||
CONTROL_STYLE_ATTR,
|
||||
TEXTLIKE_ELEMENT_TYPE
|
||||
} from '../../../../dataset/constant/Element'
|
||||
import { ControlComponent } from '../../../../dataset/enum/Control'
|
||||
import { KeyMap } from '../../../../dataset/enum/KeyMap'
|
||||
import { DeepRequired } from '../../../../interface/Common'
|
||||
import {
|
||||
IControlContext,
|
||||
IControlInstance,
|
||||
IControlRuleOption
|
||||
} from '../../../../interface/Control'
|
||||
import { IEditorOption } from '../../../../interface/Editor'
|
||||
import { IElement } from '../../../../interface/Element'
|
||||
import { omitObject, pickObject } from '../../../../utils'
|
||||
import { formatElementContext } from '../../../../utils/element'
|
||||
import { Control } from '../Control'
|
||||
|
||||
export class TextControl implements IControlInstance {
|
||||
private element: IElement
|
||||
private control: Control
|
||||
private options: DeepRequired<IEditorOption>
|
||||
|
||||
constructor(element: IElement, control: Control) {
|
||||
const draw = control.getDraw()
|
||||
this.options = draw.getOptions()
|
||||
this.element = element
|
||||
this.control = control
|
||||
}
|
||||
|
||||
public setElement(element: IElement) {
|
||||
this.element = element
|
||||
}
|
||||
|
||||
public getElement(): IElement {
|
||||
return this.element
|
||||
}
|
||||
|
||||
public getValue(context: IControlContext = {}): IElement[] {
|
||||
const elementList = context.elementList || this.control.getElementList()
|
||||
const { startIndex } = context.range || this.control.getRange()
|
||||
const startElement = elementList[startIndex]
|
||||
const data: IElement[] = []
|
||||
// 向左查找
|
||||
let preIndex = startIndex
|
||||
while (preIndex > 0) {
|
||||
const preElement = elementList[preIndex]
|
||||
if (
|
||||
preElement.controlId !== startElement.controlId ||
|
||||
preElement.controlComponent === ControlComponent.PREFIX ||
|
||||
preElement.controlComponent === ControlComponent.PRE_TEXT
|
||||
) {
|
||||
break
|
||||
}
|
||||
if (preElement.controlComponent === ControlComponent.VALUE) {
|
||||
data.unshift(preElement)
|
||||
}
|
||||
preIndex--
|
||||
}
|
||||
// 向右查找
|
||||
let nextIndex = startIndex + 1
|
||||
while (nextIndex < elementList.length) {
|
||||
const nextElement = elementList[nextIndex]
|
||||
if (
|
||||
nextElement.controlId !== startElement.controlId ||
|
||||
nextElement.controlComponent === ControlComponent.POSTFIX ||
|
||||
nextElement.controlComponent === ControlComponent.POST_TEXT
|
||||
) {
|
||||
break
|
||||
}
|
||||
if (nextElement.controlComponent === ControlComponent.VALUE) {
|
||||
data.push(nextElement)
|
||||
}
|
||||
nextIndex++
|
||||
}
|
||||
return data
|
||||
}
|
||||
|
||||
public setValue(
|
||||
data: IElement[],
|
||||
context: IControlContext = {},
|
||||
options: IControlRuleOption = {}
|
||||
): number {
|
||||
// 校验是否可以设置
|
||||
if (
|
||||
!options.isIgnoreDisabledRule &&
|
||||
this.control.getIsDisabledControl(context)
|
||||
) {
|
||||
return -1
|
||||
}
|
||||
const elementList = context.elementList || this.control.getElementList()
|
||||
const range = context.range || this.control.getRange()
|
||||
// 收缩边界到Value内
|
||||
this.control.shrinkBoundary(context)
|
||||
const { startIndex, endIndex } = range
|
||||
const draw = this.control.getDraw()
|
||||
// 移除选区元素
|
||||
if (startIndex !== endIndex) {
|
||||
draw.spliceElementList(
|
||||
elementList,
|
||||
startIndex + 1,
|
||||
endIndex - startIndex,
|
||||
[],
|
||||
{
|
||||
isIgnoreDeletedRule: options.isIgnoreDeletedRule
|
||||
}
|
||||
)
|
||||
} else {
|
||||
// 移除空白占位符
|
||||
this.control.removePlaceholder(startIndex, context)
|
||||
}
|
||||
// 非文本类元素或前缀过渡掉样式属性
|
||||
const startElement = elementList[startIndex]
|
||||
const anchorElement =
|
||||
(startElement.type &&
|
||||
!TEXTLIKE_ELEMENT_TYPE.includes(startElement.type)) ||
|
||||
startElement.controlComponent === ControlComponent.PREFIX ||
|
||||
startElement.controlComponent === ControlComponent.PRE_TEXT
|
||||
? pickObject(startElement, [
|
||||
'control',
|
||||
'controlId',
|
||||
...CONTROL_STYLE_ATTR
|
||||
])
|
||||
: omitObject(startElement, ['type'])
|
||||
// 插入起始位置
|
||||
const start = range.startIndex + 1
|
||||
for (let i = 0; i < data.length; i++) {
|
||||
const newElement: IElement = {
|
||||
...anchorElement,
|
||||
...data[i],
|
||||
controlComponent: ControlComponent.VALUE
|
||||
}
|
||||
formatElementContext(elementList, [newElement], startIndex, {
|
||||
editorOptions: this.options
|
||||
})
|
||||
draw.spliceElementList(elementList, start + i, 0, [newElement])
|
||||
}
|
||||
return start + data.length - 1
|
||||
}
|
||||
|
||||
public clearValue(
|
||||
context: IControlContext = {},
|
||||
options: IControlRuleOption = {}
|
||||
): number {
|
||||
// 校验是否可以设置
|
||||
if (
|
||||
!options.isIgnoreDisabledRule &&
|
||||
this.control.getIsDisabledControl(context)
|
||||
) {
|
||||
return -1
|
||||
}
|
||||
const elementList = context.elementList || this.control.getElementList()
|
||||
const range = context.range || this.control.getRange()
|
||||
const { startIndex, endIndex } = range
|
||||
this.control
|
||||
.getDraw()
|
||||
.spliceElementList(
|
||||
elementList,
|
||||
startIndex + 1,
|
||||
endIndex - startIndex,
|
||||
[],
|
||||
{
|
||||
isIgnoreDeletedRule: options.isIgnoreDeletedRule
|
||||
}
|
||||
)
|
||||
const value = this.getValue(context)
|
||||
if (!value.length) {
|
||||
this.control.addPlaceholder(startIndex, context)
|
||||
}
|
||||
return startIndex
|
||||
}
|
||||
|
||||
public keydown(evt: KeyboardEvent): number | null {
|
||||
if (this.control.getIsDisabledControl()) {
|
||||
return null
|
||||
}
|
||||
const elementList = this.control.getElementList()
|
||||
const range = this.control.getRange()
|
||||
// 收缩边界到Value内
|
||||
this.control.shrinkBoundary()
|
||||
const { startIndex, endIndex } = range
|
||||
const startElement = elementList[startIndex]
|
||||
const endElement = elementList[endIndex]
|
||||
const draw = this.control.getDraw()
|
||||
// backspace
|
||||
if (evt.key === KeyMap.Backspace) {
|
||||
// 移除选区元素
|
||||
if (startIndex !== endIndex) {
|
||||
draw.spliceElementList(
|
||||
elementList,
|
||||
startIndex + 1,
|
||||
endIndex - startIndex
|
||||
)
|
||||
const value = this.getValue()
|
||||
if (!value.length) {
|
||||
this.control.addPlaceholder(startIndex)
|
||||
}
|
||||
return startIndex
|
||||
} else {
|
||||
if (
|
||||
startElement.controlComponent === ControlComponent.PREFIX ||
|
||||
startElement.controlComponent === ControlComponent.PRE_TEXT ||
|
||||
endElement.controlComponent === ControlComponent.POSTFIX ||
|
||||
endElement.controlComponent === ControlComponent.POST_TEXT ||
|
||||
startElement.controlComponent === ControlComponent.PLACEHOLDER
|
||||
) {
|
||||
// 前缀、后缀、占位符
|
||||
return this.control.removeControl(startIndex)
|
||||
} else {
|
||||
// 文本
|
||||
draw.spliceElementList(elementList, startIndex, 1)
|
||||
const value = this.getValue()
|
||||
if (!value.length) {
|
||||
this.control.addPlaceholder(startIndex - 1)
|
||||
}
|
||||
return startIndex - 1
|
||||
}
|
||||
}
|
||||
} else if (evt.key === KeyMap.Delete) {
|
||||
// 移除选区元素
|
||||
if (startIndex !== endIndex) {
|
||||
draw.spliceElementList(
|
||||
elementList,
|
||||
startIndex + 1,
|
||||
endIndex - startIndex
|
||||
)
|
||||
const value = this.getValue()
|
||||
if (!value.length) {
|
||||
this.control.addPlaceholder(startIndex)
|
||||
}
|
||||
return startIndex
|
||||
} else {
|
||||
const endNextElement = elementList[endIndex + 1]
|
||||
if (
|
||||
((startElement.controlComponent === ControlComponent.PREFIX ||
|
||||
startElement.controlComponent === ControlComponent.PRE_TEXT) &&
|
||||
endNextElement.controlComponent === ControlComponent.PLACEHOLDER) ||
|
||||
endNextElement.controlComponent === ControlComponent.POSTFIX ||
|
||||
endNextElement.controlComponent === ControlComponent.POST_TEXT ||
|
||||
startElement.controlComponent === ControlComponent.PLACEHOLDER
|
||||
) {
|
||||
// 前缀、后缀、占位符
|
||||
return this.control.removeControl(startIndex)
|
||||
} else {
|
||||
// 文本
|
||||
draw.spliceElementList(elementList, startIndex + 1, 1)
|
||||
const value = this.getValue()
|
||||
if (!value.length) {
|
||||
this.control.addPlaceholder(startIndex)
|
||||
}
|
||||
return startIndex
|
||||
}
|
||||
}
|
||||
}
|
||||
return endIndex
|
||||
}
|
||||
|
||||
public cut(): number {
|
||||
if (this.control.getIsDisabledControl()) {
|
||||
return -1
|
||||
}
|
||||
this.control.shrinkBoundary()
|
||||
const { startIndex, endIndex } = this.control.getRange()
|
||||
if (startIndex === endIndex) {
|
||||
return startIndex
|
||||
}
|
||||
const draw = this.control.getDraw()
|
||||
const elementList = this.control.getElementList()
|
||||
draw.spliceElementList(elementList, startIndex + 1, endIndex - startIndex)
|
||||
const value = this.getValue()
|
||||
if (!value.length) {
|
||||
this.control.addPlaceholder(startIndex)
|
||||
}
|
||||
return startIndex
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
import {
|
||||
BackgroundRepeat,
|
||||
BackgroundSize
|
||||
} from '../../../dataset/enum/Background'
|
||||
import { DeepRequired } from '../../../interface/Common'
|
||||
import { IEditorOption } from '../../../interface/Editor'
|
||||
import { Draw } from '../Draw'
|
||||
|
||||
export class Background {
|
||||
private draw: Draw
|
||||
private options: DeepRequired<IEditorOption>
|
||||
private imageCache: Map<string, HTMLImageElement>
|
||||
|
||||
constructor(draw: Draw) {
|
||||
this.draw = draw
|
||||
this.options = draw.getOptions()
|
||||
this.imageCache = new Map()
|
||||
}
|
||||
|
||||
private _renderBackgroundColor(
|
||||
ctx: CanvasRenderingContext2D,
|
||||
color: string,
|
||||
width: number,
|
||||
height: number
|
||||
) {
|
||||
ctx.save()
|
||||
ctx.fillStyle = color
|
||||
ctx.fillRect(0, 0, width, height)
|
||||
ctx.restore()
|
||||
}
|
||||
|
||||
private _drawImage(
|
||||
ctx: CanvasRenderingContext2D,
|
||||
imageElement: HTMLImageElement,
|
||||
width: number,
|
||||
height: number
|
||||
) {
|
||||
const { background, scale } = this.options
|
||||
// contain
|
||||
if (background.size === BackgroundSize.CONTAIN) {
|
||||
const imageWidth = imageElement.width * scale
|
||||
const imageHeight = imageElement.height * scale
|
||||
if (
|
||||
!background.repeat ||
|
||||
background.repeat === BackgroundRepeat.NO_REPEAT
|
||||
) {
|
||||
ctx.drawImage(imageElement, 0, 0, imageWidth, imageHeight)
|
||||
} else {
|
||||
let startX = 0
|
||||
let startY = 0
|
||||
const repeatXCount =
|
||||
background.repeat === BackgroundRepeat.REPEAT ||
|
||||
background.repeat === BackgroundRepeat.REPEAT_X
|
||||
? Math.ceil((width * scale) / imageWidth)
|
||||
: 1
|
||||
const repeatYCount =
|
||||
background.repeat === BackgroundRepeat.REPEAT ||
|
||||
background.repeat === BackgroundRepeat.REPEAT_Y
|
||||
? Math.ceil((height * scale) / imageHeight)
|
||||
: 1
|
||||
for (let x = 0; x < repeatXCount; x++) {
|
||||
for (let y = 0; y < repeatYCount; y++) {
|
||||
ctx.drawImage(imageElement, startX, startY, imageWidth, imageHeight)
|
||||
startY += imageHeight
|
||||
}
|
||||
startY = 0
|
||||
startX += imageWidth
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// cover
|
||||
ctx.drawImage(imageElement, 0, 0, width * scale, height * scale)
|
||||
}
|
||||
}
|
||||
|
||||
private _renderBackgroundImage(
|
||||
ctx: CanvasRenderingContext2D,
|
||||
width: number,
|
||||
height: number
|
||||
) {
|
||||
const { background } = this.options
|
||||
const imageElementCache = this.imageCache.get(background.image)
|
||||
if (imageElementCache) {
|
||||
this._drawImage(ctx, imageElementCache, width, height)
|
||||
} else {
|
||||
const img = new Image()
|
||||
img.setAttribute('crossOrigin', 'Anonymous')
|
||||
img.src = background.image
|
||||
img.onload = () => {
|
||||
this.imageCache.set(background.image, img)
|
||||
this._drawImage(ctx, img, width, height)
|
||||
// 避免层级上浮,触发编辑器二次渲染
|
||||
this.draw.render({
|
||||
isCompute: false,
|
||||
isSubmitHistory: false
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public render(ctx: CanvasRenderingContext2D, pageNo: number) {
|
||||
const {
|
||||
background: { image, color, applyPageNumbers }
|
||||
} = this.options
|
||||
if (
|
||||
image &&
|
||||
(!applyPageNumbers?.length || applyPageNumbers.includes(pageNo))
|
||||
) {
|
||||
const { width, height } = this.options
|
||||
this._renderBackgroundImage(ctx, width, height)
|
||||
} else {
|
||||
const width = this.draw.getCanvasWidth(pageNo)
|
||||
const height = this.draw.getCanvasHeight(pageNo)
|
||||
this._renderBackgroundColor(ctx, color, width, height)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
import { IAreaBadge, IBadge } from '../../../interface/Badge'
|
||||
import { DeepRequired } from '../../../interface/Common'
|
||||
import { IEditorOption } from '../../../interface/Editor'
|
||||
import { Draw } from '../Draw'
|
||||
|
||||
export class Badge {
|
||||
private draw: Draw
|
||||
private options: DeepRequired<IEditorOption>
|
||||
private imageCache: Map<string, HTMLImageElement>
|
||||
private mainBadge: IBadge | null
|
||||
private areaBadgeMap: Map<string, IBadge>
|
||||
|
||||
constructor(draw: Draw) {
|
||||
this.draw = draw
|
||||
this.options = draw.getOptions()
|
||||
this.imageCache = new Map()
|
||||
this.mainBadge = null
|
||||
this.areaBadgeMap = new Map()
|
||||
}
|
||||
|
||||
public setMainBadge(payload: IBadge | null) {
|
||||
this.mainBadge = payload
|
||||
}
|
||||
|
||||
public setAreaBadgeMap(payload: IAreaBadge[]) {
|
||||
this.areaBadgeMap.clear()
|
||||
payload.forEach(areaBadge => {
|
||||
this.areaBadgeMap.set(areaBadge.areaId, areaBadge.badge)
|
||||
})
|
||||
}
|
||||
|
||||
private _drawImage(
|
||||
ctx: CanvasRenderingContext2D,
|
||||
x: number,
|
||||
y: number,
|
||||
width: number,
|
||||
height: number,
|
||||
value: string
|
||||
) {
|
||||
if (this.imageCache.has(value)) {
|
||||
const img = this.imageCache.get(value)!
|
||||
ctx.drawImage(img, x, y, width, height)
|
||||
} else {
|
||||
const img = new Image()
|
||||
img.setAttribute('crossOrigin', 'Anonymous')
|
||||
img.src = value
|
||||
img.onload = () => {
|
||||
this.imageCache.set(value, img)
|
||||
ctx.drawImage(img, x, y, width, height)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public render(ctx: CanvasRenderingContext2D, pageNo: number) {
|
||||
// 文档签章
|
||||
if (pageNo === 0 && this.mainBadge) {
|
||||
const { scale, badge } = this.options
|
||||
const { left, top, width, height, value } = this.mainBadge
|
||||
// 默认从页眉下开始
|
||||
const headerTop =
|
||||
this.draw.getMargins()[0] + this.draw.getHeader().getExtraHeight()
|
||||
const x = (left || badge.left) * scale
|
||||
const y = (top || badge.top) * scale + headerTop
|
||||
this._drawImage(ctx, x, y, width * scale, height * scale, value)
|
||||
}
|
||||
// 区域签章
|
||||
if (this.areaBadgeMap.size) {
|
||||
const areaInfo = this.draw.getArea().getAreaInfo()
|
||||
if (areaInfo.size) {
|
||||
const { scale, badge } = this.options
|
||||
for (const areaItem of areaInfo) {
|
||||
// 忽略非本页区域
|
||||
const { positionList } = areaItem[1]
|
||||
const firstPosition = positionList[0]
|
||||
if (firstPosition.pageNo !== pageNo) continue
|
||||
// 忽略未设置签章区域
|
||||
const badgeItem = this.areaBadgeMap.get(areaItem[0])
|
||||
if (!badgeItem) continue
|
||||
const { left, top, width, height, value } = badgeItem
|
||||
const x = (left || badge.left) * scale
|
||||
const y =
|
||||
(top || badge.top) * scale + firstPosition.coordinate.leftTop[1]
|
||||
this._drawImage(ctx, x, y, width * scale, height * scale, value)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
import { maxHeightRadioMapping } from '../../../dataset/constant/Common'
|
||||
import { EditorZone } from '../../../dataset/enum/Editor'
|
||||
import { DeepRequired } from '../../../interface/Common'
|
||||
import { IEditorOption } from '../../../interface/Editor'
|
||||
import { IElement, IElementPosition } from '../../../interface/Element'
|
||||
import { IRow } from '../../../interface/Row'
|
||||
import { Position } from '../../position/Position'
|
||||
import { Zone } from '../../zone/Zone'
|
||||
import { Draw } from '../Draw'
|
||||
|
||||
export class Footer {
|
||||
private draw: Draw
|
||||
private position: Position
|
||||
private zone: Zone
|
||||
private options: DeepRequired<IEditorOption>
|
||||
|
||||
private elementList: IElement[]
|
||||
private rowList: IRow[]
|
||||
private positionList: IElementPosition[]
|
||||
|
||||
constructor(draw: Draw, data?: IElement[]) {
|
||||
this.draw = draw
|
||||
this.position = draw.getPosition()
|
||||
this.zone = draw.getZone()
|
||||
this.options = draw.getOptions()
|
||||
|
||||
this.elementList = data || []
|
||||
this.rowList = []
|
||||
this.positionList = []
|
||||
}
|
||||
|
||||
public getRowList(): IRow[] {
|
||||
return this.rowList
|
||||
}
|
||||
|
||||
public setElementList(elementList: IElement[]) {
|
||||
this.elementList = elementList
|
||||
}
|
||||
|
||||
public getElementList(): IElement[] {
|
||||
return this.elementList
|
||||
}
|
||||
|
||||
public getPositionList(): IElementPosition[] {
|
||||
return this.positionList
|
||||
}
|
||||
|
||||
public compute() {
|
||||
this.recovery()
|
||||
this._computeRowList()
|
||||
this._computePositionList()
|
||||
}
|
||||
|
||||
public recovery() {
|
||||
this.rowList = []
|
||||
this.positionList = []
|
||||
}
|
||||
|
||||
private _computeRowList() {
|
||||
const innerWidth = this.draw.getInnerWidth()
|
||||
this.rowList = this.draw.computeRowList({
|
||||
innerWidth,
|
||||
elementList: this.elementList
|
||||
})
|
||||
}
|
||||
|
||||
private _computePositionList() {
|
||||
const footerBottom = this.getFooterBottom()
|
||||
const innerWidth = this.draw.getInnerWidth()
|
||||
const margins = this.draw.getMargins()
|
||||
const startX = margins[3]
|
||||
// 页面高度 - 页脚顶部距离页面底部高度
|
||||
const pageHeight = this.draw.getHeight()
|
||||
const footerHeight = this.getHeight()
|
||||
const startY = pageHeight - footerBottom - footerHeight
|
||||
this.position.computePageRowPosition({
|
||||
positionList: this.positionList,
|
||||
rowList: this.rowList,
|
||||
pageNo: 0,
|
||||
startRowIndex: 0,
|
||||
startIndex: 0,
|
||||
startX,
|
||||
startY,
|
||||
innerWidth,
|
||||
zone: EditorZone.FOOTER
|
||||
})
|
||||
}
|
||||
|
||||
public getFooterBottom(): number {
|
||||
const {
|
||||
footer: { bottom, disabled },
|
||||
scale
|
||||
} = this.options
|
||||
if (disabled) return 0
|
||||
return Math.floor(bottom * scale)
|
||||
}
|
||||
|
||||
public getMaxHeight(): number {
|
||||
const {
|
||||
footer: { maxHeightRadio }
|
||||
} = this.options
|
||||
const height = this.draw.getHeight()
|
||||
return Math.floor(height * maxHeightRadioMapping[maxHeightRadio])
|
||||
}
|
||||
|
||||
public getHeight(): number {
|
||||
const maxHeight = this.getMaxHeight()
|
||||
const rowHeight = this.getRowHeight()
|
||||
return rowHeight > maxHeight ? maxHeight : rowHeight
|
||||
}
|
||||
|
||||
public getRowHeight(): number {
|
||||
return this.rowList.reduce((pre, cur) => pre + cur.height, 0)
|
||||
}
|
||||
|
||||
public getExtraHeight(): number {
|
||||
// 页脚下边距 + 实际高 - 页面上边距
|
||||
const margins = this.draw.getMargins()
|
||||
const footerHeight = this.getHeight()
|
||||
const footerBottom = this.getFooterBottom()
|
||||
const extraHeight = footerBottom + footerHeight - margins[2]
|
||||
return extraHeight <= 0 ? 0 : extraHeight
|
||||
}
|
||||
|
||||
public render(ctx: CanvasRenderingContext2D, pageNo: number) {
|
||||
ctx.save()
|
||||
ctx.globalAlpha = this.zone.isFooterActive()
|
||||
? 1
|
||||
: this.options.footer.inactiveAlpha
|
||||
const innerWidth = this.draw.getInnerWidth()
|
||||
const maxHeight = this.getMaxHeight()
|
||||
// 超出最大高度不渲染
|
||||
const rowList: IRow[] = []
|
||||
let curRowHeight = 0
|
||||
for (let r = 0; r < this.rowList.length; r++) {
|
||||
const row = this.rowList[r]
|
||||
if (curRowHeight + row.height > maxHeight) {
|
||||
break
|
||||
}
|
||||
rowList.push(row)
|
||||
curRowHeight += row.height
|
||||
}
|
||||
this.draw.drawRow(ctx, {
|
||||
elementList: this.elementList,
|
||||
positionList: this.positionList,
|
||||
rowList,
|
||||
pageNo,
|
||||
startIndex: 0,
|
||||
innerWidth,
|
||||
zone: EditorZone.FOOTER
|
||||
})
|
||||
ctx.restore()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,157 @@
|
||||
import { maxHeightRadioMapping } from '../../../dataset/constant/Common'
|
||||
import { EditorZone } from '../../../dataset/enum/Editor'
|
||||
import { DeepRequired } from '../../../interface/Common'
|
||||
import { IEditorOption } from '../../../interface/Editor'
|
||||
import { IElement, IElementPosition } from '../../../interface/Element'
|
||||
import { IRow } from '../../../interface/Row'
|
||||
import { pickSurroundElementList } from '../../../utils/element'
|
||||
import { Position } from '../../position/Position'
|
||||
import { Zone } from '../../zone/Zone'
|
||||
import { Draw } from '../Draw'
|
||||
|
||||
export class Header {
|
||||
private draw: Draw
|
||||
private position: Position
|
||||
private zone: Zone
|
||||
private options: DeepRequired<IEditorOption>
|
||||
|
||||
private elementList: IElement[]
|
||||
private rowList: IRow[]
|
||||
private positionList: IElementPosition[]
|
||||
|
||||
constructor(draw: Draw, data?: IElement[]) {
|
||||
this.draw = draw
|
||||
this.position = draw.getPosition()
|
||||
this.zone = draw.getZone()
|
||||
this.options = draw.getOptions()
|
||||
|
||||
this.elementList = data || []
|
||||
this.rowList = []
|
||||
this.positionList = []
|
||||
}
|
||||
|
||||
public getRowList(): IRow[] {
|
||||
return this.rowList
|
||||
}
|
||||
|
||||
public setElementList(elementList: IElement[]) {
|
||||
this.elementList = elementList
|
||||
}
|
||||
|
||||
public getElementList(): IElement[] {
|
||||
return this.elementList
|
||||
}
|
||||
|
||||
public getPositionList(): IElementPosition[] {
|
||||
return this.positionList
|
||||
}
|
||||
|
||||
public compute() {
|
||||
this.recovery()
|
||||
this._computeRowList()
|
||||
this._computePositionList()
|
||||
}
|
||||
|
||||
public recovery() {
|
||||
this.rowList = []
|
||||
this.positionList = []
|
||||
}
|
||||
|
||||
private _computeRowList() {
|
||||
const innerWidth = this.draw.getInnerWidth()
|
||||
const margins = this.draw.getMargins()
|
||||
const surroundElementList = pickSurroundElementList(this.elementList)
|
||||
this.rowList = this.draw.computeRowList({
|
||||
startX: margins[3],
|
||||
startY: this.getHeaderTop(),
|
||||
innerWidth,
|
||||
elementList: this.elementList,
|
||||
surroundElementList
|
||||
})
|
||||
}
|
||||
|
||||
private _computePositionList() {
|
||||
const headerTop = this.getHeaderTop()
|
||||
const innerWidth = this.draw.getInnerWidth()
|
||||
const margins = this.draw.getMargins()
|
||||
const startX = margins[3]
|
||||
const startY = headerTop
|
||||
this.position.computePageRowPosition({
|
||||
positionList: this.positionList,
|
||||
rowList: this.rowList,
|
||||
pageNo: 0,
|
||||
startRowIndex: 0,
|
||||
startIndex: 0,
|
||||
startX,
|
||||
startY,
|
||||
innerWidth,
|
||||
zone: EditorZone.HEADER
|
||||
})
|
||||
}
|
||||
|
||||
public getHeaderTop(): number {
|
||||
const {
|
||||
header: { top, disabled },
|
||||
scale
|
||||
} = this.options
|
||||
if (disabled) return 0
|
||||
return Math.floor(top * scale)
|
||||
}
|
||||
|
||||
public getMaxHeight(): number {
|
||||
const {
|
||||
header: { maxHeightRadio }
|
||||
} = this.options
|
||||
const height = this.draw.getHeight()
|
||||
return Math.floor(height * maxHeightRadioMapping[maxHeightRadio])
|
||||
}
|
||||
|
||||
public getHeight(): number {
|
||||
const maxHeight = this.getMaxHeight()
|
||||
const rowHeight = this.getRowHeight()
|
||||
return rowHeight > maxHeight ? maxHeight : rowHeight
|
||||
}
|
||||
|
||||
public getRowHeight(): number {
|
||||
return this.rowList.reduce((pre, cur) => pre + cur.height, 0)
|
||||
}
|
||||
|
||||
public getExtraHeight(): number {
|
||||
// 页眉上边距 + 实际高 - 页面上边距
|
||||
const margins = this.draw.getMargins()
|
||||
const headerHeight = this.getHeight()
|
||||
const headerTop = this.getHeaderTop()
|
||||
const extraHeight = headerTop + headerHeight - margins[0]
|
||||
return extraHeight <= 0 ? 0 : extraHeight
|
||||
}
|
||||
|
||||
public render(ctx: CanvasRenderingContext2D, pageNo: number) {
|
||||
ctx.save()
|
||||
ctx.globalAlpha = this.zone.isHeaderActive()
|
||||
? 1
|
||||
: this.options.header.inactiveAlpha
|
||||
const innerWidth = this.draw.getInnerWidth()
|
||||
const maxHeight = this.getMaxHeight()
|
||||
// 超出最大高度不渲染
|
||||
const rowList: IRow[] = []
|
||||
let curRowHeight = 0
|
||||
for (let r = 0; r < this.rowList.length; r++) {
|
||||
const row = this.rowList[r]
|
||||
if (curRowHeight + row.height > maxHeight) {
|
||||
break
|
||||
}
|
||||
rowList.push(row)
|
||||
curRowHeight += row.height
|
||||
}
|
||||
this.draw.drawRow(ctx, {
|
||||
elementList: this.elementList,
|
||||
positionList: this.positionList,
|
||||
rowList,
|
||||
pageNo,
|
||||
startIndex: 0,
|
||||
innerWidth,
|
||||
zone: EditorZone.HEADER
|
||||
})
|
||||
ctx.restore()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
import { LineNumberType } from '../../../dataset/enum/LineNumber'
|
||||
import { DeepRequired } from '../../../interface/Common'
|
||||
import { IEditorOption } from '../../../interface/Editor'
|
||||
import { Draw } from '../Draw'
|
||||
|
||||
export class LineNumber {
|
||||
private draw: Draw
|
||||
private options: DeepRequired<IEditorOption>
|
||||
|
||||
constructor(draw: Draw) {
|
||||
this.draw = draw
|
||||
this.options = draw.getOptions()
|
||||
}
|
||||
|
||||
public render(ctx: CanvasRenderingContext2D, pageNo: number) {
|
||||
const {
|
||||
scale,
|
||||
lineNumber: { color, size, font, right, type }
|
||||
} = this.options
|
||||
const textParticle = this.draw.getTextParticle()
|
||||
const margins = this.draw.getMargins()
|
||||
const positionList = this.draw.getPosition().getOriginalMainPositionList()
|
||||
const pageRowList = this.draw.getPageRowList()
|
||||
const rowList = pageRowList[pageNo]
|
||||
ctx.save()
|
||||
ctx.fillStyle = color
|
||||
ctx.font = `${size * scale}px ${font}`
|
||||
for (let i = 0; i < rowList.length; i++) {
|
||||
const row = rowList[i]
|
||||
const {
|
||||
coordinate: { leftBottom }
|
||||
} = positionList[row.startIndex]
|
||||
const seq = type === LineNumberType.PAGE ? i + 1 : row.rowIndex + 1
|
||||
const textMetrics = textParticle.measureText(ctx, {
|
||||
value: `${seq}`
|
||||
})
|
||||
const x = margins[3] - (textMetrics.width + right) * scale
|
||||
const y = leftBottom[1] - textMetrics.actualBoundingBoxAscent * scale
|
||||
ctx.fillText(`${seq}`, x, y)
|
||||
}
|
||||
ctx.restore()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
import { PageMode } from '../../../dataset/enum/Editor'
|
||||
import { IEditorOption } from '../../../interface/Editor'
|
||||
import { Draw } from '../Draw'
|
||||
|
||||
export class Margin {
|
||||
private draw: Draw
|
||||
private options: Required<IEditorOption>
|
||||
|
||||
constructor(draw: Draw) {
|
||||
this.draw = draw
|
||||
this.options = draw.getOptions()
|
||||
}
|
||||
|
||||
public render(ctx: CanvasRenderingContext2D, pageNo: number) {
|
||||
const { marginIndicatorColor, pageMode } = this.options
|
||||
const width = this.draw.getWidth()
|
||||
const height =
|
||||
pageMode === PageMode.CONTINUITY
|
||||
? this.draw.getCanvasHeight(pageNo) / this.draw.getPagePixelRatio()
|
||||
: this.draw.getHeight()
|
||||
const margins = this.draw.getMargins()
|
||||
const marginIndicatorSize = this.draw.getMarginIndicatorSize()
|
||||
ctx.save()
|
||||
ctx.translate(0.5, 0.5)
|
||||
ctx.strokeStyle = marginIndicatorColor
|
||||
ctx.beginPath()
|
||||
const leftTopPoint: [number, number] = [margins[3], margins[0]]
|
||||
const rightTopPoint: [number, number] = [width - margins[1], margins[0]]
|
||||
const leftBottomPoint: [number, number] = [margins[3], height - margins[2]]
|
||||
const rightBottomPoint: [number, number] = [
|
||||
width - margins[1],
|
||||
height - margins[2]
|
||||
]
|
||||
// 上左
|
||||
ctx.moveTo(leftTopPoint[0] - marginIndicatorSize, leftTopPoint[1])
|
||||
ctx.lineTo(...leftTopPoint)
|
||||
ctx.lineTo(leftTopPoint[0], leftTopPoint[1] - marginIndicatorSize)
|
||||
// 上右
|
||||
ctx.moveTo(rightTopPoint[0] + marginIndicatorSize, rightTopPoint[1])
|
||||
ctx.lineTo(...rightTopPoint)
|
||||
ctx.lineTo(rightTopPoint[0], rightTopPoint[1] - marginIndicatorSize)
|
||||
// 下左
|
||||
ctx.moveTo(leftBottomPoint[0] - marginIndicatorSize, leftBottomPoint[1])
|
||||
ctx.lineTo(...leftBottomPoint)
|
||||
ctx.lineTo(leftBottomPoint[0], leftBottomPoint[1] + marginIndicatorSize)
|
||||
// 下右
|
||||
ctx.moveTo(rightBottomPoint[0] + marginIndicatorSize, rightBottomPoint[1])
|
||||
ctx.lineTo(...rightBottomPoint)
|
||||
ctx.lineTo(rightBottomPoint[0], rightBottomPoint[1] + marginIndicatorSize)
|
||||
ctx.stroke()
|
||||
ctx.restore()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
import { DeepRequired } from '../../../interface/Common'
|
||||
import { IEditorOption } from '../../../interface/Editor'
|
||||
import { Draw } from '../Draw'
|
||||
import { Footer } from './Footer'
|
||||
import { Header } from './Header'
|
||||
|
||||
export class PageBorder {
|
||||
private draw: Draw
|
||||
private header: Header
|
||||
private footer: Footer
|
||||
private options: DeepRequired<IEditorOption>
|
||||
|
||||
constructor(draw: Draw) {
|
||||
this.draw = draw
|
||||
this.header = draw.getHeader()
|
||||
this.footer = draw.getFooter()
|
||||
this.options = draw.getOptions()
|
||||
}
|
||||
|
||||
public render(ctx: CanvasRenderingContext2D) {
|
||||
const {
|
||||
scale,
|
||||
pageBorder: { color, lineWidth, padding }
|
||||
} = this.options
|
||||
ctx.save()
|
||||
ctx.translate(0.5, 0.5)
|
||||
ctx.strokeStyle = color
|
||||
ctx.lineWidth = lineWidth * scale
|
||||
const margins = this.draw.getMargins()
|
||||
// x:左边距 - 左距离正文距离
|
||||
const x = margins[3] - padding[3] * scale
|
||||
// y:页眉上边距 + 页眉高度 - 上距离正文距离
|
||||
const y = margins[0] + this.header.getExtraHeight() - padding[0] * scale
|
||||
// width:页面宽度 + 左右距离正文距离
|
||||
const width = this.draw.getInnerWidth() + (padding[1] + padding[3]) * scale
|
||||
// height:页面高度 - 正文起始位置 - 页脚高度 - 下边距 - 下距离正文距离
|
||||
const height =
|
||||
this.draw.getHeight() -
|
||||
y -
|
||||
this.footer.getExtraHeight() -
|
||||
margins[2] +
|
||||
padding[2] * scale
|
||||
ctx.rect(x, y, width, height)
|
||||
ctx.stroke()
|
||||
ctx.restore()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
import { FORMAT_PLACEHOLDER } from '../../../dataset/constant/PageNumber'
|
||||
import { NumberType } from '../../../dataset/enum/Common'
|
||||
import { RowFlex } from '../../../dataset/enum/Row'
|
||||
import { DeepRequired } from '../../../interface/Common'
|
||||
import { IEditorOption } from '../../../interface/Editor'
|
||||
import { convertNumberToChinese } from '../../../utils'
|
||||
import { Draw } from '../Draw'
|
||||
|
||||
export class PageNumber {
|
||||
private draw: Draw
|
||||
private options: DeepRequired<IEditorOption>
|
||||
|
||||
constructor(draw: Draw) {
|
||||
this.draw = draw
|
||||
this.options = draw.getOptions()
|
||||
}
|
||||
|
||||
static formatNumberPlaceholder(
|
||||
text: string,
|
||||
pageNo: number,
|
||||
replaceReg: RegExp,
|
||||
numberType: NumberType
|
||||
) {
|
||||
const pageNoText =
|
||||
numberType === NumberType.CHINESE
|
||||
? convertNumberToChinese(pageNo)
|
||||
: `${pageNo}`
|
||||
return text.replace(replaceReg, pageNoText)
|
||||
}
|
||||
|
||||
public render(ctx: CanvasRenderingContext2D, pageNo: number) {
|
||||
const {
|
||||
scale,
|
||||
pageNumber: {
|
||||
size,
|
||||
font,
|
||||
color,
|
||||
rowFlex,
|
||||
numberType,
|
||||
format,
|
||||
startPageNo,
|
||||
fromPageNo
|
||||
}
|
||||
} = this.options
|
||||
if (pageNo < fromPageNo) return
|
||||
// 处理页码格式
|
||||
let text = format
|
||||
const pageNoReg = new RegExp(FORMAT_PLACEHOLDER.PAGE_NO)
|
||||
if (pageNoReg.test(text)) {
|
||||
text = PageNumber.formatNumberPlaceholder(
|
||||
text,
|
||||
pageNo + startPageNo - fromPageNo,
|
||||
pageNoReg,
|
||||
numberType
|
||||
)
|
||||
}
|
||||
const pageCountReg = new RegExp(FORMAT_PLACEHOLDER.PAGE_COUNT)
|
||||
if (pageCountReg.test(text)) {
|
||||
text = PageNumber.formatNumberPlaceholder(
|
||||
text,
|
||||
this.draw.getPageCount() - fromPageNo,
|
||||
pageCountReg,
|
||||
numberType
|
||||
)
|
||||
}
|
||||
const width = this.draw.getWidth()
|
||||
// 计算y位置
|
||||
const height = this.draw.getHeight()
|
||||
const pageNumberBottom = this.draw.getPageNumberBottom()
|
||||
const y = height - pageNumberBottom
|
||||
ctx.save()
|
||||
ctx.fillStyle = color
|
||||
ctx.font = `${size * scale}px ${font}`
|
||||
// 计算x位置-居左、居中、居右
|
||||
let x = 0
|
||||
const margins = this.draw.getMargins()
|
||||
const { width: textWidth } = ctx.measureText(text)
|
||||
if (rowFlex === RowFlex.CENTER) {
|
||||
x = (width - textWidth) / 2
|
||||
} else if (rowFlex === RowFlex.RIGHT) {
|
||||
x = width - textWidth - margins[1]
|
||||
} else {
|
||||
x = margins[3]
|
||||
}
|
||||
ctx.fillText(text, x, y)
|
||||
ctx.restore()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
import { IEditorOption, IElement } from '../../..'
|
||||
import { DeepRequired } from '../../../interface/Common'
|
||||
import { IElementPosition } from '../../../interface/Element'
|
||||
import { IPlaceholder } from '../../../interface/Placeholder'
|
||||
import { IRow } from '../../../interface/Row'
|
||||
import { formatElementList } from '../../../utils/element'
|
||||
import { Position } from '../../position/Position'
|
||||
import { Draw } from '../Draw'
|
||||
import { LineBreakParticle } from '../particle/LineBreakParticle'
|
||||
|
||||
export interface IPlaceholderRenderOption {
|
||||
placeholder: Required<IPlaceholder>
|
||||
startY?: number
|
||||
}
|
||||
|
||||
export class Placeholder {
|
||||
private draw: Draw
|
||||
private position: Position
|
||||
private options: DeepRequired<IEditorOption>
|
||||
|
||||
private elementList: IElement[]
|
||||
private rowList: IRow[]
|
||||
private positionList: IElementPosition[]
|
||||
|
||||
constructor(draw: Draw) {
|
||||
this.draw = draw
|
||||
this.position = draw.getPosition()
|
||||
this.options = <DeepRequired<IEditorOption>>draw.getOptions()
|
||||
|
||||
this.elementList = []
|
||||
this.rowList = []
|
||||
this.positionList = []
|
||||
}
|
||||
|
||||
private _recovery() {
|
||||
this.elementList = []
|
||||
this.rowList = []
|
||||
this.positionList = []
|
||||
}
|
||||
|
||||
public _compute(options?: IPlaceholderRenderOption) {
|
||||
this._computeRowList()
|
||||
this._computePositionList(options)
|
||||
}
|
||||
|
||||
private _computeRowList() {
|
||||
const innerWidth = this.draw.getInnerWidth()
|
||||
this.rowList = this.draw.computeRowList({
|
||||
innerWidth,
|
||||
elementList: this.elementList
|
||||
})
|
||||
}
|
||||
|
||||
private _computePositionList(options?: IPlaceholderRenderOption) {
|
||||
const { lineBreak, scale } = this.options
|
||||
const headerExtraHeight = this.draw.getHeader().getExtraHeight()
|
||||
const innerWidth = this.draw.getInnerWidth()
|
||||
const margins = this.draw.getMargins()
|
||||
let startX = margins[3]
|
||||
// 换行符绘制开启时,移动起始位置
|
||||
if (!lineBreak.disabled) {
|
||||
startX += (LineBreakParticle.WIDTH + LineBreakParticle.GAP) * scale
|
||||
}
|
||||
const startY = options?.startY || margins[0] + headerExtraHeight
|
||||
this.position.computePageRowPosition({
|
||||
positionList: this.positionList,
|
||||
rowList: this.rowList,
|
||||
pageNo: 0,
|
||||
startRowIndex: 0,
|
||||
startIndex: 0,
|
||||
startX,
|
||||
startY,
|
||||
innerWidth
|
||||
})
|
||||
}
|
||||
|
||||
public render(
|
||||
ctx: CanvasRenderingContext2D,
|
||||
options?: IPlaceholderRenderOption
|
||||
) {
|
||||
const { placeholder = this.options.placeholder } = options || {}
|
||||
const { data, font, size, color, opacity } = placeholder
|
||||
this._recovery()
|
||||
// 构建元素列表并格式化
|
||||
this.elementList = [
|
||||
{
|
||||
value: data,
|
||||
font,
|
||||
size,
|
||||
color
|
||||
}
|
||||
]
|
||||
formatElementList(this.elementList, {
|
||||
editorOptions: this.options,
|
||||
isForceCompensation: true
|
||||
})
|
||||
// 计算
|
||||
this._compute(options)
|
||||
const innerWidth = this.draw.getInnerWidth()
|
||||
// 绘制
|
||||
ctx.save()
|
||||
ctx.globalAlpha = opacity
|
||||
this.draw.drawRow(ctx, {
|
||||
elementList: this.elementList,
|
||||
positionList: this.positionList,
|
||||
rowList: this.rowList,
|
||||
pageNo: 0,
|
||||
startIndex: 0,
|
||||
innerWidth,
|
||||
isDrawLineBreak: false
|
||||
})
|
||||
ctx.restore()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,188 @@
|
||||
import { IEditorOption } from '../../..'
|
||||
import { FORMAT_PLACEHOLDER } from '../../../dataset/constant/PageNumber'
|
||||
import { WatermarkType } from '../../../dataset/enum/Watermark'
|
||||
import { DeepRequired } from '../../../interface/Common'
|
||||
import { Draw } from '../Draw'
|
||||
import { PageNumber } from './PageNumber'
|
||||
|
||||
export class Watermark {
|
||||
private draw: Draw
|
||||
private options: DeepRequired<IEditorOption>
|
||||
private imageCache: Map<string, HTMLImageElement>
|
||||
|
||||
constructor(draw: Draw) {
|
||||
this.draw = draw
|
||||
this.options = <DeepRequired<IEditorOption>>draw.getOptions()
|
||||
this.imageCache = new Map()
|
||||
}
|
||||
|
||||
public renderText(ctx: CanvasRenderingContext2D, pageNo: number) {
|
||||
const {
|
||||
watermark: { data, opacity, font, size, color, repeat, gap, numberType },
|
||||
scale
|
||||
} = this.options
|
||||
const width = this.draw.getWidth()
|
||||
const height = this.draw.getHeight()
|
||||
// 开始绘制
|
||||
ctx.save()
|
||||
ctx.globalAlpha = opacity
|
||||
ctx.font = `${size * scale}px ${font}`
|
||||
// 格式化文本
|
||||
let text = data
|
||||
const pageNoReg = new RegExp(FORMAT_PLACEHOLDER.PAGE_NO)
|
||||
if (pageNoReg.test(text)) {
|
||||
text = PageNumber.formatNumberPlaceholder(
|
||||
text,
|
||||
pageNo + 1,
|
||||
pageNoReg,
|
||||
numberType
|
||||
)
|
||||
}
|
||||
const pageCountReg = new RegExp(FORMAT_PLACEHOLDER.PAGE_COUNT)
|
||||
if (pageCountReg.test(text)) {
|
||||
text = PageNumber.formatNumberPlaceholder(
|
||||
text,
|
||||
this.draw.getPageCount(),
|
||||
pageCountReg,
|
||||
numberType
|
||||
)
|
||||
}
|
||||
// 测量长度并绘制
|
||||
const measureText = ctx.measureText(text)
|
||||
if (repeat) {
|
||||
const dpr = this.draw.getPagePixelRatio()
|
||||
const temporaryCanvas = document.createElement('canvas')
|
||||
const temporaryCtx = temporaryCanvas.getContext('2d')!
|
||||
// 勾股定理计算旋转后的宽高对角线尺寸 a^2 + b^2 = c^2
|
||||
const textWidth = measureText.width
|
||||
const textHeight =
|
||||
measureText.actualBoundingBoxAscent +
|
||||
measureText.actualBoundingBoxDescent
|
||||
const diagonalLength = Math.sqrt(
|
||||
Math.pow(textWidth, 2) + Math.pow(textHeight, 2)
|
||||
)
|
||||
// 加上 gap 间距
|
||||
const patternWidth = diagonalLength + 2 * gap[0] * scale
|
||||
const patternHeight = diagonalLength + 2 * gap[1] * scale
|
||||
// 宽高设置
|
||||
temporaryCanvas.width = patternWidth
|
||||
temporaryCanvas.height = patternHeight
|
||||
temporaryCanvas.style.width = `${patternWidth * dpr}px`
|
||||
temporaryCanvas.style.height = `${patternHeight * dpr}px`
|
||||
// 旋转45度
|
||||
temporaryCtx.translate(patternWidth / 2, patternHeight / 2)
|
||||
temporaryCtx.rotate((-45 * Math.PI) / 180)
|
||||
temporaryCtx.translate(-patternWidth / 2, -patternHeight / 2)
|
||||
// 绘制文本
|
||||
temporaryCtx.font = `${size * scale}px ${font}`
|
||||
temporaryCtx.fillStyle = color
|
||||
temporaryCtx.fillText(
|
||||
text,
|
||||
(patternWidth - textWidth) / 2,
|
||||
(patternHeight - textHeight) / 2 + measureText.actualBoundingBoxAscent
|
||||
)
|
||||
// 创建平铺模式
|
||||
const pattern = ctx.createPattern(temporaryCanvas, 'repeat')
|
||||
if (pattern) {
|
||||
ctx.fillStyle = pattern
|
||||
ctx.fillRect(0, 0, width, height)
|
||||
}
|
||||
} else {
|
||||
const x = width / 2
|
||||
const y = height / 2
|
||||
ctx.fillStyle = color
|
||||
ctx.translate(x, y)
|
||||
ctx.rotate((-45 * Math.PI) / 180)
|
||||
ctx.fillText(
|
||||
text,
|
||||
-measureText.width / 2,
|
||||
measureText.actualBoundingBoxAscent - (size * scale) / 2
|
||||
)
|
||||
}
|
||||
ctx.restore()
|
||||
}
|
||||
|
||||
public renderImage(ctx: CanvasRenderingContext2D) {
|
||||
const {
|
||||
watermark: { width, height, data, opacity, repeat, gap },
|
||||
scale
|
||||
} = this.options
|
||||
if (!this.imageCache.has(data)) {
|
||||
const img = new Image()
|
||||
img.setAttribute('crossOrigin', 'Anonymous')
|
||||
img.src = data
|
||||
img.onload = () => {
|
||||
this.imageCache.set(data, img)
|
||||
// 避免层级上浮,触发编辑器二次渲染
|
||||
this.draw.render({
|
||||
isCompute: false,
|
||||
isSubmitHistory: false
|
||||
})
|
||||
}
|
||||
return
|
||||
}
|
||||
const docWidth = this.draw.getWidth()
|
||||
const docHeight = this.draw.getHeight()
|
||||
const imageWidth = width * scale
|
||||
const imageHeight = height * scale
|
||||
// 开始绘制
|
||||
ctx.save()
|
||||
ctx.globalAlpha = opacity
|
||||
if (repeat) {
|
||||
const dpr = this.draw.getPagePixelRatio()
|
||||
const temporaryCanvas = document.createElement('canvas')
|
||||
const temporaryCtx = temporaryCanvas.getContext('2d')!
|
||||
// 勾股定理计算旋转后的宽高对角线尺寸 a^2 + b^2 = c^2
|
||||
const diagonalLength = Math.sqrt(
|
||||
Math.pow(imageWidth, 2) + Math.pow(imageHeight, 2)
|
||||
)
|
||||
// 加上 gap 间距
|
||||
const patternWidth = diagonalLength + 2 * gap[0] * scale
|
||||
const patternHeight = diagonalLength + 2 * gap[1] * scale
|
||||
// 宽高设置
|
||||
temporaryCanvas.width = patternWidth
|
||||
temporaryCanvas.height = patternHeight
|
||||
temporaryCanvas.style.width = `${patternWidth * dpr}px`
|
||||
temporaryCanvas.style.height = `${patternHeight * dpr}px`
|
||||
// 旋转45度
|
||||
temporaryCtx.translate(patternWidth / 2, patternHeight / 2)
|
||||
temporaryCtx.rotate((-45 * Math.PI) / 180)
|
||||
temporaryCtx.translate(-patternWidth / 2, -patternHeight / 2)
|
||||
// 绘制图片
|
||||
temporaryCtx.drawImage(
|
||||
this.imageCache.get(data)!,
|
||||
(patternWidth - imageWidth) / 2,
|
||||
(patternHeight - imageHeight) / 2,
|
||||
imageWidth,
|
||||
imageHeight
|
||||
)
|
||||
// 创建平铺模式
|
||||
const pattern = ctx.createPattern(temporaryCanvas, 'repeat')
|
||||
if (pattern) {
|
||||
ctx.fillStyle = pattern
|
||||
ctx.fillRect(0, 0, docWidth, docHeight)
|
||||
}
|
||||
} else {
|
||||
const x = docWidth / 2
|
||||
const y = docHeight / 2
|
||||
ctx.translate(x, y)
|
||||
ctx.rotate((-45 * Math.PI) / 180)
|
||||
ctx.drawImage(
|
||||
this.imageCache.get(data)!,
|
||||
-imageWidth / 2,
|
||||
-imageHeight / 2,
|
||||
imageWidth,
|
||||
imageHeight
|
||||
)
|
||||
}
|
||||
ctx.restore()
|
||||
}
|
||||
|
||||
public render(ctx: CanvasRenderingContext2D, pageNo: number) {
|
||||
if (this.options.watermark.type === WatermarkType.IMAGE) {
|
||||
this.renderImage(ctx)
|
||||
} else {
|
||||
this.renderText(ctx, pageNo)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,309 @@
|
||||
import { Draw } from '../Draw'
|
||||
import { deepClone, getUUID, isNonValue } from '../../../utils'
|
||||
import { ElementType } from '../../../dataset/enum/Element'
|
||||
import {
|
||||
IArea,
|
||||
IAreaInfo,
|
||||
IGetAreaValueOption,
|
||||
IGetAreaValueResult,
|
||||
IInsertAreaOption,
|
||||
ILocationAreaOption,
|
||||
ISetAreaPropertiesOption,
|
||||
ISetAreaValueOption
|
||||
} from '../../../interface/Area'
|
||||
import { EditorZone } from '../../../dataset/enum/Editor'
|
||||
import { LocationPosition } from '../../../dataset/enum/Common'
|
||||
import { RangeManager } from '../../range/RangeManager'
|
||||
import { Zone } from '../../zone/Zone'
|
||||
import { Position } from '../../position/Position'
|
||||
import { formatElementList, zipElementList } from '../../../utils/element'
|
||||
import { AreaMode } from '../../../dataset/enum/Area'
|
||||
import { IRange } from '../../../interface/Range'
|
||||
import { IElementPosition } from '../../../interface/Element'
|
||||
import { Placeholder } from '../frame/Placeholder'
|
||||
import { defaultPlaceholderOption } from '../../../dataset/constant/Placeholder'
|
||||
import { DeepRequired } from '../../../interface/Common'
|
||||
import { IEditorOption } from '../../../interface/Editor'
|
||||
|
||||
export class Area {
|
||||
private draw: Draw
|
||||
private zone: Zone
|
||||
private range: RangeManager
|
||||
private position: Position
|
||||
private options: DeepRequired<IEditorOption>
|
||||
private areaInfoMap = new Map<string, IAreaInfo>()
|
||||
|
||||
constructor(draw: Draw) {
|
||||
this.draw = draw
|
||||
this.options = draw.getOptions()
|
||||
this.zone = draw.getZone()
|
||||
this.range = draw.getRange()
|
||||
this.position = draw.getPosition()
|
||||
}
|
||||
|
||||
public getAreaInfo(): Map<string, IAreaInfo> {
|
||||
return this.areaInfoMap
|
||||
}
|
||||
|
||||
public getActiveAreaId(): string | null {
|
||||
if (!this.areaInfoMap.size) return null
|
||||
const { startIndex } = this.range.getRange()
|
||||
const elementList = this.draw.getElementList()
|
||||
const element = elementList[startIndex]
|
||||
return element?.areaId || null
|
||||
}
|
||||
|
||||
public getActiveAreaInfo(): IAreaInfo | null {
|
||||
const activeAreaId = this.getActiveAreaId()
|
||||
if (!activeAreaId) return null
|
||||
return this.areaInfoMap.get(activeAreaId) || null
|
||||
}
|
||||
|
||||
public isReadonly() {
|
||||
const activeAreaInfo = this.getActiveAreaInfo()
|
||||
if (!activeAreaInfo?.area) return false
|
||||
switch (activeAreaInfo.area.mode) {
|
||||
case AreaMode.EDIT:
|
||||
return false
|
||||
case AreaMode.READONLY:
|
||||
return true
|
||||
case AreaMode.FORM:
|
||||
return !this.draw.getControl().getIsRangeWithinControl()
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
public insertArea(payload: IInsertAreaOption): string | null {
|
||||
const { id, value, area, position, range } = payload
|
||||
// 切换至正文
|
||||
if (this.zone.getZone() !== EditorZone.MAIN) {
|
||||
this.zone.setZone(EditorZone.MAIN)
|
||||
}
|
||||
// 跳出表格
|
||||
this.draw.getPosition().setPositionContext({
|
||||
isTable: false
|
||||
})
|
||||
// 通过光标插入area && 不能在area内再次插入area
|
||||
if (range && !this.getActiveAreaId()) {
|
||||
const { startIndex, endIndex } = range
|
||||
// 校验位置合法性
|
||||
const elementList = this.draw.getOriginalMainElementList()
|
||||
if (!elementList[startIndex] || !elementList[endIndex]) {
|
||||
return null
|
||||
}
|
||||
this.range.setRange(range.startIndex, range.endIndex)
|
||||
} else {
|
||||
// 设置插入位置
|
||||
if (position === LocationPosition.BEFORE) {
|
||||
this.range.setRange(0, 0)
|
||||
} else {
|
||||
const elementList = this.draw.getOriginalMainElementList()
|
||||
const lastIndex = elementList.length - 1
|
||||
this.range.setRange(lastIndex, lastIndex)
|
||||
}
|
||||
}
|
||||
const areaId = id || getUUID()
|
||||
this.draw.insertElementList([
|
||||
{
|
||||
type: ElementType.AREA,
|
||||
value: '',
|
||||
areaId,
|
||||
valueList: value,
|
||||
area: deepClone(area)
|
||||
}
|
||||
])
|
||||
return areaId
|
||||
}
|
||||
|
||||
public render(ctx: CanvasRenderingContext2D, pageNo: number) {
|
||||
if (!this.areaInfoMap.size) return
|
||||
ctx.save()
|
||||
const margins = this.draw.getMargins()
|
||||
const width = this.draw.getInnerWidth()
|
||||
for (const areaInfoItem of this.areaInfoMap) {
|
||||
const { area, positionList } = areaInfoItem[1]
|
||||
if (
|
||||
area?.hide ||
|
||||
(!area?.backgroundColor && !area?.borderColor && !area?.placeholder)
|
||||
) {
|
||||
continue
|
||||
}
|
||||
const pagePositionList = positionList.filter(p => p.pageNo === pageNo)
|
||||
if (!pagePositionList.length) continue
|
||||
ctx.translate(0.5, 0.5)
|
||||
const firstPosition = pagePositionList[0]
|
||||
const lastPosition = pagePositionList[pagePositionList.length - 1]
|
||||
// 起始位置
|
||||
const x = margins[3]
|
||||
const y = Math.ceil(firstPosition.coordinate.leftTop[1])
|
||||
const height = Math.ceil(lastPosition.coordinate.rightBottom[1] - y)
|
||||
// 背景色
|
||||
if (area.backgroundColor) {
|
||||
ctx.fillStyle = area.backgroundColor
|
||||
ctx.fillRect(x, y, width, height)
|
||||
}
|
||||
// 边框
|
||||
if (area.borderColor) {
|
||||
ctx.strokeStyle = area.borderColor
|
||||
ctx.strokeRect(x, y, width, height)
|
||||
}
|
||||
// 提示词
|
||||
if (area.placeholder && positionList.length <= 1) {
|
||||
const placeholder = new Placeholder(this.draw)
|
||||
placeholder.render(ctx, {
|
||||
placeholder: {
|
||||
...defaultPlaceholderOption,
|
||||
...area.placeholder
|
||||
},
|
||||
startY: firstPosition.coordinate.leftTop[1]
|
||||
})
|
||||
}
|
||||
ctx.translate(-0.5, -0.5)
|
||||
}
|
||||
ctx.restore()
|
||||
}
|
||||
|
||||
public compute() {
|
||||
this.areaInfoMap.clear()
|
||||
const elementList = this.draw.getOriginalMainElementList()
|
||||
const positionList = this.position.getOriginalMainPositionList()
|
||||
for (let e = 0; e < elementList.length; e++) {
|
||||
const element = elementList[e]
|
||||
const areaId = element.areaId
|
||||
if (areaId) {
|
||||
const areaInfo = this.areaInfoMap.get(areaId)
|
||||
if (!areaInfo) {
|
||||
this.areaInfoMap.set(areaId, {
|
||||
id: areaId,
|
||||
area: element.area!,
|
||||
elementList: [element],
|
||||
positionList: [positionList[e]]
|
||||
})
|
||||
} else {
|
||||
areaInfo.elementList.push(element)
|
||||
areaInfo.positionList.push(positionList[e])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public getAreaValue(
|
||||
options: IGetAreaValueOption = {}
|
||||
): IGetAreaValueResult | null {
|
||||
const areaId = options.id || this.getActiveAreaId()
|
||||
if (!areaId) return null
|
||||
const areaInfo = this.areaInfoMap.get(areaId)
|
||||
if (!areaInfo) return null
|
||||
return {
|
||||
area: areaInfo.area,
|
||||
id: areaInfo.id,
|
||||
startPageNo: areaInfo.positionList[0].pageNo,
|
||||
endPageNo: areaInfo.positionList[areaInfo.positionList.length - 1].pageNo,
|
||||
value: zipElementList(areaInfo.elementList)
|
||||
}
|
||||
}
|
||||
|
||||
public getContextByAreaId(
|
||||
areaId: string,
|
||||
options?: ILocationAreaOption
|
||||
): { range: IRange; elementPosition: IElementPosition } | null {
|
||||
const elementList = this.draw.getOriginalMainElementList()
|
||||
for (let e = 0; e < elementList.length; e++) {
|
||||
const element = elementList[e]
|
||||
if (options?.position === LocationPosition.OUTER_BEFORE) {
|
||||
// 区域外面最前
|
||||
if (elementList[e + 1]?.areaId !== areaId) continue
|
||||
} else if (options?.position === LocationPosition.AFTER) {
|
||||
// 区域内部最后
|
||||
if (
|
||||
!(element.areaId === areaId && elementList[e + 1]?.areaId !== areaId)
|
||||
) {
|
||||
continue
|
||||
}
|
||||
} else if (options?.position === LocationPosition.OUTER_AFTER) {
|
||||
// 区域外部最后
|
||||
if (
|
||||
!(element.areaId !== areaId && elementList[e - 1]?.areaId === areaId)
|
||||
) {
|
||||
continue
|
||||
}
|
||||
} else {
|
||||
// 区域内部最前
|
||||
if (element.areaId !== areaId) continue
|
||||
}
|
||||
const positionList = this.position.getOriginalMainPositionList()
|
||||
return {
|
||||
range: {
|
||||
startIndex: e,
|
||||
endIndex: e
|
||||
},
|
||||
elementPosition: positionList[e]
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
public setAreaProperties(payload: ISetAreaPropertiesOption) {
|
||||
const areaId = payload.id || this.getActiveAreaId()
|
||||
if (!areaId) return
|
||||
const areaInfo = this.areaInfoMap.get(areaId)
|
||||
if (!areaInfo) return
|
||||
if (!areaInfo.area) {
|
||||
areaInfo.area = {}
|
||||
}
|
||||
// 需要计算的属性
|
||||
let isCompute = false
|
||||
const computeProps: Array<keyof IArea> = ['top', 'hide']
|
||||
// 循环设置
|
||||
Object.entries(payload.properties).forEach(([key, value]) => {
|
||||
if (isNonValue(value)) return
|
||||
const propKey = key as keyof IArea
|
||||
areaInfo.area[propKey] = value
|
||||
if (computeProps.includes(propKey)) {
|
||||
isCompute = true
|
||||
}
|
||||
})
|
||||
this.draw.render({
|
||||
isCompute,
|
||||
isSetCursor: false
|
||||
})
|
||||
}
|
||||
|
||||
public setAreaValue(payload: ISetAreaValueOption) {
|
||||
const areaId = payload.id || this.getActiveAreaId()
|
||||
if (!areaId) return
|
||||
const areaInfo = this.areaInfoMap.get(areaId)
|
||||
if (!areaInfo) return
|
||||
// 删除旧数据并替换新的格式化数据
|
||||
const { positionList } = areaInfo
|
||||
const elementList = this.draw.getOriginalMainElementList()
|
||||
const valueList = payload.value
|
||||
formatElementList(
|
||||
[
|
||||
{
|
||||
type: ElementType.AREA,
|
||||
value: '',
|
||||
valueList,
|
||||
areaId: areaInfo.id,
|
||||
area: areaInfo.area
|
||||
}
|
||||
],
|
||||
{
|
||||
editorOptions: this.options
|
||||
}
|
||||
)
|
||||
this.draw.spliceElementList(
|
||||
elementList,
|
||||
positionList[0].index,
|
||||
positionList.length,
|
||||
valueList,
|
||||
{
|
||||
isIgnoreDeletedRule: true
|
||||
}
|
||||
)
|
||||
this.draw.render({
|
||||
isSetCursor: false
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,198 @@
|
||||
import { EditorZone } from '../../../dataset/enum/Editor'
|
||||
import { ElementType } from '../../../dataset/enum/Element'
|
||||
import { DeepRequired } from '../../../interface/Common'
|
||||
import { IEditorOption } from '../../../interface/Editor'
|
||||
import { IElement, IElementFillRect } from '../../../interface/Element'
|
||||
import { IPositionContext } from '../../../interface/Position'
|
||||
import { IRange } from '../../../interface/Range'
|
||||
import { getUUID } from '../../../utils'
|
||||
import { RangeManager } from '../../range/RangeManager'
|
||||
import { Draw } from '../Draw'
|
||||
|
||||
export class Group {
|
||||
private draw: Draw
|
||||
private options: DeepRequired<IEditorOption>
|
||||
private range: RangeManager
|
||||
private fillRectMap: Map<string, IElementFillRect>
|
||||
|
||||
constructor(draw: Draw) {
|
||||
this.draw = draw
|
||||
this.options = draw.getOptions()
|
||||
this.range = draw.getRange()
|
||||
this.fillRectMap = new Map()
|
||||
}
|
||||
|
||||
public setGroup(): string | null {
|
||||
if (
|
||||
this.draw.isReadonly() ||
|
||||
this.draw.getZone().getZone() !== EditorZone.MAIN
|
||||
) {
|
||||
return null
|
||||
}
|
||||
const selection = this.range.getSelection()
|
||||
if (!selection) return null
|
||||
const groupId = getUUID()
|
||||
selection.forEach(el => {
|
||||
if (!Array.isArray(el.groupIds)) {
|
||||
el.groupIds = []
|
||||
}
|
||||
el.groupIds.push(groupId)
|
||||
})
|
||||
this.draw.render({
|
||||
isSetCursor: false,
|
||||
isCompute: false
|
||||
})
|
||||
return groupId
|
||||
}
|
||||
|
||||
public getElementListByGroupId(
|
||||
elementList: IElement[],
|
||||
groupId: string
|
||||
): IElement[] {
|
||||
const groupElementList: IElement[] = []
|
||||
for (let e = 0; e < elementList.length; e++) {
|
||||
const element = elementList[e]
|
||||
if (element.type === ElementType.TABLE) {
|
||||
const trList = element.trList!
|
||||
for (let r = 0; r < trList.length; r++) {
|
||||
const tr = trList[r]
|
||||
for (let d = 0; d < tr.tdList.length; d++) {
|
||||
const td = tr.tdList[d]
|
||||
const tdGroupElementList = this.getElementListByGroupId(
|
||||
td.value,
|
||||
groupId
|
||||
)
|
||||
if (tdGroupElementList.length) {
|
||||
groupElementList.push(...tdGroupElementList)
|
||||
return groupElementList
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (element?.groupIds?.includes(groupId)) {
|
||||
groupElementList.push(element)
|
||||
const nextElement = elementList[e + 1]
|
||||
if (!nextElement?.groupIds?.includes(groupId)) break
|
||||
}
|
||||
}
|
||||
return groupElementList
|
||||
}
|
||||
|
||||
public deleteGroup(groupId: string) {
|
||||
if (this.draw.isReadonly()) return
|
||||
// 仅主体内容可以成组
|
||||
const elementList = this.draw.getOriginalMainElementList()
|
||||
const groupElementList = this.getElementListByGroupId(elementList, groupId)
|
||||
if (!groupElementList.length) return
|
||||
for (let e = 0; e < groupElementList.length; e++) {
|
||||
const element = groupElementList[e]
|
||||
const groupIds = element.groupIds!
|
||||
const groupIndex = groupIds.findIndex(id => id === groupId)
|
||||
groupIds.splice(groupIndex, 1)
|
||||
// 不包含成组时删除字段,减少存储及内存占用
|
||||
if (!groupIds.length) {
|
||||
delete element.groupIds
|
||||
}
|
||||
}
|
||||
this.draw.render({
|
||||
isSetCursor: false,
|
||||
isCompute: false
|
||||
})
|
||||
}
|
||||
|
||||
public getContextByGroupId(
|
||||
elementList: IElement[],
|
||||
groupId: string
|
||||
): (IRange & IPositionContext) | null {
|
||||
for (let e = 0; e < elementList.length; e++) {
|
||||
const element = elementList[e]
|
||||
if (element.type === ElementType.TABLE) {
|
||||
const trList = element.trList!
|
||||
for (let r = 0; r < trList.length; r++) {
|
||||
const tr = trList[r]
|
||||
for (let d = 0; d < tr.tdList.length; d++) {
|
||||
const td = tr.tdList[d]
|
||||
const range = this.getContextByGroupId(td.value, groupId)
|
||||
if (range) {
|
||||
return {
|
||||
...range,
|
||||
isTable: true,
|
||||
index: e,
|
||||
trIndex: r,
|
||||
tdIndex: d,
|
||||
tdId: td.id,
|
||||
trId: tr.id,
|
||||
tableId: element.tableId
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
const nextElement = elementList[e + 1]
|
||||
if (
|
||||
element.groupIds?.includes(groupId) &&
|
||||
!nextElement?.groupIds?.includes(groupId)
|
||||
) {
|
||||
return {
|
||||
isTable: false,
|
||||
startIndex: e,
|
||||
endIndex: e
|
||||
}
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
public clearFillInfo() {
|
||||
this.fillRectMap.clear()
|
||||
}
|
||||
|
||||
public recordFillInfo(
|
||||
element: IElement,
|
||||
x: number,
|
||||
y: number,
|
||||
width: number,
|
||||
height: number
|
||||
) {
|
||||
const groupIds = element.groupIds
|
||||
if (!groupIds) return
|
||||
for (const groupId of groupIds) {
|
||||
const fillRect = this.fillRectMap.get(groupId)
|
||||
if (!fillRect) {
|
||||
this.fillRectMap.set(groupId, {
|
||||
x,
|
||||
y,
|
||||
width,
|
||||
height
|
||||
})
|
||||
} else {
|
||||
fillRect.width += width
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public render(ctx: CanvasRenderingContext2D) {
|
||||
if (!this.fillRectMap.size) return
|
||||
// 当前激活组信息
|
||||
const range = this.range.getRange()
|
||||
const elementList = this.draw.getElementList()
|
||||
const anchorGroupIds = elementList[range.endIndex]?.groupIds
|
||||
const {
|
||||
group: { backgroundColor, opacity, activeOpacity, activeBackgroundColor }
|
||||
} = this.options
|
||||
ctx.save()
|
||||
this.fillRectMap.forEach((fillRect, groupId) => {
|
||||
const { x, y, width, height } = fillRect
|
||||
if (anchorGroupIds?.includes(groupId)) {
|
||||
ctx.globalAlpha = activeOpacity
|
||||
ctx.fillStyle = activeBackgroundColor
|
||||
} else {
|
||||
ctx.globalAlpha = opacity
|
||||
ctx.fillStyle = backgroundColor
|
||||
}
|
||||
ctx.fillRect(x, y, width, height)
|
||||
})
|
||||
ctx.restore()
|
||||
this.clearFillInfo()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,491 @@
|
||||
import { ZERO } from '../../../dataset/constant/Common'
|
||||
import { TEXTLIKE_ELEMENT_TYPE } from '../../../dataset/constant/Element'
|
||||
import { ControlComponent } from '../../../dataset/enum/Control'
|
||||
import { EditorContext } from '../../../dataset/enum/Editor'
|
||||
import { ElementType } from '../../../dataset/enum/Element'
|
||||
import { IEditorOption } from '../../../interface/Editor'
|
||||
import { IElement, IElementPosition } from '../../../interface/Element'
|
||||
import {
|
||||
IReplaceOption,
|
||||
ISearchResult,
|
||||
ISearchResultRestArgs
|
||||
} from '../../../interface/Search'
|
||||
import { getUUID, isNumber } from '../../../utils'
|
||||
import { Position } from '../../position/Position'
|
||||
import { Draw } from '../Draw'
|
||||
|
||||
export interface INavigateInfo {
|
||||
index: number
|
||||
count: number
|
||||
}
|
||||
|
||||
export class Search {
|
||||
private draw: Draw
|
||||
private options: Required<IEditorOption>
|
||||
private position: Position
|
||||
private searchKeyword: string | null
|
||||
private searchNavigateIndex: number | null
|
||||
private searchMatchList: ISearchResult[]
|
||||
|
||||
constructor(draw: Draw) {
|
||||
this.draw = draw
|
||||
this.options = draw.getOptions()
|
||||
this.position = draw.getPosition()
|
||||
this.searchNavigateIndex = null
|
||||
this.searchKeyword = null
|
||||
this.searchMatchList = []
|
||||
}
|
||||
|
||||
public getSearchKeyword(): string | null {
|
||||
return this.searchKeyword
|
||||
}
|
||||
|
||||
public setSearchKeyword(payload: string | null) {
|
||||
this.searchKeyword = payload
|
||||
this.searchNavigateIndex = null
|
||||
}
|
||||
|
||||
public searchNavigatePre(): number | null {
|
||||
if (!this.searchMatchList.length || !this.searchKeyword) return null
|
||||
if (this.searchNavigateIndex === null) {
|
||||
this.searchNavigateIndex = 0
|
||||
} else {
|
||||
let index = this.searchNavigateIndex - 1
|
||||
let isExistPre = false
|
||||
const searchNavigateId =
|
||||
this.searchMatchList[this.searchNavigateIndex].groupId
|
||||
while (index >= 0) {
|
||||
const match = this.searchMatchList[index]
|
||||
if (searchNavigateId !== match.groupId) {
|
||||
isExistPre = true
|
||||
this.searchNavigateIndex = index - (this.searchKeyword.length - 1)
|
||||
break
|
||||
}
|
||||
index--
|
||||
}
|
||||
if (!isExistPre) {
|
||||
const lastSearchMatch =
|
||||
this.searchMatchList[this.searchMatchList.length - 1]
|
||||
if (lastSearchMatch.groupId === searchNavigateId) return null
|
||||
this.searchNavigateIndex =
|
||||
this.searchMatchList.length - 1 - (this.searchKeyword.length - 1)
|
||||
}
|
||||
}
|
||||
return this.searchNavigateIndex
|
||||
}
|
||||
|
||||
public searchNavigateNext(): number | null {
|
||||
if (!this.searchMatchList.length || !this.searchKeyword) return null
|
||||
if (this.searchNavigateIndex === null) {
|
||||
this.searchNavigateIndex = 0
|
||||
} else {
|
||||
let index = this.searchNavigateIndex + 1
|
||||
let isExistNext = false
|
||||
const searchNavigateId =
|
||||
this.searchMatchList[this.searchNavigateIndex].groupId
|
||||
while (index < this.searchMatchList.length) {
|
||||
const match = this.searchMatchList[index]
|
||||
if (searchNavigateId !== match.groupId) {
|
||||
isExistNext = true
|
||||
this.searchNavigateIndex = index
|
||||
break
|
||||
}
|
||||
index++
|
||||
}
|
||||
if (!isExistNext) {
|
||||
const firstSearchMatch = this.searchMatchList[0]
|
||||
if (firstSearchMatch.groupId === searchNavigateId) return null
|
||||
this.searchNavigateIndex = 0
|
||||
}
|
||||
}
|
||||
return this.searchNavigateIndex
|
||||
}
|
||||
|
||||
public searchNavigateScrollIntoView(position: IElementPosition) {
|
||||
const {
|
||||
coordinate: { leftTop, leftBottom, rightTop },
|
||||
pageNo
|
||||
} = position
|
||||
const height = this.draw.getHeight()
|
||||
const pageGap = this.draw.getPageGap()
|
||||
const preY = pageNo * (height + pageGap)
|
||||
// 创建定位锚点
|
||||
const anchor = document.createElement('div')
|
||||
anchor.style.position = 'absolute'
|
||||
// 扩大搜索词尺寸,使可视范围更广
|
||||
const ANCHOR_OVERFLOW_SIZE = 50
|
||||
anchor.style.width = `${rightTop[0] - leftTop[0] + ANCHOR_OVERFLOW_SIZE}px`
|
||||
anchor.style.height = `${
|
||||
leftBottom[1] - leftTop[1] + ANCHOR_OVERFLOW_SIZE
|
||||
}px`
|
||||
anchor.style.left = `${leftTop[0]}px`
|
||||
anchor.style.top = `${leftTop[1] + preY}px`
|
||||
this.draw.getContainer().append(anchor)
|
||||
// 移动到可视范围
|
||||
anchor.scrollIntoView(false)
|
||||
anchor.remove()
|
||||
}
|
||||
|
||||
public getSearchNavigateIndexList() {
|
||||
if (this.searchNavigateIndex === null || !this.searchKeyword) return []
|
||||
return new Array(this.searchKeyword.length)
|
||||
.fill(this.searchNavigateIndex)
|
||||
.map((navigate, index) => navigate + index)
|
||||
}
|
||||
|
||||
public getSearchMatchList(): ISearchResult[] {
|
||||
return this.searchMatchList
|
||||
}
|
||||
|
||||
public getSearchNavigateInfo(): null | INavigateInfo {
|
||||
if (!this.searchKeyword || !this.searchMatchList.length) return null
|
||||
const index =
|
||||
this.searchNavigateIndex !== null
|
||||
? this.searchNavigateIndex / this.searchKeyword.length + 1
|
||||
: 0
|
||||
let count = 0
|
||||
let groupId = null
|
||||
for (let s = 0; s < this.searchMatchList.length; s++) {
|
||||
const match = this.searchMatchList[s]
|
||||
if (groupId === match.groupId) continue
|
||||
groupId = match.groupId
|
||||
count += 1
|
||||
}
|
||||
return {
|
||||
index,
|
||||
count
|
||||
}
|
||||
}
|
||||
|
||||
public getMatchList(
|
||||
payload: string,
|
||||
originalElementList: IElement[]
|
||||
): ISearchResult[] {
|
||||
const keyword = payload.toLocaleLowerCase()
|
||||
const searchMatchList: ISearchResult[] = []
|
||||
// 分组
|
||||
const elementListGroup: {
|
||||
type: EditorContext
|
||||
elementList: IElement[]
|
||||
index: number
|
||||
}[] = []
|
||||
const originalElementListLength = originalElementList.length
|
||||
// 查找表格所在位置
|
||||
const tableIndexList = []
|
||||
for (let e = 0; e < originalElementListLength; e++) {
|
||||
const element = originalElementList[e]
|
||||
if (element.type === ElementType.TABLE) {
|
||||
tableIndexList.push(e)
|
||||
}
|
||||
}
|
||||
let i = 0
|
||||
let elementIndex = 0
|
||||
while (elementIndex < originalElementListLength - 1) {
|
||||
const endIndex = tableIndexList.length
|
||||
? tableIndexList[i]
|
||||
: originalElementListLength
|
||||
const pageElement = originalElementList.slice(elementIndex, endIndex)
|
||||
if (pageElement.length) {
|
||||
elementListGroup.push({
|
||||
index: elementIndex,
|
||||
type: EditorContext.PAGE,
|
||||
elementList: pageElement
|
||||
})
|
||||
}
|
||||
const tableElement = originalElementList[endIndex]
|
||||
if (tableElement) {
|
||||
elementListGroup.push({
|
||||
index: endIndex,
|
||||
type: EditorContext.TABLE,
|
||||
elementList: [tableElement]
|
||||
})
|
||||
}
|
||||
elementIndex = endIndex + 1
|
||||
i++
|
||||
}
|
||||
// 搜索文本
|
||||
function searchClosure(
|
||||
payload: string | null,
|
||||
type: EditorContext,
|
||||
elementList: IElement[],
|
||||
restArgs?: ISearchResultRestArgs
|
||||
) {
|
||||
if (!payload) return
|
||||
const text = elementList
|
||||
.map(e =>
|
||||
!e.type ||
|
||||
(TEXTLIKE_ELEMENT_TYPE.includes(e.type) &&
|
||||
e.controlComponent !== ControlComponent.CHECKBOX &&
|
||||
!e.hide &&
|
||||
!e.control?.hide &&
|
||||
!e.area?.hide)
|
||||
? e.value
|
||||
: ZERO
|
||||
)
|
||||
.filter(Boolean)
|
||||
.join('')
|
||||
.toLocaleLowerCase()
|
||||
const matchStartIndexList = []
|
||||
let index = text.indexOf(payload)
|
||||
while (index !== -1) {
|
||||
matchStartIndexList.push(index)
|
||||
index = text.indexOf(payload, index + payload.length)
|
||||
}
|
||||
for (let m = 0; m < matchStartIndexList.length; m++) {
|
||||
const startIndex = matchStartIndexList[m]
|
||||
const groupId = getUUID()
|
||||
for (let i = 0; i < payload.length; i++) {
|
||||
const index = startIndex + i + (restArgs?.startIndex || 0)
|
||||
searchMatchList.push({
|
||||
type,
|
||||
index,
|
||||
groupId,
|
||||
...restArgs
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
for (let e = 0; e < elementListGroup.length; e++) {
|
||||
const group = elementListGroup[e]
|
||||
if (group.type === EditorContext.TABLE) {
|
||||
const tableElement = group.elementList[0]
|
||||
for (let t = 0; t < tableElement.trList!.length; t++) {
|
||||
const tr = tableElement.trList![t]
|
||||
for (let d = 0; d < tr.tdList.length; d++) {
|
||||
const td = tr.tdList[d]
|
||||
const restArgs: ISearchResultRestArgs = {
|
||||
tableId: tableElement.id,
|
||||
tableIndex: group.index,
|
||||
trIndex: t,
|
||||
tdIndex: d,
|
||||
tdId: td.id
|
||||
}
|
||||
searchClosure(keyword, group.type, td.value, restArgs)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
searchClosure(keyword, group.type, group.elementList, {
|
||||
startIndex: group.index
|
||||
})
|
||||
}
|
||||
}
|
||||
return searchMatchList
|
||||
}
|
||||
|
||||
public compute(payload: string) {
|
||||
this.searchMatchList = this.getMatchList(
|
||||
payload,
|
||||
this.draw.getOriginalElementList()
|
||||
)
|
||||
}
|
||||
|
||||
public render(ctx: CanvasRenderingContext2D, pageIndex: number) {
|
||||
if (
|
||||
!this.searchMatchList ||
|
||||
!this.searchMatchList.length ||
|
||||
!this.searchKeyword
|
||||
) {
|
||||
return
|
||||
}
|
||||
const { searchMatchAlpha, searchMatchColor, searchNavigateMatchColor } =
|
||||
this.options
|
||||
const positionList = this.position.getOriginalPositionList()
|
||||
const elementList = this.draw.getOriginalElementList()
|
||||
ctx.save()
|
||||
ctx.globalAlpha = searchMatchAlpha
|
||||
for (let s = 0; s < this.searchMatchList.length; s++) {
|
||||
const searchMatch = this.searchMatchList[s]
|
||||
let position: IElementPosition | null = null
|
||||
if (searchMatch.type === EditorContext.TABLE) {
|
||||
const { tableIndex, trIndex, tdIndex, index } = searchMatch
|
||||
position =
|
||||
elementList[tableIndex!]?.trList![trIndex!].tdList[tdIndex!]
|
||||
?.positionList![index]
|
||||
} else {
|
||||
position = positionList[searchMatch.index]
|
||||
}
|
||||
if (!position) continue
|
||||
const {
|
||||
coordinate: { leftTop, leftBottom, rightTop },
|
||||
pageNo
|
||||
} = position
|
||||
if (pageNo !== pageIndex) continue
|
||||
// 高亮并定位当前搜索词
|
||||
const searchMatchIndexList = this.getSearchNavigateIndexList()
|
||||
if (searchMatchIndexList.includes(s)) {
|
||||
ctx.fillStyle = searchNavigateMatchColor
|
||||
// 是否是第一个字符,则移动到可视范围
|
||||
const preSearchMatch = this.searchMatchList[s - 1]
|
||||
if (!preSearchMatch || preSearchMatch.groupId !== searchMatch.groupId) {
|
||||
this.searchNavigateScrollIntoView(position)
|
||||
}
|
||||
} else {
|
||||
ctx.fillStyle = searchMatchColor
|
||||
}
|
||||
const x = leftTop[0]
|
||||
const y = leftTop[1]
|
||||
const width = rightTop[0] - leftTop[0]
|
||||
const height = leftBottom[1] - leftTop[1]
|
||||
ctx.fillRect(x, y, width, height)
|
||||
}
|
||||
ctx.restore()
|
||||
}
|
||||
|
||||
public replace(payload: string, option?: IReplaceOption) {
|
||||
const isReadonly = this.draw.isReadonly()
|
||||
if (isReadonly) return
|
||||
if (payload === undefined || payload === null) return
|
||||
let matchList = this.getSearchMatchList()
|
||||
// 替换搜索项
|
||||
const replaceIndex = option?.index
|
||||
if (isNumber(replaceIndex)) {
|
||||
const matchGroup: ISearchResult[][] = []
|
||||
matchList.forEach(match => {
|
||||
const last = matchGroup[matchGroup.length - 1]
|
||||
if (!last || last[0].groupId !== match.groupId) {
|
||||
matchGroup.push([match])
|
||||
} else {
|
||||
last.push(match)
|
||||
}
|
||||
})
|
||||
matchList = matchGroup[replaceIndex]
|
||||
}
|
||||
if (!matchList?.length) return
|
||||
const isDesignMode = this.draw.isDesignMode()
|
||||
// 匹配index变化的差值
|
||||
let pageDiffCount = 0
|
||||
let tableDiffCount = 0
|
||||
// 匹配搜索词的组标识
|
||||
let curGroupId = ''
|
||||
// 表格上下文
|
||||
let curTdId = ''
|
||||
// 搜索值 > 替换值:增加元素;搜索值 < 替换值:减少元素
|
||||
let firstMatchIndex = -1
|
||||
const elementList = this.draw.getOriginalElementList()
|
||||
for (let m = 0; m < matchList.length; m++) {
|
||||
const match = matchList[m]
|
||||
if (match.type === EditorContext.TABLE) {
|
||||
const { tableIndex, trIndex, tdIndex, index, tdId } = match
|
||||
if (curTdId && tdId !== curTdId) {
|
||||
tableDiffCount = 0
|
||||
}
|
||||
curTdId = tdId!
|
||||
const curTableIndex = tableIndex! + pageDiffCount
|
||||
const tableElementList =
|
||||
elementList[curTableIndex].trList![trIndex!].tdList[tdIndex!].value
|
||||
// 表格内元素
|
||||
const curIndex = index + tableDiffCount
|
||||
const tableElement = tableElementList[curIndex]
|
||||
// 非设计模式下设置元素不可删除 || 控件结构元素 => 禁止替换
|
||||
if (
|
||||
!isDesignMode &&
|
||||
(tableElement?.control?.deletable === false ||
|
||||
tableElement?.title?.deletable === false)
|
||||
) {
|
||||
continue
|
||||
}
|
||||
if (payload === '') {
|
||||
this.draw.spliceElementList(tableElementList, curIndex, 1)
|
||||
tableDiffCount--
|
||||
if (!~firstMatchIndex) {
|
||||
firstMatchIndex = m
|
||||
}
|
||||
continue
|
||||
}
|
||||
if (curGroupId === match.groupId) {
|
||||
this.draw.spliceElementList(tableElementList, curIndex, 1)
|
||||
tableDiffCount--
|
||||
continue
|
||||
}
|
||||
if (!~firstMatchIndex) {
|
||||
firstMatchIndex = m
|
||||
}
|
||||
for (let p = 0; p < payload.length; p++) {
|
||||
const value = payload[p]
|
||||
if (p === 0) {
|
||||
tableElement.value = value
|
||||
} else {
|
||||
this.draw.spliceElementList(tableElementList, curIndex + p, 0, [
|
||||
{
|
||||
...tableElement,
|
||||
value
|
||||
}
|
||||
])
|
||||
tableDiffCount++
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const curIndex = match.index + pageDiffCount
|
||||
const element = elementList[curIndex]
|
||||
// 非设计模式下设置元素不可删除 || 控件结构元素 => 禁止替换
|
||||
if (
|
||||
(!isDesignMode &&
|
||||
(element?.control?.deletable === false ||
|
||||
element?.title?.deletable === false)) ||
|
||||
(element.type === ElementType.CONTROL &&
|
||||
element.controlComponent !== ControlComponent.VALUE)
|
||||
) {
|
||||
continue
|
||||
}
|
||||
if (payload === '') {
|
||||
this.draw.spliceElementList(elementList, curIndex, 1)
|
||||
pageDiffCount--
|
||||
if (!~firstMatchIndex) {
|
||||
firstMatchIndex = m
|
||||
}
|
||||
continue
|
||||
}
|
||||
if (!~firstMatchIndex) {
|
||||
firstMatchIndex = m
|
||||
}
|
||||
if (curGroupId === match.groupId) {
|
||||
this.draw.spliceElementList(elementList, curIndex, 1)
|
||||
pageDiffCount--
|
||||
continue
|
||||
}
|
||||
for (let p = 0; p < payload.length; p++) {
|
||||
const value = payload[p]
|
||||
if (p === 0) {
|
||||
element.value = value
|
||||
} else {
|
||||
this.draw.spliceElementList(elementList, curIndex + p, 0, [
|
||||
{
|
||||
...element,
|
||||
value
|
||||
}
|
||||
])
|
||||
pageDiffCount++
|
||||
}
|
||||
}
|
||||
}
|
||||
curGroupId = match.groupId
|
||||
}
|
||||
if (!~firstMatchIndex) return
|
||||
// 定位-首个被匹配关键词后
|
||||
const firstMatch = matchList[firstMatchIndex]
|
||||
const firstIndex = firstMatch.index + (payload.length - 1)
|
||||
if (firstMatch.type === EditorContext.TABLE) {
|
||||
const { tableIndex, trIndex, tdIndex, index } = firstMatch
|
||||
const element =
|
||||
elementList[tableIndex!].trList![trIndex!].tdList[tdIndex!].value[index]
|
||||
this.position.setPositionContext({
|
||||
isTable: true,
|
||||
index: tableIndex,
|
||||
trIndex,
|
||||
tdIndex,
|
||||
tdId: element.tdId,
|
||||
trId: element.trId,
|
||||
tableId: element.tableId
|
||||
})
|
||||
} else {
|
||||
this.position.setPositionContext({
|
||||
isTable: false
|
||||
})
|
||||
}
|
||||
this.draw.getRange().setRange(firstIndex, firstIndex)
|
||||
// 重新渲染
|
||||
this.draw.render({
|
||||
curIndex: firstIndex
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
import { NBSP, ZERO } from '../../../dataset/constant/Common'
|
||||
import { VerticalAlign } from '../../../dataset/enum/VerticalAlign'
|
||||
import { DeepRequired } from '../../../interface/Common'
|
||||
import { IEditorOption } from '../../../interface/Editor'
|
||||
import { IElement } from '../../../interface/Element'
|
||||
import { IRow, IRowElement } from '../../../interface/Row'
|
||||
import { Draw } from '../Draw'
|
||||
|
||||
interface ICheckboxRenderOption {
|
||||
ctx: CanvasRenderingContext2D
|
||||
x: number
|
||||
y: number
|
||||
row: IRow
|
||||
index: number
|
||||
}
|
||||
|
||||
export class CheckboxParticle {
|
||||
private draw: Draw
|
||||
private options: DeepRequired<IEditorOption>
|
||||
|
||||
constructor(draw: Draw) {
|
||||
this.draw = draw
|
||||
this.options = draw.getOptions()
|
||||
}
|
||||
|
||||
public setSelect(element: IElement) {
|
||||
const { checkbox } = element
|
||||
if (checkbox) {
|
||||
checkbox.value = !checkbox.value
|
||||
} else {
|
||||
element.checkbox = {
|
||||
value: true
|
||||
}
|
||||
}
|
||||
this.draw.render({
|
||||
isCompute: false,
|
||||
isSetCursor: false
|
||||
})
|
||||
}
|
||||
|
||||
public render(payload: ICheckboxRenderOption) {
|
||||
const { ctx, x, index, row } = payload
|
||||
let { y } = payload
|
||||
const {
|
||||
checkbox: { gap, lineWidth, fillStyle, strokeStyle, verticalAlign },
|
||||
scale
|
||||
} = this.options
|
||||
const { metrics, checkbox } = row.elementList[index]
|
||||
// 垂直布局设置
|
||||
if (
|
||||
verticalAlign === VerticalAlign.TOP ||
|
||||
verticalAlign === VerticalAlign.MIDDLE
|
||||
) {
|
||||
let nextIndex = index + 1
|
||||
let nextElement: IRowElement | null = null
|
||||
while (nextIndex < row.elementList.length) {
|
||||
nextElement = row.elementList[nextIndex]
|
||||
if (nextElement.value !== ZERO && nextElement.value !== NBSP) break
|
||||
nextIndex++
|
||||
}
|
||||
// 以后一个非空格元素为基准
|
||||
if (nextElement) {
|
||||
const {
|
||||
metrics: { boundingBoxAscent, boundingBoxDescent }
|
||||
} = nextElement
|
||||
const textHeight = boundingBoxAscent + boundingBoxDescent
|
||||
if (textHeight > metrics.height) {
|
||||
if (verticalAlign === VerticalAlign.TOP) {
|
||||
y -= boundingBoxAscent - metrics.height
|
||||
} else if (verticalAlign === VerticalAlign.MIDDLE) {
|
||||
y -= (textHeight - metrics.height) / 2
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// left top 四舍五入避免1像素问题
|
||||
const left = Math.round(x + gap * scale)
|
||||
const top = Math.round(y - metrics.height + lineWidth)
|
||||
const width = metrics.width - gap * 2 * scale
|
||||
const height = metrics.height
|
||||
ctx.save()
|
||||
ctx.beginPath()
|
||||
ctx.translate(0.5, 0.5)
|
||||
// 绘制勾选状态
|
||||
if (checkbox?.value) {
|
||||
// 边框
|
||||
ctx.lineWidth = lineWidth
|
||||
ctx.strokeStyle = fillStyle
|
||||
ctx.rect(left, top, width, height)
|
||||
ctx.stroke()
|
||||
// 背景色
|
||||
ctx.beginPath()
|
||||
ctx.fillStyle = fillStyle
|
||||
ctx.fillRect(left, top, width, height)
|
||||
// 勾选对号
|
||||
ctx.beginPath()
|
||||
ctx.strokeStyle = strokeStyle
|
||||
ctx.lineWidth = lineWidth * 2 * scale
|
||||
ctx.moveTo(left + 2 * scale, top + height / 2)
|
||||
ctx.lineTo(left + width / 2, top + height - 3 * scale)
|
||||
ctx.lineTo(left + width - 2 * scale, top + 3 * scale)
|
||||
ctx.stroke()
|
||||
} else {
|
||||
ctx.lineWidth = lineWidth
|
||||
ctx.rect(left, top, width, height)
|
||||
ctx.stroke()
|
||||
}
|
||||
ctx.closePath()
|
||||
ctx.restore()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
import { IElement } from '../../..'
|
||||
import { EDITOR_PREFIX } from '../../../dataset/constant/Editor'
|
||||
import { IEditorOption } from '../../../interface/Editor'
|
||||
import { IElementPosition } from '../../../interface/Element'
|
||||
import { IRowElement } from '../../../interface/Row'
|
||||
import { Draw } from '../Draw'
|
||||
|
||||
export class HyperlinkParticle {
|
||||
private draw: Draw
|
||||
private options: Required<IEditorOption>
|
||||
private container: HTMLDivElement
|
||||
private hyperlinkPopupContainer: HTMLDivElement
|
||||
private hyperlinkDom: HTMLAnchorElement
|
||||
|
||||
constructor(draw: Draw) {
|
||||
this.draw = draw
|
||||
this.options = draw.getOptions()
|
||||
this.container = draw.getContainer()
|
||||
const { hyperlinkPopupContainer, hyperlinkDom } =
|
||||
this._createHyperlinkPopupDom()
|
||||
this.hyperlinkDom = hyperlinkDom
|
||||
this.hyperlinkPopupContainer = hyperlinkPopupContainer
|
||||
}
|
||||
|
||||
private _createHyperlinkPopupDom() {
|
||||
const hyperlinkPopupContainer = document.createElement('div')
|
||||
hyperlinkPopupContainer.classList.add(`${EDITOR_PREFIX}-hyperlink-popup`)
|
||||
const hyperlinkDom = document.createElement('a')
|
||||
hyperlinkDom.target = '_blank'
|
||||
hyperlinkDom.rel = 'noopener'
|
||||
hyperlinkPopupContainer.append(hyperlinkDom)
|
||||
this.container.append(hyperlinkPopupContainer)
|
||||
return { hyperlinkPopupContainer, hyperlinkDom }
|
||||
}
|
||||
|
||||
public drawHyperlinkPopup(element: IElement, position: IElementPosition) {
|
||||
const {
|
||||
coordinate: {
|
||||
leftTop: [left, top]
|
||||
},
|
||||
lineHeight
|
||||
} = position
|
||||
const height = this.draw.getHeight()
|
||||
const pageGap = this.draw.getPageGap()
|
||||
const preY = this.draw.getPageNo() * (height + pageGap)
|
||||
// 位置
|
||||
this.hyperlinkPopupContainer.style.display = 'block'
|
||||
this.hyperlinkPopupContainer.style.left = `${left}px`
|
||||
this.hyperlinkPopupContainer.style.top = `${top + preY + lineHeight}px`
|
||||
// 标签
|
||||
const url = element.url || '#'
|
||||
this.hyperlinkDom.href = url
|
||||
this.hyperlinkDom.title = url
|
||||
this.hyperlinkDom.innerText = url
|
||||
}
|
||||
|
||||
public clearHyperlinkPopup() {
|
||||
this.hyperlinkPopupContainer.style.display = 'none'
|
||||
}
|
||||
|
||||
public openHyperlink(element: IElement) {
|
||||
const newTab = window.open(element.url, '_blank')
|
||||
if (newTab) {
|
||||
newTab.opener = null
|
||||
}
|
||||
}
|
||||
|
||||
public render(
|
||||
ctx: CanvasRenderingContext2D,
|
||||
element: IRowElement,
|
||||
x: number,
|
||||
y: number
|
||||
) {
|
||||
ctx.save()
|
||||
ctx.font = element.style
|
||||
if (!element.color) {
|
||||
element.color = this.options.defaultHyperlinkColor
|
||||
}
|
||||
ctx.fillStyle = element.color
|
||||
if (element.underline === undefined) {
|
||||
element.underline = true
|
||||
}
|
||||
ctx.fillText(element.value, x, y)
|
||||
ctx.restore()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,166 @@
|
||||
import { EDITOR_PREFIX } from '../../../dataset/constant/Editor'
|
||||
import { ImageDisplay } from '../../../dataset/enum/Common'
|
||||
import { ElementType } from '../../../dataset/enum/Element'
|
||||
import { IEditorOption } from '../../../interface/Editor'
|
||||
import { IElement } from '../../../interface/Element'
|
||||
import { convertStringToBase64 } from '../../../utils'
|
||||
import { Draw } from '../Draw'
|
||||
|
||||
export class ImageParticle {
|
||||
private draw: Draw
|
||||
protected options: Required<IEditorOption>
|
||||
protected imageCache: Map<string, HTMLImageElement>
|
||||
private container: HTMLDivElement
|
||||
private floatImageContainer: HTMLDivElement | null
|
||||
private floatImage: HTMLImageElement | null
|
||||
|
||||
constructor(draw: Draw) {
|
||||
this.draw = draw
|
||||
this.options = draw.getOptions()
|
||||
this.container = draw.getContainer()
|
||||
this.imageCache = new Map()
|
||||
this.floatImageContainer = null
|
||||
this.floatImage = null
|
||||
}
|
||||
|
||||
public getOriginalMainImageList(): IElement[] {
|
||||
const imageList: IElement[] = []
|
||||
const getImageList = (elementList: IElement[]) => {
|
||||
for (const element of elementList) {
|
||||
if (element.type === ElementType.TABLE) {
|
||||
const trList = element.trList!
|
||||
for (let r = 0; r < trList.length; r++) {
|
||||
const tr = trList[r]
|
||||
for (let d = 0; d < tr.tdList.length; d++) {
|
||||
const td = tr.tdList[d]
|
||||
getImageList(td.value)
|
||||
}
|
||||
}
|
||||
} else if (element.type === ElementType.IMAGE) {
|
||||
imageList.push(element)
|
||||
}
|
||||
}
|
||||
}
|
||||
// 获取正文图片列表
|
||||
getImageList(this.draw.getOriginalMainElementList())
|
||||
return imageList
|
||||
}
|
||||
|
||||
public createFloatImage(element: IElement) {
|
||||
const { scale } = this.options
|
||||
// 复用浮动元素
|
||||
let floatImageContainer = this.floatImageContainer
|
||||
let floatImage = this.floatImage
|
||||
if (!floatImageContainer) {
|
||||
floatImageContainer = document.createElement('div')
|
||||
floatImageContainer.classList.add(`${EDITOR_PREFIX}-float-image`)
|
||||
this.container.append(floatImageContainer)
|
||||
this.floatImageContainer = floatImageContainer
|
||||
}
|
||||
if (!floatImage) {
|
||||
floatImage = document.createElement('img')
|
||||
floatImageContainer.append(floatImage)
|
||||
this.floatImage = floatImage
|
||||
}
|
||||
floatImageContainer.style.display = 'none'
|
||||
floatImage.style.width = `${element.width! * scale}px`
|
||||
floatImage.style.height = `${element.height! * scale}px`
|
||||
// 浮动图片初始信息
|
||||
const height = this.draw.getHeight()
|
||||
const pageGap = this.draw.getPageGap()
|
||||
const preY = this.draw.getPageNo() * (height + pageGap)
|
||||
const imgFloatPosition = element.imgFloatPosition!
|
||||
floatImageContainer.style.left = `${imgFloatPosition.x * scale}px`
|
||||
floatImageContainer.style.top = `${preY + imgFloatPosition.y * scale}px`
|
||||
floatImage.src = element.value
|
||||
}
|
||||
|
||||
public dragFloatImage(movementX: number, movementY: number) {
|
||||
if (!this.floatImageContainer) return
|
||||
this.floatImageContainer.style.display = 'block'
|
||||
// 之前的坐标加移动长度
|
||||
const x = parseFloat(this.floatImageContainer.style.left) + movementX
|
||||
const y = parseFloat(this.floatImageContainer.style.top) + movementY
|
||||
this.floatImageContainer.style.left = `${x}px`
|
||||
this.floatImageContainer.style.top = `${y}px`
|
||||
}
|
||||
|
||||
public destroyFloatImage() {
|
||||
if (this.floatImageContainer) {
|
||||
this.floatImageContainer.style.display = 'none'
|
||||
}
|
||||
}
|
||||
|
||||
protected addImageObserver(promise: Promise<unknown>) {
|
||||
this.draw.getImageObserver().add(promise)
|
||||
}
|
||||
|
||||
protected getFallbackImage(width: number, height: number): HTMLImageElement {
|
||||
const tileSize = 8
|
||||
const x = (width - Math.ceil(width / tileSize) * tileSize) / 2
|
||||
const y = (height - Math.ceil(height / tileSize) * tileSize) / 2
|
||||
const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}" viewBox="0 0 ${width} ${height}">
|
||||
<rect width="${width}" height="${height}" fill="url(#mosaic)" />
|
||||
<defs>
|
||||
<pattern id="mosaic" x="${x}" y="${y}" width="${
|
||||
tileSize * 2
|
||||
}" height="${tileSize * 2}" patternUnits="userSpaceOnUse">
|
||||
<rect width="${tileSize}" height="${tileSize}" fill="#cccccc" />
|
||||
<rect width="${tileSize}" height="${tileSize}" fill="#cccccc" transform="translate(${tileSize}, ${tileSize})" />
|
||||
</pattern>
|
||||
</defs>
|
||||
</svg>`
|
||||
const fallbackImage = new Image()
|
||||
fallbackImage.src = `data:image/svg+xml;base64,${convertStringToBase64(
|
||||
svg
|
||||
)}`
|
||||
return fallbackImage
|
||||
}
|
||||
|
||||
public render(
|
||||
ctx: CanvasRenderingContext2D,
|
||||
element: IElement,
|
||||
x: number,
|
||||
y: number
|
||||
) {
|
||||
const { scale } = this.options
|
||||
const width = element.width! * scale
|
||||
const height = element.height! * scale
|
||||
if (this.imageCache.has(element.value)) {
|
||||
const img = this.imageCache.get(element.value)!
|
||||
ctx.drawImage(img, x, y, width, height)
|
||||
} else {
|
||||
const cacheRenderCount = this.draw.getRenderCount()
|
||||
const imageLoadPromise = new Promise((resolve, reject) => {
|
||||
const img = new Image()
|
||||
img.setAttribute('crossOrigin', 'Anonymous')
|
||||
img.src = element.value
|
||||
img.onload = () => {
|
||||
this.imageCache.set(element.value, img)
|
||||
resolve(element)
|
||||
// 因图片加载异步,图片加载后可能属于上一次渲染方法
|
||||
if (cacheRenderCount !== this.draw.getRenderCount()) return
|
||||
// 衬于文字下方图片需要重新首先绘制
|
||||
if (element.imgDisplay === ImageDisplay.FLOAT_BOTTOM) {
|
||||
this.draw.render({
|
||||
isCompute: false,
|
||||
isSetCursor: false,
|
||||
isSubmitHistory: false
|
||||
})
|
||||
} else {
|
||||
ctx.drawImage(img, x, y, width, height)
|
||||
}
|
||||
}
|
||||
img.onerror = error => {
|
||||
const fallbackImage = this.getFallbackImage(width, height)
|
||||
fallbackImage.onload = () => {
|
||||
ctx.drawImage(fallbackImage, x, y, width, height)
|
||||
this.imageCache.set(element.value, fallbackImage)
|
||||
}
|
||||
reject(error)
|
||||
}
|
||||
})
|
||||
this.addImageObserver(imageLoadPromise)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
import { DeepRequired } from '../../../interface/Common'
|
||||
import { IEditorOption } from '../../../interface/Editor'
|
||||
import { IRowElement } from '../../../interface/Row'
|
||||
import { Draw } from '../Draw'
|
||||
|
||||
export class LineBreakParticle {
|
||||
private options: DeepRequired<IEditorOption>
|
||||
public static readonly WIDTH = 12
|
||||
public static readonly HEIGHT = 9
|
||||
public static readonly GAP = 3 // 距离左边间隙
|
||||
|
||||
constructor(draw: Draw) {
|
||||
this.options = draw.getOptions()
|
||||
}
|
||||
|
||||
public render(
|
||||
ctx: CanvasRenderingContext2D,
|
||||
element: IRowElement,
|
||||
x: number,
|
||||
y: number
|
||||
) {
|
||||
const {
|
||||
scale,
|
||||
lineBreak: { color, lineWidth }
|
||||
} = this.options
|
||||
ctx.save()
|
||||
ctx.beginPath()
|
||||
// 换行符尺寸设置为9像素
|
||||
const top = y - (LineBreakParticle.HEIGHT * scale) / 2
|
||||
const left = x + element.metrics.width
|
||||
// 移动位置并设置缩放
|
||||
ctx.translate(left, top)
|
||||
ctx.scale(scale, scale)
|
||||
// 样式设置
|
||||
ctx.strokeStyle = color
|
||||
ctx.lineWidth = lineWidth
|
||||
ctx.lineCap = 'round'
|
||||
ctx.lineJoin = 'round'
|
||||
ctx.beginPath()
|
||||
// 回车折线
|
||||
ctx.moveTo(8, 0)
|
||||
ctx.lineTo(12, 0)
|
||||
ctx.lineTo(12, 6)
|
||||
ctx.lineTo(3, 6)
|
||||
// 箭头向上
|
||||
ctx.moveTo(3, 6)
|
||||
ctx.lineTo(6, 3)
|
||||
// 箭头向下
|
||||
ctx.moveTo(3, 6)
|
||||
ctx.lineTo(6, 9)
|
||||
ctx.stroke()
|
||||
ctx.closePath()
|
||||
ctx.restore()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,230 @@
|
||||
import { ZERO } from '../../../dataset/constant/Common'
|
||||
import { ulStyleMapping } from '../../../dataset/constant/List'
|
||||
import { ElementType } from '../../../dataset/enum/Element'
|
||||
import { KeyMap } from '../../../dataset/enum/KeyMap'
|
||||
import { ListStyle, ListType, UlStyle } from '../../../dataset/enum/List'
|
||||
import { DeepRequired } from '../../../interface/Common'
|
||||
import { IEditorOption } from '../../../interface/Editor'
|
||||
import { IElement, IElementPosition } from '../../../interface/Element'
|
||||
import { IRow, IRowElement } from '../../../interface/Row'
|
||||
import { getUUID } from '../../../utils'
|
||||
import { RangeManager } from '../../range/RangeManager'
|
||||
import { Draw } from '../Draw'
|
||||
|
||||
export class ListParticle {
|
||||
private draw: Draw
|
||||
private range: RangeManager
|
||||
private options: DeepRequired<IEditorOption>
|
||||
|
||||
// 非递增样式直接返回默认值
|
||||
private readonly UN_COUNT_STYLE_WIDTH = 20
|
||||
private readonly MEASURE_BASE_TEXT = '0'
|
||||
private readonly LIST_GAP = 10
|
||||
|
||||
constructor(draw: Draw) {
|
||||
this.draw = draw
|
||||
this.range = draw.getRange()
|
||||
this.options = draw.getOptions()
|
||||
}
|
||||
|
||||
public setList(listType: ListType | null, listStyle?: ListStyle) {
|
||||
const isReadonly = this.draw.isReadonly()
|
||||
if (isReadonly) return
|
||||
const { startIndex, endIndex } = this.range.getRange()
|
||||
if (!~startIndex && !~endIndex) return
|
||||
// 需要改变的元素列表
|
||||
const changeElementList = this.range.getRangeParagraphElementList()
|
||||
if (!changeElementList || !changeElementList.length) return
|
||||
// 如果包含列表则设置为取消列表
|
||||
const isUnsetList = changeElementList.find(
|
||||
el => el.listType === listType && el.listStyle === listStyle
|
||||
)
|
||||
if (isUnsetList || !listType) {
|
||||
this.unsetList()
|
||||
return
|
||||
}
|
||||
// 设置值
|
||||
const listId = getUUID()
|
||||
changeElementList.forEach(el => {
|
||||
el.listId = listId
|
||||
el.listType = listType
|
||||
el.listStyle = listStyle
|
||||
})
|
||||
// 光标定位
|
||||
const isSetCursor = startIndex === endIndex
|
||||
const curIndex = isSetCursor ? endIndex : startIndex
|
||||
this.draw.render({ curIndex, isSetCursor })
|
||||
}
|
||||
|
||||
public unsetList() {
|
||||
const isReadonly = this.draw.isReadonly()
|
||||
if (isReadonly) return
|
||||
const { startIndex, endIndex } = this.range.getRange()
|
||||
if (!~startIndex && !~endIndex) return
|
||||
// 需要改变的元素列表
|
||||
const changeElementList = this.range
|
||||
.getRangeParagraphElementList()
|
||||
?.filter(el => el.listId)
|
||||
if (!changeElementList || !changeElementList.length) return
|
||||
// 如果列表最后字符不是换行符则需插入换行符
|
||||
const elementList = this.draw.getElementList()
|
||||
const endElement = elementList[endIndex]
|
||||
if (endElement.listId) {
|
||||
let start = endIndex + 1
|
||||
while (start < elementList.length) {
|
||||
const element = elementList[start]
|
||||
if (element.value === ZERO && !element.listWrap) break
|
||||
if (element.listId !== endElement.listId) {
|
||||
this.draw.spliceElementList(elementList, start, 0, [
|
||||
{
|
||||
value: ZERO
|
||||
}
|
||||
])
|
||||
break
|
||||
}
|
||||
start++
|
||||
}
|
||||
}
|
||||
// 取消设置
|
||||
changeElementList.forEach(el => {
|
||||
delete el.listId
|
||||
delete el.listType
|
||||
delete el.listStyle
|
||||
delete el.listWrap
|
||||
})
|
||||
// 光标定位
|
||||
const isSetCursor = startIndex === endIndex
|
||||
const curIndex = isSetCursor ? endIndex : startIndex
|
||||
this.draw.render({ curIndex, isSetCursor })
|
||||
}
|
||||
|
||||
public computeListStyle(
|
||||
ctx: CanvasRenderingContext2D,
|
||||
elementList: IElement[]
|
||||
): Map<string, number> {
|
||||
const listStyleMap = new Map<string, number>()
|
||||
let start = 0
|
||||
let curListId = elementList[start].listId
|
||||
let curElementList: IElement[] = []
|
||||
const elementLength = elementList.length
|
||||
while (start < elementLength) {
|
||||
const curElement = elementList[start]
|
||||
if (curListId && curListId === curElement.listId) {
|
||||
curElementList.push(curElement)
|
||||
} else {
|
||||
if (curElement.listId && curElement.listId !== curListId) {
|
||||
// 列表结束
|
||||
if (curElementList.length) {
|
||||
const width = this.getListStyleWidth(ctx, curElementList)
|
||||
listStyleMap.set(curListId!, width)
|
||||
}
|
||||
curListId = curElement.listId
|
||||
curElementList = curListId ? [curElement] : []
|
||||
}
|
||||
}
|
||||
start++
|
||||
}
|
||||
if (curElementList.length) {
|
||||
const width = this.getListStyleWidth(ctx, curElementList)
|
||||
listStyleMap.set(curListId!, width)
|
||||
}
|
||||
return listStyleMap
|
||||
}
|
||||
|
||||
public getListStyleWidth(
|
||||
ctx: CanvasRenderingContext2D,
|
||||
listElementList: IElement[]
|
||||
): number {
|
||||
const { scale, checkbox } = this.options
|
||||
const startElement = listElementList[0]
|
||||
// 非递增样式返回固定值
|
||||
if (
|
||||
startElement.listStyle &&
|
||||
startElement.listStyle !== ListStyle.DECIMAL
|
||||
) {
|
||||
if (startElement.listStyle === ListStyle.CHECKBOX) {
|
||||
return (checkbox.width + this.LIST_GAP) * scale
|
||||
}
|
||||
return this.UN_COUNT_STYLE_WIDTH * scale
|
||||
}
|
||||
// 计算列表数量
|
||||
const count = listElementList.reduce((pre, cur) => {
|
||||
if (cur.value === ZERO) {
|
||||
pre += 1
|
||||
}
|
||||
return pre
|
||||
}, 0)
|
||||
if (!count) return 0
|
||||
// 以递增样式最大宽度为准
|
||||
const text = `${this.MEASURE_BASE_TEXT.repeat(String(count).length)}${
|
||||
KeyMap.PERIOD
|
||||
}`
|
||||
const textMetrics = ctx.measureText(text)
|
||||
return Math.ceil((textMetrics.width + this.LIST_GAP) * scale)
|
||||
}
|
||||
|
||||
public drawListStyle(
|
||||
ctx: CanvasRenderingContext2D,
|
||||
row: IRow,
|
||||
position: IElementPosition
|
||||
) {
|
||||
const { elementList, offsetX, listIndex, ascent } = row
|
||||
const startElement = elementList[0]
|
||||
if (startElement.value !== ZERO || startElement.listWrap) return
|
||||
// tab width
|
||||
let tabWidth = 0
|
||||
const { defaultTabWidth, scale, defaultFont, defaultSize } = this.options
|
||||
for (let i = 1; i < elementList.length; i++) {
|
||||
const element = elementList[i]
|
||||
if (element?.type !== ElementType.TAB) break
|
||||
tabWidth += defaultTabWidth * scale
|
||||
}
|
||||
// 列表样式渲染
|
||||
const {
|
||||
coordinate: {
|
||||
leftTop: [startX, startY]
|
||||
}
|
||||
} = position
|
||||
const x = startX - offsetX! + tabWidth
|
||||
const y = startY + ascent
|
||||
// 复选框样式特殊处理
|
||||
if (startElement.listStyle === ListStyle.CHECKBOX) {
|
||||
const { width, height, gap } = this.options.checkbox
|
||||
const checkboxRowElement: IRowElement = {
|
||||
...startElement,
|
||||
checkbox: {
|
||||
value: !!startElement.checkbox?.value
|
||||
},
|
||||
metrics: {
|
||||
...startElement.metrics,
|
||||
width: (width + gap * 2) * scale,
|
||||
height: height * scale
|
||||
}
|
||||
}
|
||||
this.draw.getCheckboxParticle().render({
|
||||
ctx,
|
||||
x: x - gap * scale,
|
||||
y,
|
||||
index: 0,
|
||||
row: {
|
||||
...row,
|
||||
elementList: [checkboxRowElement, ...row.elementList]
|
||||
}
|
||||
})
|
||||
} else {
|
||||
let text = ''
|
||||
if (startElement.listType === ListType.UL) {
|
||||
text =
|
||||
ulStyleMapping[<UlStyle>(<unknown>startElement.listStyle)] ||
|
||||
ulStyleMapping[UlStyle.DISC]
|
||||
} else {
|
||||
text = `${listIndex! + 1}${KeyMap.PERIOD}`
|
||||
}
|
||||
if (!text) return
|
||||
ctx.save()
|
||||
ctx.font = `${defaultSize * scale}px ${defaultFont}`
|
||||
ctx.fillText(text, x, y)
|
||||
ctx.restore()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
import { DeepRequired } from '../../../interface/Common'
|
||||
import { IEditorOption } from '../../../interface/Editor'
|
||||
import { IRowElement } from '../../../interface/Row'
|
||||
import { I18n } from '../../i18n/I18n'
|
||||
import { Draw } from '../Draw'
|
||||
|
||||
export class PageBreakParticle {
|
||||
private draw: Draw
|
||||
private options: DeepRequired<IEditorOption>
|
||||
private i18n: I18n
|
||||
|
||||
constructor(draw: Draw) {
|
||||
this.draw = draw
|
||||
this.options = draw.getOptions()
|
||||
this.i18n = draw.getI18n()
|
||||
}
|
||||
|
||||
public render(
|
||||
ctx: CanvasRenderingContext2D,
|
||||
element: IRowElement,
|
||||
x: number,
|
||||
y: number
|
||||
) {
|
||||
const {
|
||||
pageBreak: { font, fontSize, lineDash }
|
||||
} = this.options
|
||||
const displayName = this.i18n.t('pageBreak.displayName')
|
||||
const { scale, defaultRowMargin } = this.options
|
||||
const size = fontSize * scale
|
||||
const elementWidth = element.width! * scale
|
||||
const offsetY =
|
||||
this.draw.getDefaultBasicRowMarginHeight() * defaultRowMargin
|
||||
ctx.save()
|
||||
ctx.font = `${size}px ${font}`
|
||||
const textMeasure = ctx.measureText(displayName)
|
||||
const halfX = (elementWidth - textMeasure.width) / 2
|
||||
// 线段
|
||||
ctx.setLineDash(lineDash)
|
||||
ctx.translate(0, 0.5 + offsetY)
|
||||
ctx.beginPath()
|
||||
ctx.moveTo(x, y)
|
||||
ctx.lineTo(x + halfX, y)
|
||||
ctx.moveTo(x + halfX + textMeasure.width, y)
|
||||
ctx.lineTo(x + elementWidth, y)
|
||||
ctx.stroke()
|
||||
// 文字
|
||||
ctx.fillText(
|
||||
displayName,
|
||||
x + halfX,
|
||||
y + textMeasure.actualBoundingBoxAscent - size / 2
|
||||
)
|
||||
ctx.restore()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
import { NBSP, ZERO } from '../../../dataset/constant/Common'
|
||||
import { VerticalAlign } from '../../../dataset/enum/VerticalAlign'
|
||||
import { DeepRequired } from '../../../interface/Common'
|
||||
import { IEditorOption } from '../../../interface/Editor'
|
||||
import { IElement } from '../../../interface/Element'
|
||||
import { IRow, IRowElement } from '../../../interface/Row'
|
||||
import { Draw } from '../Draw'
|
||||
|
||||
interface IRadioRenderOption {
|
||||
ctx: CanvasRenderingContext2D
|
||||
x: number
|
||||
y: number
|
||||
row: IRow
|
||||
index: number
|
||||
}
|
||||
|
||||
export class RadioParticle {
|
||||
private draw: Draw
|
||||
private options: DeepRequired<IEditorOption>
|
||||
|
||||
constructor(draw: Draw) {
|
||||
this.draw = draw
|
||||
this.options = draw.getOptions()
|
||||
}
|
||||
|
||||
public setSelect(element: IElement) {
|
||||
const { radio } = element
|
||||
if (radio) {
|
||||
radio.value = !radio.value
|
||||
} else {
|
||||
element.radio = {
|
||||
value: true
|
||||
}
|
||||
}
|
||||
this.draw.render({
|
||||
isCompute: false,
|
||||
isSetCursor: false
|
||||
})
|
||||
}
|
||||
|
||||
public render(payload: IRadioRenderOption) {
|
||||
const { ctx, x, index, row } = payload
|
||||
let { y } = payload
|
||||
const {
|
||||
radio: { gap, lineWidth, fillStyle, strokeStyle, verticalAlign },
|
||||
scale
|
||||
} = this.options
|
||||
const { metrics, radio } = row.elementList[index]
|
||||
// 垂直布局设置
|
||||
if (
|
||||
verticalAlign === VerticalAlign.TOP ||
|
||||
verticalAlign === VerticalAlign.MIDDLE
|
||||
) {
|
||||
let nextIndex = index + 1
|
||||
let nextElement: IRowElement | null = null
|
||||
while (nextIndex < row.elementList.length) {
|
||||
nextElement = row.elementList[nextIndex]
|
||||
if (nextElement.value !== ZERO && nextElement.value !== NBSP) break
|
||||
nextIndex++
|
||||
}
|
||||
// 以后一个非空格元素为基准
|
||||
if (nextElement) {
|
||||
const {
|
||||
metrics: { boundingBoxAscent, boundingBoxDescent }
|
||||
} = nextElement
|
||||
const textHeight = boundingBoxAscent + boundingBoxDescent
|
||||
if (textHeight > metrics.height) {
|
||||
if (verticalAlign === VerticalAlign.TOP) {
|
||||
y -= boundingBoxAscent - metrics.height
|
||||
} else if (verticalAlign === VerticalAlign.MIDDLE) {
|
||||
y -= (textHeight - metrics.height) / 2
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// left top 四舍五入避免1像素问题
|
||||
const left = Math.round(x + gap * scale)
|
||||
const top = Math.round(y - metrics.height + lineWidth)
|
||||
const width = metrics.width - gap * 2 * scale
|
||||
const height = metrics.height
|
||||
ctx.save()
|
||||
ctx.beginPath()
|
||||
ctx.translate(0.5, 0.5)
|
||||
// 边框
|
||||
ctx.strokeStyle = radio?.value ? fillStyle : strokeStyle
|
||||
ctx.lineWidth = lineWidth
|
||||
ctx.arc(left + width / 2, top + height / 2, width / 2, 0, Math.PI * 2)
|
||||
ctx.stroke()
|
||||
// 填充选中色
|
||||
if (radio?.value) {
|
||||
ctx.beginPath()
|
||||
ctx.fillStyle = fillStyle
|
||||
ctx.arc(left + width / 2, top + height / 2, width / 3, 0, Math.PI * 2)
|
||||
ctx.fill()
|
||||
}
|
||||
ctx.closePath()
|
||||
ctx.restore()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import { DeepRequired } from '../../../interface/Common'
|
||||
import { IEditorOption } from '../../../interface/Editor'
|
||||
import { IRowElement } from '../../../interface/Row'
|
||||
import { Draw } from '../Draw'
|
||||
|
||||
export class SeparatorParticle {
|
||||
private options: DeepRequired<IEditorOption>
|
||||
|
||||
constructor(draw: Draw) {
|
||||
this.options = draw.getOptions()
|
||||
}
|
||||
|
||||
public render(
|
||||
ctx: CanvasRenderingContext2D,
|
||||
element: IRowElement,
|
||||
x: number,
|
||||
y: number
|
||||
) {
|
||||
ctx.save()
|
||||
const {
|
||||
scale,
|
||||
separator: { lineWidth, strokeStyle }
|
||||
} = this.options
|
||||
ctx.lineWidth = lineWidth * scale
|
||||
ctx.strokeStyle = element.color || strokeStyle
|
||||
if (element.dashArray?.length) {
|
||||
ctx.setLineDash(element.dashArray)
|
||||
}
|
||||
const offsetY = Math.round(y) // 四舍五入避免绘制模糊
|
||||
ctx.translate(0, ctx.lineWidth / 2)
|
||||
ctx.beginPath()
|
||||
ctx.moveTo(x, offsetY)
|
||||
ctx.lineTo(x + element.width! * scale, offsetY)
|
||||
ctx.stroke()
|
||||
ctx.restore()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import { IRowElement } from '../../../interface/Row'
|
||||
|
||||
export class SubscriptParticle {
|
||||
// 向下偏移字高的一半
|
||||
public getOffsetY(element: IRowElement): number {
|
||||
return element.metrics.height / 2
|
||||
}
|
||||
|
||||
public render(
|
||||
ctx: CanvasRenderingContext2D,
|
||||
element: IRowElement,
|
||||
x: number,
|
||||
y: number
|
||||
) {
|
||||
ctx.save()
|
||||
ctx.font = element.style
|
||||
if (element.color) {
|
||||
ctx.fillStyle = element.color
|
||||
}
|
||||
ctx.fillText(element.value, x, y + this.getOffsetY(element))
|
||||
ctx.restore()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import { IRowElement } from '../../../interface/Row'
|
||||
|
||||
export class SuperscriptParticle {
|
||||
// 向上偏移字高的一半
|
||||
public getOffsetY(element: IRowElement): number {
|
||||
return -element.metrics.height / 2
|
||||
}
|
||||
|
||||
public render(
|
||||
ctx: CanvasRenderingContext2D,
|
||||
element: IRowElement,
|
||||
x: number,
|
||||
y: number
|
||||
) {
|
||||
ctx.save()
|
||||
ctx.font = element.style
|
||||
if (element.color) {
|
||||
ctx.fillStyle = element.color
|
||||
}
|
||||
ctx.fillText(element.value, x, y + this.getOffsetY(element))
|
||||
ctx.restore()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,166 @@
|
||||
import { ElementType, IEditorOption, IElement, RenderMode } from '../../..'
|
||||
import {
|
||||
PUNCTUATION_LIST,
|
||||
METRICS_BASIS_TEXT
|
||||
} from '../../../dataset/constant/Common'
|
||||
import { DeepRequired } from '../../../interface/Common'
|
||||
import { IRowElement } from '../../../interface/Row'
|
||||
import { ITextMetrics } from '../../../interface/Text'
|
||||
import { Draw } from '../Draw'
|
||||
|
||||
export interface IMeasureWordResult {
|
||||
width: number
|
||||
endElement: IElement
|
||||
}
|
||||
|
||||
export class TextParticle {
|
||||
private draw: Draw
|
||||
private options: DeepRequired<IEditorOption>
|
||||
|
||||
private ctx: CanvasRenderingContext2D
|
||||
private curX: number
|
||||
private curY: number
|
||||
private text: string
|
||||
private curStyle: string
|
||||
private curColor?: string
|
||||
public cacheMeasureText: Map<string, TextMetrics>
|
||||
|
||||
constructor(draw: Draw) {
|
||||
this.draw = draw
|
||||
this.options = draw.getOptions()
|
||||
this.ctx = draw.getCtx()
|
||||
this.curX = -1
|
||||
this.curY = -1
|
||||
this.text = ''
|
||||
this.curStyle = ''
|
||||
this.cacheMeasureText = new Map()
|
||||
}
|
||||
|
||||
public measureBasisWord(
|
||||
ctx: CanvasRenderingContext2D,
|
||||
font: string
|
||||
): ITextMetrics {
|
||||
ctx.save()
|
||||
ctx.font = font
|
||||
const textMetrics = this.measureText(ctx, {
|
||||
value: METRICS_BASIS_TEXT
|
||||
})
|
||||
ctx.restore()
|
||||
return textMetrics
|
||||
}
|
||||
|
||||
public measureWord(
|
||||
ctx: CanvasRenderingContext2D,
|
||||
elementList: IElement[],
|
||||
curIndex: number
|
||||
): IMeasureWordResult {
|
||||
const LETTER_REG = this.draw.getLetterReg()
|
||||
let width = 0
|
||||
let endElement: IElement = elementList[curIndex]
|
||||
let i = curIndex
|
||||
while (i < elementList.length) {
|
||||
const element = elementList[i]
|
||||
if (
|
||||
(element.type && element.type !== ElementType.TEXT) ||
|
||||
!LETTER_REG.test(element.value)
|
||||
) {
|
||||
endElement = element
|
||||
break
|
||||
}
|
||||
width += this.measureText(ctx, element).width
|
||||
i++
|
||||
}
|
||||
return {
|
||||
width,
|
||||
endElement
|
||||
}
|
||||
}
|
||||
|
||||
public measurePunctuationWidth(
|
||||
ctx: CanvasRenderingContext2D,
|
||||
element: IElement
|
||||
): number {
|
||||
if (!element || !PUNCTUATION_LIST.includes(element.value)) return 0
|
||||
return this.measureText(ctx, element).width
|
||||
}
|
||||
|
||||
public measureText(
|
||||
ctx: CanvasRenderingContext2D,
|
||||
element: IElement
|
||||
): ITextMetrics {
|
||||
// 优先使用自定义字宽设置
|
||||
if (element.width) {
|
||||
const textMetrics = ctx.measureText(element.value)
|
||||
// TextMetrics是类无法解构
|
||||
return {
|
||||
width: element.width,
|
||||
actualBoundingBoxAscent: textMetrics.actualBoundingBoxAscent,
|
||||
actualBoundingBoxDescent: textMetrics.actualBoundingBoxDescent,
|
||||
actualBoundingBoxLeft: textMetrics.actualBoundingBoxLeft,
|
||||
actualBoundingBoxRight: textMetrics.actualBoundingBoxRight,
|
||||
fontBoundingBoxAscent: textMetrics.fontBoundingBoxAscent,
|
||||
fontBoundingBoxDescent: textMetrics.fontBoundingBoxDescent
|
||||
}
|
||||
}
|
||||
const id = `${element.value}${ctx.font}`
|
||||
const cacheTextMetrics = this.cacheMeasureText.get(id)
|
||||
if (cacheTextMetrics) {
|
||||
return cacheTextMetrics
|
||||
}
|
||||
const textMetrics = ctx.measureText(element.value)
|
||||
this.cacheMeasureText.set(id, textMetrics)
|
||||
return textMetrics
|
||||
}
|
||||
|
||||
public complete() {
|
||||
this._render()
|
||||
this.text = ''
|
||||
}
|
||||
|
||||
public record(
|
||||
ctx: CanvasRenderingContext2D,
|
||||
element: IRowElement,
|
||||
x: number,
|
||||
y: number
|
||||
) {
|
||||
this.ctx = ctx
|
||||
// 兼容模式立即绘制
|
||||
if (this.options.renderMode === RenderMode.COMPATIBILITY) {
|
||||
this._setCurXY(x, y)
|
||||
this.text = element.value
|
||||
this.curStyle = element.style
|
||||
this.curColor = element.color
|
||||
this.complete()
|
||||
return
|
||||
}
|
||||
// 主动完成的重设起始点
|
||||
if (!this.text) {
|
||||
this._setCurXY(x, y)
|
||||
}
|
||||
// 样式发生改变
|
||||
if (
|
||||
(this.curStyle && element.style !== this.curStyle) ||
|
||||
element.color !== this.curColor
|
||||
) {
|
||||
this.complete()
|
||||
this._setCurXY(x, y)
|
||||
}
|
||||
this.text += element.value
|
||||
this.curStyle = element.style
|
||||
this.curColor = element.color
|
||||
}
|
||||
|
||||
private _setCurXY(x: number, y: number) {
|
||||
this.curX = x
|
||||
this.curY = y
|
||||
}
|
||||
|
||||
private _render() {
|
||||
if (!this.text || !~this.curX || !~this.curX) return
|
||||
this.ctx.save()
|
||||
this.ctx.font = this.curStyle
|
||||
this.ctx.fillStyle = this.curColor || this.options.defaultColor
|
||||
this.ctx.fillText(this.text, this.curX, this.curY)
|
||||
this.ctx.restore()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
import { EDITOR_PREFIX } from '../../../../dataset/constant/Editor'
|
||||
import { ElementType } from '../../../../dataset/enum/Element'
|
||||
import { IRowElement } from '../../../../interface/Row'
|
||||
import { Draw } from '../../Draw'
|
||||
import { BaseBlock } from './modules/BaseBlock'
|
||||
|
||||
export class BlockParticle {
|
||||
private draw: Draw
|
||||
private container: HTMLDivElement
|
||||
private blockContainer: HTMLDivElement
|
||||
private blockMap: Map<string, BaseBlock>
|
||||
|
||||
constructor(draw: Draw) {
|
||||
this.draw = draw
|
||||
this.container = draw.getContainer()
|
||||
this.blockMap = new Map()
|
||||
this.blockContainer = this._createBlockContainer()
|
||||
this.container.append(this.blockContainer)
|
||||
}
|
||||
|
||||
private _createBlockContainer(): HTMLDivElement {
|
||||
const blockContainer = document.createElement('div')
|
||||
blockContainer.classList.add(`${EDITOR_PREFIX}-block-container`)
|
||||
return blockContainer
|
||||
}
|
||||
|
||||
public getDraw(): Draw {
|
||||
return this.draw
|
||||
}
|
||||
|
||||
public getBlockContainer(): HTMLDivElement {
|
||||
return this.blockContainer
|
||||
}
|
||||
|
||||
public render(
|
||||
ctx: CanvasRenderingContext2D,
|
||||
pageNo: number,
|
||||
element: IRowElement,
|
||||
x: number,
|
||||
y: number
|
||||
) {
|
||||
// 优先使用缓存block
|
||||
const id = element.id!
|
||||
let cacheBlock = this.blockMap.get(id)
|
||||
if (!cacheBlock) {
|
||||
cacheBlock = new BaseBlock(this, element)
|
||||
cacheBlock.render()
|
||||
this.blockMap.set(id, cacheBlock)
|
||||
}
|
||||
// 打印模式截图,其他模式更新位置
|
||||
if (this.draw.isPrintMode()) {
|
||||
cacheBlock.snapshot(ctx, x, y)
|
||||
} else {
|
||||
cacheBlock.setClientRects(pageNo, x, y)
|
||||
}
|
||||
}
|
||||
|
||||
public clear() {
|
||||
if (!this.blockMap.size) return
|
||||
const elementList = this.draw.getOriginalMainElementList()
|
||||
const blockElementIds: string[] = []
|
||||
for (let e = 0; e < elementList.length; e++) {
|
||||
const element = elementList[e]
|
||||
if (element.type === ElementType.BLOCK) {
|
||||
blockElementIds.push(element.id!)
|
||||
}
|
||||
}
|
||||
this.blockMap.forEach(block => {
|
||||
const id = block.getBlockElement().id!
|
||||
if (!blockElementIds.includes(id)) {
|
||||
block.remove()
|
||||
this.blockMap.delete(id)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,280 @@
|
||||
import { EDITOR_PREFIX } from '../../../../../dataset/constant/Editor'
|
||||
import { BlockType } from '../../../../../dataset/enum/Block'
|
||||
import { IEditorOption } from '../../../../../interface/Editor'
|
||||
import { IRowElement } from '../../../../../interface/Row'
|
||||
import { Draw } from '../../../Draw'
|
||||
import { BlockParticle } from '../BlockParticle'
|
||||
import { IFrameBlock } from './IFrameBlock'
|
||||
import { VideoBlock } from './VideoBlock'
|
||||
|
||||
export class BaseBlock {
|
||||
private draw: Draw
|
||||
private options: Required<IEditorOption>
|
||||
private element: IRowElement
|
||||
private block: IFrameBlock | VideoBlock | null
|
||||
private blockContainer: HTMLDivElement
|
||||
private blockItem: HTMLDivElement
|
||||
protected blockCache: Map<string, IFrameBlock | VideoBlock>
|
||||
// 缩放业务
|
||||
private resizerMask: HTMLDivElement
|
||||
private resizerSelection: HTMLDivElement
|
||||
private resizerHandleList: HTMLDivElement[]
|
||||
private width: number
|
||||
private height: number
|
||||
private mousedownX: number
|
||||
private mousedownY: number
|
||||
private curHandleIndex: number
|
||||
private isAllowResize: boolean
|
||||
|
||||
constructor(blockParticle: BlockParticle, element: IRowElement) {
|
||||
this.draw = blockParticle.getDraw()
|
||||
this.options = this.draw.getOptions()
|
||||
this.blockContainer = blockParticle.getBlockContainer()
|
||||
this.element = element
|
||||
this.block = null
|
||||
const { blockItem, resizerMask, resizerSelection, resizerHandleList } =
|
||||
this._createBlockItem()
|
||||
this.blockItem = blockItem
|
||||
this.blockContainer.append(this.blockItem)
|
||||
this.blockCache = new Map()
|
||||
this.resizerMask = resizerMask
|
||||
this.resizerSelection = resizerSelection
|
||||
this.resizerHandleList = resizerHandleList
|
||||
this.width = 0
|
||||
this.height = 0
|
||||
this.mousedownX = 0
|
||||
this.mousedownY = 0
|
||||
this.curHandleIndex = 0
|
||||
this.isAllowResize = false
|
||||
}
|
||||
|
||||
public getBlockElement(): IRowElement {
|
||||
return this.element
|
||||
}
|
||||
|
||||
public getBlockWidth(): number {
|
||||
return this.element.width || this.element.metrics.width
|
||||
}
|
||||
|
||||
private _createBlockItem() {
|
||||
const { scale, resizerColor } = this.options
|
||||
const blockItem = document.createElement('div')
|
||||
blockItem.classList.add(`${EDITOR_PREFIX}-block-item`)
|
||||
// 拖拽边框
|
||||
const resizerSelection = document.createElement('div')
|
||||
resizerSelection.style.display = 'none'
|
||||
resizerSelection.classList.add(`${EDITOR_PREFIX}-resizer-selection`)
|
||||
resizerSelection.style.borderColor = resizerColor
|
||||
resizerSelection.style.borderWidth = `${scale}px`
|
||||
// 拖拽点
|
||||
const resizerHandleList: HTMLDivElement[] = []
|
||||
for (let i = 0; i < 8; i++) {
|
||||
const handleDom = document.createElement('div')
|
||||
handleDom.style.background = resizerColor
|
||||
handleDom.classList.add(`resizer-handle`)
|
||||
handleDom.classList.add(`handle-${i}`)
|
||||
handleDom.setAttribute('data-index', String(i))
|
||||
handleDom.onmousedown = this._mousedown.bind(this)
|
||||
resizerSelection.append(handleDom)
|
||||
resizerHandleList.push(handleDom)
|
||||
}
|
||||
// 拖拽元素遮盖(不遮盖影响mouseup事件执行)
|
||||
const resizerMask = document.createElement('div')
|
||||
resizerMask.classList.add(`${EDITOR_PREFIX}-resizer-mask`)
|
||||
resizerMask.style.display = 'none'
|
||||
blockItem.append(resizerMask)
|
||||
// 光标进入block时显示拖拽边框
|
||||
blockItem.onmouseenter = () => {
|
||||
const isReadonly = this.draw.isReadonly()
|
||||
if (isReadonly) return
|
||||
const { width, height } = this.element.metrics
|
||||
this._updateResizerRect(width, height)
|
||||
resizerSelection.style.display = 'block'
|
||||
}
|
||||
// 光标离开block时隐藏拖拽边框
|
||||
blockItem.onmouseleave = () => {
|
||||
if (this.isAllowResize) return
|
||||
resizerSelection.style.display = 'none'
|
||||
}
|
||||
blockItem.append(resizerSelection)
|
||||
return {
|
||||
blockItem,
|
||||
resizerMask,
|
||||
resizerSelection,
|
||||
resizerHandleList
|
||||
}
|
||||
}
|
||||
|
||||
private _updateResizerRect(width: number, height: number) {
|
||||
const { resizerSize: handleSize, scale } = this.options
|
||||
this.resizerSelection.style.width = `${width}px`
|
||||
this.resizerSelection.style.height = `${height}px`
|
||||
for (let i = 0; i < 8; i++) {
|
||||
const left =
|
||||
i === 0 || i === 6 || i === 7
|
||||
? -handleSize
|
||||
: i === 1 || i === 5
|
||||
? width / 2
|
||||
: width - handleSize
|
||||
const top =
|
||||
i === 0 || i === 1 || i === 2
|
||||
? -handleSize
|
||||
: i === 3 || i === 7
|
||||
? height / 2 - handleSize
|
||||
: height - handleSize
|
||||
this.resizerHandleList[i].style.transform = `scale(${scale})`
|
||||
this.resizerHandleList[i].style.left = `${left}px`
|
||||
this.resizerHandleList[i].style.top = `${top}px`
|
||||
}
|
||||
}
|
||||
|
||||
private _mousedown(evt: MouseEvent) {
|
||||
const canvas = this.draw.getPage()
|
||||
this.mousedownX = evt.x
|
||||
this.mousedownY = evt.y
|
||||
this.isAllowResize = true
|
||||
const target = evt.target as HTMLDivElement
|
||||
this.curHandleIndex = Number(target.dataset.index)
|
||||
// 显示遮盖元素
|
||||
this.resizerMask.style.display = 'block'
|
||||
// 改变光标样式
|
||||
const cursor = window.getComputedStyle(target).cursor
|
||||
document.body.style.cursor = cursor
|
||||
canvas.style.cursor = cursor
|
||||
// 追加mousemove事件
|
||||
const mousemoveFn = this._mousemove.bind(this)
|
||||
document.addEventListener('mousemove', mousemoveFn)
|
||||
// 追加mouseup事件
|
||||
document.addEventListener(
|
||||
'mouseup',
|
||||
() => {
|
||||
this.element.width = Math.min(this.width, this.draw.getInnerWidth())
|
||||
this.element.height = this.height
|
||||
this.isAllowResize = false
|
||||
this.resizerSelection.style.display = 'none'
|
||||
this.resizerMask.style.display = 'none'
|
||||
document.removeEventListener('mousemove', mousemoveFn)
|
||||
document.body.style.cursor = ''
|
||||
canvas.style.cursor = 'text'
|
||||
// 更新文档
|
||||
this.draw.render()
|
||||
},
|
||||
{
|
||||
once: true
|
||||
}
|
||||
)
|
||||
evt.preventDefault()
|
||||
}
|
||||
|
||||
private _mousemove(evt: MouseEvent) {
|
||||
if (!this.isAllowResize) return
|
||||
const { scale } = this.options
|
||||
let dx = 0
|
||||
let dy = 0
|
||||
switch (this.curHandleIndex) {
|
||||
case 0:
|
||||
{
|
||||
const offsetX = this.mousedownX - evt.x
|
||||
const offsetY = this.mousedownY - evt.y
|
||||
dx = Math.cbrt(offsetX ** 3 + offsetY ** 3)
|
||||
dy = (this.element.height! * dx) / this.getBlockWidth()
|
||||
}
|
||||
break
|
||||
case 1:
|
||||
dy = this.mousedownY - evt.y
|
||||
break
|
||||
case 2:
|
||||
{
|
||||
const offsetX = evt.x - this.mousedownX
|
||||
const offsetY = this.mousedownY - evt.y
|
||||
dx = Math.cbrt(offsetX ** 3 + offsetY ** 3)
|
||||
dy = (this.element.height! * dx) / this.getBlockWidth()
|
||||
}
|
||||
break
|
||||
case 4:
|
||||
{
|
||||
const offsetX = evt.x - this.mousedownX
|
||||
const offsetY = evt.y - this.mousedownY
|
||||
dx = Math.cbrt(offsetX ** 3 + offsetY ** 3)
|
||||
dy = (this.element.height! * dx) / this.getBlockWidth()
|
||||
}
|
||||
break
|
||||
case 3:
|
||||
dx = evt.x - this.mousedownX
|
||||
break
|
||||
case 5:
|
||||
dy = evt.y - this.mousedownY
|
||||
break
|
||||
case 6:
|
||||
{
|
||||
const offsetX = this.mousedownX - evt.x
|
||||
const offsetY = evt.y - this.mousedownY
|
||||
dx = Math.cbrt(offsetX ** 3 + offsetY ** 3)
|
||||
dy = (this.element.height! * dx) / this.getBlockWidth()
|
||||
}
|
||||
break
|
||||
case 7:
|
||||
dx = this.mousedownX - evt.x
|
||||
break
|
||||
}
|
||||
// 图片实际宽高(变化大小除掉缩放比例)
|
||||
const dw = this.getBlockWidth() + dx / scale
|
||||
const dh = this.element.height! + dy / scale
|
||||
if (dw <= 0 || dh <= 0) return
|
||||
this.width = dw
|
||||
this.height = dh
|
||||
// 图片显示宽高
|
||||
const elementWidth = dw * scale
|
||||
const elementHeight = dh * scale
|
||||
// 更新预览包围框尺寸
|
||||
this._updateResizerRect(elementWidth, elementHeight)
|
||||
this.blockItem.style.width = `${elementWidth}px`
|
||||
this.blockItem.style.height = `${elementHeight}px`
|
||||
evt.preventDefault()
|
||||
}
|
||||
|
||||
public snapshot(ctx: CanvasRenderingContext2D, x: number, y: number) {
|
||||
const block = this.element.block!
|
||||
if (block.type === BlockType.VIDEO) {
|
||||
this.blockItem.style.display = 'none'
|
||||
if (this.blockCache.has(this.element.id!)) {
|
||||
const videoBlock = <VideoBlock>this.blockCache.get(this.element.id!)
|
||||
videoBlock.snapshot(ctx, x, y)
|
||||
} else {
|
||||
this.block = new VideoBlock(this.element)
|
||||
const promise = this.block.snapshot(ctx, x, y)
|
||||
this.draw.getImageObserver().add(promise)
|
||||
this.blockCache.set(this.element.id!, this.block)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public render() {
|
||||
const block = this.element.block!
|
||||
if (block.type === BlockType.IFRAME) {
|
||||
this.block = new IFrameBlock(this.element)
|
||||
this.block.render(this.blockItem)
|
||||
} else if (block.type === BlockType.VIDEO) {
|
||||
this.block = new VideoBlock(this.element)
|
||||
this.block.render(this.blockItem)
|
||||
}
|
||||
}
|
||||
|
||||
public setClientRects(pageNo: number, x: number, y: number) {
|
||||
const height = this.draw.getHeight()
|
||||
const pageGap = this.draw.getPageGap()
|
||||
const preY = pageNo * (height + pageGap)
|
||||
// 尺寸
|
||||
const { metrics } = this.element
|
||||
this.blockItem.style.display = 'block'
|
||||
this.blockItem.style.width = `${metrics.width}px`
|
||||
this.blockItem.style.height = `${metrics.height}px`
|
||||
// 位置
|
||||
this.blockItem.style.left = `${x}px`
|
||||
this.blockItem.style.top = `${preY + y}px`
|
||||
}
|
||||
|
||||
public remove() {
|
||||
this.blockItem.remove()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import { IRowElement } from '../../../../../interface/Row'
|
||||
|
||||
export class IFrameBlock {
|
||||
public static readonly sandbox = ['allow-scripts', 'allow-same-origin']
|
||||
private element: IRowElement
|
||||
|
||||
constructor(element: IRowElement) {
|
||||
this.element = element
|
||||
}
|
||||
|
||||
private _defineIframeProperties(iframeWindow: Window) {
|
||||
Object.defineProperties(iframeWindow, {
|
||||
// 禁止获取parent避免安全漏洞
|
||||
parent: {
|
||||
get: () => null
|
||||
},
|
||||
// 用于区分上下文
|
||||
__POWERED_BY_CANVAS_EDITOR__: {
|
||||
get: () => true
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
public render(blockItemContainer: HTMLDivElement) {
|
||||
const block = this.element.block!
|
||||
const iframe = document.createElement('iframe')
|
||||
iframe.setAttribute('data-id', this.element.id!)
|
||||
iframe.sandbox.add(...IFrameBlock.sandbox)
|
||||
iframe.style.border = 'none'
|
||||
iframe.style.width = '100%'
|
||||
iframe.style.height = '100%'
|
||||
if (block.iframeBlock?.src) {
|
||||
iframe.src = block.iframeBlock.src
|
||||
} else if (block.iframeBlock?.srcdoc) {
|
||||
iframe.srcdoc = block.iframeBlock.srcdoc
|
||||
}
|
||||
blockItemContainer.append(iframe)
|
||||
// 重新定义iframe上属性
|
||||
this._defineIframeProperties(iframe.contentWindow!)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
import { IRowElement } from '../../../../../interface/Row'
|
||||
|
||||
export class VideoBlock {
|
||||
private element: IRowElement
|
||||
protected videoCache: Map<string, HTMLVideoElement>
|
||||
|
||||
constructor(element: IRowElement) {
|
||||
this.element = element
|
||||
this.videoCache = new Map()
|
||||
}
|
||||
|
||||
public snapshot(ctx: CanvasRenderingContext2D, x: number, y: number) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const src = this.element.block?.videoBlock?.src || ''
|
||||
if (this.videoCache.has(src)) {
|
||||
const video = this.videoCache.get(src)!
|
||||
ctx.drawImage(
|
||||
video,
|
||||
x,
|
||||
y,
|
||||
this.element.metrics.width,
|
||||
this.element.metrics.height
|
||||
)
|
||||
resolve(this.element)
|
||||
} else {
|
||||
const video = document.createElement('video')
|
||||
video.src = src
|
||||
video.muted = true
|
||||
video.crossOrigin = 'anonymous'
|
||||
video.onloadeddata = () => {
|
||||
ctx.drawImage(
|
||||
video,
|
||||
x,
|
||||
y,
|
||||
this.element.metrics.width,
|
||||
this.element.metrics.height
|
||||
)
|
||||
this.videoCache.set(src, video)
|
||||
resolve(this.element)
|
||||
}
|
||||
video.onerror = error => {
|
||||
reject(error)
|
||||
}
|
||||
video.play().then(() => {
|
||||
video.pause()
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
public render(blockItemContainer: HTMLDivElement) {
|
||||
const block = this.element.block!
|
||||
const video = document.createElement('video')
|
||||
video.style.width = '100%'
|
||||
video.style.height = '100%'
|
||||
video.style.objectFit = 'contain'
|
||||
video.src = block.videoBlock?.src || ''
|
||||
video.controls = true
|
||||
blockItemContainer.append(video)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
import { ElementType } from '../../../../dataset/enum/Element'
|
||||
import { DeepRequired } from '../../../../interface/Common'
|
||||
import { IEditorOption } from '../../../../interface/Editor'
|
||||
import { IElement, IElementPosition } from '../../../../interface/Element'
|
||||
import { formatElementContext } from '../../../../utils/element'
|
||||
import { RangeManager } from '../../../range/RangeManager'
|
||||
import { Draw } from '../../Draw'
|
||||
import { DatePicker } from './DatePicker'
|
||||
|
||||
export class DateParticle {
|
||||
private draw: Draw
|
||||
private range: RangeManager
|
||||
private datePicker: DatePicker
|
||||
private options: DeepRequired<IEditorOption>
|
||||
|
||||
constructor(draw: Draw) {
|
||||
this.draw = draw
|
||||
this.options = draw.getOptions()
|
||||
this.range = draw.getRange()
|
||||
this.datePicker = new DatePicker(draw, {
|
||||
onSubmit: this._setValue.bind(this)
|
||||
})
|
||||
}
|
||||
|
||||
private _setValue(date: string) {
|
||||
if (!date) return
|
||||
const range = this.getDateElementRange()
|
||||
if (!range) return
|
||||
const [leftIndex, rightIndex] = range
|
||||
const elementList = this.draw.getElementList()
|
||||
const startElement = elementList[leftIndex + 1]
|
||||
// 删除旧时间
|
||||
this.draw.spliceElementList(
|
||||
elementList,
|
||||
leftIndex + 1,
|
||||
rightIndex - leftIndex
|
||||
)
|
||||
this.range.setRange(leftIndex, leftIndex)
|
||||
// 插入新时间
|
||||
const dateElement: IElement = {
|
||||
type: ElementType.DATE,
|
||||
value: '',
|
||||
dateFormat: startElement.dateFormat,
|
||||
valueList: [
|
||||
{
|
||||
value: date
|
||||
}
|
||||
]
|
||||
}
|
||||
formatElementContext(elementList, [dateElement], leftIndex, {
|
||||
editorOptions: this.options
|
||||
})
|
||||
this.draw.insertElementList([dateElement])
|
||||
}
|
||||
|
||||
public getDateElementRange(): [number, number] | null {
|
||||
let leftIndex = -1
|
||||
let rightIndex = -1
|
||||
const { startIndex, endIndex } = this.range.getRange()
|
||||
if (!~startIndex && !~endIndex) return null
|
||||
const elementList = this.draw.getElementList()
|
||||
const startElement = elementList[startIndex]
|
||||
if (startElement.type !== ElementType.DATE) return null
|
||||
// 向左查找
|
||||
let preIndex = startIndex
|
||||
while (preIndex >= 0) {
|
||||
const preElement = elementList[preIndex]
|
||||
if (preElement.dateId !== startElement.dateId) {
|
||||
leftIndex = preIndex
|
||||
break
|
||||
}
|
||||
preIndex--
|
||||
}
|
||||
// 向右查找
|
||||
let nextIndex = startIndex + 1
|
||||
while (nextIndex < elementList.length) {
|
||||
const nextElement = elementList[nextIndex]
|
||||
if (nextElement.dateId !== startElement.dateId) {
|
||||
rightIndex = nextIndex - 1
|
||||
break
|
||||
}
|
||||
nextIndex++
|
||||
}
|
||||
// 控件在最后
|
||||
if (nextIndex === elementList.length) {
|
||||
rightIndex = nextIndex - 1
|
||||
}
|
||||
if (!~leftIndex || !~rightIndex) return null
|
||||
return [leftIndex, rightIndex]
|
||||
}
|
||||
|
||||
public clearDatePicker() {
|
||||
this.datePicker.dispose()
|
||||
}
|
||||
|
||||
public renderDatePicker(element: IElement, position: IElementPosition) {
|
||||
const elementList = this.draw.getElementList()
|
||||
const range = this.getDateElementRange()
|
||||
const value = range
|
||||
? elementList
|
||||
.slice(range[0] + 1, range[1] + 1)
|
||||
.map(el => el.value)
|
||||
.join('')
|
||||
: ''
|
||||
this.datePicker.render({
|
||||
value,
|
||||
position,
|
||||
dateFormat: element.dateFormat
|
||||
})
|
||||
}
|
||||
}
|
||||