fixes
This commit is contained in:
+386
-98
@@ -441,8 +441,8 @@ textarea:focus {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.workspace > .editor-column,
|
||||
.workspace > .preview-column {
|
||||
.workspace>.editor-column,
|
||||
.workspace>.preview-column {
|
||||
transition: opacity .18s ease, transform .18s ease;
|
||||
}
|
||||
|
||||
@@ -2138,6 +2138,12 @@ dialog::backdrop {
|
||||
border-radius: 4px;
|
||||
outline: none;
|
||||
cursor: text;
|
||||
user-select: text;
|
||||
-webkit-user-select: text;
|
||||
}
|
||||
|
||||
.preview-editable[contenteditable="true"] {
|
||||
caret-color: auto;
|
||||
}
|
||||
|
||||
.preview-editable:hover {
|
||||
@@ -3799,7 +3805,7 @@ dialog::backdrop {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.emoji-picker > summary {
|
||||
.emoji-picker>summary {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
min-height: 34px;
|
||||
@@ -3813,12 +3819,12 @@ dialog::backdrop {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.emoji-picker > summary::-webkit-details-marker {
|
||||
.emoji-picker>summary::-webkit-details-marker {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.emoji-picker > summary:hover,
|
||||
.emoji-picker[open] > summary {
|
||||
.emoji-picker>summary:hover,
|
||||
.emoji-picker[open]>summary {
|
||||
border-color: var(--border);
|
||||
background: var(--surface-2);
|
||||
color: white;
|
||||
@@ -4014,10 +4020,13 @@ dialog::backdrop {
|
||||
}
|
||||
|
||||
@media (max-width: 720px) {
|
||||
.authorship-layer { left: 42px; }
|
||||
.authorship-layer {
|
||||
left: 42px;
|
||||
}
|
||||
|
||||
.hide-editor-line-numbers .authorship-layer {
|
||||
left: 0;
|
||||
}
|
||||
left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* Multiple authors can contribute to one line. */
|
||||
@@ -4245,7 +4254,7 @@ dialog::backdrop {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.pad-page .header-actions > * {
|
||||
.pad-page .header-actions>* {
|
||||
width: 100%;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
@@ -4255,6 +4264,23 @@ dialog::backdrop {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.pad-page .header-actions .page-settings>summary {
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
width: 100%;
|
||||
min-height: 36px;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.pad-page .header-actions .page-settings-menu {
|
||||
position: static;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
margin-top: 8px;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.pad-page .header-actions .public-task-toggle {
|
||||
min-height: 40px;
|
||||
padding: 8px 10px;
|
||||
@@ -4420,7 +4446,10 @@ dialog::backdrop {
|
||||
}
|
||||
|
||||
/* Quick actions appear when the editor header switches to its compact menu. */
|
||||
.mobile-editor-bubble { display: none; }
|
||||
.mobile-editor-bubble {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media (max-width: 1499px) {
|
||||
.pad-page .mobile-editor-bubble {
|
||||
position: fixed;
|
||||
@@ -4432,13 +4461,14 @@ dialog::backdrop {
|
||||
gap: 5px;
|
||||
max-width: calc(100vw - 20px);
|
||||
padding: 5px;
|
||||
border: 1px solid rgba(255,255,255,.12);
|
||||
border: 1px solid rgba(255, 255, 255, .12);
|
||||
border-radius: 999px;
|
||||
background: rgba(17,21,28,.88);
|
||||
box-shadow: 0 8px 24px rgba(0,0,0,.28);
|
||||
background: rgba(17, 21, 28, .88);
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, .28);
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
.mobile-editor-bubble > button {
|
||||
|
||||
.mobile-editor-bubble>button {
|
||||
display: inline-grid;
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
@@ -4446,17 +4476,19 @@ dialog::backdrop {
|
||||
place-items: center;
|
||||
border: 0;
|
||||
border-radius: 50%;
|
||||
background: rgba(255,255,255,.07);
|
||||
background: rgba(255, 255, 255, .07);
|
||||
color: var(--text);
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.mobile-color-dot {
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
border: 2px solid rgba(255,255,255,.75);
|
||||
border: 2px solid rgba(255, 255, 255, .75);
|
||||
border-radius: 50%;
|
||||
background: var(--owner, var(--accent));
|
||||
}
|
||||
|
||||
.mobile-connection-status {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
@@ -4467,12 +4499,16 @@ dialog::backdrop {
|
||||
font-size: .72rem;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.mobile-connection-status #mobile-status-text {
|
||||
max-width: 92px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.pad-page .toast { bottom: calc(62px + env(safe-area-inset-bottom, 0px)); }
|
||||
|
||||
.pad-page .toast {
|
||||
bottom: calc(62px + env(safe-area-inset-bottom, 0px));
|
||||
}
|
||||
}
|
||||
|
||||
/* Movable quick actions for compact editor screens. */
|
||||
@@ -4496,35 +4532,12 @@ dialog::backdrop {
|
||||
max-height: min(560px, calc(100dvh - 96px));
|
||||
}
|
||||
|
||||
.mobile-time-button {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.mobile-time-preview {
|
||||
position: absolute;
|
||||
right: 8px;
|
||||
bottom: calc(100% + 8px);
|
||||
padding: 6px 9px;
|
||||
border: 1px solid var(--border-strong);
|
||||
border-radius: 8px;
|
||||
background: var(--surface);
|
||||
box-shadow: 0 8px 24px rgb(0 0 0 / 32%);
|
||||
color: var(--text);
|
||||
font-size: .78rem;
|
||||
font-variant-numeric: tabular-nums;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.mobile-time-preview[hidden] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.pad-page .mobile-editor-bubble.is-dragging {
|
||||
transition: none;
|
||||
cursor: grabbing;
|
||||
}
|
||||
|
||||
.mobile-editor-bubble > .mobile-bubble-drag {
|
||||
.mobile-editor-bubble>.mobile-bubble-drag {
|
||||
width: 24px;
|
||||
background: transparent;
|
||||
color: var(--muted-2);
|
||||
@@ -4562,39 +4575,120 @@ dialog::backdrop {
|
||||
}
|
||||
|
||||
/* Authorship display modes */
|
||||
.editor-column-label { display: flex; align-items: center; justify-content: space-between; gap: 12px; }
|
||||
.authorship-mode-control { display: inline-flex; gap: 2px; padding: 2px; border: 1px solid var(--border); border-radius: 8px; background: color-mix(in srgb, var(--panel) 88%, transparent); }
|
||||
.authorship-mode-control button { min-height: 26px; padding: 0 9px; border: 0; border-radius: 6px; background: transparent; color: var(--muted); font-size: 11px; }
|
||||
.authorship-mode-control button.active { background: var(--surface-strong, #262b35); color: var(--text); }
|
||||
.participant-badges { display: flex; flex-wrap: wrap; align-items: center; align-content: center; gap: 6px; min-width: 0; min-height: 0; padding: 7px 12px; border-bottom: 1px solid var(--border); }
|
||||
.participant-badges[hidden] { display: none; }
|
||||
.participant-badge { display: inline-flex; flex: 0 0 auto; align-items: center; width: auto; max-width: 100%; min-height: 0; padding: 3px 8px; border: 1px solid color-mix(in srgb, var(--owner) 65%, transparent); border-radius: 999px; background: color-mix(in srgb, var(--owner) 18%, transparent); font: 600 11px/1.3 system-ui, sans-serif; }
|
||||
@media (max-width: 720px) {
|
||||
.editor-column-label { align-items: flex-start; }
|
||||
.authorship-mode-control button { padding-inline: 7px; }
|
||||
.participant-badges { padding: 6px 8px; }
|
||||
.public-task-toggle { width: 100%; }
|
||||
.editor-column-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.public-page-options { display: grid; gap: 4px; align-content: center; }
|
||||
.public-page-options .public-task-toggle { min-height: 24px; }
|
||||
@media (max-width: 720px) { .public-page-options { width: 100%; } }
|
||||
.authorship-mode-control {
|
||||
display: inline-flex;
|
||||
gap: 2px;
|
||||
padding: 2px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
background: color-mix(in srgb, var(--panel) 88%, transparent);
|
||||
}
|
||||
|
||||
.authorship-mode-control button {
|
||||
min-height: 26px;
|
||||
padding: 0 9px;
|
||||
border: 0;
|
||||
border-radius: 6px;
|
||||
background: transparent;
|
||||
color: var(--muted);
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.authorship-mode-control button.active {
|
||||
background: var(--surface-strong, #262b35);
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.participant-badges {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
align-content: center;
|
||||
gap: 6px;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
padding: 7px 12px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.participant-badges[hidden] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.participant-badge {
|
||||
display: inline-flex;
|
||||
flex: 0 0 auto;
|
||||
align-items: center;
|
||||
width: auto;
|
||||
max-width: 100%;
|
||||
min-height: 0;
|
||||
padding: 3px 8px;
|
||||
border: 1px solid color-mix(in srgb, var(--owner) 65%, transparent);
|
||||
border-radius: 999px;
|
||||
background: color-mix(in srgb, var(--owner) 18%, transparent);
|
||||
font: 600 11px/1.3 system-ui, sans-serif;
|
||||
}
|
||||
|
||||
@media (max-width: 720px) {
|
||||
.editor-column-label {
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.authorship-mode-control button {
|
||||
padding-inline: 7px;
|
||||
}
|
||||
|
||||
.participant-badges {
|
||||
padding: 6px 8px;
|
||||
}
|
||||
|
||||
.public-task-toggle {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.public-page-options {
|
||||
display: grid;
|
||||
gap: 4px;
|
||||
align-content: center;
|
||||
}
|
||||
|
||||
.public-page-options .public-task-toggle {
|
||||
min-height: 24px;
|
||||
}
|
||||
|
||||
@media (max-width: 720px) {
|
||||
.public-page-options {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
/* Keep the whole editor surface consistent in Simple and Full modes. */
|
||||
.editor-shell {
|
||||
background: #0d1015;
|
||||
}
|
||||
|
||||
.editor-shell textarea {
|
||||
z-index: 3;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.authorship-layer {
|
||||
z-index: 2;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.owner-labels {
|
||||
z-index: 4;
|
||||
}
|
||||
|
||||
.line-gutter {
|
||||
z-index: 5;
|
||||
}
|
||||
@@ -4688,37 +4782,105 @@ dialog::backdrop {
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.pad-page .editor-column > .column-label,
|
||||
.pad-page .editor-column > .participant-badges {
|
||||
.pad-page .editor-column>.column-label,
|
||||
.pad-page .editor-column>.participant-badges {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.pad-page .editor-column > .editor-shell {
|
||||
.pad-page .editor-column>.editor-shell {
|
||||
flex: 1 1 auto;
|
||||
width: 100%;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
/* Compact Page settings dropdown. */
|
||||
.page-settings { position: relative; }
|
||||
.page-settings > summary { list-style: none; cursor: pointer; }
|
||||
.page-settings > summary::-webkit-details-marker { display: none; }
|
||||
.page-settings-menu { position: absolute; z-index: 30; top: calc(100% + 6px); right: 0; display: grid; gap: 4px; min-width: 220px; padding: 8px; border: 1px solid var(--border); border-radius: 9px; background: var(--panel); box-shadow: 0 12px 30px rgba(0,0,0,.28); }
|
||||
.page-settings-menu .public-task-toggle { min-height: 30px; padding: 5px 7px; border-radius: 6px; }
|
||||
.page-settings-menu .public-task-toggle:hover { background: color-mix(in srgb, var(--surface-strong, #262b35) 72%, transparent); }
|
||||
#publish-page:disabled { opacity: .45; cursor: not-allowed; }
|
||||
.page-settings {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.page-settings>summary {
|
||||
list-style: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.page-settings>summary::-webkit-details-marker {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.page-settings-menu {
|
||||
position: absolute;
|
||||
z-index: 30;
|
||||
top: calc(100% + 6px);
|
||||
right: 0;
|
||||
display: grid;
|
||||
gap: 4px;
|
||||
min-width: 220px;
|
||||
padding: 8px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 9px;
|
||||
background: var(--panel);
|
||||
box-shadow: 0 12px 30px rgba(0, 0, 0, .28);
|
||||
}
|
||||
|
||||
.page-settings-menu .public-task-toggle {
|
||||
min-height: 30px;
|
||||
padding: 5px 7px;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.page-settings-menu .public-task-toggle:hover {
|
||||
background: color-mix(in srgb, var(--surface-strong, #262b35) 72%, transparent);
|
||||
}
|
||||
|
||||
#publish-page:disabled {
|
||||
opacity: .45;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* Keep the Simple/Full switch inside the editor label frame. */
|
||||
.authorship-mode-control { padding: 1px; border-radius: 7px; }
|
||||
.authorship-mode-control button { min-height: 20px; height: 20px; padding: 0 7px; border-radius: 5px; line-height: 20px; }
|
||||
.authorship-mode-control {
|
||||
padding: 1px;
|
||||
border-radius: 7px;
|
||||
}
|
||||
|
||||
.authorship-mode-control button {
|
||||
min-height: 20px;
|
||||
height: 20px;
|
||||
padding: 0 7px;
|
||||
border-radius: 5px;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
/* Make secondary editor actions read clearly as buttons. */
|
||||
.markdown-more > summary,
|
||||
.pad-page .toolbar-action { display: inline-flex; align-items: center; justify-content: center; min-height: 30px; padding: 5px 10px; border: 1px solid var(--border); border-radius: 7px; background: var(--surface-strong, #262b35); color: var(--text); font-weight: 600; cursor: pointer; box-shadow: inset 0 1px 0 rgba(255,255,255,.04); }
|
||||
.markdown-more > summary:hover,
|
||||
.pad-page .toolbar-action:hover { border-color: color-mix(in srgb, var(--accent) 55%, var(--border)); filter: brightness(1.08); }
|
||||
.markdown-more > summary { list-style: none; }
|
||||
.markdown-more > summary::-webkit-details-marker { display: none; }
|
||||
.markdown-more>summary,
|
||||
.pad-page .toolbar-action {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 30px;
|
||||
padding: 5px 10px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 7px;
|
||||
background: var(--surface-strong, #262b35);
|
||||
color: var(--text);
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
box-shadow: inset 0 1px 0 rgba(255, 255, 255, .04);
|
||||
}
|
||||
|
||||
.markdown-more>summary:hover,
|
||||
.pad-page .toolbar-action:hover {
|
||||
border-color: color-mix(in srgb, var(--accent) 55%, var(--border));
|
||||
filter: brightness(1.08);
|
||||
}
|
||||
|
||||
.markdown-more>summary {
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.markdown-more>summary::-webkit-details-marker {
|
||||
display: none;
|
||||
}
|
||||
|
||||
|
||||
/* Final UI fixes: opaque Page settings and per-note authorship controls. */
|
||||
@@ -4727,32 +4889,151 @@ dialog::backdrop {
|
||||
opacity: 1;
|
||||
backdrop-filter: none;
|
||||
}
|
||||
.authorship-controls { display: inline-flex; align-items: center; gap: 7px; }
|
||||
.authorship-color-toggle { display: inline-flex; align-items: center; gap: 5px; min-height: 22px; color: var(--muted); font-size: 11px; cursor: pointer; }
|
||||
.authorship-color-toggle input { width: 28px; height: 16px; margin: 0; accent-color: var(--accent); }
|
||||
.authorship-layer { overflow: hidden; padding: 0; }
|
||||
.authorship-canvas { position: absolute; top: 0; left: 0; box-sizing: border-box; color: transparent; white-space: pre; tab-size: 4; will-change: transform; }
|
||||
.share-link-row .share-link-info { justify-self: end; width: min(100%, 520px); text-align: right; }
|
||||
.share-link-row .share-link-inline { width: 100%; text-align: left; }
|
||||
@media (max-width: 800px) {
|
||||
.share-link-row .share-link-info { justify-self: stretch; width: 100%; text-align: left; }
|
||||
.authorship-controls { max-width: 100%; gap: 5px; }
|
||||
|
||||
.authorship-controls {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 7px;
|
||||
}
|
||||
|
||||
.page-settings-menu .public-task-toggle { background: #171c24; }
|
||||
.page-settings-menu .public-task-toggle:hover { background: #242b36; }
|
||||
.authorship-color-toggle {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
min-height: 22px;
|
||||
color: var(--muted);
|
||||
font-size: 11px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.authorship-color-toggle input {
|
||||
width: 28px;
|
||||
height: 16px;
|
||||
margin: 0;
|
||||
accent-color: var(--accent);
|
||||
}
|
||||
|
||||
.authorship-layer {
|
||||
overflow: hidden;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.authorship-canvas {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
box-sizing: border-box;
|
||||
color: transparent;
|
||||
white-space: pre;
|
||||
tab-size: 4;
|
||||
will-change: transform;
|
||||
}
|
||||
|
||||
.share-link-row .share-link-info {
|
||||
justify-self: end;
|
||||
width: min(100%, 520px);
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.share-link-row .share-link-inline {
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
@media (max-width: 800px) {
|
||||
.share-link-row .share-link-info {
|
||||
justify-self: stretch;
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.authorship-controls {
|
||||
max-width: 100%;
|
||||
gap: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
.page-settings-menu .public-task-toggle {
|
||||
background: #171c24;
|
||||
}
|
||||
|
||||
.page-settings-menu .public-task-toggle:hover {
|
||||
background: #242b36;
|
||||
}
|
||||
|
||||
/* Shared editor display settings */
|
||||
.authorship-controls { display: flex; align-items: center; gap: 7px; flex-wrap: wrap; }
|
||||
.switch-control { display: inline-flex; align-items: center; gap: 6px; cursor: pointer; font-size: 11px; color: var(--muted); user-select: none; }
|
||||
.switch-control input { position: absolute; opacity: 0; pointer-events: none; }
|
||||
.switch-control__track { position: relative; width: 30px; height: 16px; border: 1px solid var(--border); border-radius: 999px; background: var(--surface-strong, #262b35); transition: .15s ease; }
|
||||
.switch-control__track::after { content: ""; position: absolute; width: 10px; height: 10px; left: 2px; top: 2px; border-radius: 50%; background: var(--muted); transition: .15s ease; }
|
||||
.switch-control input:checked + .switch-control__track { border-color: var(--accent); background: color-mix(in srgb, var(--accent) 28%, var(--surface-strong, #262b35)); }
|
||||
.switch-control input:checked + .switch-control__track::after { transform: translateX(14px); background: var(--accent); }
|
||||
.editor-settings-save { min-height: 22px; height: 22px; padding: 0 8px; font-size: 11px; }
|
||||
.editor-settings-save:disabled { opacity: .45; cursor: not-allowed; }
|
||||
.share-link-row .share-link-info { margin-top: 8px; justify-self: start; text-align: left; }
|
||||
.authorship-controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 7px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.switch-control {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
cursor: pointer;
|
||||
font-size: 11px;
|
||||
color: var(--muted);
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.switch-control input {
|
||||
position: absolute;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.switch-control__track {
|
||||
position: relative;
|
||||
width: 30px;
|
||||
height: 16px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 999px;
|
||||
background: var(--surface-strong, #262b35);
|
||||
transition: .15s ease;
|
||||
}
|
||||
|
||||
.switch-control__track::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
left: 2px;
|
||||
top: 2px;
|
||||
border-radius: 50%;
|
||||
background: var(--muted);
|
||||
transition: .15s ease;
|
||||
}
|
||||
|
||||
.switch-control input:checked+.switch-control__track {
|
||||
border-color: var(--accent);
|
||||
background: color-mix(in srgb, var(--accent) 28%, var(--surface-strong, #262b35));
|
||||
}
|
||||
|
||||
.switch-control input:checked+.switch-control__track::after {
|
||||
transform: translateX(14px);
|
||||
background: var(--accent);
|
||||
}
|
||||
|
||||
.editor-settings-save {
|
||||
min-height: 22px;
|
||||
height: 22px;
|
||||
padding: 0 8px;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.editor-settings-save:disabled {
|
||||
opacity: .45;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.share-link-row .share-link-info {
|
||||
margin-top: 8px;
|
||||
justify-self: start;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.preview-editable--source {
|
||||
font-family: var(--font-mono, ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace);
|
||||
@@ -4768,17 +5049,21 @@ dialog::backdrop {
|
||||
padding-top: 12px;
|
||||
border-top: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.share-link-list-wrap h5 {
|
||||
margin: 0;
|
||||
font-size: .86rem;
|
||||
}
|
||||
|
||||
.share-link-list {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.share-link-row {
|
||||
grid-template-columns: minmax(130px, auto) 100px auto auto;
|
||||
align-items: end;
|
||||
}
|
||||
|
||||
.share-link-row .share-link-info {
|
||||
grid-column: 1 / -1;
|
||||
width: 100%;
|
||||
@@ -4786,14 +5071,17 @@ dialog::backdrop {
|
||||
text-align: left;
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.share-link-row .share-link-inline {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@media (max-width: 800px) {
|
||||
.share-link-row {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.share-link-row .share-link-info {
|
||||
grid-column: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
+3
-1
@@ -118,7 +118,9 @@
|
||||
<header class="resources-panel__header">
|
||||
<h2>My notes and workspaces</h2>
|
||||
<p class="dialog-copy">Items created while signed in are assigned to your account.</p>
|
||||
<p class="resources-access-rules"><strong>Access rules:</strong> Public items open from their link; a password adds link-based protection. Private items are visible only to their owner and explicitly shared accounts or valid share links. Unauthorized visitors receive a not-found response.</p>
|
||||
<p class="resources-access-rules"><strong>Access rules:</strong> Public items open from their link; a password
|
||||
adds link-based protection. Private items are visible only to their owner and explicitly shared accounts or
|
||||
valid share links. Unauthorized visitors receive a not-found response.</p>
|
||||
</header>
|
||||
<div id="resources-list" class="resources-list"></div>
|
||||
<p id="resources-error" class="form-message error" role="alert"></p>
|
||||
|
||||
+12
-1
@@ -171,6 +171,17 @@ export function lineOwners(content, model) {
|
||||
});
|
||||
}
|
||||
|
||||
export function syncAuthorshipLayer(layer, editor) {
|
||||
if (!layer || !editor) return;
|
||||
const canvas = layer.querySelector(".authorship-canvas");
|
||||
if (!canvas) return;
|
||||
canvas.style.transform = `translate3d(${-editor.scrollLeft}px, ${-editor.scrollTop}px, 0)`;
|
||||
// The canvas already mirrors the textarea scroll through its transform.
|
||||
// Keeping the overlay element itself at zero prevents a double offset.
|
||||
layer.scrollTop = 0;
|
||||
layer.scrollLeft = 0;
|
||||
}
|
||||
|
||||
export function renderAuthorshipLayer(layer, editor, model, colorFor) {
|
||||
if (!layer) return;
|
||||
const style = getComputedStyle(editor);
|
||||
@@ -202,6 +213,6 @@ export function renderAuthorshipLayer(layer, editor, model, colorFor) {
|
||||
}
|
||||
if (cursor < text.length) canvas.append(document.createTextNode(text.slice(cursor)));
|
||||
if (!text.endsWith("\n")) canvas.append(document.createTextNode("\n"));
|
||||
canvas.style.transform = `translate3d(${-editor.scrollLeft}px, ${-editor.scrollTop}px, 0)`;
|
||||
layer.replaceChildren(canvas);
|
||||
syncAuthorshipLayer(layer, editor);
|
||||
}
|
||||
|
||||
@@ -11,6 +11,18 @@ function toggleWrap(editor, before, after = before, placeholder = "tekst") {
|
||||
editor.setRangeText(selected, start - before.length, end + after.length, "select");
|
||||
return;
|
||||
}
|
||||
if (selected.includes("\n") && !before.includes("\n") && !after.includes("\n")) {
|
||||
const lines = selected.split("\n");
|
||||
const nonEmpty = lines.filter(line => line.length > 0);
|
||||
const allWrapped = nonEmpty.length > 0 && nonEmpty.every(line => line.startsWith(before) && line.endsWith(after) && line.length >= before.length + after.length);
|
||||
const replacement = lines.map(line => {
|
||||
if (!line) return line;
|
||||
return allWrapped ? line.slice(before.length, line.length - after.length) : `${before}${line}${after}`;
|
||||
}).join("\n");
|
||||
editor.setRangeText(replacement, start, end, "select");
|
||||
return;
|
||||
}
|
||||
|
||||
if (selected.startsWith(before) && selected.endsWith(after) && selected.length >= before.length + after.length) {
|
||||
editor.setRangeText(selected.slice(before.length, -after.length), start, end, "select");
|
||||
return;
|
||||
|
||||
@@ -93,7 +93,7 @@ function normalizeLanguage(value) {
|
||||
return languageAliases[language] || language.replace(/[^a-z0-9_-]/g, "");
|
||||
}
|
||||
|
||||
const attrs = (line, editable = false, prefix = "", suffix = "", lineOffset = 0) => ` class="preview-source-line${editable ? " preview-editable" : ""}" data-source-line="${line + lineOffset + 1}"${editable ? ` contenteditable="true" spellcheck="true" data-source-prefix="${escapeHtml(prefix)}" data-source-suffix="${escapeHtml(suffix)}"` : ""}`;
|
||||
const attrs = (line, editable = false, prefix = "", suffix = "", lineOffset = 0) => ` class="preview-source-line${editable ? " preview-editable" : ""}" data-source-line="${line + lineOffset + 1}"${editable ? ` data-source-prefix="${escapeHtml(prefix)}" data-source-suffix="${escapeHtml(suffix)}"` : ""}`;
|
||||
const isPlainText = line => !/[`*_~^=\[\]<>|:#]/.test(line) && !/^\s*(?:[-+*>]|\d+\.)\s/.test(line);
|
||||
|
||||
|
||||
@@ -135,7 +135,7 @@ function renderList(lines, start, lineOffset = 0, baseIndent = null, forcedType
|
||||
if (item.checked != null) {
|
||||
body += `<input type="checkbox" class="task-checkbox" data-source-line="${sourceLine}"${item.checked ? " checked" : ""}>`;
|
||||
}
|
||||
body += `<span class="preview-editable list-item-content" contenteditable="true" spellcheck="true" data-source-line="${sourceLine}" data-source-prefix="${escapeHtml(prefix)}">${inline(item.text)}</span>`;
|
||||
body += `<span class="preview-editable list-item-content" data-source-line="${sourceLine}" data-source-prefix="${escapeHtml(prefix)}">${inline(item.text)}</span>`;
|
||||
index++;
|
||||
|
||||
while (index < lines.length) {
|
||||
@@ -275,13 +275,13 @@ export function renderMarkdown(source, lineOffset = 0) {
|
||||
closeList();
|
||||
const headers = splitTableRow(line);
|
||||
html += `<div class="table-wrap preview-source-line" data-source-line="${index + lineOffset + 1}"><table><thead><tr>`;
|
||||
headers.forEach((cell, i) => html += `<th class="preview-editable" contenteditable="true" spellcheck="true" data-source-line="${index + lineOffset + 1}" data-table-cell="${i}" style="text-align:${delimiter[i] || "left"}">${inline(cell)}</th>`);
|
||||
headers.forEach((cell, i) => html += `<th class="preview-editable" data-source-line="${index + lineOffset + 1}" data-table-cell="${i}" style="text-align:${delimiter[i] || "left"}">${inline(cell)}</th>`);
|
||||
html += `</tr></thead><tbody>`;
|
||||
index += 2;
|
||||
while (index < lines.length && lines[index].includes("|") && lines[index].trim()) {
|
||||
const cells = splitTableRow(lines[index]);
|
||||
html += `<tr>`;
|
||||
headers.forEach((_, i) => html += `<td class="preview-editable" contenteditable="true" spellcheck="true" data-source-line="${index + lineOffset + 1}" data-table-cell="${i}" style="text-align:${delimiter[i] || "left"}">${inline(cells[i] || "")}</td>`);
|
||||
headers.forEach((_, i) => html += `<td class="preview-editable" data-source-line="${index + lineOffset + 1}" data-table-cell="${i}" style="text-align:${delimiter[i] || "left"}">${inline(cells[i] || "")}</td>`);
|
||||
html += `</tr>`;
|
||||
index++;
|
||||
}
|
||||
|
||||
+425
-51
@@ -1,7 +1,7 @@
|
||||
import { installGlobalDiagnostics, logInfo } from "@rustpad/logger";
|
||||
installGlobalDiagnostics();
|
||||
|
||||
import { applyAuthorshipEdit, authorshipOwners, lineAuthors, mapSelectionThroughEdit, parseAuthorship, renderAuthorshipLayer, replaceAuthorshipOwner, serializeAuthorship } from "@rustpad/authorship";
|
||||
import { applyAuthorshipEdit, authorshipOwners, lineAuthors, mapSelectionThroughEdit, parseAuthorship, renderAuthorshipLayer, replaceAuthorshipOwner, serializeAuthorship, syncAuthorshipLayer } from "@rustpad/authorship";
|
||||
import { copyText } from "@rustpad/clipboard";
|
||||
import { applyFormat, bindFormatShortcuts } from "@rustpad/editor-format";
|
||||
import { bindEmojiPicker } from "@rustpad/emoji-picker";
|
||||
@@ -102,6 +102,18 @@ export function startNoteEditor(adapter) {
|
||||
return badge;
|
||||
}));
|
||||
}
|
||||
function syncOwnerLabels() {
|
||||
ownerLabels.querySelectorAll(".owner-label-group[data-content-top]").forEach(group => {
|
||||
group.style.top = `${Number(group.dataset.contentTop) - editor.scrollTop}px`;
|
||||
});
|
||||
}
|
||||
|
||||
function syncEditorLayers() {
|
||||
gutter.scrollTop = editor.scrollTop;
|
||||
syncAuthorshipLayer(authorshipLayer, editor);
|
||||
syncOwnerLabels();
|
||||
}
|
||||
|
||||
function renderGutter() {
|
||||
const lineCount = Math.max(1, (editor.value.match(/\n/g) || []).length + 1);
|
||||
const lines = Array.from({ length: lineCount });
|
||||
@@ -123,44 +135,98 @@ export function startNoteEditor(adapter) {
|
||||
ownerLabels.innerHTML = lines.map((_, i) => {
|
||||
const authors = authorsByLine[i] || [];
|
||||
if (!authors.length) return "";
|
||||
const top = paddingTop + i * lineHeight - editor.scrollTop;
|
||||
const top = paddingTop + i * lineHeight;
|
||||
const signature = authors.map(owner => ownerName(owner)).sort((a, b) => a.localeCompare(b)).join("\u0000");
|
||||
if (signature === previousAuthorSignature) return "";
|
||||
previousAuthorSignature = signature;
|
||||
const badges = authors.map(owner => `<span class="owner-label" style="--owner:${colorFor(owner)}">${escapeHtml(ownerName(owner))}</span>`).join("");
|
||||
return `<span class="owner-label-group" style="top:${top}px">${badges}</span>`;
|
||||
return `<span class="owner-label-group" data-content-top="${top}">${badges}</span>`;
|
||||
}).join("");
|
||||
} else ownerLabels.replaceChildren();
|
||||
if (showAuthorship) renderAuthorshipLayer(authorshipLayer, editor, authorship, colorFor);
|
||||
else authorshipLayer.replaceChildren();
|
||||
syncEditorLayers();
|
||||
document.body.classList.toggle("hide-editor-line-numbers", !lineToggle.checked);
|
||||
document.body.classList.toggle("hide-preview-line-numbers", !previewLineToggle.checked);
|
||||
}
|
||||
function escapeHtml(v) { return String(v).replace(/[&<>"']/g, c => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[c])); } function formatDate(value) { const raw = String(value ?? "").trim(); let normalized = raw; if (/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}(?:\.\d+)?[+-]\d{2}$/.test(normalized)) normalized = normalized.replace(" ", "T") + ":00"; else if (/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}(?:\.\d+)?[+-]\d{2}:\d{2}$/.test(normalized)) normalized = normalized.replace(" ", "T"); else if (/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}(?:\.\d+)?$/.test(normalized)) normalized = normalized.replace(" ", "T") + "Z"; const date = new Date(normalized); return Number.isNaN(date.getTime()) ? raw : date.toLocaleString("pl-PL", { year: "numeric", month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit", second: "2-digit" }); }
|
||||
|
||||
function previewNodeMarkdownParts(current) {
|
||||
if (current.nodeType !== Node.ELEMENT_NODE) return { open: "", close: "", atomic: null };
|
||||
const tag = current.tagName.toLowerCase();
|
||||
if (tag === "strong" || tag === "b") return { open: "**", close: "**", atomic: null };
|
||||
if (tag === "em" || tag === "i") return { open: "*", close: "*", atomic: null };
|
||||
if (tag === "s" || tag === "del") return { open: "~~", close: "~~", atomic: null };
|
||||
if (tag === "mark") return { open: "==", close: "==", atomic: null };
|
||||
if (tag === "code") return { open: "`", close: "`", atomic: null };
|
||||
if (tag === "sub") return { open: "~", close: "~", atomic: null };
|
||||
if (tag === "sup" && !current.classList.contains("footnote-ref")) return { open: "^", close: "^", atomic: null };
|
||||
if (tag === "a") return { open: "[", close: `](${current.getAttribute("href") || "#"})`, atomic: null };
|
||||
if (tag === "img") {
|
||||
const src = current.getAttribute("src") || "";
|
||||
const alt = current.getAttribute("alt") || "";
|
||||
const title = current.getAttribute("title");
|
||||
return { open: "", close: "", atomic: `}"` : ""})` };
|
||||
}
|
||||
if (tag === "br") return { open: "", close: "", atomic: "\n" };
|
||||
return { open: "", close: "", atomic: null };
|
||||
}
|
||||
|
||||
function previewNodeMarkdown(current) {
|
||||
if (current.nodeType === Node.TEXT_NODE) return (current.nodeValue || "").replace(/\u00a0/g, " ");
|
||||
if (current.nodeType !== Node.ELEMENT_NODE) return "";
|
||||
const parts = previewNodeMarkdownParts(current);
|
||||
if (parts.atomic !== null) return parts.atomic;
|
||||
const body = [...current.childNodes].map(previewNodeMarkdown).join("");
|
||||
return `${parts.open}${body}${parts.close}`;
|
||||
}
|
||||
|
||||
function markdownFromPreview(node) {
|
||||
const walk = current => {
|
||||
if (current.nodeType === Node.TEXT_NODE) return current.nodeValue || "";
|
||||
if (current.nodeType !== Node.ELEMENT_NODE) return "";
|
||||
const tag = current.tagName.toLowerCase(), body = [...current.childNodes].map(walk).join("");
|
||||
if (tag === "strong" || tag === "b") return `**${body}**`;
|
||||
if (tag === "em" || tag === "i") return `*${body}*`;
|
||||
if (tag === "s" || tag === "del") return `~~${body}~~`;
|
||||
if (tag === "mark") return `==${body}==`;
|
||||
if (tag === "code") return "`" + body + "`";
|
||||
if (tag === "sub") return `~${body}~`;
|
||||
if (tag === "sup" && !current.classList.contains("footnote-ref")) return `^${body}^`;
|
||||
if (tag === "a") return `[${body}](${current.getAttribute("href") || "#"})`;
|
||||
if (tag === "img") {
|
||||
const src = current.getAttribute("src") || "";
|
||||
const alt = current.getAttribute("alt") || "";
|
||||
const title = current.getAttribute("title");
|
||||
return `}"` : ""})`;
|
||||
return [...node.childNodes].map(previewNodeMarkdown).join("");
|
||||
}
|
||||
|
||||
function markdownPointOffset(root, container, offset) {
|
||||
let result = 0;
|
||||
let found = false;
|
||||
const contains = (parent, child) => parent === child || (parent.nodeType === Node.ELEMENT_NODE && parent.contains(child));
|
||||
const walk = (current, isRoot = false) => {
|
||||
if (found) return;
|
||||
if (current === container) {
|
||||
if (current.nodeType === Node.TEXT_NODE) result += Math.max(0, Math.min(offset, (current.nodeValue || "").length));
|
||||
else if (current.nodeType === Node.ELEMENT_NODE) {
|
||||
const parts = isRoot ? { open: "", close: "", atomic: null } : previewNodeMarkdownParts(current);
|
||||
if (parts.atomic !== null) result += offset > 0 ? parts.atomic.length : 0;
|
||||
else {
|
||||
result += parts.open.length;
|
||||
const children = [...current.childNodes];
|
||||
for (let index = 0; index < Math.min(offset, children.length); index++) result += previewNodeMarkdown(children[index]).length;
|
||||
}
|
||||
}
|
||||
found = true;
|
||||
return;
|
||||
}
|
||||
if (tag === "br") return "\n";
|
||||
return body;
|
||||
if (current.nodeType === Node.TEXT_NODE) {
|
||||
result += (current.nodeValue || "").length;
|
||||
return;
|
||||
}
|
||||
if (current.nodeType !== Node.ELEMENT_NODE) return;
|
||||
const parts = isRoot ? { open: "", close: "", atomic: null } : previewNodeMarkdownParts(current);
|
||||
if (parts.atomic !== null) {
|
||||
result += parts.atomic.length;
|
||||
return;
|
||||
}
|
||||
result += parts.open.length;
|
||||
for (const child of current.childNodes) {
|
||||
if (contains(child, container)) {
|
||||
walk(child);
|
||||
return;
|
||||
}
|
||||
result += previewNodeMarkdown(child).length;
|
||||
}
|
||||
result += parts.close.length;
|
||||
};
|
||||
return [...node.childNodes].map(walk).join("").replace(/\u00a0/g, " ");
|
||||
walk(root, true);
|
||||
return found ? result : 0;
|
||||
}
|
||||
|
||||
function previewCaretOffset(target) {
|
||||
@@ -186,11 +252,40 @@ export function startNoteEditor(adapter) {
|
||||
const range = document.createRange(); range.selectNodeContents(target); range.collapse(false);
|
||||
const selection = window.getSelection(); selection.removeAllRanges(); selection.addRange(range);
|
||||
}
|
||||
function deactivatePreviewEdit(target) {
|
||||
if (!target) return;
|
||||
target.removeAttribute("contenteditable");
|
||||
target.removeAttribute("spellcheck");
|
||||
target.classList.remove("preview-editable--active");
|
||||
}
|
||||
|
||||
function activatePreviewEdit(target, offset = null) {
|
||||
if (!target) return;
|
||||
const active = preview.querySelector('.preview-editable[contenteditable="true"]');
|
||||
if (active && active !== target) {
|
||||
const targetIndex = [...preview.querySelectorAll(".preview-editable")].indexOf(target);
|
||||
active.blur();
|
||||
target = preview.querySelectorAll(".preview-editable")[targetIndex];
|
||||
if (!target) return;
|
||||
}
|
||||
target.setAttribute("contenteditable", "true");
|
||||
target.setAttribute("spellcheck", "true");
|
||||
target.classList.add("preview-editable--active");
|
||||
target.focus({ preventScroll: true });
|
||||
placePreviewCaret(target, offset == null ? previewCaretOffset(target) : offset);
|
||||
}
|
||||
|
||||
function movePreviewCaret(target, direction) {
|
||||
const editables = [...preview.querySelectorAll(".preview-editable")];
|
||||
const index = editables.indexOf(target), next = editables[index + direction];
|
||||
const index = editables.indexOf(target);
|
||||
if (index < 0 || !editables[index + direction]) return false;
|
||||
const offset = previewCaretOffset(target);
|
||||
target.blur();
|
||||
const next = [...preview.querySelectorAll(".preview-editable")][index + direction];
|
||||
if (!next) return false;
|
||||
const offset = previewCaretOffset(target); next.focus(); placePreviewCaret(next, offset); next.scrollIntoView({ block: "nearest" }); return true;
|
||||
activatePreviewEdit(next, offset);
|
||||
next.scrollIntoView({ block: "nearest" });
|
||||
return true;
|
||||
}
|
||||
function continueIndentation(event) {
|
||||
if (event.key !== "Enter" || event.shiftKey || event.ctrlKey || event.metaKey || event.altKey) return;
|
||||
@@ -212,6 +307,129 @@ export function startNoteEditor(adapter) {
|
||||
const cells = body.split("|").map(cell => cell.trim()); while (cells.length <= index) cells.push(""); cells[index] = value.replace(/\|/g, "|");
|
||||
return `${leading ? "| " : ""}${cells.join(" | ")}${trailing ? " |" : ""}`;
|
||||
}
|
||||
function sourceLineBounds(lineIndex) {
|
||||
const lines = editor.value.split("\n");
|
||||
if (lineIndex < 0 || lineIndex >= lines.length) return null;
|
||||
let start = 0;
|
||||
for (let index = 0; index < lineIndex; index++) start += lines[index].length + 1;
|
||||
return { start, end: start + lines[lineIndex].length, text: lines[lineIndex], lineIndex };
|
||||
}
|
||||
|
||||
function tableCellBounds(line, cellIndex) {
|
||||
const first = line.search(/\S|$/);
|
||||
const trailingWhitespace = (line.match(/\s*$/) || [""])[0].length;
|
||||
let bodyStart = first;
|
||||
let bodyEnd = line.length - trailingWhitespace;
|
||||
if (line[bodyStart] === "|") bodyStart++;
|
||||
if (bodyEnd > bodyStart && line[bodyEnd - 1] === "|") bodyEnd--;
|
||||
const body = line.slice(bodyStart, bodyEnd);
|
||||
const segments = [];
|
||||
let segmentStart = 0;
|
||||
for (let index = 0; index <= body.length; index++) {
|
||||
if (index === body.length || body[index] === "|") {
|
||||
const raw = body.slice(segmentStart, index);
|
||||
const left = (raw.match(/^\s*/) || [""])[0].length;
|
||||
const right = (raw.match(/\s*$/) || [""])[0].length;
|
||||
segments.push({ start: bodyStart + segmentStart + left, end: bodyStart + index - right });
|
||||
segmentStart = index + 1;
|
||||
}
|
||||
}
|
||||
return segments[cellIndex] || { start: bodyStart, end: bodyStart };
|
||||
}
|
||||
|
||||
function editableSourceBounds(target) {
|
||||
const lineIndex = Number(target?.dataset.sourceLine) - 1;
|
||||
const line = sourceLineBounds(lineIndex);
|
||||
if (!line) return null;
|
||||
if (target.dataset.tableCell !== undefined) {
|
||||
const cell = tableCellBounds(line.text, Number(target.dataset.tableCell));
|
||||
return { start: line.start + cell.start, end: line.start + cell.end, line };
|
||||
}
|
||||
const prefix = target.dataset.rawSourceEdit === "true" ? "" : (target.dataset.sourcePrefix || "");
|
||||
const suffix = target.dataset.rawSourceEdit === "true" ? "" : (target.dataset.sourceSuffix || "");
|
||||
return {
|
||||
start: Math.min(line.end, line.start + prefix.length),
|
||||
end: Math.max(line.start, line.end - suffix.length),
|
||||
line,
|
||||
};
|
||||
}
|
||||
|
||||
function editableAtBoundary(container, offset, preferPrevious = false) {
|
||||
const element = container.nodeType === Node.ELEMENT_NODE ? container : container.parentElement;
|
||||
const direct = element?.closest?.(".preview-editable");
|
||||
if (direct && preview.contains(direct)) return direct;
|
||||
if (container.nodeType !== Node.ELEMENT_NODE) return null;
|
||||
const children = [...container.childNodes];
|
||||
const candidate = preferPrevious ? children[Math.max(0, offset - 1)] : children[Math.min(offset, children.length - 1)];
|
||||
const candidates = candidate ? [candidate] : [];
|
||||
for (const node of candidates) {
|
||||
const candidateElement = node.nodeType === Node.ELEMENT_NODE ? node : node.parentElement;
|
||||
if (candidateElement?.matches?.(".preview-editable")) return candidateElement;
|
||||
const nested = candidateElement?.querySelectorAll?.(".preview-editable");
|
||||
if (nested?.length) return preferPrevious ? nested[nested.length - 1] : nested[0];
|
||||
}
|
||||
const source = element?.closest?.(".preview-source-line");
|
||||
const nested = source?.querySelectorAll?.(".preview-editable");
|
||||
return nested?.length ? (preferPrevious ? nested[nested.length - 1] : nested[0]) : null;
|
||||
}
|
||||
|
||||
function previewPointSourceOffset(container, offset, preferPrevious = false) {
|
||||
const target = editableAtBoundary(container, offset, preferPrevious);
|
||||
const bounds = editableSourceBounds(target);
|
||||
if (!target || !bounds) return null;
|
||||
let bodyOffset;
|
||||
if (target === container || target.contains(container)) bodyOffset = markdownPointOffset(target, container, offset);
|
||||
else bodyOffset = preferPrevious ? bounds.end - bounds.start : 0;
|
||||
return {
|
||||
offset: Math.max(bounds.start, Math.min(bounds.end, bounds.start + bodyOffset)),
|
||||
target,
|
||||
bounds,
|
||||
};
|
||||
}
|
||||
|
||||
function previewSelectionSourceRange({ expandWholeLines = false } = {}) {
|
||||
const selection = window.getSelection();
|
||||
if (!selection?.rangeCount || selection.isCollapsed) return null;
|
||||
const range = selection.getRangeAt(0);
|
||||
const startInside = range.startContainer === preview || preview.contains(range.startContainer);
|
||||
const endInside = range.endContainer === preview || preview.contains(range.endContainer);
|
||||
if (!startInside || !endInside) return null;
|
||||
const startPoint = previewPointSourceOffset(range.startContainer, range.startOffset, false);
|
||||
const endPoint = previewPointSourceOffset(range.endContainer, range.endOffset, true);
|
||||
if (!startPoint || !endPoint) return null;
|
||||
let start = Math.min(startPoint.offset, endPoint.offset);
|
||||
let end = Math.max(startPoint.offset, endPoint.offset);
|
||||
if (expandWholeLines && startPoint.bounds.line.lineIndex !== endPoint.bounds.line.lineIndex) {
|
||||
if (start === startPoint.bounds.start) start = startPoint.bounds.line.start;
|
||||
if (end === endPoint.bounds.end) {
|
||||
end = endPoint.bounds.line.end;
|
||||
if (end < editor.value.length && editor.value[end] === "\n") end++;
|
||||
}
|
||||
}
|
||||
return { start, end };
|
||||
}
|
||||
|
||||
function deletePreviewSelection() {
|
||||
const range = previewSelectionSourceRange({ expandWholeLines: true });
|
||||
if (!range || range.end <= range.start) return false;
|
||||
editor.setRangeText("", range.start, range.end, "end");
|
||||
editor.dispatchEvent(new Event("input", { bubbles: true }));
|
||||
return true;
|
||||
}
|
||||
|
||||
function previewShortcutFormat(event) {
|
||||
const primary = event.ctrlKey || event.metaKey;
|
||||
if (primary && !event.shiftKey && event.key.toLowerCase() === "b") return "bold";
|
||||
if (primary && !event.shiftKey && event.key.toLowerCase() === "i") return "italic";
|
||||
if (primary && event.shiftKey && event.key.toLowerCase() === "x") return "strike";
|
||||
if (primary && !event.shiftKey && event.key.toLowerCase() === "k") return "link";
|
||||
if (primary && event.shiftKey && event.key === "7") return "number";
|
||||
if (primary && event.shiftKey && event.key === "8") return "bullet";
|
||||
if (primary && event.shiftKey && event.key === "9") return "task";
|
||||
if (event.altKey && /^[1-4]$/.test(event.key)) return `heading${event.key}`;
|
||||
return null;
|
||||
}
|
||||
|
||||
function syncPreviewScroll() {
|
||||
if (activeView() !== "split") return;
|
||||
const editorRange = Math.max(0, editor.scrollHeight - editor.clientHeight);
|
||||
@@ -219,7 +437,7 @@ export function startNoteEditor(adapter) {
|
||||
const ratio = editorRange > 0 ? editor.scrollTop / editorRange : 0;
|
||||
preview.scrollTop = ratio * previewRange;
|
||||
}
|
||||
function render() { if (uiState.mode === "markdown") { preview.classList.remove("preview--raw"); preview.innerHTML = renderMarkdown(editor.value); document.querySelector("#preview-label").textContent = "Preview (mermaid / markdown)"; renderMermaid(); renderCodeHighlight(); } else { preview.classList.add("preview--raw"); preview.innerHTML = editor.value.split("\n").map((line, index) => `<div class="preview-source-line preview-editable" data-source-line="${index + 1}" contenteditable="true" spellcheck="true">${escapeHtml(line) || "<br>"}</div>`).join(""); document.querySelector("#preview-label").textContent = "Text preview · editable"; } alignPreviewLineNumbers(preview); document.querySelector("#characters").textContent = `${editor.value.length} characters`; document.querySelector("#words").textContent = `${editor.value.trim() ? editor.value.trim().split(/\s+/).length : 0} words`; renderGutter(); requestAnimationFrame(syncPreviewScroll); }
|
||||
function render() { if (uiState.mode === "markdown") { preview.classList.remove("preview--raw"); preview.innerHTML = renderMarkdown(editor.value); document.querySelector("#preview-label").textContent = "Preview (mermaid / markdown)"; renderMermaid(); renderCodeHighlight(); } else { preview.classList.add("preview--raw"); preview.innerHTML = editor.value.split("\n").map((line, index) => `<div class="preview-source-line preview-editable" data-source-line="${index + 1}">${escapeHtml(line) || "<br>"}</div>`).join(""); document.querySelector("#preview-label").textContent = "Text preview · editable"; } alignPreviewLineNumbers(preview); document.querySelector("#characters").textContent = `${editor.value.length} characters`; document.querySelector("#words").textContent = `${editor.value.trim() ? editor.value.trim().split(/\s+/).length : 0} words`; renderGutter(); requestAnimationFrame(syncPreviewScroll); }
|
||||
function activeView() {
|
||||
return compactLayoutQuery.matches ? compactView : uiState.view;
|
||||
}
|
||||
@@ -242,7 +460,48 @@ export function startNoteEditor(adapter) {
|
||||
if (write) writeEditorState(uiState, { replace });
|
||||
updateAddressLabel();
|
||||
}
|
||||
function applyRemote(content, ownerMap) { if (content === editor.value && ownerMap == null) return; const previous = editor.value, start = editor.selectionStart, end = editor.selectionEnd, direction = editor.selectionDirection, scrollTop = editor.scrollTop, scrollLeft = editor.scrollLeft; const mapped = mapSelectionThroughEdit(previous, content, start, end); applyingRemote = true; editor.value = content; authorship = adoptCurrentOwnerAliases(parseAuthorship(content, ownerMap), content.length); previousContent = content; editor.setSelectionRange(mapped.start, mapped.end, direction); editor.scrollTop = scrollTop; editor.scrollLeft = scrollLeft; applyingRemote = false; render(); editor.scrollTop = scrollTop; editor.scrollLeft = scrollLeft; authorshipLayer.scrollTop = scrollTop; authorshipLayer.scrollLeft = scrollLeft; }
|
||||
function previewEditSnapshot() {
|
||||
const target = document.activeElement?.closest?.('.preview-editable[contenteditable="true"]');
|
||||
if (!target || !preview.contains(target)) return null;
|
||||
return {
|
||||
index: [...preview.querySelectorAll(".preview-editable")].indexOf(target),
|
||||
offset: previewCaretOffset(target),
|
||||
raw: target.dataset.rawSourceEdit === "true",
|
||||
};
|
||||
}
|
||||
|
||||
function restorePreviewEdit(snapshot) {
|
||||
if (!snapshot || snapshot.index < 0) return;
|
||||
const target = preview.querySelectorAll(".preview-editable")[snapshot.index];
|
||||
if (!target) return;
|
||||
activatePreviewEdit(target, snapshot.offset);
|
||||
if (snapshot.raw) editRawPreviewLine(target);
|
||||
}
|
||||
|
||||
function applyRemote(content, ownerMap) {
|
||||
if (content === editor.value) {
|
||||
if (ownerMap != null) authorship = adoptCurrentOwnerAliases(parseAuthorship(content, ownerMap), content.length);
|
||||
previousContent = content;
|
||||
renderGutter();
|
||||
return;
|
||||
}
|
||||
const previewSnapshot = previewEditSnapshot();
|
||||
const previous = editor.value, start = editor.selectionStart, end = editor.selectionEnd, direction = editor.selectionDirection, scrollTop = editor.scrollTop, scrollLeft = editor.scrollLeft;
|
||||
const mapped = mapSelectionThroughEdit(previous, content, start, end);
|
||||
applyingRemote = true;
|
||||
editor.value = content;
|
||||
authorship = adoptCurrentOwnerAliases(parseAuthorship(content, ownerMap), content.length);
|
||||
previousContent = content;
|
||||
editor.setSelectionRange(mapped.start, mapped.end, direction);
|
||||
editor.scrollTop = scrollTop;
|
||||
editor.scrollLeft = scrollLeft;
|
||||
applyingRemote = false;
|
||||
render();
|
||||
editor.scrollTop = scrollTop;
|
||||
editor.scrollLeft = scrollLeft;
|
||||
syncEditorLayers();
|
||||
restorePreviewEdit(previewSnapshot);
|
||||
}
|
||||
|
||||
const { loadFiles } = bindNoteFiles({
|
||||
editor, toast, getAccessToken: () => accessToken, canDelete: () => Boolean(info?.can_delete_files),
|
||||
@@ -339,23 +598,7 @@ export function startNoteEditor(adapter) {
|
||||
clearUnread();
|
||||
requestAnimationFrame(() => chatInput.focus());
|
||||
});
|
||||
const mobileTimeButton = document.querySelector("#mobile-time-button");
|
||||
const mobileTimePreview = document.querySelector("#mobile-time-preview");
|
||||
let mobileTimeTimer = null;
|
||||
function updateMobileTime() {
|
||||
if (!mobileTimePreview) return;
|
||||
mobileTimePreview.textContent = new Intl.DateTimeFormat(undefined, { hour: "2-digit", minute: "2-digit", second: "2-digit" }).format(new Date());
|
||||
}
|
||||
mobileTimeButton?.addEventListener("click", event => {
|
||||
event.stopPropagation();
|
||||
const willOpen = mobileTimePreview.hidden;
|
||||
mobileTimePreview.hidden = !willOpen;
|
||||
clearInterval(mobileTimeTimer);
|
||||
mobileTimeTimer = null;
|
||||
if (willOpen) { updateMobileTime(); mobileTimeTimer = setInterval(updateMobileTime, 1000); }
|
||||
});
|
||||
document.addEventListener("click", event => {
|
||||
if (!event.target.closest("#mobile-time-button, #mobile-time-preview") && mobileTimePreview) { mobileTimePreview.hidden = true; clearInterval(mobileTimeTimer); mobileTimeTimer = null; }
|
||||
if (roomDetails.classList.contains("is-mobile-open") && !event.target.closest("#mobile-chat-button, #room-details")) { roomDetails.open = false; roomDetails.classList.remove("is-mobile-open"); }
|
||||
});
|
||||
const mobileBubble = document.querySelector("#mobile-editor-bubble");
|
||||
@@ -407,13 +650,19 @@ export function startNoteEditor(adapter) {
|
||||
const value = markdownFromPreview(target);
|
||||
const lines = editor.value.split("\n");
|
||||
if (target.dataset.rawSourceEdit === "true") {
|
||||
if (value === lines[lineIndex]) return;
|
||||
if (value === lines[lineIndex]) {
|
||||
render();
|
||||
return;
|
||||
}
|
||||
lines[lineIndex] = value.replace(/\n/g, "");
|
||||
editor.value = lines.join("\n");
|
||||
editor.dispatchEvent(new Event("input", { bubbles: true }));
|
||||
return;
|
||||
}
|
||||
if (!focusNextLine && value === target.dataset.originalValue) return;
|
||||
if (!focusNextLine && value === target.dataset.originalValue) {
|
||||
deactivatePreviewEdit(target);
|
||||
return;
|
||||
}
|
||||
if (focusNextLine) cancelledPreviewEdits.add(target);
|
||||
if (target.dataset.tableCell !== undefined) {
|
||||
lines[lineIndex] = replaceTableCell(lines[lineIndex], Number(target.dataset.tableCell), value.replace(/\n/g, " "));
|
||||
@@ -429,8 +678,7 @@ export function startNoteEditor(adapter) {
|
||||
if (focusNextLine) {
|
||||
const nextLine = lineIndex + Math.max(2, value.split("\n").length);
|
||||
const next = preview.querySelector(`[data-source-line="${nextLine}"].preview-editable`);
|
||||
next?.focus();
|
||||
if (next) placePreviewCaret(next, 0);
|
||||
if (next) activatePreviewEdit(next, 0);
|
||||
}
|
||||
}
|
||||
function editRawPreviewLine(target) {
|
||||
@@ -438,6 +686,9 @@ export function startNoteEditor(adapter) {
|
||||
const lines = editor.value.split("\n");
|
||||
if (lineIndex < 0 || lineIndex >= lines.length) return;
|
||||
const caretOffset = Math.min(previewCaretOffset(target), lines[lineIndex].length);
|
||||
target.setAttribute("contenteditable", "true");
|
||||
target.setAttribute("spellcheck", "true");
|
||||
target.classList.add("preview-editable--active");
|
||||
target.dataset.rawSourceEdit = "true";
|
||||
target.dataset.originalValue = lines[lineIndex];
|
||||
target.textContent = lines[lineIndex];
|
||||
@@ -470,8 +721,7 @@ export function startNoteEditor(adapter) {
|
||||
const restoreFocus = () => {
|
||||
const next = preview.querySelector(`[data-source-line="${sourceLine}"].preview-editable`);
|
||||
if (!next) return;
|
||||
next.focus({ preventScroll: true });
|
||||
placePreviewCaret(next, 0);
|
||||
activatePreviewEdit(next, 0);
|
||||
};
|
||||
restoreFocus();
|
||||
queueMicrotask(() => {
|
||||
@@ -501,7 +751,115 @@ export function startNoteEditor(adapter) {
|
||||
saveEditorSettingsButton.disabled = !info?.can_save_editor_settings;
|
||||
}
|
||||
});
|
||||
window.addEventListener("popstate", () => { uiState = readEditorState(); applyUi(); }); window.addEventListener("rustpad:urlchange", updateAddressLabel); document.querySelector("#copy-link").addEventListener("click", async () => { try { await copyText(currentShareUrl(uiState)); toast("Link copied"); } catch (e) { toast(e.message); } }); document.querySelectorAll("[data-format]").forEach(b => b.addEventListener("click", () => { applyFormat(editor, b.dataset.format); b.closest("details")?.removeAttribute("open"); })); bindFormatShortcuts(editor); bindEmojiPicker({ editor, details: document.querySelector("#emoji-picker"), search: document.querySelector("#emoji-search"), categories: document.querySelector("#emoji-categories"), grid: document.querySelector("#emoji-grid"), empty: document.querySelector("#emoji-empty") }); document.querySelector("#shortcuts-button").addEventListener("click", () => document.querySelector("#shortcuts-dialog").showModal()); document.querySelector("#close-shortcuts").addEventListener("click", () => document.querySelector("#shortcuts-dialog").close()); preview.addEventListener("change", event => { const checkbox = event.target.closest(".task-checkbox"); if (!checkbox) return; const lineIndex = Number(checkbox.dataset.sourceLine) - 1; const lines = editor.value.split("\n"); if (lineIndex < 0 || lineIndex >= lines.length) return; lines[lineIndex] = lines[lineIndex].replace(/^(\s*[-*+]\s+\[)[ xX](\])/, `$1${checkbox.checked ? "x" : " "}$2`); editor.value = lines.join("\n"); editor.dispatchEvent(new Event("input", { bubbles: true })); }); preview.addEventListener("focusin", event => { const target = event.target.closest(".preview-editable"); if (!target) return; target.dataset.originalHtml = target.innerHTML; target.dataset.originalValue = markdownFromPreview(target); }); preview.addEventListener("beforeinput", event => { if (!event.target.closest(".preview-editable")) return; if (event.inputType === "insertParagraph" || event.inputType === "insertLineBreak") event.preventDefault(); }); preview.addEventListener("keydown", event => { const target = event.target.closest(".preview-editable"); if (!target) return; if (event.key === "Escape") { event.preventDefault(); event.stopPropagation(); if (target.dataset.rawSourceEdit === "true") { cancelledPreviewEdits.add(target); render(); } else editRawPreviewLine(target); return; } if (event.key === "Enter") { event.preventDefault(); event.stopPropagation(); if (event.altKey) insertPreviewLineBreak(target); else target.blur(); return; } if (event.key === "ArrowUp" || event.key === "ArrowDown") { if (movePreviewCaret(target, event.key === "ArrowUp" ? -1 : 1)) event.preventDefault(); } }); preview.addEventListener("blur", event => { const target = event.target.closest(".preview-editable"); if (!target) return; if (cancelledPreviewEdits.has(target)) { cancelledPreviewEdits.delete(target); return; } commitPreviewEdit(target); }, { capture: true });
|
||||
window.addEventListener("popstate", () => { uiState = readEditorState(); applyUi(); });
|
||||
window.addEventListener("rustpad:urlchange", updateAddressLabel);
|
||||
document.querySelector("#copy-link").addEventListener("click", async () => {
|
||||
try { await copyText(currentShareUrl(uiState)); toast("Link copied"); }
|
||||
catch (error) { toast(error.message); }
|
||||
});
|
||||
|
||||
let pendingPreviewFormatRange = null;
|
||||
document.querySelectorAll("[data-format]").forEach(button => {
|
||||
button.addEventListener("pointerdown", event => {
|
||||
pendingPreviewFormatRange = previewSelectionSourceRange();
|
||||
if (pendingPreviewFormatRange) event.preventDefault();
|
||||
});
|
||||
button.addEventListener("click", () => {
|
||||
const range = pendingPreviewFormatRange || previewSelectionSourceRange();
|
||||
pendingPreviewFormatRange = null;
|
||||
if (range) editor.setSelectionRange(range.start, range.end);
|
||||
applyFormat(editor, button.dataset.format);
|
||||
button.closest("details")?.removeAttribute("open");
|
||||
});
|
||||
});
|
||||
bindFormatShortcuts(editor);
|
||||
bindEmojiPicker({ editor, details: document.querySelector("#emoji-picker"), search: document.querySelector("#emoji-search"), categories: document.querySelector("#emoji-categories"), grid: document.querySelector("#emoji-grid"), empty: document.querySelector("#emoji-empty") });
|
||||
document.querySelector("#shortcuts-button").addEventListener("click", () => document.querySelector("#shortcuts-dialog").showModal());
|
||||
document.querySelector("#close-shortcuts").addEventListener("click", () => document.querySelector("#shortcuts-dialog").close());
|
||||
|
||||
preview.addEventListener("click", event => {
|
||||
const target = event.target.closest(".preview-editable");
|
||||
if (!target || event.target.closest("a, button, input, img")) return;
|
||||
const selection = window.getSelection();
|
||||
if (selection && !selection.isCollapsed) return;
|
||||
const offset = previewCaretOffset(target);
|
||||
activatePreviewEdit(target, offset);
|
||||
});
|
||||
preview.addEventListener("change", event => {
|
||||
const checkbox = event.target.closest(".task-checkbox");
|
||||
if (!checkbox) return;
|
||||
const lineIndex = Number(checkbox.dataset.sourceLine) - 1;
|
||||
const lines = editor.value.split("\n");
|
||||
if (lineIndex < 0 || lineIndex >= lines.length) return;
|
||||
lines[lineIndex] = lines[lineIndex].replace(/^(\s*[-*+]\s+\[)[ xX](\])/, `$1${checkbox.checked ? "x" : " "}$2`);
|
||||
editor.value = lines.join("\n");
|
||||
editor.dispatchEvent(new Event("input", { bubbles: true }));
|
||||
});
|
||||
preview.addEventListener("focusin", event => {
|
||||
const target = event.target.closest('.preview-editable[contenteditable="true"]');
|
||||
if (!target) return;
|
||||
target.dataset.originalHtml = target.innerHTML;
|
||||
target.dataset.originalValue = markdownFromPreview(target);
|
||||
});
|
||||
preview.addEventListener("beforeinput", event => {
|
||||
if (!event.target.closest('.preview-editable[contenteditable="true"]')) return;
|
||||
if (event.inputType === "insertParagraph" || event.inputType === "insertLineBreak") event.preventDefault();
|
||||
});
|
||||
preview.addEventListener("keydown", event => {
|
||||
const target = event.target.closest('.preview-editable[contenteditable="true"]');
|
||||
if (!target) return;
|
||||
if (event.key === "Escape") {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
if (target.dataset.rawSourceEdit === "true") {
|
||||
cancelledPreviewEdits.add(target);
|
||||
render();
|
||||
} else editRawPreviewLine(target);
|
||||
return;
|
||||
}
|
||||
if (event.key === "Enter") {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
if (event.altKey) insertPreviewLineBreak(target);
|
||||
else target.blur();
|
||||
return;
|
||||
}
|
||||
if (event.key === "ArrowUp" || event.key === "ArrowDown") {
|
||||
if (movePreviewCaret(target, event.key === "ArrowUp" ? -1 : 1)) event.preventDefault();
|
||||
}
|
||||
});
|
||||
preview.addEventListener("blur", event => {
|
||||
const target = event.target.closest(".preview-editable");
|
||||
if (!target) return;
|
||||
if (cancelledPreviewEdits.has(target)) {
|
||||
cancelledPreviewEdits.delete(target);
|
||||
return;
|
||||
}
|
||||
commitPreviewEdit(target);
|
||||
}, { capture: true });
|
||||
|
||||
document.addEventListener("keydown", event => {
|
||||
if (document.activeElement?.closest?.('.preview-editable[contenteditable="true"]')) return;
|
||||
const range = previewSelectionSourceRange();
|
||||
if (!range) return;
|
||||
if (event.key === "Backspace" || event.key === "Delete") {
|
||||
event.preventDefault();
|
||||
deletePreviewSelection();
|
||||
return;
|
||||
}
|
||||
const format = previewShortcutFormat(event);
|
||||
if (!format) return;
|
||||
event.preventDefault();
|
||||
editor.setSelectionRange(range.start, range.end);
|
||||
applyFormat(editor, format);
|
||||
});
|
||||
document.addEventListener("cut", event => {
|
||||
const range = previewSelectionSourceRange();
|
||||
if (!range) return;
|
||||
event.preventDefault();
|
||||
event.clipboardData?.setData("text/plain", window.getSelection()?.toString() || "");
|
||||
deletePreviewSelection();
|
||||
});
|
||||
const publishPageButton = document.querySelector("#publish-page");
|
||||
function updatePageControls() {
|
||||
const enabled = publicPageEnabled.checked;
|
||||
@@ -553,7 +911,23 @@ export function startNoteEditor(adapter) {
|
||||
if (socket) socket.update(editor.value, serializeAuthorship(authorship, editor.value.length));
|
||||
toast("Global profile color restored");
|
||||
});
|
||||
editor.addEventListener("keydown", continueIndentation); editor.addEventListener("scroll", () => { gutter.scrollTop = editor.scrollTop; authorshipLayer.scrollTop = editor.scrollTop; authorshipLayer.scrollLeft = editor.scrollLeft; renderGutter(); syncPreviewScroll(); }); editor.addEventListener("input", () => { const nextContent = editor.value; authorship = adoptCurrentOwnerAliases(authorship, previousContent.length); authorship = replaceAuthorshipOwner(authorship, owner => ownerName(owner) === nickname, currentOwner(), previousContent.length); authorship = applyAuthorshipEdit(authorship, previousContent, nextContent, currentOwner()); previousContent = nextContent; render(); if (applyingRemote) return; clearTimeout(saveTimer); document.querySelector("#save-state").textContent = "Saving…"; saveTimer = setTimeout(() => socket?.update(editor.value, serializeAuthorship(authorship, editor.value.length)), 250); });
|
||||
editor.addEventListener("keydown", continueIndentation);
|
||||
editor.addEventListener("scroll", () => {
|
||||
syncEditorLayers();
|
||||
syncPreviewScroll();
|
||||
});
|
||||
editor.addEventListener("input", () => {
|
||||
const nextContent = editor.value;
|
||||
authorship = adoptCurrentOwnerAliases(authorship, previousContent.length);
|
||||
authorship = replaceAuthorshipOwner(authorship, owner => ownerName(owner) === nickname, currentOwner(), previousContent.length);
|
||||
authorship = applyAuthorshipEdit(authorship, previousContent, nextContent, currentOwner());
|
||||
previousContent = nextContent;
|
||||
render();
|
||||
if (applyingRemote) return;
|
||||
clearTimeout(saveTimer);
|
||||
document.querySelector("#save-state").textContent = "Saving…";
|
||||
saveTimer = setTimeout(() => socket?.update(editor.value, serializeAuthorship(authorship, editor.value.length)), 250);
|
||||
});
|
||||
passwordDialog.addEventListener("cancel", event => {
|
||||
if (info?.protected && !resourceUnlocked) {
|
||||
event.preventDefault();
|
||||
|
||||
+58
-22
@@ -20,19 +20,33 @@
|
||||
<p id="note-url" class="document-url"></p>
|
||||
</div>
|
||||
</div>
|
||||
<span class="header-user-control user-color-control"><button id="current-user" class="user-chip"
|
||||
type="button" title="Change your color"><span class="user-chip__dot" aria-hidden="true"></span><span
|
||||
<span class="header-user-control user-color-control"><button id="current-user" class="user-chip" type="button"
|
||||
title="Change your color"><span class="user-chip__dot" aria-hidden="true"></span><span
|
||||
class="user-chip__name"></span></button><input id="user-color-picker" class="user-color-picker"
|
||||
type="color" aria-label="Override color for this note"><button id="use-global-color" class="use-global-color" type="button" title="Use global profile color" aria-label="Use global profile color" hidden>↺</button></span>
|
||||
type="color" aria-label="Override color for this note"><button id="use-global-color"
|
||||
class="use-global-color" type="button" title="Use global profile color"
|
||||
aria-label="Use global profile color" hidden>↺</button></span>
|
||||
<div class="header-navigation">
|
||||
<button id="header-menu-toggle" class="header-menu-toggle" type="button" aria-expanded="false"
|
||||
aria-controls="header-actions" aria-label="Open navigation menu">
|
||||
<span aria-hidden="true"></span><span aria-hidden="true"></span><span aria-hidden="true"></span>
|
||||
</button>
|
||||
<div id="header-actions" class="header-actions"><button id="copy-link"
|
||||
class="secondary-button">Copy link</button><button id="publish-page" class="secondary-button">Page</button><details class="page-settings"><summary class="secondary-button">Page settings</summary><div class="page-settings-menu"><label class="public-task-toggle" title="Enable or disable the published page"><input id="public-page-enabled" type="checkbox"> Enable Page</label><label class="public-task-toggle" title="Allow visitors to update task checkboxes on the published page"><input id="public-task-updates" type="checkbox"> Editable tasks</label><label class="public-task-toggle" title="Allow the published page to open without the resource password or private access"><input id="unprotect-public-page" type="checkbox"> Unprotect Page</label></div></details><button id="files-button"
|
||||
class="secondary-button">Files</button><button id="delete-note" class="secondary-button danger-button"
|
||||
hidden>Delete</button><button id="history-button" class="secondary-button">History</button></div>
|
||||
<div id="header-actions" class="header-actions"><button id="copy-link" class="secondary-button">Copy
|
||||
link</button><button id="publish-page" class="secondary-button">Page</button>
|
||||
<details class="page-settings">
|
||||
<summary class="secondary-button">Page settings</summary>
|
||||
<div class="page-settings-menu"><label class="public-task-toggle"
|
||||
title="Enable or disable the published page"><input id="public-page-enabled"
|
||||
type="checkbox"> Enable Page</label><label class="public-task-toggle"
|
||||
title="Allow visitors to update task checkboxes on the published page"><input
|
||||
id="public-task-updates" type="checkbox"> Editable tasks</label><label
|
||||
class="public-task-toggle"
|
||||
title="Allow the published page to open without the resource password or private access"><input
|
||||
id="unprotect-public-page" type="checkbox"> Unprotect Page</label></div>
|
||||
</details><button id="files-button" class="secondary-button">Files</button><button id="delete-note"
|
||||
class="secondary-button danger-button" hidden>Delete</button><button id="history-button"
|
||||
class="secondary-button">History</button>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
<main class="editor-layout">
|
||||
@@ -49,7 +63,8 @@
|
||||
<details id="emoji-picker" class="emoji-picker">
|
||||
<summary title="Insert emoji" aria-label="Insert emoji">😀 Emoji</summary>
|
||||
<div class="emoji-picker-panel">
|
||||
<input id="emoji-search" class="emoji-search" type="search" placeholder="Search emoji…" autocomplete="off" aria-label="Search emoji">
|
||||
<input id="emoji-search" class="emoji-search" type="search" placeholder="Search emoji…"
|
||||
autocomplete="off" aria-label="Search emoji">
|
||||
<div id="emoji-categories" class="emoji-categories" aria-label="Emoji categories"></div>
|
||||
<div id="emoji-grid" class="emoji-grid" role="listbox" aria-label="Emoji"></div>
|
||||
<p id="emoji-empty" class="emoji-empty" hidden>No emoji found.</p>
|
||||
@@ -101,10 +116,24 @@
|
||||
</div>
|
||||
<div id="editor-workspace" class="workspace view-split">
|
||||
<div class="editor-column">
|
||||
<div class="column-label editor-column-label"><span>Editor</span><div class="authorship-controls"><label class="switch-control authorship-colors-switch" title="Show or hide author coloring"><input id="authorship-colors-toggle" type="checkbox" checked><span class="switch-control__track" aria-hidden="true"></span><span id="authorship-colors-label">Colors on</span></label><div class="authorship-mode-control" role="group" aria-label="Authorship display"><button type="button" data-authorship-mode="simple" class="active">Simple</button><button type="button" data-authorship-mode="full">Full</button></div><button id="save-editor-settings" class="secondary-button compact-button editor-settings-save" type="button">Save</button></div></div><div id="participant-badges" class="participant-badges" aria-label="Participants"></div>
|
||||
<div class="column-label editor-column-label"><span>Editor</span>
|
||||
<div class="authorship-controls"><label class="switch-control authorship-colors-switch"
|
||||
title="Show or hide author coloring"><input id="authorship-colors-toggle"
|
||||
type="checkbox" checked><span class="switch-control__track"
|
||||
aria-hidden="true"></span><span id="authorship-colors-label">Colors
|
||||
on</span></label>
|
||||
<div class="authorship-mode-control" role="group" aria-label="Authorship display"><button
|
||||
type="button" data-authorship-mode="simple" class="active">Simple</button><button
|
||||
type="button" data-authorship-mode="full">Full</button></div><button
|
||||
id="save-editor-settings" class="secondary-button compact-button editor-settings-save"
|
||||
type="button">Save</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="participant-badges" class="participant-badges" aria-label="Participants"></div>
|
||||
<div class="editor-shell">
|
||||
<div id="line-gutter" class="line-gutter" aria-hidden="true"></div>
|
||||
<div id="authorship-layer" class="authorship-layer" aria-hidden="true"></div><div id="owner-labels" class="owner-labels" aria-hidden="true"></div><textarea id="editor"
|
||||
<div id="authorship-layer" class="authorship-layer" aria-hidden="true"></div>
|
||||
<div id="owner-labels" class="owner-labels" aria-hidden="true"></div><textarea id="editor"
|
||||
wrap="off" placeholder="Start writing…" spellcheck="false"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
@@ -114,9 +143,12 @@
|
||||
</div>
|
||||
</div>
|
||||
<footer class="editor-footer">
|
||||
<div class="footer-left"><span id="characters">0 characters</span> · <span id="words">0 words</span><span class="footer-connection-block"> · <span class="footer-status status"><span id="status-dot" class="status__dot"></span><span
|
||||
id="status-text">Connecting…</span></span> · <span id="socket-latency" class="footer-socket-latency"
|
||||
title="WebSocket round-trip time">— ms</span></span> · <details id="room-details" class="room-details">
|
||||
<div class="footer-left"><span id="characters">0 characters</span> · <span id="words">0
|
||||
words</span><span class="footer-connection-block"> · <span class="footer-status status"><span
|
||||
id="status-dot" class="status__dot"></span><span
|
||||
id="status-text">Connecting…</span></span> · <span id="socket-latency"
|
||||
class="footer-socket-latency" title="WebSocket round-trip time">— ms</span></span> ·
|
||||
<details id="room-details" class="room-details">
|
||||
<summary><span id="room-count">0 users</span><span id="chat-unread" class="chat-unread"
|
||||
hidden></span></summary>
|
||||
<div class="room-popover">
|
||||
@@ -134,7 +166,8 @@
|
||||
</div>
|
||||
</details>
|
||||
</div><span class="footer-right"><button id="shortcuts-button" class="footer-link" type="button"
|
||||
aria-haspopup="dialog">Shortcuts</button> · <span id="access-level" class="footer-access">Access: checking…</span> · <button id="footer-files" class="footer-link"
|
||||
aria-haspopup="dialog">Shortcuts</button> · <span id="access-level"
|
||||
class="footer-access">Access: checking…</span> · <button id="footer-files" class="footer-link"
|
||||
type="button">0 files</button> · <span id="save-state">Changes are saved
|
||||
automatically</span></span>
|
||||
</footer>
|
||||
@@ -185,8 +218,8 @@
|
||||
guest</button><button id="show-register" class="text-button" type="button">Register</button><button
|
||||
id="show-login" class="text-button" type="button">Log in</button></div>
|
||||
<section id="auth-panel" class="auth-panel" hidden>
|
||||
<h3 id="auth-mode-title">Log in</h3><label id="auth-email-field">E-mail / organization login<input id="auth-email"
|
||||
name="username" type="email" maxlength="320" autocomplete="username"
|
||||
<h3 id="auth-mode-title">Log in</h3><label id="auth-email-field">E-mail / organization login<input
|
||||
id="auth-email" name="username" type="email" maxlength="320" autocomplete="username"
|
||||
placeholder="you@example.com"></label><label>Password<input id="auth-password" name="password"
|
||||
type="password" minlength="8" maxlength="128" autocomplete="current-password"></label><button
|
||||
id="auth-submit" class="primary-button" type="submit">Log in and continue</button>
|
||||
@@ -207,13 +240,16 @@
|
||||
</form>
|
||||
</dialog>
|
||||
<div id="mobile-editor-bubble" class="mobile-editor-bubble" aria-label="Quick editor actions">
|
||||
<button id="mobile-bubble-drag" class="mobile-bubble-drag" type="button" title="Move quick actions" aria-label="Move quick actions">⋮⋮</button>
|
||||
<button id="mobile-bubble-drag" class="mobile-bubble-drag" type="button" title="Move quick actions"
|
||||
aria-label="Move quick actions">⋮⋮</button>
|
||||
<button id="mobile-files-button" type="button" title="Files" aria-label="Open files">📎</button>
|
||||
<button id="mobile-color-button" type="button" title="Editor color" aria-label="Change editor color"><span class="mobile-color-dot" aria-hidden="true"></span></button>
|
||||
<button id="mobile-chat-button" class="mobile-chat-button" type="button" title="Chat" aria-label="Open chat">💬<span id="mobile-chat-unread" class="mobile-chat-unread" hidden></span></button>
|
||||
<button id="mobile-time-button" class="mobile-time-button" type="button" title="Current time" aria-label="Show current time">🕒</button>
|
||||
<span id="mobile-time-preview" class="mobile-time-preview" role="status" hidden></span>
|
||||
<span id="mobile-connection-status" class="mobile-connection-status" title="WebSocket status"><span id="mobile-status-dot" class="status__dot"></span><span id="mobile-status-text">Connecting…</span></span>
|
||||
<button id="mobile-color-button" type="button" title="Editor color" aria-label="Change editor color"><span
|
||||
class="mobile-color-dot" aria-hidden="true"></span></button>
|
||||
<button id="mobile-chat-button" class="mobile-chat-button" type="button" title="Chat"
|
||||
aria-label="Open chat">💬<span id="mobile-chat-unread" class="mobile-chat-unread" hidden></span></button>
|
||||
<span id="mobile-connection-status" class="mobile-connection-status" title="WebSocket status"><span
|
||||
id="mobile-status-dot" class="status__dot"></span><span
|
||||
id="mobile-status-text">Connecting…</span></span>
|
||||
</div>
|
||||
<div id="toast" class="toast"></div>
|
||||
</body>
|
||||
|
||||
+30
-8
@@ -32,9 +32,20 @@
|
||||
<span aria-hidden="true"></span><span aria-hidden="true"></span><span aria-hidden="true"></span>
|
||||
</button>
|
||||
<div id="header-actions" class="header-actions"><button id="copy-link" class="secondary-button">Copy
|
||||
link</button><button id="publish-page" class="secondary-button">Page</button><details class="page-settings"><summary class="secondary-button">Page settings</summary><div class="page-settings-menu"><label class="public-task-toggle" title="Enable or disable the published page"><input id="public-page-enabled" type="checkbox"> Enable Page</label><label class="public-task-toggle" title="Allow visitors to update task checkboxes on the published page"><input id="public-task-updates" type="checkbox"> Editable tasks</label><label class="public-task-toggle" title="Allow the published page to open without the resource password or private access"><input id="unprotect-public-page" type="checkbox"> Unprotect Page</label></div></details><button
|
||||
id="files-button" class="secondary-button">Files</button><button id="history-button"
|
||||
class="secondary-button">History</button></div>
|
||||
link</button><button id="publish-page" class="secondary-button">Page</button>
|
||||
<details class="page-settings">
|
||||
<summary class="secondary-button">Page settings</summary>
|
||||
<div class="page-settings-menu"><label class="public-task-toggle"
|
||||
title="Enable or disable the published page"><input id="public-page-enabled"
|
||||
type="checkbox"> Enable Page</label><label class="public-task-toggle"
|
||||
title="Allow visitors to update task checkboxes on the published page"><input
|
||||
id="public-task-updates" type="checkbox"> Editable tasks</label><label
|
||||
class="public-task-toggle"
|
||||
title="Allow the published page to open without the resource password or private access"><input
|
||||
id="unprotect-public-page" type="checkbox"> Unprotect Page</label></div>
|
||||
</details><button id="files-button" class="secondary-button">Files</button><button id="history-button"
|
||||
class="secondary-button">History</button>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
<main class="editor-layout">
|
||||
@@ -104,7 +115,20 @@
|
||||
</div>
|
||||
<div id="editor-workspace" class="workspace view-split">
|
||||
<div class="editor-column">
|
||||
<div class="column-label editor-column-label"><span>Editor</span><div class="authorship-controls"><label class="switch-control authorship-colors-switch" title="Show or hide author coloring"><input id="authorship-colors-toggle" type="checkbox" checked><span class="switch-control__track" aria-hidden="true"></span><span id="authorship-colors-label">Colors on</span></label><div class="authorship-mode-control" role="group" aria-label="Authorship display"><button type="button" data-authorship-mode="simple" class="active">Simple</button><button type="button" data-authorship-mode="full">Full</button></div><button id="save-editor-settings" class="secondary-button compact-button editor-settings-save" type="button">Save</button></div></div><div id="participant-badges" class="participant-badges" aria-label="Participants"></div>
|
||||
<div class="column-label editor-column-label"><span>Editor</span>
|
||||
<div class="authorship-controls"><label class="switch-control authorship-colors-switch"
|
||||
title="Show or hide author coloring"><input id="authorship-colors-toggle"
|
||||
type="checkbox" checked><span class="switch-control__track"
|
||||
aria-hidden="true"></span><span id="authorship-colors-label">Colors
|
||||
on</span></label>
|
||||
<div class="authorship-mode-control" role="group" aria-label="Authorship display"><button
|
||||
type="button" data-authorship-mode="simple" class="active">Simple</button><button
|
||||
type="button" data-authorship-mode="full">Full</button></div><button
|
||||
id="save-editor-settings" class="secondary-button compact-button editor-settings-save"
|
||||
type="button">Save</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="participant-badges" class="participant-badges" aria-label="Participants"></div>
|
||||
<div class="editor-shell">
|
||||
<div id="line-gutter" class="line-gutter" aria-hidden="true"></div>
|
||||
<div id="authorship-layer" class="authorship-layer" aria-hidden="true"></div>
|
||||
@@ -168,7 +192,8 @@
|
||||
<div class="shortcut-grid">
|
||||
<kbd>Ctrl/Cmd+Z</kbd><span>Undo</span><kbd>Ctrl/Cmd+B</kbd><span>Bold</span><kbd>Ctrl/Cmd+I</kbd><span>Italic</span><kbd>Ctrl/Cmd+Shift+X</kbd><span>Strikethrough</span><kbd>Ctrl/Cmd+K</kbd><span>Link</span><kbd>Ctrl/Cmd+Shift+7</kbd><span>Numbered
|
||||
list</span><kbd>Ctrl/Cmd+Shift+8</kbd><span>Bullet list</span><kbd>Ctrl/Cmd+Shift+9</kbd><span>Task
|
||||
list</span><kbd>Alt+1…4</kbd><span>Headings H1–H4</span><kbd>Alt+Enter</kbd><span>New line while editing Preview</span><kbd>Esc</kbd><span>Edit raw Markdown of current Preview line</span>
|
||||
list</span><kbd>Alt+1…4</kbd><span>Headings H1–H4</span><kbd>Alt+Enter</kbd><span>New line while
|
||||
editing Preview</span><kbd>Esc</kbd><span>Edit raw Markdown of current Preview line</span>
|
||||
</div>
|
||||
</div>
|
||||
</dialog>
|
||||
@@ -223,9 +248,6 @@
|
||||
class="mobile-color-dot" aria-hidden="true"></span></button>
|
||||
<button id="mobile-chat-button" class="mobile-chat-button" type="button" title="Chat"
|
||||
aria-label="Open chat">💬<span id="mobile-chat-unread" class="mobile-chat-unread" hidden></span></button>
|
||||
<button id="mobile-time-button" class="mobile-time-button" type="button" title="Current time"
|
||||
aria-label="Show current time">🕒</button>
|
||||
<span id="mobile-time-preview" class="mobile-time-preview" role="status" hidden></span>
|
||||
<span id="mobile-connection-status" class="mobile-connection-status" title="WebSocket status"><span
|
||||
id="mobile-status-dot" class="status__dot"></span><span
|
||||
id="mobile-status-text">Connecting…</span></span>
|
||||
|
||||
+2
-1
@@ -28,7 +28,8 @@
|
||||
<form id="public-password-form" class="dialog-panel">
|
||||
<h2>Protected page</h2>
|
||||
<p>Enter the note password or sign in with an account that has access.</p>
|
||||
<input id="public-password" type="password" autocomplete="current-password" minlength="8" maxlength="128" placeholder="Password">
|
||||
<input id="public-password" type="password" autocomplete="current-password" minlength="8" maxlength="128"
|
||||
placeholder="Password">
|
||||
<p id="public-password-error" class="form-message error"></p>
|
||||
<button class="primary-button">Open page</button>
|
||||
<a class="text-button" href="/">Back to home</a>
|
||||
|
||||
@@ -13,7 +13,8 @@
|
||||
|
||||
<body data-registration-enabled="__REGISTRATION_ENABLED__">
|
||||
<header class="app-header">
|
||||
<div class="app-header__main"><a class="brand home-brand" href="/">RustPad</a><span class="header-divider"></span>
|
||||
<div class="app-header__main"><a class="brand home-brand" href="/">RustPad</a><span
|
||||
class="header-divider"></span>
|
||||
<div class="document-heading">
|
||||
<h1 id="workspace-title">__WORKSPACE_TITLE__</h1>
|
||||
<p id="workspace-url" class="document-url"></p>
|
||||
@@ -50,8 +51,8 @@
|
||||
<section id="auth-panel" class="auth-panel" hidden>
|
||||
<h3 id="auth-mode-title">Log in</h3>
|
||||
<label id="auth-email-field">E-mail / organization login
|
||||
<input id="auth-email" name="username" type="email" maxlength="320"
|
||||
autocomplete="username" placeholder="you@example.com">
|
||||
<input id="auth-email" name="username" type="email" maxlength="320" autocomplete="username"
|
||||
placeholder="you@example.com">
|
||||
</label>
|
||||
<label>Password
|
||||
<input id="auth-password" name="password" type="password" minlength="8" maxlength="128"
|
||||
|
||||
Reference in New Issue
Block a user