Фаза 5: панель форматирования — FormatToolbar/TbButton, JS-форматирование в EditorPane (инлайн/списки/задачи/заголовки/цвета/индексы/цитата/выноска/обтекание), TableEditor (2×1, полосы «+» 20px)
This commit is contained in:
@@ -20,6 +20,132 @@ Item {
|
||||
headerTitle.text = title + (documentManager.isDirty(pane.docIndex) ? " •" : "")
|
||||
}
|
||||
|
||||
readonly property bool hasFocus: editor.activeFocus || viewer.activeFocus
|
||||
|
||||
// ---------- Функции форматирования (используются FormatToolbar) ----------
|
||||
function syncText() {
|
||||
documentManager.updateContent(pane.docIndex, editor.text)
|
||||
}
|
||||
function lineStart(pos) {
|
||||
const t = editor.text
|
||||
let i = Math.min(Math.max(pos, 0), t.length)
|
||||
while (i > 0 && t[i - 1] !== "\n") --i
|
||||
return i
|
||||
}
|
||||
function lineEnd(pos) {
|
||||
const t = editor.text
|
||||
let i = Math.min(Math.max(pos, 0), t.length)
|
||||
while (i < t.length && t[i] !== "\n") ++i
|
||||
return i
|
||||
}
|
||||
function replaceRange(start, end, newText) {
|
||||
editor.remove(start, end)
|
||||
editor.insert(start, newText)
|
||||
editor.cursorPosition = start + newText.length
|
||||
syncText()
|
||||
editor.forceActiveFocus()
|
||||
}
|
||||
// Обёртка выделения pre…post (жирный, курсив и т.п.)
|
||||
function applyInline(pre, post) {
|
||||
const s = editor.selectionStart, e = editor.selectionEnd
|
||||
const sel = editor.selectedText
|
||||
if (s === e) {
|
||||
const cp = editor.cursorPosition
|
||||
editor.insert(cp, pre + post)
|
||||
editor.cursorPosition = cp + pre.length
|
||||
} else {
|
||||
const t = pre + sel + post
|
||||
editor.remove(s, e)
|
||||
editor.insert(s, t)
|
||||
editor.cursorPosition = s + pre.length + sel.length
|
||||
}
|
||||
syncText()
|
||||
editor.forceActiveFocus()
|
||||
}
|
||||
// Префикс к каждой строке выделения (списки, цитата)
|
||||
function applyLinePrefix(prefix) {
|
||||
let s = editor.selectionStart, e = editor.selectionEnd
|
||||
if (s === e) { s = lineStart(editor.cursorPosition); e = lineEnd(editor.cursorPosition) }
|
||||
else { s = lineStart(s); e = lineEnd(e) }
|
||||
const lines = editor.text.substring(s, e).split("\n")
|
||||
for (let i = 0; i < lines.length; ++i)
|
||||
lines[i] = prefix + lines[i]
|
||||
replaceRange(s, e, lines.join("\n"))
|
||||
}
|
||||
// Переключение задач: "- [ ]"/"- [x]" ⇄ "- "
|
||||
function applyTaskToggle() {
|
||||
let s = editor.selectionStart, e = editor.selectionEnd
|
||||
if (s === e) { s = lineStart(editor.cursorPosition); e = lineEnd(editor.cursorPosition) }
|
||||
else { s = lineStart(s); e = lineEnd(e) }
|
||||
const lines = editor.text.substring(s, e).split("\n")
|
||||
for (let i = 0; i < lines.length; ++i) {
|
||||
const ln = lines[i]
|
||||
if (/^[-*]\s+\[[ x]\]\s+/.test(ln))
|
||||
lines[i] = ln.replace(/^([-*])\s+\[[ x]\]\s+/, "$1 ")
|
||||
else
|
||||
lines[i] = "- [ ] " + ln
|
||||
}
|
||||
replaceRange(s, e, lines.join("\n"))
|
||||
}
|
||||
// Заголовок уровня 1..6
|
||||
function applyHeading(level) {
|
||||
let s = editor.selectionStart, e = editor.selectionEnd
|
||||
if (s === e) { s = lineStart(editor.cursorPosition); e = lineEnd(editor.cursorPosition) }
|
||||
else { s = lineStart(s); e = lineEnd(e) }
|
||||
const lines = editor.text.substring(s, e).split("\n")
|
||||
for (let i = 0; i < lines.length; ++i)
|
||||
lines[i] = "#".repeat(level) + " " + lines[i].replace(/^#+\s*/, "")
|
||||
replaceRange(s, e, lines.join("\n"))
|
||||
}
|
||||
function applyColor(hex, isHighlight) {
|
||||
if (isHighlight)
|
||||
applyInline("<mark style=\"background:" + hex + "\">", "</mark>")
|
||||
else
|
||||
applyInline("<span style=\"color:" + hex + "\">", "</span>")
|
||||
}
|
||||
function applySubSup(kind) {
|
||||
applyInline(kind === "sup" ? "<sup>" : "<sub>", kind === "sup" ? "</sup>" : "</sub>")
|
||||
}
|
||||
function insertText(t) {
|
||||
editor.insert(editor.cursorPosition, t)
|
||||
syncText()
|
||||
editor.forceActiveFocus()
|
||||
}
|
||||
// Обтекание картинок/таблиц директивой ![[...|float:left|width:40%]]
|
||||
function applyFloatWrap() {
|
||||
const s = editor.selectionStart, e = editor.selectionEnd
|
||||
const sel = editor.selectedText.trim()
|
||||
if (sel.length > 0) {
|
||||
let inner = sel
|
||||
if (/^!\[\[.+\]\]$/.test(sel))
|
||||
inner = sel.slice(3, -2)
|
||||
const text = "![[" + inner
|
||||
+ (inner.includes("|") ? "" : "|float:left|width:40%") + "]]"
|
||||
editor.remove(s, e)
|
||||
editor.insert(s, text)
|
||||
editor.cursorPosition = s + text.length
|
||||
} else {
|
||||
const t = "![[image.png|float:left|width:40%]]"
|
||||
editor.insert(editor.cursorPosition, t)
|
||||
editor.cursorPosition += t.length
|
||||
}
|
||||
syncText()
|
||||
editor.forceActiveFocus()
|
||||
}
|
||||
// Выноска: > [!TYPE] + строки выделения
|
||||
function applyCallout(type) {
|
||||
let s = editor.selectionStart, e = editor.selectionEnd
|
||||
if (s === e) { s = lineStart(editor.cursorPosition); e = lineEnd(editor.cursorPosition) }
|
||||
else { s = lineStart(s); e = lineEnd(e) }
|
||||
const lines = editor.text.substring(s, e).split("\n").filter(l => l.trim().length > 0)
|
||||
const out = ["> [!NOTE]".replace("NOTE", type)]
|
||||
if (lines.length > 0)
|
||||
for (const l of lines) out.push("> " + l)
|
||||
else
|
||||
out.push("> Текст выноски")
|
||||
replaceRange(s, e, out.join("\n"))
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
anchors.fill: parent
|
||||
spacing: 0
|
||||
|
||||
Reference in New Issue
Block a user