-
🌐 async-utils.jsWeb, 3.69 kB⬇
-
🌐 auth-storage.jsWeb, 735 B⬇
-
🌐 chat-helpers.jsWeb, 8.85 kB⬇
-
🌐 chat-renderer.jsWeb, 70.00 kB⬇
-
🌐 code-overlay.jsWeb, 21.77 kB⬇
-
🌐 confirm-dialog.jsWeb, 3.17 kB⬇
-
🌐 conversation-format-utils.jsWeb, 21.51 kB⬇
-
🌐 diagram-overlay.jsWeb, 7.93 kB⬇
-
🌐 dialog.jsWeb, 2.50 kB⬇
-
🌐 dom-utils.jsWeb, 8.33 kB⬇
-
🌐 drag-reorder.jsWeb, 4.82 kB⬇
-
🌐 elements.jsWeb, 11.06 kB⬇
-
🌐 export-utils.jsWeb, 1.53 kB⬇
-
🌐 format-utils.jsWeb, 29.60 kB⬇
-
🌐 history-navigation.jsWeb, 1.95 kB⬇
-
🌐 hljs-yaml.jsWeb, 5.07 kB⬇
-
🌐 inline-format.jsWeb, 7.35 kB⬇
-
🌐 instant-answer.jsWeb, 19.82 kB⬇
-
🌐 interactive-tools.jsWeb, 38.77 kB⬇
-
🌐 mermaid-fixes.jsWeb, 4.60 kB⬇
-
🌐 mermaid-render.jsWeb, 5.10 kB⬇
-
🌐 mermaid.jsWeb, 195 B⬇
-
🌐 migrations.jsWeb, 12.11 kB⬇
-
🌐 multi-choice-dialog.jsWeb, 1.33 kB⬇
-
🌐 notification-stack.jsWeb, 2.50 kB⬇
-
🌐 options-manager.jsWeb, 45.56 kB⬇
-
🌐 overlay-utils.jsWeb, 4.14 kB⬇
-
🌐 pdf-export.jsWeb, 23.59 kB⬇
-
🌐 pinch-zoom.jsWeb, 3.21 kB⬇
-
🌐 plugin-dialog.jsWeb, 2.17 kB⬇
-
🌐 read-aloud.jsWeb, 33.84 kB⬇
-
🌐 response-pipeline.jsWeb, 55.19 kB⬇
-
🌐 scratchpad-editor.jsWeb, 8.64 kB⬇
-
🌐 scratchpad-export.jsWeb, 3.56 kB⬇
-
🌐 scrollbars.jsWeb, 3.12 kB⬇
-
🌐 selection-manager.jsWeb, 3.76 kB⬇
-
🌐 settings-utils.jsWeb, 31.08 kB⬇
-
🌐 share.jsWeb, 10.75 kB⬇
-
🌐 side-drawer.jsWeb, 9.37 kB⬇
-
🌐 speech-text.jsWeb, 6.60 kB⬇
-
🌐 swipe-navigation.jsWeb, 6.16 kB⬇
-
🌐 table-overlay.jsWeb, 8.10 kB⬇
-
🌐 theme-utils.jsWeb, 8.34 kB⬇
-
🌐 thinking-panel-base.jsWeb, 14.88 kB⬇
-
🌐 ui-sets.jsWeb, 1.00 kB⬇
-
🌐 version-overlay.jsWeb, 4.63 kB⬇
import { escapeHtml } from './format-utils.js';
import { openPluginDialog } from './plugin-dialog.js';
/**
* Shared inline formatting for contenteditable editors. Used by the document
* editor and the sticky notes editor: execCommand-based format actions for a
* toolbar of `data-action="fmt-*"` buttons, caret-state highlighting, and the
* sup/sub unwrap that removeFormat misses.
*/
/**
* Returns the element hosting the caret, resolving element boundaries to
* the adjacent child so inline formatting at the edge is detected.
* @param {HTMLElement|null} root_el - The editor root the caret must be inside.
* @returns {HTMLElement|null} The caret host element.
*/
export function inline_host_of(root_el) {
const selection = window.getSelection();
if (!selection || selection.rangeCount === 0) return null;
const range = selection.getRangeAt(0);
if (!root_el?.contains(range.startContainer)) return null;
if (range.startContainer.nodeType === 3) {
return range.startContainer.parentElement;
}
return range.startOffset > 0 ? (range.startContainer.childNodes[range.startOffset - 1] || null) : range.startContainer;
}
/**
* Highlights the formatting buttons that are active at the caret: bold,
* italic, underline, strike, superscript, subscript, inline code, and links.
* Buttons absent from the toolbar are skipped.
* @param {HTMLElement|null} toolbar_el - The toolbar holding the fmt buttons.
* @param {HTMLElement|null} root_el - The editor root the caret must be inside.
*/
export function sync_inline_format_states(toolbar_el, root_el) {
if (!toolbar_el) return;
const host = inline_host_of(root_el);
if (!host) return;
const state = (command) => {
try { return document.queryCommandState(command); } catch { return false; }
};
const toggle = (action, active) => {
const btn = toolbar_el.querySelector(`button[data-action="${action}"]`);
if (btn) btn.classList.toggle('selected', active);
};
toggle('fmt-bold', state('bold'));
toggle('fmt-italic', state('italic'));
toggle('fmt-underline', state('underline'));
toggle('fmt-strike', state('strikeThrough'));
toggle('fmt-superscript', state('superscript'));
toggle('fmt-subscript', state('subscript'));
toggle('fmt-code', !!host.closest('code'));
toggle('fmt-link', !!host.closest('a'));
}
/**
* Unwraps sup and sub elements that intersect the current selection
* (removeFormat does not remove them).
* @param {HTMLElement|null} root_el - The editor root to clean up.
*/
export function unwrap_sup_sub_in(root_el) {
const selection = window.getSelection();
if (!selection || selection.rangeCount === 0 || !root_el) return;
const range = selection.getRangeAt(0);
for (const el of Array.from(root_el.querySelectorAll('sup, sub'))) {
if (range.intersectsNode(el)) {
while (el.firstChild) el.parentNode.insertBefore(el.firstChild, el);
el.remove();
}
}
}
/**
* Per-root memory of the element the last sup/sub apply created, so the next
* toggle can unwrap it (Chrome applies the tag but collapses the selection
* outside it, breaking execCommand's toggle).
* @type {WeakMap<HTMLElement, HTMLElement|null>}
*/
const sup_sub_state = new WeakMap();
/**
* Toggles superscript or subscript on the selection: the removal unwraps the
* element remembered from the previous apply instead of relying on
* execCommand's toggle.
* @param {'sup'|'sub'} tag - The tag to toggle.
* @param {HTMLElement|null} root_el - The editor root holding the selection.
* @param {Function} on_change - Called after the content mutated.
*/
function format_sup_sub(tag, root_el, on_change) {
const last = sup_sub_state.get(root_el);
if (last?.isConnected) {
while (last.firstChild) last.parentNode.insertBefore(last.firstChild, last);
last.remove();
sup_sub_state.delete(root_el);
on_change();
return;
}
sup_sub_state.delete(root_el);
document.execCommand(tag === 'sup' ? 'superscript' : 'subscript', false, null);
sup_sub_state.set(root_el, inline_host_of(root_el)?.closest(tag) || null);
on_change();
}
/**
* Runs an inline formatting action on the current selection: bold, italic,
* underline, strike, superscript, subscript, clear, code, or link. The link
* action opens the shared link dialog and wraps the saved selection in an
* anchor when applied.
* @param {string} action - The button's data-action identifier (fmt-*).
* @param {HTMLElement|null} root_el - The editor root holding the selection.
* @param {Function} on_change - Called after the content mutated.
* @returns {boolean} Whether the action was handled.
*/
export function apply_inline_format(action, root_el, on_change) {
const command = {
'fmt-bold': 'bold',
'fmt-italic': 'italic',
'fmt-underline': 'underline',
'fmt-strike': 'strikeThrough'
}[action];
if (command) {
document.execCommand(command, false, null);
on_change();
return true;
}
if (action === 'fmt-clear') {
document.execCommand('removeFormat', false, null);
unwrap_sup_sub_in(root_el);
on_change();
return true;
}
if (action === 'fmt-superscript') {
format_sup_sub('sup', root_el, on_change);
return true;
}
if (action === 'fmt-subscript') {
format_sup_sub('sub', root_el, on_change);
return true;
}
if (action === 'fmt-code') {
const selection = window.getSelection();
if (!selection || selection.rangeCount === 0 || selection.isCollapsed) return true;
document.execCommand('insertHTML', false, '<code>' + escapeHtml(selection.toString()) + '</code>');
on_change();
return true;
}
if (action === 'fmt-link') {
format_link(root_el, on_change);
return true;
}
return false;
}
/**
* Opens the link dialog for the current selection: the range is saved up
* front and wrapped in an anchor when the dialog is applied, independent of
* focus changes while the dialog is open.
* @param {HTMLElement|null} root_el - The editor root holding the selection.
* @param {Function} on_change - Called after the anchor was inserted.
*/
function format_link(root_el, on_change) {
const selection = window.getSelection();
if (!selection || selection.rangeCount === 0 || selection.isCollapsed) return;
const pending_range = selection.getRangeAt(0).cloneRange();
const content = document.createElement('div');
content.className = 'div-plugin-dialog-body';
const label = document.createElement('p');
const label_el = document.createElement('label');
label_el.textContent = 'URL';
label_el.setAttribute('for', 'id-inline-format-link-url');
label.appendChild(label_el);
const input = document.createElement('input');
input.type = 'text';
input.id = 'id-inline-format-link-url';
input.className = 'prompt-style-input';
input.placeholder = 'https://...';
content.append(label, input);
const apply = () => {
const url = input.value.trim();
handle.close();
if (!url) return;
const anchor = document.createElement('a');
anchor.href = url;
anchor.appendChild(pending_range.extractContents());
pending_range.insertNode(anchor);
on_change();
};
const actions = document.createElement('div');
actions.className = 'div-document-item-dialog-actions';
const apply_btn = document.createElement('button');
apply_btn.type = 'button';
apply_btn.className = 'as-icon';
apply_btn.textContent = 'Apply';
apply_btn.onclick = apply;
actions.appendChild(apply_btn);
content.appendChild(actions);
input.addEventListener('keydown', (e) => {
if (e.key === 'Enter') {
e.preventDefault();
apply();
}
});
const handle = openPluginDialog('Insert Link', content, null);
input.focus();
}
inline-format.js
×
Type: Web, text/x-java
7.35 Kilobytes
Last Modified 2026-11-04 09:48:28
⬇ Download File
Type: Web, text/x-java
7.35 Kilobytes
Last Modified 2026-11-04 09:48:28
⬇ Download File