Commit ·
d4d18df
1
Parent(s): 036913a
feat: Premium formatting toolbar redesign - Custom dropdown menus for font family and size (animated, styled) - Pill-shaped button groups with gradient active states - Alignment applies to paragraph with selection (standard behavior) - Undo/Redo works for typing + formatting (browser native stack) - Font preview in size dropdown - Professional visual polish
Browse files- src/css/components.css +134 -25
- src/index.html +38 -21
- src/js/format.js +118 -50
src/css/components.css
CHANGED
|
@@ -288,8 +288,8 @@
|
|
| 288 |
display: flex;
|
| 289 |
flex-wrap: wrap;
|
| 290 |
align-items: center;
|
| 291 |
-
gap:
|
| 292 |
-
padding:
|
| 293 |
border-bottom: 1px solid var(--color-border);
|
| 294 |
background: var(--color-surface);
|
| 295 |
}
|
|
@@ -297,23 +297,32 @@
|
|
| 297 |
.fmt-group {
|
| 298 |
display: flex;
|
| 299 |
align-items: center;
|
| 300 |
-
gap:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 301 |
}
|
| 302 |
|
| 303 |
.fmt-btn {
|
| 304 |
display: flex;
|
| 305 |
align-items: center;
|
| 306 |
justify-content: center;
|
| 307 |
-
width:
|
| 308 |
-
height:
|
| 309 |
border: none;
|
| 310 |
-
|
| 311 |
-
background: transparent;
|
| 312 |
color: var(--color-text-secondary);
|
| 313 |
cursor: pointer;
|
| 314 |
font-family: 'Georgia', 'Times New Roman', serif;
|
| 315 |
font-size: 14px;
|
| 316 |
-
transition: all
|
|
|
|
| 317 |
}
|
| 318 |
|
| 319 |
.fmt-btn:hover {
|
|
@@ -322,9 +331,8 @@
|
|
| 322 |
}
|
| 323 |
|
| 324 |
.fmt-btn.fmt-active {
|
| 325 |
-
background: var(--color-primary);
|
| 326 |
-
color:
|
| 327 |
-
border-radius: var(--radius-sm, 4px);
|
| 328 |
}
|
| 329 |
|
| 330 |
.fmt-btn svg {
|
|
@@ -333,33 +341,134 @@
|
|
| 333 |
|
| 334 |
.fmt-divider {
|
| 335 |
width: 1px;
|
| 336 |
-
height:
|
| 337 |
background: var(--color-border);
|
| 338 |
-
margin: 0
|
|
|
|
| 339 |
}
|
| 340 |
|
| 341 |
-
|
| 342 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 343 |
border: 1px solid var(--color-border);
|
| 344 |
-
border-radius:
|
| 345 |
background: var(--color-surface);
|
| 346 |
color: var(--color-text-primary);
|
| 347 |
font-family: inherit;
|
| 348 |
-
font-size:
|
|
|
|
| 349 |
cursor: pointer;
|
| 350 |
-
height:
|
| 351 |
-
min-width:
|
| 352 |
-
transition:
|
|
|
|
| 353 |
}
|
| 354 |
|
| 355 |
-
.fmt-
|
| 356 |
-
.fmt-select:focus {
|
| 357 |
border-color: var(--color-primary);
|
| 358 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 359 |
}
|
| 360 |
|
| 361 |
-
.fmt-
|
| 362 |
-
|
| 363 |
}
|
| 364 |
|
| 365 |
.editor-surface {
|
|
|
|
| 288 |
display: flex;
|
| 289 |
flex-wrap: wrap;
|
| 290 |
align-items: center;
|
| 291 |
+
gap: 6px;
|
| 292 |
+
padding: 8px var(--spacing-md);
|
| 293 |
border-bottom: 1px solid var(--color-border);
|
| 294 |
background: var(--color-surface);
|
| 295 |
}
|
|
|
|
| 297 |
.fmt-group {
|
| 298 |
display: flex;
|
| 299 |
align-items: center;
|
| 300 |
+
gap: 1px;
|
| 301 |
+
background: var(--color-border);
|
| 302 |
+
border-radius: 6px;
|
| 303 |
+
overflow: hidden;
|
| 304 |
+
}
|
| 305 |
+
|
| 306 |
+
.fmt-group--font {
|
| 307 |
+
background: transparent;
|
| 308 |
+
gap: 6px;
|
| 309 |
+
overflow: visible;
|
| 310 |
}
|
| 311 |
|
| 312 |
.fmt-btn {
|
| 313 |
display: flex;
|
| 314 |
align-items: center;
|
| 315 |
justify-content: center;
|
| 316 |
+
width: 34px;
|
| 317 |
+
height: 34px;
|
| 318 |
border: none;
|
| 319 |
+
background: var(--color-surface);
|
|
|
|
| 320 |
color: var(--color-text-secondary);
|
| 321 |
cursor: pointer;
|
| 322 |
font-family: 'Georgia', 'Times New Roman', serif;
|
| 323 |
font-size: 14px;
|
| 324 |
+
transition: all 0.15s ease;
|
| 325 |
+
position: relative;
|
| 326 |
}
|
| 327 |
|
| 328 |
.fmt-btn:hover {
|
|
|
|
| 331 |
}
|
| 332 |
|
| 333 |
.fmt-btn.fmt-active {
|
| 334 |
+
background: linear-gradient(135deg, var(--color-primary), var(--color-secondary));
|
| 335 |
+
color: #fff;
|
|
|
|
| 336 |
}
|
| 337 |
|
| 338 |
.fmt-btn svg {
|
|
|
|
| 341 |
|
| 342 |
.fmt-divider {
|
| 343 |
width: 1px;
|
| 344 |
+
height: 28px;
|
| 345 |
background: var(--color-border);
|
| 346 |
+
margin: 0 2px;
|
| 347 |
+
flex-shrink: 0;
|
| 348 |
}
|
| 349 |
|
| 350 |
+
/* ── Custom Dropdowns ── */
|
| 351 |
+
.fmt-dropdown {
|
| 352 |
+
position: relative;
|
| 353 |
+
}
|
| 354 |
+
|
| 355 |
+
.fmt-dropdown__trigger {
|
| 356 |
+
display: flex;
|
| 357 |
+
align-items: center;
|
| 358 |
+
gap: 6px;
|
| 359 |
+
padding: 6px 10px;
|
| 360 |
border: 1px solid var(--color-border);
|
| 361 |
+
border-radius: 6px;
|
| 362 |
background: var(--color-surface);
|
| 363 |
color: var(--color-text-primary);
|
| 364 |
font-family: inherit;
|
| 365 |
+
font-size: 13px;
|
| 366 |
+
font-weight: 500;
|
| 367 |
cursor: pointer;
|
| 368 |
+
height: 34px;
|
| 369 |
+
min-width: 120px;
|
| 370 |
+
transition: all 0.15s ease;
|
| 371 |
+
white-space: nowrap;
|
| 372 |
}
|
| 373 |
|
| 374 |
+
.fmt-dropdown__trigger:hover {
|
|
|
|
| 375 |
border-color: var(--color-primary);
|
| 376 |
+
background: var(--color-surface-elevated);
|
| 377 |
+
}
|
| 378 |
+
|
| 379 |
+
.fmt-dropdown__trigger--size {
|
| 380 |
+
min-width: 56px;
|
| 381 |
+
justify-content: center;
|
| 382 |
+
font-variant-numeric: tabular-nums;
|
| 383 |
+
font-weight: 600;
|
| 384 |
+
}
|
| 385 |
+
|
| 386 |
+
.fmt-dropdown__icon {
|
| 387 |
+
flex-shrink: 0;
|
| 388 |
+
color: var(--color-text-secondary);
|
| 389 |
+
}
|
| 390 |
+
|
| 391 |
+
.fmt-dropdown__chevron {
|
| 392 |
+
flex-shrink: 0;
|
| 393 |
+
color: var(--color-text-secondary);
|
| 394 |
+
transition: transform 0.2s ease;
|
| 395 |
+
margin-inline-start: auto;
|
| 396 |
+
}
|
| 397 |
+
|
| 398 |
+
.fmt-dropdown.open .fmt-dropdown__chevron {
|
| 399 |
+
transform: rotate(180deg);
|
| 400 |
+
}
|
| 401 |
+
|
| 402 |
+
.fmt-dropdown__menu {
|
| 403 |
+
position: absolute;
|
| 404 |
+
top: calc(100% + 4px);
|
| 405 |
+
right: 0;
|
| 406 |
+
min-width: 180px;
|
| 407 |
+
max-height: 280px;
|
| 408 |
+
overflow-y: auto;
|
| 409 |
+
background: var(--color-surface);
|
| 410 |
+
border: 1px solid var(--color-border);
|
| 411 |
+
border-radius: 8px;
|
| 412 |
+
box-shadow: 0 8px 24px rgba(0,0,0,0.15);
|
| 413 |
+
padding: 4px;
|
| 414 |
+
z-index: 100;
|
| 415 |
+
opacity: 0;
|
| 416 |
+
visibility: hidden;
|
| 417 |
+
transform: translateY(-8px);
|
| 418 |
+
transition: all 0.2s ease;
|
| 419 |
+
}
|
| 420 |
+
|
| 421 |
+
.fmt-dropdown.open .fmt-dropdown__menu {
|
| 422 |
+
opacity: 1;
|
| 423 |
+
visibility: visible;
|
| 424 |
+
transform: translateY(0);
|
| 425 |
+
}
|
| 426 |
+
|
| 427 |
+
.fmt-dropdown__menu--size {
|
| 428 |
+
min-width: 100px;
|
| 429 |
+
}
|
| 430 |
+
|
| 431 |
+
.fmt-dropdown__item {
|
| 432 |
+
display: flex;
|
| 433 |
+
align-items: center;
|
| 434 |
+
gap: 8px;
|
| 435 |
+
width: 100%;
|
| 436 |
+
padding: 8px 12px;
|
| 437 |
+
border: none;
|
| 438 |
+
border-radius: 6px;
|
| 439 |
+
background: transparent;
|
| 440 |
+
color: var(--color-text-primary);
|
| 441 |
+
font-family: inherit;
|
| 442 |
+
font-size: 13px;
|
| 443 |
+
cursor: pointer;
|
| 444 |
+
transition: background 0.12s ease;
|
| 445 |
+
text-align: right;
|
| 446 |
+
}
|
| 447 |
+
|
| 448 |
+
.fmt-dropdown__item:hover {
|
| 449 |
+
background: var(--color-surface-elevated);
|
| 450 |
+
}
|
| 451 |
+
|
| 452 |
+
.fmt-dropdown__item--active {
|
| 453 |
+
background: linear-gradient(135deg, var(--color-primary), var(--color-secondary));
|
| 454 |
+
color: #fff;
|
| 455 |
+
}
|
| 456 |
+
|
| 457 |
+
.fmt-dropdown__item--active:hover {
|
| 458 |
+
opacity: 0.9;
|
| 459 |
+
}
|
| 460 |
+
|
| 461 |
+
.fmt-size-preview {
|
| 462 |
+
display: inline-block;
|
| 463 |
+
width: 24px;
|
| 464 |
+
text-align: center;
|
| 465 |
+
font-weight: 700;
|
| 466 |
+
color: var(--color-text-secondary);
|
| 467 |
+
line-height: 1;
|
| 468 |
}
|
| 469 |
|
| 470 |
+
.fmt-dropdown__item--active .fmt-size-preview {
|
| 471 |
+
color: #fff;
|
| 472 |
}
|
| 473 |
|
| 474 |
.editor-surface {
|
src/index.html
CHANGED
|
@@ -492,6 +492,41 @@
|
|
| 492 |
</div>
|
| 493 |
<!-- Formatting Toolbar -->
|
| 494 |
<div class="format-toolbar" id="format-toolbar">
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 495 |
<div class="fmt-group">
|
| 496 |
<button id="fmt-bold" class="fmt-btn" onclick="formatBold()" type="button" title="غامق (Ctrl+B)"><b>B</b></button>
|
| 497 |
<button id="fmt-italic" class="fmt-btn" onclick="formatItalic()" type="button" title="مائل (Ctrl+I)"><i>I</i></button>
|
|
@@ -499,8 +534,9 @@
|
|
| 499 |
<button id="fmt-strikethrough" class="fmt-btn" onclick="formatStrikethrough()" type="button" title="يتوسطه خط"><s>S</s></button>
|
| 500 |
</div>
|
| 501 |
<div class="fmt-divider"></div>
|
|
|
|
| 502 |
<div class="fmt-group">
|
| 503 |
-
<button id="fmt-align-right" class="fmt-btn" onclick="formatAlignRight()" type="button" title="محاذاة لليمين">
|
| 504 |
<svg width="16" height="16" fill="currentColor" viewBox="0 0 24 24"><path d="M3 5h18v2H3V5zm4 4h14v2H7V9zm-4 4h18v2H3v-2zm4 4h14v2H7v-2z"/></svg>
|
| 505 |
</button>
|
| 506 |
<button id="fmt-align-center" class="fmt-btn" onclick="formatAlignCenter()" type="button" title="توسيط">
|
|
@@ -511,6 +547,7 @@
|
|
| 511 |
</button>
|
| 512 |
</div>
|
| 513 |
<div class="fmt-divider"></div>
|
|
|
|
| 514 |
<div class="fmt-group">
|
| 515 |
<button class="fmt-btn" onclick="formatUndo()" type="button" title="تراجع (Ctrl+Z)">
|
| 516 |
<svg width="16" height="16" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 10h10a5 5 0 015 5v2M3 10l4-4M3 10l4 4"/></svg>
|
|
@@ -519,26 +556,6 @@
|
|
| 519 |
<svg width="16" height="16" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 10H11a5 5 0 00-5 5v2M21 10l-4-4M21 10l-4 4"/></svg>
|
| 520 |
</button>
|
| 521 |
</div>
|
| 522 |
-
<div class="fmt-divider"></div>
|
| 523 |
-
<div class="fmt-group">
|
| 524 |
-
<select id="fmt-font-family" class="fmt-select" title="نوع الخط">
|
| 525 |
-
<option value="Cairo" selected>Cairo</option>
|
| 526 |
-
<option value="Arial">Arial</option>
|
| 527 |
-
<option value="Times New Roman">Times New Roman</option>
|
| 528 |
-
<option value="Courier New">Courier New</option>
|
| 529 |
-
<option value="Tahoma">Tahoma</option>
|
| 530 |
-
<option value="Simplified Arabic">Simplified Arabic</option>
|
| 531 |
-
<option value="Traditional Arabic">Traditional Arabic</option>
|
| 532 |
-
</select>
|
| 533 |
-
<select id="fmt-font-size" class="fmt-select fmt-select--size" title="حجم الخط">
|
| 534 |
-
<option value="12px">صغير</option>
|
| 535 |
-
<option value="16px" selected>عادي</option>
|
| 536 |
-
<option value="20px">فوق المتوسط</option>
|
| 537 |
-
<option value="24px">كبير</option>
|
| 538 |
-
<option value="32px">كبير جداً</option>
|
| 539 |
-
<option value="48px">الأقصى</option>
|
| 540 |
-
</select>
|
| 541 |
-
</div>
|
| 542 |
</div>
|
| 543 |
<input type="file" id="doc-import-input" class="sr-only" accept=".txt,.docx,text/plain,application/vnd.openxmlformats-officedocument.wordprocessingml.document" aria-hidden="true">
|
| 544 |
<div id="write-area">
|
|
|
|
| 492 |
</div>
|
| 493 |
<!-- Formatting Toolbar -->
|
| 494 |
<div class="format-toolbar" id="format-toolbar">
|
| 495 |
+
<!-- Font group -->
|
| 496 |
+
<div class="fmt-group fmt-group--font">
|
| 497 |
+
<div class="fmt-dropdown" id="fmt-font-wrap">
|
| 498 |
+
<button class="fmt-dropdown__trigger" type="button" id="fmt-font-trigger" aria-haspopup="true" aria-expanded="false">
|
| 499 |
+
<svg class="fmt-dropdown__icon" width="14" height="14" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 7V4h16v3M9 20h6M12 4v16"/></svg>
|
| 500 |
+
<span id="fmt-font-label">Cairo</span>
|
| 501 |
+
<svg class="fmt-dropdown__chevron" width="10" height="10" fill="currentColor" viewBox="0 0 20 20"><path fill-rule="evenodd" d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z"/></svg>
|
| 502 |
+
</button>
|
| 503 |
+
<div class="fmt-dropdown__menu" id="fmt-font-menu" role="menu">
|
| 504 |
+
<button class="fmt-dropdown__item" data-font="Cairo" role="menuitem" style="font-family:Cairo">Cairo</button>
|
| 505 |
+
<button class="fmt-dropdown__item" data-font="Arial" role="menuitem" style="font-family:Arial">Arial</button>
|
| 506 |
+
<button class="fmt-dropdown__item" data-font="Times New Roman" role="menuitem" style="font-family:'Times New Roman'">Times New Roman</button>
|
| 507 |
+
<button class="fmt-dropdown__item" data-font="Tahoma" role="menuitem" style="font-family:Tahoma">Tahoma</button>
|
| 508 |
+
<button class="fmt-dropdown__item" data-font="Simplified Arabic" role="menuitem" style="font-family:'Simplified Arabic'">Simplified Arabic</button>
|
| 509 |
+
<button class="fmt-dropdown__item" data-font="Traditional Arabic" role="menuitem" style="font-family:'Traditional Arabic'">Traditional Arabic</button>
|
| 510 |
+
<button class="fmt-dropdown__item" data-font="Courier New" role="menuitem" style="font-family:'Courier New'">Courier New</button>
|
| 511 |
+
</div>
|
| 512 |
+
</div>
|
| 513 |
+
<div class="fmt-dropdown" id="fmt-size-wrap">
|
| 514 |
+
<button class="fmt-dropdown__trigger fmt-dropdown__trigger--size" type="button" id="fmt-size-trigger" aria-haspopup="true" aria-expanded="false">
|
| 515 |
+
<span id="fmt-size-label">16</span>
|
| 516 |
+
<svg class="fmt-dropdown__chevron" width="10" height="10" fill="currentColor" viewBox="0 0 20 20"><path fill-rule="evenodd" d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z"/></svg>
|
| 517 |
+
</button>
|
| 518 |
+
<div class="fmt-dropdown__menu fmt-dropdown__menu--size" id="fmt-size-menu" role="menu">
|
| 519 |
+
<button class="fmt-dropdown__item" data-size="12px" role="menuitem"><span class="fmt-size-preview" style="font-size:12px">أ</span> 12</button>
|
| 520 |
+
<button class="fmt-dropdown__item fmt-dropdown__item--active" data-size="16px" role="menuitem"><span class="fmt-size-preview" style="font-size:16px">أ</span> 16</button>
|
| 521 |
+
<button class="fmt-dropdown__item" data-size="20px" role="menuitem"><span class="fmt-size-preview" style="font-size:18px">أ</span> 20</button>
|
| 522 |
+
<button class="fmt-dropdown__item" data-size="24px" role="menuitem"><span class="fmt-size-preview" style="font-size:20px">أ</span> 24</button>
|
| 523 |
+
<button class="fmt-dropdown__item" data-size="32px" role="menuitem"><span class="fmt-size-preview" style="font-size:24px">أ</span> 32</button>
|
| 524 |
+
<button class="fmt-dropdown__item" data-size="48px" role="menuitem"><span class="fmt-size-preview" style="font-size:28px">أ</span> 48</button>
|
| 525 |
+
</div>
|
| 526 |
+
</div>
|
| 527 |
+
</div>
|
| 528 |
+
<div class="fmt-divider"></div>
|
| 529 |
+
<!-- Text style group -->
|
| 530 |
<div class="fmt-group">
|
| 531 |
<button id="fmt-bold" class="fmt-btn" onclick="formatBold()" type="button" title="غامق (Ctrl+B)"><b>B</b></button>
|
| 532 |
<button id="fmt-italic" class="fmt-btn" onclick="formatItalic()" type="button" title="مائل (Ctrl+I)"><i>I</i></button>
|
|
|
|
| 534 |
<button id="fmt-strikethrough" class="fmt-btn" onclick="formatStrikethrough()" type="button" title="يتوسطه خط"><s>S</s></button>
|
| 535 |
</div>
|
| 536 |
<div class="fmt-divider"></div>
|
| 537 |
+
<!-- Alignment group -->
|
| 538 |
<div class="fmt-group">
|
| 539 |
+
<button id="fmt-align-right" class="fmt-btn fmt-active" onclick="formatAlignRight()" type="button" title="محاذاة لليمين">
|
| 540 |
<svg width="16" height="16" fill="currentColor" viewBox="0 0 24 24"><path d="M3 5h18v2H3V5zm4 4h14v2H7V9zm-4 4h18v2H3v-2zm4 4h14v2H7v-2z"/></svg>
|
| 541 |
</button>
|
| 542 |
<button id="fmt-align-center" class="fmt-btn" onclick="formatAlignCenter()" type="button" title="توسيط">
|
|
|
|
| 547 |
</button>
|
| 548 |
</div>
|
| 549 |
<div class="fmt-divider"></div>
|
| 550 |
+
<!-- Undo/Redo group -->
|
| 551 |
<div class="fmt-group">
|
| 552 |
<button class="fmt-btn" onclick="formatUndo()" type="button" title="تراجع (Ctrl+Z)">
|
| 553 |
<svg width="16" height="16" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 10h10a5 5 0 015 5v2M3 10l4-4M3 10l4 4"/></svg>
|
|
|
|
| 556 |
<svg width="16" height="16" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 10H11a5 5 0 00-5 5v2M21 10l-4-4M21 10l-4 4"/></svg>
|
| 557 |
</button>
|
| 558 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 559 |
</div>
|
| 560 |
<input type="file" id="doc-import-input" class="sr-only" accept=".txt,.docx,text/plain,application/vnd.openxmlformats-officedocument.wordprocessingml.document" aria-hidden="true">
|
| 561 |
<div id="write-area">
|
src/js/format.js
CHANGED
|
@@ -17,11 +17,11 @@ function formatItalic() { execFormat('italic'); }
|
|
| 17 |
function formatUnderline() { execFormat('underline'); }
|
| 18 |
function formatStrikethrough() { execFormat('strikethrough'); }
|
| 19 |
|
| 20 |
-
/* ── Undo / Redo ── */
|
| 21 |
function formatUndo() { execFormat('undo'); }
|
| 22 |
function formatRedo() { execFormat('redo'); }
|
| 23 |
|
| 24 |
-
/* ── Alignment ── */
|
| 25 |
function formatAlignRight() { execFormat('justifyRight'); }
|
| 26 |
function formatAlignCenter() { execFormat('justifyCenter'); }
|
| 27 |
function formatAlignLeft() { execFormat('justifyLeft'); }
|
|
@@ -29,36 +29,64 @@ function formatAlignLeft() { execFormat('justifyLeft'); }
|
|
| 29 |
/* ── Font family ── */
|
| 30 |
function formatFont(fontName) {
|
| 31 |
execFormat('fontName', fontName);
|
|
|
|
|
|
|
|
|
|
|
|
|
| 32 |
}
|
| 33 |
|
| 34 |
/* ── Font size ── */
|
| 35 |
-
// execCommand fontSize only supports 1-7, so we use CSS instead
|
| 36 |
function formatFontSize(size) {
|
| 37 |
const sel = window.getSelection();
|
| 38 |
if (!sel.rangeCount) return;
|
| 39 |
|
| 40 |
const range = sel.getRangeAt(0);
|
| 41 |
-
if (range.collapsed)
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
range.
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 60 |
}
|
| 61 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 62 |
updateFormatState();
|
| 63 |
}
|
| 64 |
|
|
@@ -71,33 +99,45 @@ function updateFormatState() {
|
|
| 71 |
'fmt-italic': 'italic',
|
| 72 |
'fmt-underline': 'underline',
|
| 73 |
'fmt-strikethrough': 'strikeThrough',
|
| 74 |
-
'fmt-align-right': 'justifyRight',
|
| 75 |
-
'fmt-align-center': 'justifyCenter',
|
| 76 |
-
'fmt-align-left': 'justifyLeft',
|
| 77 |
};
|
| 78 |
|
| 79 |
Object.entries(btnMap).forEach(([id, command]) => {
|
| 80 |
const btn = document.getElementById(id);
|
| 81 |
if (btn) {
|
| 82 |
-
|
| 83 |
-
btn.classList.toggle('fmt-active', isActive);
|
| 84 |
}
|
| 85 |
});
|
| 86 |
|
| 87 |
-
//
|
| 88 |
-
const
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
}
|
| 98 |
-
}
|
| 99 |
}
|
| 100 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 101 |
}
|
| 102 |
|
| 103 |
/**
|
|
@@ -114,19 +154,47 @@ function initFormatToolbar() {
|
|
| 114 |
}
|
| 115 |
});
|
| 116 |
|
| 117 |
-
// Font
|
| 118 |
-
const
|
| 119 |
-
if (
|
| 120 |
-
|
| 121 |
-
|
|
|
|
| 122 |
});
|
| 123 |
}
|
| 124 |
|
| 125 |
-
// Font
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 130 |
});
|
| 131 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 132 |
}
|
|
|
|
| 17 |
function formatUnderline() { execFormat('underline'); }
|
| 18 |
function formatStrikethrough() { execFormat('strikethrough'); }
|
| 19 |
|
| 20 |
+
/* ── Undo / Redo (handles both typing and formatting) ── */
|
| 21 |
function formatUndo() { execFormat('undo'); }
|
| 22 |
function formatRedo() { execFormat('redo'); }
|
| 23 |
|
| 24 |
+
/* ── Alignment (applies to paragraph containing selection) ── */
|
| 25 |
function formatAlignRight() { execFormat('justifyRight'); }
|
| 26 |
function formatAlignCenter() { execFormat('justifyCenter'); }
|
| 27 |
function formatAlignLeft() { execFormat('justifyLeft'); }
|
|
|
|
| 29 |
/* ── Font family ── */
|
| 30 |
function formatFont(fontName) {
|
| 31 |
execFormat('fontName', fontName);
|
| 32 |
+
// Update the dropdown label
|
| 33 |
+
const label = document.getElementById('fmt-font-label');
|
| 34 |
+
if (label) label.textContent = fontName;
|
| 35 |
+
closeAllFmtDropdowns();
|
| 36 |
}
|
| 37 |
|
| 38 |
/* ── Font size ── */
|
|
|
|
| 39 |
function formatFontSize(size) {
|
| 40 |
const sel = window.getSelection();
|
| 41 |
if (!sel.rangeCount) return;
|
| 42 |
|
| 43 |
const range = sel.getRangeAt(0);
|
| 44 |
+
if (range.collapsed) {
|
| 45 |
+
// No selection — size will apply to next typed text
|
| 46 |
+
// Use a zero-width space trick
|
| 47 |
+
const span = document.createElement('span');
|
| 48 |
+
span.style.fontSize = size;
|
| 49 |
+
span.textContent = '\u200B';
|
| 50 |
+
range.insertNode(span);
|
| 51 |
+
// Place cursor after the span
|
| 52 |
+
const newRange = document.createRange();
|
| 53 |
+
newRange.setStartAfter(span);
|
| 54 |
+
newRange.collapse(true);
|
| 55 |
+
sel.removeAllRanges();
|
| 56 |
+
sel.addRange(newRange);
|
| 57 |
+
} else {
|
| 58 |
+
// Wrap selected text
|
| 59 |
+
const span = document.createElement('span');
|
| 60 |
+
span.style.fontSize = size;
|
| 61 |
+
try {
|
| 62 |
+
range.surroundContents(span);
|
| 63 |
+
} catch (e) {
|
| 64 |
+
// Fallback: use execCommand
|
| 65 |
+
execFormat('fontSize', '4');
|
| 66 |
+
const editor = getEditorElement();
|
| 67 |
+
if (editor) {
|
| 68 |
+
editor.querySelectorAll('font[size="4"]').forEach(f => {
|
| 69 |
+
const s = document.createElement('span');
|
| 70 |
+
s.style.fontSize = size;
|
| 71 |
+
s.innerHTML = f.innerHTML;
|
| 72 |
+
f.replaceWith(s);
|
| 73 |
+
});
|
| 74 |
+
}
|
| 75 |
}
|
| 76 |
}
|
| 77 |
+
|
| 78 |
+
// Update label
|
| 79 |
+
const label = document.getElementById('fmt-size-label');
|
| 80 |
+
if (label) label.textContent = parseInt(size);
|
| 81 |
+
|
| 82 |
+
// Update active item
|
| 83 |
+
document.querySelectorAll('#fmt-size-menu .fmt-dropdown__item').forEach(item => {
|
| 84 |
+
item.classList.toggle('fmt-dropdown__item--active', item.dataset.size === size);
|
| 85 |
+
});
|
| 86 |
+
|
| 87 |
+
closeAllFmtDropdowns();
|
| 88 |
+
const editor = getEditorElement();
|
| 89 |
+
if (editor) editor.focus();
|
| 90 |
updateFormatState();
|
| 91 |
}
|
| 92 |
|
|
|
|
| 99 |
'fmt-italic': 'italic',
|
| 100 |
'fmt-underline': 'underline',
|
| 101 |
'fmt-strikethrough': 'strikeThrough',
|
|
|
|
|
|
|
|
|
|
| 102 |
};
|
| 103 |
|
| 104 |
Object.entries(btnMap).forEach(([id, command]) => {
|
| 105 |
const btn = document.getElementById(id);
|
| 106 |
if (btn) {
|
| 107 |
+
btn.classList.toggle('fmt-active', document.queryCommandState(command));
|
|
|
|
| 108 |
}
|
| 109 |
});
|
| 110 |
|
| 111 |
+
// Alignment — mutually exclusive
|
| 112 |
+
const alignMap = {
|
| 113 |
+
'fmt-align-right': 'justifyRight',
|
| 114 |
+
'fmt-align-center': 'justifyCenter',
|
| 115 |
+
'fmt-align-left': 'justifyLeft',
|
| 116 |
+
};
|
| 117 |
+
Object.entries(alignMap).forEach(([id, command]) => {
|
| 118 |
+
const btn = document.getElementById(id);
|
| 119 |
+
if (btn) {
|
| 120 |
+
btn.classList.toggle('fmt-active', document.queryCommandState(command));
|
|
|
|
|
|
|
| 121 |
}
|
| 122 |
+
});
|
| 123 |
+
}
|
| 124 |
+
|
| 125 |
+
/**
|
| 126 |
+
* Close all formatting dropdowns
|
| 127 |
+
*/
|
| 128 |
+
function closeAllFmtDropdowns() {
|
| 129 |
+
document.querySelectorAll('.fmt-dropdown').forEach(d => d.classList.remove('open'));
|
| 130 |
+
}
|
| 131 |
+
|
| 132 |
+
/**
|
| 133 |
+
* Toggle a specific dropdown
|
| 134 |
+
*/
|
| 135 |
+
function toggleFmtDropdown(wrapperId) {
|
| 136 |
+
const wrap = document.getElementById(wrapperId);
|
| 137 |
+
if (!wrap) return;
|
| 138 |
+
const isOpen = wrap.classList.contains('open');
|
| 139 |
+
closeAllFmtDropdowns();
|
| 140 |
+
if (!isOpen) wrap.classList.add('open');
|
| 141 |
}
|
| 142 |
|
| 143 |
/**
|
|
|
|
| 154 |
}
|
| 155 |
});
|
| 156 |
|
| 157 |
+
// Font dropdown trigger
|
| 158 |
+
const fontTrigger = document.getElementById('fmt-font-trigger');
|
| 159 |
+
if (fontTrigger) {
|
| 160 |
+
fontTrigger.addEventListener('click', (e) => {
|
| 161 |
+
e.stopPropagation();
|
| 162 |
+
toggleFmtDropdown('fmt-font-wrap');
|
| 163 |
});
|
| 164 |
}
|
| 165 |
|
| 166 |
+
// Font items
|
| 167 |
+
document.querySelectorAll('#fmt-font-menu .fmt-dropdown__item').forEach(item => {
|
| 168 |
+
item.addEventListener('click', () => {
|
| 169 |
+
formatFont(item.dataset.font);
|
| 170 |
+
});
|
| 171 |
+
});
|
| 172 |
+
|
| 173 |
+
// Size dropdown trigger
|
| 174 |
+
const sizeTrigger = document.getElementById('fmt-size-trigger');
|
| 175 |
+
if (sizeTrigger) {
|
| 176 |
+
sizeTrigger.addEventListener('click', (e) => {
|
| 177 |
+
e.stopPropagation();
|
| 178 |
+
toggleFmtDropdown('fmt-size-wrap');
|
| 179 |
});
|
| 180 |
}
|
| 181 |
+
|
| 182 |
+
// Size items
|
| 183 |
+
document.querySelectorAll('#fmt-size-menu .fmt-dropdown__item').forEach(item => {
|
| 184 |
+
item.addEventListener('click', () => {
|
| 185 |
+
formatFontSize(item.dataset.size);
|
| 186 |
+
});
|
| 187 |
+
});
|
| 188 |
+
|
| 189 |
+
// Close dropdowns when clicking outside
|
| 190 |
+
document.addEventListener('click', (e) => {
|
| 191 |
+
if (!e.target.closest('.fmt-dropdown')) {
|
| 192 |
+
closeAllFmtDropdowns();
|
| 193 |
+
}
|
| 194 |
+
});
|
| 195 |
+
|
| 196 |
+
// Close dropdowns on Escape
|
| 197 |
+
document.addEventListener('keydown', (e) => {
|
| 198 |
+
if (e.key === 'Escape') closeAllFmtDropdowns();
|
| 199 |
+
});
|
| 200 |
}
|