Patch #43259 » 0001-wip.patch
| app/assets/images/icons.svg | ||
|---|---|---|
| 82 | 82 |
<symbol viewBox="0 0 24 24" stroke-linecap="round" stroke-linejoin="round" id="icon--attachment"> |
| 83 | 83 |
<path d="M15 7l-6.5 6.5a1.5 1.5 0 0 0 3 3l6.5 -6.5a3 3 0 0 0 -6 -6l-6.5 6.5a4.5 4.5 0 0 0 9 9l6.5 -6.5"/> |
| 84 | 84 |
</symbol> |
| 85 |
<symbol viewBox="0 0 24 24" stroke-linecap="round" stroke-linejoin="round" id="icon--bold"> |
|
| 86 |
<path d="M7 5h6a3.5 3.5 0 0 1 0 7h-6z"/> |
|
| 87 |
<path d="M13 12h1a3.5 3.5 0 0 1 0 7h-7v-7"/> |
|
| 88 |
</symbol> |
|
| 85 | 89 |
<symbol viewBox="0 0 24 24" stroke-linecap="round" stroke-linejoin="round" id="icon--bookmark-add"> |
| 86 | 90 |
<path d="M12 17l-6 4v-14a4 4 0 0 1 4 -4h4a4 4 0 0 1 4 4v5"/> |
| 87 | 91 |
<path d="M16 19h6"/> |
| ... | ... | |
| 235 | 239 |
<path d="M5 5a2 2 0 1 0 4 0a2 2 0 0 0 -4 0"/> |
| 236 | 240 |
<path d="M3 13v-1a2 2 0 0 1 2 -2h2"/> |
| 237 | 241 |
</symbol> |
| 242 |
<symbol viewBox="0 0 24 24" stroke-linecap="round" stroke-linejoin="round" id="icon--h1"> |
|
| 243 |
<path d="M19 18v-8l-2 2"/> |
|
| 244 |
<path d="M4 6v12"/> |
|
| 245 |
<path d="M12 6v12"/> |
|
| 246 |
<path d="M11 18h2"/> |
|
| 247 |
<path d="M3 18h2"/> |
|
| 248 |
<path d="M4 12h8"/> |
|
| 249 |
<path d="M3 6h2"/> |
|
| 250 |
<path d="M11 6h2"/> |
|
| 251 |
</symbol> |
|
| 252 |
<symbol viewBox="0 0 24 24" stroke-linecap="round" stroke-linejoin="round" id="icon--h2"> |
|
| 253 |
<path d="M17 12a2 2 0 1 1 4 0c0 .591 -.417 1.318 -.816 1.858l-3.184 4.143l4 0"/> |
|
| 254 |
<path d="M4 6v12"/> |
|
| 255 |
<path d="M12 6v12"/> |
|
| 256 |
<path d="M11 18h2"/> |
|
| 257 |
<path d="M3 18h2"/> |
|
| 258 |
<path d="M4 12h8"/> |
|
| 259 |
<path d="M3 6h2"/> |
|
| 260 |
<path d="M11 6h2"/> |
|
| 261 |
</symbol> |
|
| 262 |
<symbol viewBox="0 0 24 24" stroke-linecap="round" stroke-linejoin="round" id="icon--h3"> |
|
| 263 |
<path d="M19 14a2 2 0 1 0 -2 -2"/> |
|
| 264 |
<path d="M17 16a2 2 0 1 0 2 -2"/> |
|
| 265 |
<path d="M4 6v12"/> |
|
| 266 |
<path d="M12 6v12"/> |
|
| 267 |
<path d="M11 18h2"/> |
|
| 268 |
<path d="M3 18h2"/> |
|
| 269 |
<path d="M4 12h8"/> |
|
| 270 |
<path d="M3 6h2"/> |
|
| 271 |
<path d="M11 6h2"/> |
|
| 272 |
</symbol> |
|
| 238 | 273 |
<symbol viewBox="0 0 24 24" stroke-linecap="round" stroke-linejoin="round" id="icon--help"> |
| 239 | 274 |
<path d="M3 12a9 9 0 1 0 18 0a9 9 0 0 0 -18 0"/> |
| 240 | 275 |
<path d="M12 9h.01"/> |
| ... | ... | |
| 258 | 293 |
<path d="M16 19h6"/> |
| 259 | 294 |
<path d="M19 16l3 3l-3 3"/> |
| 260 | 295 |
</symbol> |
| 296 |
<symbol viewBox="0 0 24 24" stroke-linecap="round" stroke-linejoin="round" id="icon--indent-decrease"> |
|
| 297 |
<path d="M20 6l-7 0"/> |
|
| 298 |
<path d="M20 12l-9 0"/> |
|
| 299 |
<path d="M20 18l-7 0"/> |
|
| 300 |
<path d="M8 8l-4 4l4 4"/> |
|
| 301 |
</symbol> |
|
| 302 |
<symbol viewBox="0 0 24 24" stroke-linecap="round" stroke-linejoin="round" id="icon--indent-increase"> |
|
| 303 |
<path d="M20 6l-11 0"/> |
|
| 304 |
<path d="M20 12l-7 0"/> |
|
| 305 |
<path d="M20 18l-11 0"/> |
|
| 306 |
<path d="M4 8l4 4l-4 4"/> |
|
| 307 |
</symbol> |
|
| 308 |
<symbol viewBox="0 0 24 24" stroke-linecap="round" stroke-linejoin="round" id="icon--inline-code"> |
|
| 309 |
<path d="M18 9a5 5 0 0 0 -5 -5h-2a5 5 0 0 0 -5 5v6a5 5 0 0 0 5 5h2a5 5 0 0 0 5 -5"/> |
|
| 310 |
</symbol> |
|
| 261 | 311 |
<symbol viewBox="0 0 24 24" stroke-linecap="round" stroke-linejoin="round" id="icon--issue"> |
| 262 | 312 |
<path d="M13 20l7 -7"/> |
| 263 | 313 |
<path d="M13 20v-6a1 1 0 0 1 1 -1h6v-7a2 2 0 0 0 -2 -2h-12a2 2 0 0 0 -2 2v12a2 2 0 0 0 2 2h7"/> |
| ... | ... | |
| 278 | 328 |
<path d="M16 19h6"/> |
| 279 | 329 |
<path d="M19 16v6"/> |
| 280 | 330 |
</symbol> |
| 331 |
<symbol viewBox="0 0 24 24" stroke-linecap="round" stroke-linejoin="round" id="icon--italic"> |
|
| 332 |
<path d="M11 5l6 0"/> |
|
| 333 |
<path d="M7 19l6 0"/> |
|
| 334 |
<path d="M14 5l-4 14"/> |
|
| 335 |
</symbol> |
|
| 281 | 336 |
<symbol viewBox="0 0 24 24" stroke-linecap="round" stroke-linejoin="round" id="icon--key"> |
| 282 | 337 |
<path d="M16.555 3.843l3.602 3.602a2.877 2.877 0 0 1 0 4.069l-2.643 2.643a2.877 2.877 0 0 1 -4.069 0l-.301 -.301l-6.558 6.558a2 2 0 0 1 -1.239 .578l-.175 .008h-1.172a1 1 0 0 1 -.993 -.883l-.007 -.117v-1.172a2 2 0 0 1 .467 -1.284l.119 -.13l.414 -.414h2v-2h2v-2l2.144 -2.144l-.301 -.301a2.877 2.877 0 0 1 0 -4.069l2.643 -2.643a2.877 2.877 0 0 1 4.069 0z"/> |
| 283 | 338 |
<path d="M15 9h.01"/> |
| ... | ... | |
| 301 | 356 |
<path d="M5 12l0 .01"/> |
| 302 | 357 |
<path d="M5 18l0 .01"/> |
| 303 | 358 |
</symbol> |
| 359 |
<symbol viewBox="0 0 24 24" stroke-linecap="round" stroke-linejoin="round" id="icon--list-check"> |
|
| 360 |
<path d="M3.5 5.5l1.5 1.5l2.5 -2.5"/> |
|
| 361 |
<path d="M3.5 11.5l1.5 1.5l2.5 -2.5"/> |
|
| 362 |
<path d="M3.5 17.5l1.5 1.5l2.5 -2.5"/> |
|
| 363 |
<path d="M11 6l9 0"/> |
|
| 364 |
<path d="M11 12l9 0"/> |
|
| 365 |
<path d="M11 18l9 0"/> |
|
| 366 |
</symbol> |
|
| 367 |
<symbol viewBox="0 0 24 24" stroke-linecap="round" stroke-linejoin="round" id="icon--list-numbers"> |
|
| 368 |
<path d="M11 6h9"/> |
|
| 369 |
<path d="M11 12h9"/> |
|
| 370 |
<path d="M12 18h8"/> |
|
| 371 |
<path d="M4 16a2 2 0 1 1 4 0c0 .591 -.5 1 -1 1.5l-3 2.5h4"/> |
|
| 372 |
<path d="M6 10v-6l-2 2"/> |
|
| 373 |
</symbol> |
|
| 304 | 374 |
<symbol viewBox="0 0 24 24" stroke-linecap="round" stroke-linejoin="round" id="icon--loader"> |
| 305 | 375 |
<path d="M12 3a9 9 0 1 0 9 9"/> |
| 306 | 376 |
</symbol> |
| ... | ... | |
| 434 | 504 |
<path d="M9 5a1 1 0 0 1 1 -1h4a1 1 0 0 1 1 1v14a1 1 0 0 1 -1 1h-4a1 1 0 0 1 -1 -1z"/> |
| 435 | 505 |
<path d="M4 20h14"/> |
| 436 | 506 |
</symbol> |
| 507 |
<symbol viewBox="0 0 24 24" stroke-linecap="round" stroke-linejoin="round" id="icon--strikethrough"> |
|
| 508 |
<path d="M5 12l14 0"/> |
|
| 509 |
<path d="M16 6.5a4 2 0 0 0 -4 -1.5h-1a3.5 3.5 0 0 0 0 7h2a3.5 3.5 0 0 1 0 7h-1.5a4 2 0 0 1 -4 -1.5"/> |
|
| 510 |
</symbol> |
|
| 437 | 511 |
<symbol viewBox="0 0 24 24" stroke-linecap="round" stroke-linejoin="round" id="icon--summary"> |
| 438 | 512 |
<path d="M13 3l0 7l6 0l-8 11l0 -7l-6 0l8 -11"/> |
| 439 | 513 |
</symbol> |
| 514 |
<symbol viewBox="0 0 24 24" stroke-linecap="round" stroke-linejoin="round" id="icon--table"> |
|
| 515 |
<path d="M12.5 21h-7.5a2 2 0 0 1 -2 -2v-14a2 2 0 0 1 2 -2h14a2 2 0 0 1 2 2v7.5"/> |
|
| 516 |
<path d="M3 10h18"/> |
|
| 517 |
<path d="M10 3v18"/> |
|
| 518 |
<path d="M16 19h6"/> |
|
| 519 |
<path d="M19 16v6"/> |
|
| 520 |
</symbol> |
|
| 440 | 521 |
<symbol viewBox="0 0 24 24" stroke-linecap="round" stroke-linejoin="round" id="icon--table-multiple"> |
| 441 | 522 |
<path d="M20 11a8.1 8.1 0 0 0 -15.5 -2m-.5 -4v4h4"/> |
| 442 | 523 |
<path d="M4 13a8.1 8.1 0 0 0 15.5 2m.5 4v-4h-4"/> |
| ... | ... | |
| 534 | 615 |
<path d="M15 12h-6"/> |
| 535 | 616 |
<path d="M12 9v6"/> |
| 536 | 617 |
</symbol> |
| 618 |
<symbol viewBox="0 0 24 24" stroke-linecap="round" stroke-linejoin="round" id="icon--underline"> |
|
| 619 |
<path d="M7 5v5a5 5 0 0 0 10 0v-5"/> |
|
| 620 |
<path d="M5 19h14"/> |
|
| 621 |
</symbol> |
|
| 537 | 622 |
<symbol viewBox="0 0 24 24" stroke-linecap="round" stroke-linejoin="round" id="icon--unlock"> |
| 538 | 623 |
<path d="M5 11m0 2a2 2 0 0 1 2 -2h10a2 2 0 0 1 2 2v6a2 2 0 0 1 -2 2h-10a2 2 0 0 1 -2 -2z"/> |
| 539 | 624 |
<path d="M12 16m-1 0a1 1 0 1 0 2 0a1 1 0 1 0 -2 0"/> |
| ... | ... | |
| 562 | 647 |
<path d="M15.066 20.502a4 4 0 1 0 1.934 -7.502c-.706 0 -1.424 .179 -2 .5l-3 -5.5"/> |
| 563 | 648 |
<path d="M16 8a4 4 0 1 0 -8 0c0 1.506 .77 2.818 2 3.5l-3 5.5"/> |
| 564 | 649 |
</symbol> |
| 650 |
<symbol viewBox="0 0 24 24" stroke-linecap="round" stroke-linejoin="round" id="icon--wiki-link"> |
|
| 651 |
<path d="M15.536 17.586a2.123 2.123 0 0 0 -2.929 0a1.951 1.951 0 0 0 0 2.828c.809 .781 2.12 .781 2.929 0c.809 -.781 -.805 .778 0 0l1.46 -1.41l1.46 -1.419"/> |
|
| 652 |
<path d="M15.54 17.582l1.46 1.42l1.46 1.41c.809 .78 -.805 -.779 0 0s2.12 .781 2.929 0a1.951 1.951 0 0 0 0 -2.828a2.123 2.123 0 0 0 -2.929 0"/> |
|
| 653 |
<path d="M14 3v4a1 1 0 0 0 1 1h4"/> |
|
| 654 |
<path d="M9.5 21h-2.5a2 2 0 0 1 -2 -2v-14a2 2 0 0 1 2 -2h7l5 5v6"/> |
|
| 655 |
</symbol> |
|
| 565 | 656 |
<symbol viewBox="0 0 24 24" stroke-linecap="round" stroke-linejoin="round" id="icon--wiki-page"> |
| 566 | 657 |
<path d="M6 4h11a2 2 0 0 1 2 2v12a2 2 0 0 1 -2 2h-11a1 1 0 0 1 -1 -1v-14a1 1 0 0 1 1 -1m3 0v18"/> |
| 567 | 658 |
<path d="M13 8l2 0"/> |
| app/assets/stylesheets/jstoolbar.css | ||
|---|---|---|
| 144 | 144 |
.jstb_unbq {
|
| 145 | 145 |
background-image: url(/jstoolbar/indent-decrease.svg); |
| 146 | 146 |
} |
| 147 |
.jstb_pre::before {
|
|
| 147 |
.jstBlock .jstb_pre::before {
|
|
| 148 | 148 |
content: "pre"; |
| 149 | 149 |
font-size: 10px; |
| 150 | 150 |
color: var(--oc-gray-9); |
| app/assets/stylesheets/wiki_editor.css | ||
|---|---|---|
| 1 |
.wiki-editor {
|
|
| 2 |
border: 1px solid #d7d7d7; |
|
| 3 |
border-radius: 3px; |
|
| 4 |
margin-bottom: 10px; |
|
| 5 |
background: #fff; |
|
| 6 |
} |
|
| 7 | ||
| 8 |
.wiki-editor:has(.wiki-editor-edit:not(.hidden)):focus-within {
|
|
| 9 |
border-color: var(--oc-blue-5); |
|
| 10 |
} |
|
| 11 | ||
| 12 |
.wiki-editor-header {
|
|
| 13 |
display: flex; |
|
| 14 |
flex-wrap: wrap; |
|
| 15 |
align-items: flex-end; |
|
| 16 |
/* Align tabs to bottom */ |
|
| 17 |
justify-content: flex-start; |
|
| 18 |
padding-left: 0.5em; |
|
| 19 |
/* Remove bottom padding */ |
|
| 20 |
background-color: #f6f6f6; |
|
| 21 |
border-bottom: 1px solid #d7d7d7; |
|
| 22 |
gap: 10px; |
|
| 23 |
} |
|
| 24 | ||
| 25 |
.wiki-editor-tabs {
|
|
| 26 |
display: flex; |
|
| 27 |
align-items: stretch; |
|
| 28 |
gap: 4px; |
|
| 29 |
/* No gap between tabs */ |
|
| 30 |
} |
|
| 31 | ||
| 32 |
.wiki-editor-tab {
|
|
| 33 |
border: 1px solid transparent; |
|
| 34 |
height: auto; |
|
| 35 |
border-bottom: none; |
|
| 36 |
/* No bottom border to merge with header border */ |
|
| 37 |
background: none; |
|
| 38 |
padding: 6px 12px; |
|
| 39 |
border-radius: 3px 3px 0 0; |
|
| 40 |
cursor: pointer; |
|
| 41 |
color: #555; |
|
| 42 |
font-size: 0.9em; |
|
| 43 |
font-weight: 500; |
|
| 44 |
margin-bottom: -1px; |
|
| 45 |
margin-top: -1px; |
|
| 46 |
} |
|
| 47 | ||
| 48 |
.wiki-editor-tab:hover {
|
|
| 49 |
background-color: rgba(0, 0, 0, 0.05); |
|
| 50 |
color: #333; |
|
| 51 |
} |
|
| 52 | ||
| 53 |
.wiki-editor-tab.selected {
|
|
| 54 |
background-color: #fff; |
|
| 55 |
border-color: #d7d7d7; |
|
| 56 |
color: #222; |
|
| 57 |
font-weight: 600; |
|
| 58 |
z-index: 2; |
|
| 59 |
/* Bring above header border */ |
|
| 60 |
} |
|
| 61 | ||
| 62 |
.wiki-editor-buttons {
|
|
| 63 |
display: flex; |
|
| 64 |
align-items: center; |
|
| 65 |
flex-wrap: wrap; |
|
| 66 |
gap: 2px; |
|
| 67 |
padding-bottom: 0; |
|
| 68 |
} |
|
| 69 | ||
| 70 |
.wiki-editor-buttons.hidden {
|
|
| 71 |
display: none; |
|
| 72 |
} |
|
| 73 | ||
| 74 |
.wiki-editor-buttons button {
|
|
| 75 |
background: none; |
|
| 76 |
border: 1px solid transparent; |
|
| 77 |
border-radius: 3px; |
|
| 78 |
padding: 2px; |
|
| 79 |
cursor: pointer; |
|
| 80 |
min-width: 24px; |
|
| 81 |
height: 24px; |
|
| 82 |
display: flex; |
|
| 83 |
align-items: center; |
|
| 84 |
justify-content: center; |
|
| 85 |
color: #555; |
|
| 86 |
} |
|
| 87 | ||
| 88 |
.wiki-editor-buttons button:hover {
|
|
| 89 |
background-color: #e6e6e6; |
|
| 90 |
border-color: #d0d0d0; |
|
| 91 |
color: #111; |
|
| 92 |
} |
|
| 93 | ||
| 94 |
.wiki-editor-buttons button svg {
|
|
| 95 |
stroke-width: 2; |
|
| 96 |
} |
|
| 97 | ||
| 98 |
/* Icons are usually background images or fonts in Redmine. |
|
| 99 |
Assuming render_wikitoolbar_buttons outputs buttons with classes that have background icons. |
|
| 100 |
Existing jstoolbar.css usually handles .jstb_strong etc. |
|
| 101 |
We might need to ensure those classes still work or adapt them. |
|
| 102 |
For now, I'm setting structural CSS. |
|
| 103 |
*/ |
|
| 104 | ||
| 105 |
.wiki-editor-pane {
|
|
| 106 |
padding: 0; |
|
| 107 |
} |
|
| 108 | ||
| 109 |
.wiki-editor-pane textarea {
|
|
| 110 |
width: 100%; |
|
| 111 |
border: none; |
|
| 112 |
padding: 10px; |
|
| 113 |
box-sizing: border-box; |
|
| 114 |
min-height: 200px; |
|
| 115 |
resize: vertical; |
|
| 116 |
display: block; |
|
| 117 |
outline: none; |
|
| 118 |
/* Let the container border handle focus focus indicator potentially, or keep default */ |
|
| 119 |
} |
|
| 120 | ||
| 121 |
/* Adjustments for Redmine default styles overriding */ |
|
| 122 |
/* .wiki-editor-pane textarea:focus {
|
|
| 123 |
box-shadow: none; |
|
| 124 |
outline: none; |
|
| 125 |
} */ |
|
| 126 | ||
| 127 |
.wiki-editor-preview {
|
|
| 128 |
padding: 10px; |
|
| 129 |
min-height: 200px; |
|
| 130 |
background-color: #fff; |
|
| 131 |
} |
|
| 132 | ||
| 133 |
.wiki-editor-preview>p:first-child {
|
|
| 134 |
padding-top: 0 !important; |
|
| 135 |
margin-top: 0 !important; |
|
| 136 |
} |
|
| 137 | ||
| 138 |
.wiki-editor-preview>p:last-child {
|
|
| 139 |
padding-bottom: 0 !important; |
|
| 140 |
margin-bottom: 0 !important; |
|
| 141 |
} |
|
| 142 | ||
| 143 |
/* Container alignment fix for tabular forms */ |
|
| 144 |
.tabular .wiki-edit-container {
|
|
| 145 |
margin: 0; |
|
| 146 |
padding: 3px 0 3px 0; |
|
| 147 |
padding-left: 180px; |
|
| 148 |
min-height: 2em; |
|
| 149 |
clear: left; |
|
| 150 |
overflow: hidden; |
|
| 151 |
} |
|
| 152 | ||
| 153 |
/* Table Generator Picker */ |
|
| 154 |
.table-generator {
|
|
| 155 |
position: absolute; |
|
| 156 |
border-collapse: collapse; |
|
| 157 |
z-index: 1000; |
|
| 158 |
background-color: #fff; |
|
| 159 |
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); |
|
| 160 |
border: 1px solid #d7d7d7; |
|
| 161 |
} |
|
| 162 | ||
| 163 |
.table-generator td {
|
|
| 164 |
border: 1px solid #eee; |
|
| 165 |
padding: 10px; |
|
| 166 |
cursor: pointer; |
|
| 167 |
width: 10px; |
|
| 168 |
height: 10px; |
|
| 169 |
} |
|
| 170 | ||
| 171 |
.table-generator td.selected-cell {
|
|
| 172 |
background-color: var(--oc-blue-1); |
|
| 173 |
border-color: var(--oc-blue-3); |
|
| 174 |
} |
|
| app/helpers/application_helper.rb | ||
|---|---|---|
| 29 | 29 |
include Redmine::Hook::Helper |
| 30 | 30 |
include Redmine::Helpers::URL |
| 31 | 31 |
include IconsHelper |
| 32 |
include WikiToolbarHelper |
|
| 33 |
include BrowserHelper |
|
| 32 | 34 | |
| 33 | 35 |
extend Forwardable |
| 34 |
def_delegators :wiki_helper, :wikitoolbar_for, :heads_for_wiki_formatter |
|
| 36 |
def_delegators :wiki_helper, :wikitoolbar_for, :heads_for_wiki_formatter, :wikitoolbar_buttons, :wikitoolbar_controller_name
|
|
| 35 | 37 | |
| 36 | 38 |
# Return true if user is authorized for controller/action, otherwise false |
| 37 | 39 |
def authorize_for(controller, action) |
| app/helpers/browser_helper.rb | ||
|---|---|---|
| 1 |
# frozen_string_literal: true |
|
| 2 | ||
| 3 |
module BrowserHelper |
|
| 4 |
def mac_os? |
|
| 5 |
request.user_agent.downcase.include?('macintosh')
|
|
| 6 |
end |
|
| 7 | ||
| 8 |
def modifier_key |
|
| 9 |
mac_os? ? '⌘' : 'Ctrl' |
|
| 10 |
end |
|
| 11 |
end |
|
| app/helpers/wiki_toolbar_helper.rb | ||
|---|---|---|
| 1 |
# frozen_string_literal: true |
|
| 2 | ||
| 3 |
module WikiToolbarHelper |
|
| 4 |
def wikitoolbar(field_id, preview_url = preview_text_path, &block) |
|
| 5 |
controller_name = wikitoolbar_controller_name |
|
| 6 | ||
| 7 |
content_tag(:div, |
|
| 8 |
class: 'wiki-editor', |
|
| 9 |
data: {
|
|
| 10 |
controller: controller_name, |
|
| 11 |
field_id: field_id, |
|
| 12 |
"#{controller_name}-preview-url-value": preview_url,
|
|
| 13 |
"#{controller_name}-help-url-value": help_wiki_syntax_path,
|
|
| 14 |
"#{controller_name}-languages-value": (User.current && User.current.pref.toolbar_language_options || UserPreference::DEFAULT_TOOLBAR_LANGUAGE_OPTIONS).split(',')
|
|
| 15 |
}) do |
|
| 16 |
concat( |
|
| 17 |
content_tag(:div, class: 'wiki-editor-header') do |
|
| 18 |
concat( |
|
| 19 |
content_tag(:div, class: 'wiki-editor-tabs') do |
|
| 20 |
concat button_tag(l(:button_edit), type: 'button', class: 'wiki-editor-tab selected', data: { action: "#{controller_name}#edit", "#{controller_name}-target": "editTab" })
|
|
| 21 |
concat button_tag(l(:label_preview), type: 'button', class: 'wiki-editor-tab', data: { action: "#{controller_name}#preview", "#{controller_name}-target": "previewTab" })
|
|
| 22 |
end |
|
| 23 |
) |
|
| 24 |
concat( |
|
| 25 |
content_tag(:div, class: 'wiki-editor-buttons', data: { "#{controller_name}-target": "buttons" }) do
|
|
| 26 |
render_wikitoolbar_buttons |
|
| 27 |
end |
|
| 28 |
) |
|
| 29 |
end |
|
| 30 |
) |
|
| 31 | ||
| 32 |
concat( |
|
| 33 |
content_tag(:div, class: 'wiki-editor-pane wiki-editor-edit', data: { "#{controller_name}-target": "editPane" }, &block)
|
|
| 34 |
) |
|
| 35 | ||
| 36 |
concat( |
|
| 37 |
content_tag(:div, '', class: 'wiki-editor-pane wiki-editor-preview wiki hidden', data: { "#{controller_name}-target": "previewPane" })
|
|
| 38 |
) |
|
| 39 |
end |
|
| 40 |
end |
|
| 41 | ||
| 42 |
def render_wikitoolbar_buttons |
|
| 43 |
buttons = wikitoolbar_buttons |
|
| 44 |
controller_name = wikitoolbar_controller_name |
|
| 45 | ||
| 46 |
buttons.map do |button| |
|
| 47 |
if button[:class] == 'jstSpacer' || button[:class] == 'spacer' |
|
| 48 |
content_tag('span', '', class: 'jstSpacer')
|
|
| 49 |
else |
|
| 50 |
title = l(button[:label]) |
|
| 51 |
title += " (#{modifier_key}+#{button[:shortcut].upcase})" if button[:shortcut]
|
|
| 52 | ||
| 53 |
tag.button( |
|
| 54 |
wikitoolbar_button_content(button, title), |
|
| 55 |
type: 'button', |
|
| 56 |
class: button[:class], |
|
| 57 |
title: title, |
|
| 58 |
data: { action: "#{controller_name}##{button[:action]}" }
|
|
| 59 |
) |
|
| 60 |
end |
|
| 61 |
end.join.html_safe |
|
| 62 |
end |
|
| 63 | ||
| 64 |
private |
|
| 65 | ||
| 66 |
def wikitoolbar_button_content(button, title) |
|
| 67 |
if button[:icon] == 'pre' |
|
| 68 |
content_tag(:svg, class: 's16 icon-svg', aria: { hidden: true }, viewBox: '0 0 24 24', fill: 'none', stroke: 'currentColor', 'stroke-width': '2', 'stroke-linecap': 'round',
|
|
| 69 |
'stroke-linejoin': 'round') do |
|
| 70 |
concat tag.rect(x: '3', y: '5', width: '18', height: '14', rx: '2') |
|
| 71 |
concat tag.text('pre', x: '12', y: '15', 'text-anchor': 'middle', style: 'font-size: 8px; font-weight: bold; font-family: monospace;', fill: 'currentColor', stroke: 'none')
|
|
| 72 |
end |
|
| 73 |
else |
|
| 74 |
sprite_icon(button[:icon], title, icon_only: true, size: 16) |
|
| 75 |
end |
|
| 76 |
end |
|
| 77 |
end |
|
| app/javascript/controllers/common_mark_toolbar_controller.js | ||
|---|---|---|
| 1 |
import WikiToolbarController from "controllers/wiki_toolbar_controller" |
|
| 2 | ||
| 3 |
export default class extends WikiToolbarController {
|
|
| 4 |
strong() {
|
|
| 5 |
this.singleTag('**')
|
|
| 6 |
} |
|
| 7 | ||
| 8 |
italic() {
|
|
| 9 |
this.singleTag('*')
|
|
| 10 |
} |
|
| 11 | ||
| 12 |
underline() {
|
|
| 13 |
this.singleTag('<u>', '</u>')
|
|
| 14 |
} |
|
| 15 | ||
| 16 |
deleted() {
|
|
| 17 |
this.singleTag('~~')
|
|
| 18 |
} |
|
| 19 | ||
| 20 |
inlineCode() {
|
|
| 21 |
this.singleTag('`')
|
|
| 22 |
} |
|
| 23 | ||
| 24 |
h1() {
|
|
| 25 |
this.encloseLineSelection('# ', '', (str) => {
|
|
| 26 |
return str.replace(/^#+\s+/, '') |
|
| 27 |
}) |
|
| 28 |
} |
|
| 29 | ||
| 30 |
h2() {
|
|
| 31 |
this.encloseLineSelection('## ', '', (str) => {
|
|
| 32 |
return str.replace(/^#+\s+/, '') |
|
| 33 |
}) |
|
| 34 |
} |
|
| 35 | ||
| 36 |
h3() {
|
|
| 37 |
this.encloseLineSelection('### ', '', (str) => {
|
|
| 38 |
return str.replace(/^#+\s+/, '') |
|
| 39 |
}) |
|
| 40 |
} |
|
| 41 | ||
| 42 |
unorderedList() {
|
|
| 43 |
this.encloseLineSelection('', '', (str) => {
|
|
| 44 |
str = str.replace(/\r/g, '') |
|
| 45 |
return str.replace(/(\n|^)[#-]?\s*/g, "$1* ") |
|
| 46 |
}) |
|
| 47 |
} |
|
| 48 | ||
| 49 |
orderedList() {
|
|
| 50 |
this.encloseLineSelection('', '', (str) => {
|
|
| 51 |
str = str.replace(/\r/g, '') |
|
| 52 |
return str.replace(/(\n|^)[*-]?\s*/g, "$11. ") |
|
| 53 |
}) |
|
| 54 |
} |
|
| 55 | ||
| 56 |
taskList() {
|
|
| 57 |
this.encloseLineSelection('', '', (str) => {
|
|
| 58 |
str = str.replace(/\r/g, '') |
|
| 59 |
return str.replace(/(\n|^)[*-]?\s*/g, "$1* [ ] ") |
|
| 60 |
}) |
|
| 61 |
} |
|
| 62 | ||
| 63 |
quote() {
|
|
| 64 |
this.encloseLineSelection('', '', (str) => {
|
|
| 65 |
str = str.replace(/\r/g, '') |
|
| 66 |
return str.replace(/(\n|^)( *)([^\n]*)/g, "$1> $2$3") |
|
| 67 |
}) |
|
| 68 |
} |
|
| 69 | ||
| 70 |
unquote() {
|
|
| 71 |
this.encloseLineSelection('', '', (str) => {
|
|
| 72 |
str = str.replace(/\r/g, '') |
|
| 73 |
return str.replace(/(\n|^) *(> ?)?( *)([^\n]*)/g, "$1$3$4") |
|
| 74 |
}) |
|
| 75 |
} |
|
| 76 | ||
| 77 |
pre() {
|
|
| 78 |
this.encloseLineSelection('```\n', '\n```')
|
|
| 79 |
} |
|
| 80 | ||
| 81 |
image() {
|
|
| 82 |
this.encloseSelection("")
|
|
| 83 |
} |
|
| 84 | ||
| 85 |
insertTable(cols, rows) {
|
|
| 86 |
const alphabets = "ABCDEFGHIJ".split('')
|
|
| 87 |
const header = '|' + alphabets.slice(0, cols).join(' |') + ' |\n'
|
|
| 88 |
const separator = Array(cols + 1).join('|--') + '|\n'
|
|
| 89 |
const cells = Array(rows + 1).join(Array(cols + 1).join('| ') + '|\n')
|
|
| 90 |
this.encloseLineSelection(header + separator + cells, '') |
|
| 91 |
} |
|
| 92 | ||
| 93 |
insertPrecode(lang) {
|
|
| 94 |
this.encloseLineSelection('``` ' + lang + '\n', '\n```\n')
|
|
| 95 |
} |
|
| 96 |
} |
|
| app/javascript/controllers/textile_toolbar_controller.js | ||
|---|---|---|
| 1 |
import WikiToolbarController from "controllers/wiki_toolbar_controller" |
|
| 2 | ||
| 3 |
export default class extends WikiToolbarController {
|
|
| 4 |
strong() {
|
|
| 5 |
this.singleTag('*')
|
|
| 6 |
} |
|
| 7 | ||
| 8 |
italic() {
|
|
| 9 |
this.singleTag('_')
|
|
| 10 |
} |
|
| 11 | ||
| 12 |
underline() {
|
|
| 13 |
this.singleTag('+')
|
|
| 14 |
} |
|
| 15 | ||
| 16 |
deleted() {
|
|
| 17 |
this.singleTag('-')
|
|
| 18 |
} |
|
| 19 | ||
| 20 |
inlineCode() {
|
|
| 21 |
this.singleTag('@')
|
|
| 22 |
} |
|
| 23 | ||
| 24 |
h1() {
|
|
| 25 |
this.encloseLineSelection('h1. ', '', (str) => {
|
|
| 26 |
return str.replace(/^h\d+\.\s+/, '') |
|
| 27 |
}) |
|
| 28 |
} |
|
| 29 | ||
| 30 |
h2() {
|
|
| 31 |
this.encloseLineSelection('h2. ', '', (str) => {
|
|
| 32 |
return str.replace(/^h\d+\.\s+/, '') |
|
| 33 |
}) |
|
| 34 |
} |
|
| 35 | ||
| 36 |
h3() {
|
|
| 37 |
this.encloseLineSelection('h3. ', '', (str) => {
|
|
| 38 |
return str.replace(/^h\d+\.\s+/, '') |
|
| 39 |
}) |
|
| 40 |
} |
|
| 41 | ||
| 42 |
unorderedList() {
|
|
| 43 |
this.encloseLineSelection('', '', (str) => {
|
|
| 44 |
str = str.replace(/\r/g, '') |
|
| 45 |
return str.replace(/(\n|^)[#-]?\s*/g, "$1* ") |
|
| 46 |
}) |
|
| 47 |
} |
|
| 48 | ||
| 49 |
orderedList() {
|
|
| 50 |
this.encloseLineSelection('', '', (str) => {
|
|
| 51 |
str = str.replace(/\r/g, '') |
|
| 52 |
return str.replace(/(\n|^)[*-]?\s*/g, "$1# ") |
|
| 53 |
}) |
|
| 54 |
} |
|
| 55 | ||
| 56 |
quote() {
|
|
| 57 |
this.encloseLineSelection('', '', (str) => {
|
|
| 58 |
str = str.replace(/\r/g, '') |
|
| 59 |
return str.replace(/(\n|^)( *)([^\n]*)/g, "$1> $2$3") |
|
| 60 |
}) |
|
| 61 |
} |
|
| 62 | ||
| 63 |
unquote() {
|
|
| 64 |
this.encloseLineSelection('', '', (str) => {
|
|
| 65 |
str = str.replace(/\r/g, '') |
|
| 66 |
return str.replace(/(\n|^) *(> ?)?( *)([^\n]*)/g, "$1$3$4") |
|
| 67 |
}) |
|
| 68 |
} |
|
| 69 | ||
| 70 |
pre() {
|
|
| 71 |
this.encloseLineSelection('<pre>\n', '\n</pre>')
|
|
| 72 |
} |
|
| 73 | ||
| 74 |
image() {
|
|
| 75 |
this.encloseSelection("!", "!")
|
|
| 76 |
} |
|
| 77 | ||
| 78 |
insertTable(cols, rows) {
|
|
| 79 |
const alphabets = "ABCDEFGHIJ".split('')
|
|
| 80 |
const header = '|_.' + alphabets.slice(0, cols).join('|_.') + '|\n'
|
|
| 81 |
const cells = Array(rows + 1).join(Array(cols + 1).join('| ') + '|\n')
|
|
| 82 |
this.encloseLineSelection(header + cells, '') |
|
| 83 |
} |
|
| 84 | ||
| 85 |
insertPrecode(lang) {
|
|
| 86 |
this.encloseLineSelection('<pre><code class="' + lang + '">\n', '\n</code></pre>\n')
|
|
| 87 |
} |
|
| 88 |
} |
|
| app/javascript/controllers/wiki_toolbar_controller.js | ||
|---|---|---|
| 1 |
import { Controller } from "@hotwired/stimulus"
|
|
| 2 | ||
| 3 |
export default class extends Controller {
|
|
| 4 | ||
| 5 |
static targets = ["field", "previewPane", "editPane", "editTab", "previewTab", "buttons"] |
|
| 6 |
static values = {
|
|
| 7 |
languages: Array, |
|
| 8 |
previewUrl: String, |
|
| 9 |
helpUrl: String |
|
| 10 |
} |
|
| 11 | ||
| 12 |
connect() {
|
|
| 13 |
this.field = this.hasFieldTarget ? this.fieldTarget : document.getElementById(this.element.dataset.fieldId) |
|
| 14 |
if (!this.field) return |
|
| 15 | ||
| 16 |
this.setupShortcuts() |
|
| 17 |
} |
|
| 18 | ||
| 19 |
strong() {
|
|
| 20 |
// abstract |
|
| 21 |
} |
|
| 22 | ||
| 23 |
italic() {
|
|
| 24 |
// abstract |
|
| 25 |
} |
|
| 26 | ||
| 27 |
underline() {
|
|
| 28 |
// abstract |
|
| 29 |
} |
|
| 30 | ||
| 31 |
deleted() {
|
|
| 32 |
// abstract |
|
| 33 |
} |
|
| 34 | ||
| 35 |
inlineCode() {
|
|
| 36 |
// abstract |
|
| 37 |
} |
|
| 38 | ||
| 39 |
h1() {
|
|
| 40 |
// abstract |
|
| 41 |
} |
|
| 42 | ||
| 43 |
h2() {
|
|
| 44 |
// abstract |
|
| 45 |
} |
|
| 46 | ||
| 47 |
h3() {
|
|
| 48 |
// abstract |
|
| 49 |
} |
|
| 50 | ||
| 51 |
unorderedList() {
|
|
| 52 |
// abstract |
|
| 53 |
} |
|
| 54 | ||
| 55 |
orderedList() {
|
|
| 56 |
// abstract |
|
| 57 |
} |
|
| 58 | ||
| 59 |
quote() {
|
|
| 60 |
// abstract |
|
| 61 |
} |
|
| 62 | ||
| 63 |
unquote() {
|
|
| 64 |
// abstract |
|
| 65 |
} |
|
| 66 | ||
| 67 |
pre() {
|
|
| 68 |
// abstract |
|
| 69 |
} |
|
| 70 | ||
| 71 |
help() {
|
|
| 72 |
window.open(this.helpUrlValue, "_blank") |
|
| 73 |
} |
|
| 74 | ||
| 75 |
precode(event) {
|
|
| 76 |
event.preventDefault() |
|
| 77 |
this.precodeMenu(event.currentTarget) |
|
| 78 |
} |
|
| 79 | ||
| 80 |
table(event) {
|
|
| 81 |
event.preventDefault() |
|
| 82 |
this.tableMenu(event.currentTarget) |
|
| 83 |
} |
|
| 84 | ||
| 85 |
wikiLink() {
|
|
| 86 |
this.encloseSelection("[[", "]]")
|
|
| 87 |
} |
|
| 88 | ||
| 89 |
image() {
|
|
| 90 |
// abstract |
|
| 91 |
} |
|
| 92 | ||
| 93 |
// Common text manipulation methods |
|
| 94 |
singleTag(startTag, endTag) {
|
|
| 95 |
endTag = endTag || startTag |
|
| 96 |
this.encloseSelection(startTag, endTag) |
|
| 97 |
} |
|
| 98 | ||
| 99 |
encloseSelection(prefix, suffix, fn) {
|
|
| 100 |
this.field.focus() |
|
| 101 |
prefix = prefix || '' |
|
| 102 |
suffix = suffix || '' |
|
| 103 | ||
| 104 |
let start = this.field.selectionStart |
|
| 105 |
let end = this.field.selectionEnd |
|
| 106 |
let scrollPos = this.field.scrollTop |
|
| 107 |
let sel = this.field.value.substring(start, end) |
|
| 108 |
let subst |
|
| 109 | ||
| 110 |
if (start > 0 && this.field.value.charAt(start - 1).match(/\S/)) {
|
|
| 111 |
prefix = ' ' + prefix |
|
| 112 |
} |
|
| 113 |
if (this.field.value.charAt(end).match(/\S/)) {
|
|
| 114 |
suffix = suffix + ' ' |
|
| 115 |
} |
|
| 116 | ||
| 117 |
if (sel.match(/ $/)) {
|
|
| 118 |
sel = sel.substring(0, sel.length - 1) |
|
| 119 |
suffix = suffix + " " |
|
| 120 |
} |
|
| 121 | ||
| 122 |
let res = (typeof fn === 'function') ? ((sel) ? fn.call(this, sel) : fn('')) : (sel ? sel : '')
|
|
| 123 |
subst = prefix + res + suffix |
|
| 124 | ||
| 125 |
this.field.setRangeText(subst, start, end, 'select') |
|
| 126 |
this.field.selectionStart = start + prefix.length |
|
| 127 |
this.field.selectionEnd = start + prefix.length + res.length |
|
| 128 |
this.field.scrollTop = scrollPos |
|
| 129 |
} |
|
| 130 | ||
| 131 |
encloseLineSelection(prefix, suffix, fn) {
|
|
| 132 |
this.field.focus() |
|
| 133 |
prefix = prefix || '' |
|
| 134 |
suffix = suffix || '' |
|
| 135 | ||
| 136 |
let start = this.field.selectionStart |
|
| 137 |
let end = this.field.selectionEnd |
|
| 138 |
let scrollPos = this.field.scrollTop |
|
| 139 | ||
| 140 |
// Go to start of line |
|
| 141 |
while (start > 0 && this.field.value.charAt(start - 1) !== '\n' && this.field.value.charAt(start - 1) !== '\r') {
|
|
| 142 |
start-- |
|
| 143 |
} |
|
| 144 | ||
| 145 |
// Go to end of line |
|
| 146 |
while (end < this.field.value.length && this.field.value.charAt(end) !== '\n' && this.field.value.charAt(end) !== '\r') {
|
|
| 147 |
end++ |
|
| 148 |
} |
|
| 149 | ||
| 150 |
let sel = this.field.value.substring(start, end) |
|
| 151 |
if (sel.match(/ $/)) {
|
|
| 152 |
sel = sel.substring(0, sel.length - 1) |
|
| 153 |
suffix = suffix + " " |
|
| 154 |
} |
|
| 155 | ||
| 156 |
let res = (typeof fn === 'function') ? ((sel) ? fn.call(this, sel) : fn('')) : (sel ? sel : '')
|
|
| 157 |
let subst = prefix + res + suffix |
|
| 158 | ||
| 159 |
this.field.setRangeText(subst, start, end, 'select') |
|
| 160 |
this.field.selectionStart = start + prefix.length |
|
| 161 |
this.field.selectionEnd = start + prefix.length + res.length |
|
| 162 |
this.field.scrollTop = scrollPos |
|
| 163 |
} |
|
| 164 | ||
| 165 |
setupShortcuts() {
|
|
| 166 |
this.field.addEventListener('keydown', (e) => {
|
|
| 167 |
if (this.isModifierKey(e)) {
|
|
| 168 |
const key = e.key.toLowerCase() |
|
| 169 |
const shortcuts = {
|
|
| 170 |
'b': 'strong', |
|
| 171 |
'i': 'italic', |
|
| 172 |
'u': 'underline' |
|
| 173 |
} |
|
| 174 |
if (shortcuts[key]) {
|
|
| 175 |
e.preventDefault() |
|
| 176 |
this[shortcuts[key]]() |
|
| 177 |
} |
|
| 178 |
} |
|
| 179 |
}) |
|
| 180 |
} |
|
| 181 | ||
| 182 |
isModifierKey(e) {
|
|
| 183 |
const isMac = navigator.platform.toLowerCase().indexOf('mac') > -1
|
|
| 184 |
return isMac ? e.metaKey : e.ctrlKey |
|
| 185 |
} |
|
| 186 | ||
| 187 |
async preview(event) {
|
|
| 188 |
event.preventDefault() |
|
| 189 |
const previewTab = event.currentTarget |
|
| 190 |
if (previewTab.classList.contains('selected')) return
|
|
| 191 | ||
| 192 |
const formData = new FormData() |
|
| 193 |
formData.append('text', this.field.value)
|
|
| 194 |
const form = this.field.closest('form')
|
|
| 195 |
const attachmentInputs = form.querySelectorAll('.attachments_fields input');
|
|
| 196 | ||
| 197 |
attachmentInputs.forEach(input => {
|
|
| 198 |
if (input.name && input.value) {
|
|
| 199 |
formData.append(input.name, input.value); |
|
| 200 |
} |
|
| 201 |
}); |
|
| 202 | ||
| 203 |
// Add authenticity token |
|
| 204 |
const token = document.querySelector('meta[name="csrf-token"]')
|
|
| 205 |
if (token) {
|
|
| 206 |
formData.append('authenticity_token', token.content)
|
|
| 207 |
} |
|
| 208 | ||
| 209 |
try {
|
|
| 210 |
const response = await fetch(this.previewUrlValue, {
|
|
| 211 |
method: 'POST', |
|
| 212 |
body: formData, |
|
| 213 |
headers: {
|
|
| 214 |
'X-Requested-With': 'XMLHttpRequest' |
|
| 215 |
} |
|
| 216 |
}) |
|
| 217 | ||
| 218 |
if (response.ok) {
|
|
| 219 |
const html = await response.text() |
|
| 220 |
this.showPreview(html) |
|
| 221 |
} |
|
| 222 |
} catch (error) {
|
|
| 223 |
console.error("Preview failed", error)
|
|
| 224 |
} |
|
| 225 |
} |
|
| 226 | ||
| 227 |
edit(event) {
|
|
| 228 |
event.preventDefault() |
|
| 229 |
const editTab = event.currentTarget |
|
| 230 |
if (editTab.classList.contains('selected')) return
|
|
| 231 | ||
| 232 |
this.hidePreview(editTab) |
|
| 233 |
} |
|
| 234 | ||
| 235 |
showPreview(html) {
|
|
| 236 |
// Find or create preview div |
|
| 237 |
this.previewPaneTarget.innerHTML = html |
|
| 238 |
this.editPaneTarget.classList.add('hidden')
|
|
| 239 |
this.previewPaneTarget.classList.remove('hidden')
|
|
| 240 |
this.editTabTarget.classList.remove('selected')
|
|
| 241 |
this.previewTabTarget.classList.add('selected')
|
|
| 242 |
this.buttonsTarget.classList.add('hidden')
|
|
| 243 |
} |
|
| 244 | ||
| 245 |
hidePreview(editTab) {
|
|
| 246 |
this.editPaneTarget.classList.remove('hidden')
|
|
| 247 |
this.previewPaneTarget.classList.add('hidden')
|
|
| 248 |
this.editTabTarget.classList.add('selected')
|
|
| 249 |
this.previewTabTarget.classList.remove('selected')
|
|
| 250 |
this.buttonsTarget.classList.remove('hidden')
|
|
| 251 |
} |
|
| 252 | ||
| 253 |
tableMenu(button) {
|
|
| 254 |
if (this.menu) {
|
|
| 255 |
this.menu.remove() |
|
| 256 |
this.menu = null |
|
| 257 |
return |
|
| 258 |
} |
|
| 259 | ||
| 260 |
const alphabets = "ABCDEFGHIJ".split('')
|
|
| 261 |
const menu = document.createElement('table')
|
|
| 262 |
menu.className = 'table-generator' |
|
| 263 |
this.menu = menu |
|
| 264 | ||
| 265 |
for (let r = 1; r <= 5; r++) {
|
|
| 266 |
const row = document.createElement('tr')
|
|
| 267 |
for (let c = 1; c <= 10; c++) {
|
|
| 268 |
const cell = document.createElement('td')
|
|
| 269 |
cell.dataset.row = r |
|
| 270 |
cell.dataset.col = c |
|
| 271 |
cell.title = `${c}×${r}`
|
|
| 272 | ||
| 273 |
cell.addEventListener('mousedown', (e) => {
|
|
| 274 |
e.preventDefault() |
|
| 275 |
this.insertTable(c, r) |
|
| 276 |
this.menu.remove() |
|
| 277 |
this.menu = null |
|
| 278 |
}) |
|
| 279 | ||
| 280 |
cell.addEventListener('mouseenter', () => {
|
|
| 281 |
const cells = menu.querySelectorAll('td')
|
|
| 282 |
cells.forEach(el => {
|
|
| 283 |
if (parseInt(el.dataset.row) <= r && parseInt(el.dataset.col) <= c) {
|
|
| 284 |
el.classList.add('selected-cell')
|
|
| 285 |
} else {
|
|
| 286 |
el.classList.remove('selected-cell')
|
|
| 287 |
} |
|
| 288 |
}) |
|
| 289 |
}) |
|
| 290 | ||
| 291 |
row.appendChild(cell) |
|
| 292 |
} |
|
| 293 |
menu.appendChild(row) |
|
| 294 |
} |
|
| 295 | ||
| 296 |
document.body.appendChild(menu) |
|
| 297 | ||
| 298 |
// Positioning |
|
| 299 |
const rect = button.getBoundingClientRect() |
|
| 300 |
menu.style.left = `${rect.left + window.scrollX}px`
|
|
| 301 |
menu.style.top = `${rect.bottom + window.scrollY}px`
|
|
| 302 | ||
| 303 |
const closeMenu = (e) => {
|
|
| 304 |
if (this.menu && !this.menu.contains(e.target) && e.target !== button && !button.contains(e.target)) {
|
|
| 305 |
this.menu.remove() |
|
| 306 |
this.menu = null |
|
| 307 |
document.removeEventListener('mousedown', closeMenu)
|
|
| 308 |
} |
|
| 309 |
} |
|
| 310 |
document.addEventListener('mousedown', closeMenu)
|
|
| 311 |
} |
|
| 312 | ||
| 313 |
insertTable(cols, rows) {
|
|
| 314 |
// abstract |
|
| 315 |
} |
|
| 316 | ||
| 317 |
precodeMenu(button) {
|
|
| 318 |
if (this.menu) {
|
|
| 319 |
this.menu.remove() |
|
| 320 |
this.menu = null |
|
| 321 |
return |
|
| 322 |
} |
|
| 323 | ||
| 324 |
const menu = document.createElement('ul')
|
|
| 325 |
menu.className = 'drdn-items' |
|
| 326 |
menu.style.position = 'absolute' |
|
| 327 |
menu.style.zIndex = '1000' |
|
| 328 |
menu.style.backgroundColor = '#fff' |
|
| 329 |
menu.style.boxShadow = '0 4px 12px rgba(0, 0, 0, 0.15)' |
|
| 330 |
menu.style.border = '1px solid #d7d7d7' |
|
| 331 |
menu.style.listStyle = 'none' |
|
| 332 |
menu.style.padding = '5px 0' |
|
| 333 |
menu.style.margin = '0' |
|
| 334 |
menu.style.minWidth = '150px' |
|
| 335 |
menu.style.maxHeight = '300px' |
|
| 336 |
menu.style.overflowY = 'auto' |
|
| 337 | ||
| 338 |
this.menu = menu |
|
| 339 | ||
| 340 |
this.languagesValue.forEach(lang => {
|
|
| 341 |
const item = document.createElement('li')
|
|
| 342 |
const link = document.createElement('a')
|
|
| 343 |
link.href = '#' |
|
| 344 |
link.textContent = lang |
|
| 345 |
link.style.display = 'block' |
|
| 346 |
link.style.padding = '4px 12px' |
|
| 347 |
link.style.color = '#333' |
|
| 348 |
link.style.textDecoration = 'none' |
|
| 349 |
link.style.fontSize = '0.9em' |
|
| 350 | ||
| 351 |
link.addEventListener('mouseenter', () => {
|
|
| 352 |
link.style.backgroundColor = '#3e70ad' |
|
| 353 |
link.style.color = '#fff' |
|
| 354 |
}) |
|
| 355 |
link.addEventListener('mouseleave', () => {
|
|
| 356 |
link.style.backgroundColor = 'transparent' |
|
| 357 |
link.style.color = '#333' |
|
| 358 |
}) |
|
| 359 | ||
| 360 |
link.addEventListener('mousedown', (e) => {
|
|
| 361 |
e.preventDefault() |
|
| 362 |
this.insertPrecode(lang) |
|
| 363 |
this.menu.remove() |
|
| 364 |
this.menu = null |
|
| 365 |
}) |
|
| 366 | ||
| 367 |
item.appendChild(link) |
|
| 368 |
menu.appendChild(item) |
|
| 369 |
}) |
|
| 370 | ||
| 371 |
document.body.appendChild(menu) |
|
| 372 | ||
| 373 |
// Positioning |
|
| 374 |
const rect = button.getBoundingClientRect() |
|
| 375 |
menu.style.left = `${rect.left + window.scrollX}px`
|
|
| 376 |
menu.style.top = `${rect.bottom + window.scrollY}px`
|
|
| 377 | ||
| 378 |
const closeMenu = (e) => {
|
|
| 379 |
if (this.menu && !this.menu.contains(e.target) && e.target !== button && !button.contains(e.target)) {
|
|
| 380 |
this.menu.remove() |
|
| 381 |
this.menu = null |
|
| 382 |
document.removeEventListener('mousedown', closeMenu)
|
|
| 383 |
} |
|
| 384 |
} |
|
| 385 |
document.addEventListener('mousedown', closeMenu)
|
|
| 386 |
} |
|
| 387 | ||
| 388 |
insertPrecode(lang) {
|
|
| 389 |
// abstract |
|
| 390 |
} |
|
| 391 |
} |
|
| app/views/issues/_edit.html.erb | ||
|---|---|---|
| 30 | 30 |
<% end %> |
| 31 | 31 |
<% if @issue.notes_addable? %> |
| 32 | 32 |
<fieldset id="add_notes"><legend><%= l(:field_notes) %></legend> |
| 33 |
<%= f.textarea :notes, :cols => 60, :rows => 10, :class => 'wiki-edit', |
|
| 33 |
<%= wikitoolbar 'issue_notes', preview_issue_path(:project_id => @project, :issue_id => @issue) do %> |
|
| 34 |
<%= f.textarea :notes, :cols => 60, :rows => 10, |
|
| 34 | 35 |
:data => {
|
| 35 | 36 |
:auto_complete => true |
| 36 | 37 |
}.merge(list_autofill_data_attributes), |
| 37 | 38 |
:no_label => true %> |
| 38 |
<%= wikitoolbar_for 'issue_notes', preview_issue_path(:project_id => @project, :issue_id => @issue) %>
|
|
| 39 |
<% end %>
|
|
| 39 | 40 | |
| 40 | 41 |
<% if @issue.safe_attribute? 'private_notes' %> |
| 41 | 42 |
<%= f.check_box :private_notes, :no_label => true %> <label for="issue_private_notes"><%= l(:field_private_notes) %></label> |
| app/views/issues/_form.html.erb | ||
|---|---|---|
| 30 | 30 |
<% end %> |
| 31 | 31 | |
| 32 | 32 |
<% if @issue.safe_attribute? 'description' %> |
| 33 |
<p>
|
|
| 33 |
<div id="issue_description_and_toolbar_parent" class="wiki-edit-container">
|
|
| 34 | 34 |
<%= f.label_for_field :description, :required => @issue.required_attribute?('description') %>
|
| 35 | 35 |
<%= content_tag 'span', :id => "issue_description_and_toolbar", :style => (@issue.new_record? ? nil : 'display:none') do %> |
| 36 |
<%= wikitoolbar 'issue_description', preview_issue_path(:project_id => @issue.project, :issue_id => @issue.id) do |t| %> |
|
| 36 | 37 |
<%= f.textarea :description, :cols => 60, :accesskey => accesskey(:edit), :class => 'wiki-edit', |
| 37 | 38 |
:rows => [[10, @issue.description.to_s.length / 50].max, 20].min, |
| 38 | 39 |
:data => {
|
| ... | ... | |
| 40 | 41 |
}.merge(list_autofill_data_attributes), |
| 41 | 42 |
:no_label => true %> |
| 42 | 43 |
<% end %> |
| 44 |
<% end %> |
|
| 43 | 45 |
<%= link_to_function content_tag(:span, sprite_icon('edit', l(:button_edit))), '$(this).hide(); $("#issue_description_and_toolbar").show()', :class => 'icon icon-edit' unless @issue.new_record? %>
|
| 44 |
</p> |
|
| 45 |
<%= wikitoolbar_for 'issue_description', preview_issue_path(:project_id => @issue.project, :issue_id => @issue.id) %> |
|
| 46 |
</div> |
|
| 46 | 47 |
<% end %> |
| 47 | 48 | |
| 48 | 49 |
<div id="attributes" class="attributes"> |
| ... | ... | |
| 52 | 53 |
<%= call_hook(:view_issues_form_details_bottom, { :issue => @issue, :form => f }) %>
|
| 53 | 54 |
<% end %> |
| 54 | 55 | |
| 55 |
<% heads_for_wiki_formatter %> |
|
| 56 | 56 |
<%= heads_for_auto_complete(@issue.project) %> |
| 57 | 57 | |
| 58 | 58 |
<% if User.current.allowed_to?(:add_issue_watchers, @issue.project)%> |
| app/views/issues/_form_custom_fields.html.erb | ||
|---|---|---|
| 20 | 20 | |
| 21 | 21 |
<% custom_field_values_full_width.each do |value| %> |
| 22 | 22 |
<p><%= custom_field_tag_with_label :issue, value, :required => @issue.required_attribute?(value.custom_field_id) %></p> |
| 23 |
<%= wikitoolbar_for "issue_custom_field_values_#{value.custom_field_id}", preview_issue_path(:project_id => @issue.project, :issue_id => @issue.id) if value.custom_field.full_text_formatting? %>
|
|
| 23 |
<%# wikitoolbar_for "issue_custom_field_values_#{value.custom_field_id}", preview_issue_path(:project_id => @issue.project, :issue_id => @issue.id) if value.custom_field.full_text_formatting? %>
|
|
| 24 | 24 |
<% end %> |
| app/views/layouts/base.html.erb | ||
|---|---|---|
| 8 | 8 |
<meta name="keywords" content="issue,bug,tracker" /> |
| 9 | 9 |
<%= csrf_meta_tag %> |
| 10 | 10 |
<%= favicon %> |
| 11 |
<%= stylesheet_link_tag 'jquery/jquery-ui-1.13.2', 'tribute-5.1.3', 'application', 'responsive', :media => 'all' %> |
|
| 11 |
<%= stylesheet_link_tag 'jquery/jquery-ui-1.13.2', 'tribute-5.1.3', 'application', 'wiki_editor', 'responsive', :media => 'all' %>
|
|
| 12 | 12 |
<%= javascript_importmap_tags %> |
| 13 | 13 |
<%= javascript_heads %> |
| 14 | 14 |
<%= heads_for_theme %> |
| config/locales/en.yml | ||
|---|---|---|
| 888 | 888 |
label_index_by_date: Index by date |
| 889 | 889 |
label_current_version: Current version |
| 890 | 890 |
label_preview: Preview |
| 891 |
label_wiki_toolbar_strong: Strong |
|
| 892 |
label_wiki_toolbar_italic: Italic |
|
| 893 |
label_wiki_toolbar_underline: Underline |
|
| 894 |
label_wiki_toolbar_deleted: Deleted |
|
| 895 |
label_wiki_toolbar_code: Inline Code |
|
| 896 |
label_wiki_toolbar_heading_1: Heading 1 |
|
| 897 |
label_wiki_toolbar_heading_2: Heading 2 |
|
| 898 |
label_wiki_toolbar_heading_3: Heading 3 |
|
| 899 |
label_wiki_toolbar_unordered_list: Unordered list |
|
| 900 |
label_wiki_toolbar_ordered_list: Ordered list |
|
| 901 |
label_wiki_toolbar_task_list: Task list |
|
| 902 |
label_wiki_toolbar_quote: Quote |
|
| 903 |
label_wiki_toolbar_unquote: Remove quote |
|
| 904 |
label_wiki_toolbar_table: Table |
|
| 905 |
label_wiki_toolbar_preformatted_text: Preformatted text |
|
| 906 |
label_wiki_toolbar_preformatted_code: Highlighted code |
|
| 907 |
label_wiki_toolbar_wiki_link: Link to a Wiki page |
|
| 908 |
label_wiki_toolbar_image: Image |
|
| 909 |
label_wiki_toolbar_help: Help |
|
| 891 | 910 |
label_feed_plural: Feeds |
| 892 | 911 |
label_changes_details: Details of all changes |
| 893 | 912 |
label_issue_tracking: Issue tracking |
| lib/redmine/wiki_formatting/common_mark/helper.rb | ||
|---|---|---|
| 21 | 21 |
module WikiFormatting |
| 22 | 22 |
module CommonMark |
| 23 | 23 |
module Helper |
| 24 |
def wikitoolbar_controller_name |
|
| 25 |
'common-mark-toolbar' |
|
| 26 |
end |
|
| 27 | ||
| 28 |
def wikitoolbar_buttons |
|
| 29 |
[ |
|
| 30 |
{ label: :label_wiki_toolbar_strong, class: 'jstb_strong', icon: 'bold', action: 'strong', shortcut: 'b' },
|
|
| 31 |
{ label: :label_wiki_toolbar_italic, class: 'jstb_em', icon: 'italic', action: 'italic', shortcut: 'i' },
|
|
| 32 |
{ label: :label_wiki_toolbar_underline, class: 'jstb_ins', icon: 'underline', action: 'underline', shortcut: 'u' },
|
|
| 33 |
{ label: :label_wiki_toolbar_deleted, class: 'jstb_del', icon: 'strikethrough', action: 'deleted' },
|
|
| 34 |
{ label: :label_wiki_toolbar_code, class: 'jstb_code', icon: 'inline-code', action: 'inlineCode' },
|
|
| 35 |
{ class: 'jstSpacer' },
|
|
| 36 |
{ label: :label_wiki_toolbar_heading_1, class: 'jstb_h1', icon: 'h1', action: 'h1' },
|
|
| 37 |
{ label: :label_wiki_toolbar_heading_2, class: 'jstb_h2', icon: 'h2', action: 'h2' },
|
|
| 38 |
{ label: :label_wiki_toolbar_heading_3, class: 'jstb_h3', icon: 'h3', action: 'h3' },
|
|
| 39 |
{ class: 'jstSpacer' },
|
|
| 40 |
{ label: :label_wiki_toolbar_unordered_list, class: 'jstb_ul', icon: 'list', action: 'unorderedList' },
|
|
| 41 |
{ label: :label_wiki_toolbar_ordered_list, class: 'jstb_ol', icon: 'list-numbers', action: 'orderedList' },
|
|
| 42 |
{ label: :label_wiki_toolbar_task_list, class: 'jstb_tl', icon: 'list-check', action: 'taskList' },
|
|
| 43 |
{ class: 'jstSpacer' },
|
|
| 44 |
{ label: :label_wiki_toolbar_quote, class: 'jstb_bq', icon: 'indent-increase', action: 'quote' },
|
|
| 45 |
{ label: :label_wiki_toolbar_unquote, class: 'jstb_unbq', icon: 'indent-decrease', action: 'unquote' },
|
|
| 46 |
{ label: :label_wiki_toolbar_table, class: 'jstb_table', icon: 'table', action: 'table' },
|
|
| 47 |
{ label: :label_wiki_toolbar_preformatted_text, class: 'jstb_pre', icon: 'pre', action: 'pre' },
|
|
| 48 |
{ label: :label_wiki_toolbar_preformatted_code, class: 'jstb_precode', icon: 'changeset', action: 'precode' },
|
|
| 49 |
{ label: :label_wiki_toolbar_wiki_link, class: 'jstb_wiki_link', icon: 'wiki-link', action: 'wikiLink' },
|
|
| 50 |
{ label: :label_wiki_toolbar_image, class: 'jstb_image', icon: 'image', action: 'image' },
|
|
| 51 |
{ class: 'spacer' },
|
|
| 52 |
{ label: :label_wiki_toolbar_help, class: 'jstb_help', icon: 'help', action: 'help' }
|
|
| 53 |
] |
|
| 54 |
end |
|
| 55 | ||
| 24 | 56 |
def wikitoolbar_for(field_id, preview_url = preview_text_path) |
| 25 | 57 |
heads_for_wiki_formatter |
| 26 | 58 | |
| lib/redmine/wiki_formatting/textile/helper.rb | ||
|---|---|---|
| 21 | 21 |
module WikiFormatting |
| 22 | 22 |
module Textile |
| 23 | 23 |
module Helper |
| 24 |
def wikitoolbar_controller_name |
|
| 25 |
'textile-toolbar' |
|
| 26 |
end |
|
| 27 | ||
| 24 | 28 |
def wikitoolbar_for(field_id, preview_url = preview_text_path) |
| 25 | 29 |
heads_for_wiki_formatter |
| 26 | 30 | |
| ... | ... | |
| 56 | 60 |
@heads_for_wiki_formatter_included = true |
| 57 | 61 |
end |
| 58 | 62 |
end |
| 63 | ||
| 64 |
def wikitoolbar_buttons |
|
| 65 |
[ |
|
| 66 |
{ label: :label_wiki_toolbar_strong, class: 'jstb_strong', icon: 'bold', action: 'strong', shortcut: 'b' },
|
|
| 67 |
{ label: :label_wiki_toolbar_italic, class: 'jstb_em', icon: 'italic', action: 'italic', shortcut: 'i' },
|
|
| 68 |
{ label: :label_wiki_toolbar_underline, class: 'jstb_ins', icon: 'underline', action: 'underline', shortcut: 'u' },
|
|
| 69 |
{ label: :label_wiki_toolbar_deleted, class: 'jstb_del', icon: 'strikethrough', action: 'deleted' },
|
|
| 70 |
{ label: :label_wiki_toolbar_code, class: 'jstb_code', icon: 'inline-code', action: 'inlineCode' },
|
|
| 71 |
{ class: 'spacer' },
|
|
| 72 |
{ label: :label_wiki_toolbar_heading_1, class: 'jstb_h1', icon: 'h1', action: 'h1' },
|
|
| 73 |
{ label: :label_wiki_toolbar_heading_2, class: 'jstb_h2', icon: 'h2', action: 'h2' },
|
|
| 74 |
{ label: :label_wiki_toolbar_heading_3, class: 'jstb_h3', icon: 'h3', action: 'h3' },
|
|
| 75 |
{ class: 'spacer' },
|
|
| 76 |
{ label: :label_wiki_toolbar_unordered_list, class: 'jstb_ul', icon: 'list', action: 'unorderedList' },
|
|
| 77 |
{ label: :label_wiki_toolbar_ordered_list, class: 'jstb_ol', icon: 'list-numbers', action: 'orderedList' },
|
|
| 78 |
{ class: 'spacer' },
|
|
| 79 |
{ label: :label_wiki_toolbar_quote, class: 'jstb_bq', icon: 'indent-increase', action: 'quote' },
|
|
| 80 |
{ label: :label_wiki_toolbar_unquote, class: 'jstb_unbq', icon: 'indent-decrease', action: 'unquote' },
|
|
| 81 |
{ label: :label_wiki_toolbar_table, class: 'jstb_table', icon: 'table', action: 'table' },
|
|
| 82 |
{ label: :label_wiki_toolbar_preformatted_text, class: 'jstb_pre', icon: 'pre', action: 'pre' },
|
|
| 83 |
{ label: :label_wiki_toolbar_preformatted_code, class: 'jstb_precode', icon: 'changeset', action: 'precode' },
|
|
| 84 |
{ label: :label_wiki_toolbar_wiki_link, class: 'jstb_wiki_link', icon: 'wiki-link', action: 'wikiLink' },
|
|
| 85 |
{ label: :label_wiki_toolbar_image, class: 'jstb_image', icon: 'image', action: 'image' },
|
|
| 86 |
{ class: 'spacer' },
|
|
| 87 |
{ label: :label_wiki_toolbar_help, class: 'jstb_help', icon: 'help', action: 'help' }
|
|
| 88 |
] |
|
| 89 |
end |
|
| 59 | 90 |
end |
| 60 | 91 |
end |
| 61 | 92 |
end |
| test/unit/lib/redmine/views/labelled_form_builder_test.rb | ||
|---|---|---|
| 47 | 47 |
assert_include 'value="2.z"', f.hours_field(:hours) |
| 48 | 48 |
end |
| 49 | 49 |
end |
| 50 | ||
| 51 |
def test_wiki_textarea |
|
| 52 |
issue = Issue.new(:description => 'test description') |
|
| 53 |
labelled_form_for(issue) do |f| |
|
| 54 |
output = f.wiki_textarea(:description) |
|
| 55 |
assert_include '<label for="issue_description">Description</label>', output |
|
| 56 |
assert_include '<div class="wiki-editor" data-controller="common-mark-toolbar"', output |
|
| 57 |
assert_include 'test description', output |
|
| 58 |
end |
|
| 59 |
end |
|
| 50 | 60 |
end |
- « Previous
- 1
- 2
- Next »