Improve markdown editor
This commit is contained in:
166
assets/editor.js
166
assets/editor.js
@@ -2,6 +2,10 @@
|
||||
var textarea = document.getElementById('editor');
|
||||
if (!textarea) return;
|
||||
|
||||
var form = textarea.closest('form');
|
||||
|
||||
// --- DOM helpers ---
|
||||
|
||||
function wrap(before, after, placeholder) {
|
||||
var start = textarea.selectionStart;
|
||||
var end = textarea.selectionEnd;
|
||||
@@ -26,24 +30,63 @@
|
||||
textarea.focus();
|
||||
}
|
||||
|
||||
var form = textarea.closest('form');
|
||||
function insertAtCursor(s) {
|
||||
var start = textarea.selectionStart;
|
||||
var end = textarea.selectionEnd;
|
||||
textarea.value = textarea.value.slice(0, start) + s + textarea.value.slice(end);
|
||||
textarea.selectionStart = textarea.selectionEnd = start + s.length;
|
||||
textarea.dispatchEvent(new Event('input'));
|
||||
textarea.focus();
|
||||
}
|
||||
|
||||
function applyResult(result) {
|
||||
textarea.value = result.text;
|
||||
textarea.selectionStart = textarea.selectionEnd = result.cursor;
|
||||
textarea.dispatchEvent(new Event('input'));
|
||||
textarea.focus();
|
||||
}
|
||||
|
||||
function applyTableOp(fn, arg) {
|
||||
var result = arg !== undefined
|
||||
? fn(textarea.value, textarea.selectionStart, arg)
|
||||
: fn(textarea.value, textarea.selectionStart);
|
||||
if (result) applyResult(result);
|
||||
}
|
||||
|
||||
// --- Actions ---
|
||||
|
||||
var T = EditorTables;
|
||||
var L = EditorLists;
|
||||
var D = EditorDates;
|
||||
|
||||
var actions = {
|
||||
save: function () { form.submit(); },
|
||||
bold: function () { wrap('**', '**', 'bold text'); },
|
||||
italic: function () { wrap('*', '*', 'italic text'); },
|
||||
h1: function () { linePrefix('# '); },
|
||||
h2: function () { linePrefix('## '); },
|
||||
h3: function () { linePrefix('### '); },
|
||||
code: function () { wrap('`', '`', 'code'); },
|
||||
codeblock: function () { wrap('```\n', '\n```', 'code'); },
|
||||
quote: function () { linePrefix('> '); },
|
||||
link: function () { wrap('[', '](url)', 'link text'); },
|
||||
ul: function () { linePrefix('- '); },
|
||||
ol: function () { linePrefix('1. '); },
|
||||
hr: function () { wrap('\n\n---\n\n', '', ''); },
|
||||
save: function () { form.submit(); },
|
||||
bold: function () { wrap('**', '**', 'bold text'); },
|
||||
italic: function () { wrap('*', '*', 'italic text'); },
|
||||
h1: function () { linePrefix('# '); },
|
||||
h2: function () { linePrefix('## '); },
|
||||
h3: function () { linePrefix('### '); },
|
||||
code: function () { wrap('`', '`', 'code'); },
|
||||
codeblock: function () { wrap('```\n', '\n```', 'code'); },
|
||||
quote: function () { linePrefix('> '); },
|
||||
link: function () { wrap('[', '](url)', 'link text'); },
|
||||
ul: function () { linePrefix('- '); },
|
||||
ol: function () { linePrefix('1. '); },
|
||||
hr: function () { wrap('\n\n---\n\n', '', ''); },
|
||||
fmttable: function () { applyTableOp(T.formatTableText); },
|
||||
tblalignleft: function () { applyTableOp(T.setColumnAlignment, 'left'); },
|
||||
tblaligncenter: function () { applyTableOp(T.setColumnAlignment, 'center'); },
|
||||
tblalignright: function () { applyTableOp(T.setColumnAlignment, 'right'); },
|
||||
tblinsertcol: function () { applyTableOp(T.insertColumn); },
|
||||
tbldeletecol: function () { applyTableOp(T.deleteColumn); },
|
||||
tblinsertrow: function () { applyTableOp(T.insertRow); },
|
||||
tbldeleterow: function () { applyTableOp(T.deleteRow); },
|
||||
dateiso: function () { insertAtCursor(D.isoDate()); },
|
||||
datelong: function () { insertAtCursor(D.longDate()); },
|
||||
};
|
||||
|
||||
// --- Keyboard shortcut registration ---
|
||||
|
||||
var keyMap = {};
|
||||
document.querySelectorAll('[data-action]').forEach(function (btn) {
|
||||
btn.addEventListener('click', function () {
|
||||
@@ -55,6 +98,10 @@
|
||||
}
|
||||
});
|
||||
|
||||
keyMap['T'] = actions.fmttable;
|
||||
keyMap['D'] = actions.dateiso;
|
||||
keyMap['W'] = actions.datelong;
|
||||
|
||||
document.addEventListener('keydown', function (e) {
|
||||
if (!e.altKey || !e.shiftKey) return;
|
||||
var action = keyMap[e.key];
|
||||
@@ -63,4 +110,95 @@
|
||||
action();
|
||||
}
|
||||
});
|
||||
|
||||
// --- Textarea key handling ---
|
||||
|
||||
textarea.addEventListener('keydown', function (e) {
|
||||
if (e.key === 'Delete' && e.shiftKey) {
|
||||
var result = T.deleteRow(textarea.value, textarea.selectionStart)
|
||||
|| L.deleteOrderedLine(textarea.value, textarea.selectionStart);
|
||||
if (!result) return;
|
||||
e.preventDefault();
|
||||
applyResult(result);
|
||||
return;
|
||||
}
|
||||
if (e.key === 'Enter' && e.shiftKey) {
|
||||
var result = T.insertRowBelow(textarea.value, textarea.selectionStart);
|
||||
if (!result) return;
|
||||
e.preventDefault();
|
||||
applyResult(result);
|
||||
return;
|
||||
}
|
||||
if (e.key !== 'Enter') return;
|
||||
var result = L.handleEnterKey(textarea.value, textarea.selectionStart);
|
||||
if (!result) return;
|
||||
e.preventDefault();
|
||||
applyResult(result);
|
||||
});
|
||||
|
||||
// --- Dropdown helper ---
|
||||
|
||||
var openMenus = [];
|
||||
|
||||
function makeDropdown(triggerBtn, items) {
|
||||
var menu = document.createElement('div');
|
||||
menu.className = 'toolbar-dropdown-menu';
|
||||
items.forEach(function (item) {
|
||||
var btn = document.createElement('button');
|
||||
btn.type = 'button';
|
||||
btn.className = 'btn btn-tool toolbar-dropdown-item';
|
||||
btn.textContent = item.label;
|
||||
btn.addEventListener('mousedown', function (e) {
|
||||
e.preventDefault();
|
||||
actions[item.action]();
|
||||
menu.classList.remove('is-open');
|
||||
});
|
||||
menu.appendChild(btn);
|
||||
});
|
||||
triggerBtn.appendChild(menu);
|
||||
openMenus.push(menu);
|
||||
|
||||
triggerBtn.addEventListener('click', function (e) {
|
||||
if (e.target !== triggerBtn) return;
|
||||
var wasOpen = menu.classList.contains('is-open');
|
||||
openMenus.forEach(function (m) { m.classList.remove('is-open'); });
|
||||
if (!wasOpen) menu.classList.add('is-open');
|
||||
});
|
||||
}
|
||||
|
||||
document.addEventListener('click', function (e) {
|
||||
var insideAny = openMenus.some(function (m) {
|
||||
return m.parentElement && m.parentElement.contains(e.target);
|
||||
});
|
||||
if (!insideAny) openMenus.forEach(function (m) { m.classList.remove('is-open'); });
|
||||
});
|
||||
document.addEventListener('keydown', function (e) {
|
||||
if (e.key === 'Escape') openMenus.forEach(function (m) { m.classList.remove('is-open'); });
|
||||
});
|
||||
|
||||
// --- Table dropdown ---
|
||||
|
||||
var tblDropBtn = document.querySelector('[data-action="tbldrop"]');
|
||||
if (tblDropBtn) {
|
||||
makeDropdown(tblDropBtn, [
|
||||
{ label: 'Format table', action: 'fmttable' },
|
||||
{ label: 'Align left', action: 'tblalignleft' },
|
||||
{ label: 'Align center', action: 'tblaligncenter' },
|
||||
{ label: 'Align right', action: 'tblalignright' },
|
||||
{ label: 'Insert column', action: 'tblinsertcol' },
|
||||
{ label: 'Delete column', action: 'tbldeletecol' },
|
||||
{ label: 'Insert row', action: 'tblinsertrow' },
|
||||
{ label: 'Delete row', action: 'tbldeleterow' },
|
||||
]);
|
||||
}
|
||||
|
||||
// --- Date dropdown ---
|
||||
|
||||
var dateDropBtn = document.querySelector('[data-action="datedrop"]');
|
||||
if (dateDropBtn) {
|
||||
makeDropdown(dateDropBtn, [
|
||||
{ label: 'YYYY-MM-DD', action: 'dateiso' },
|
||||
{ label: 'DE Long', action: 'datelong' },
|
||||
]);
|
||||
}
|
||||
})();
|
||||
|
||||
17
assets/editor/dates.js
Normal file
17
assets/editor/dates.js
Normal file
@@ -0,0 +1,17 @@
|
||||
window.EditorDates = (function () {
|
||||
|
||||
function pad2(n) { return n < 10 ? '0' + n : '' + n; }
|
||||
|
||||
function isoDate() {
|
||||
var d = new Date();
|
||||
return d.getFullYear() + '-' + pad2(d.getMonth() + 1) + '-' + pad2(d.getDate());
|
||||
}
|
||||
|
||||
function longDate() {
|
||||
return new Date().toLocaleDateString('de-DE', {
|
||||
weekday: 'long', year: 'numeric', month: 'long', day: 'numeric'
|
||||
});
|
||||
}
|
||||
|
||||
return { isoDate: isoDate, longDate: longDate };
|
||||
})();
|
||||
86
assets/editor/lists.js
Normal file
86
assets/editor/lists.js
Normal file
@@ -0,0 +1,86 @@
|
||||
window.EditorLists = (function () {
|
||||
|
||||
function detectListPrefix(lineText) {
|
||||
var m;
|
||||
m = lineText.match(/^(\s*)(- \[[ x]\] )/);
|
||||
if (m) return { indent: m[1], prefix: m[2], type: 'task' };
|
||||
m = lineText.match(/^(\s*)([-*+] )/);
|
||||
if (m) return { indent: m[1], prefix: m[2], type: 'unordered' };
|
||||
m = lineText.match(/^(\s*)(\d+)\. /);
|
||||
if (m) return { indent: m[1], prefix: m[2] + '. ', type: 'ordered', num: parseInt(m[2], 10) };
|
||||
m = lineText.match(/^(\s*)(> )/);
|
||||
if (m) return { indent: m[1], prefix: m[2], type: 'blockquote' };
|
||||
return null;
|
||||
}
|
||||
|
||||
function continuationPrefix(info) {
|
||||
if (info.type === 'task') return info.indent + '- [ ] ';
|
||||
if (info.type === 'ordered') return info.indent + (info.num + 1) + '. ';
|
||||
return info.indent + info.prefix;
|
||||
}
|
||||
|
||||
function renumberOrderedList(text, fromLineIndex, indent, startNum) {
|
||||
var lines = text.split('\n');
|
||||
var re = new RegExp('^' + indent.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + '(\\d+)\\. ');
|
||||
var num = startNum !== undefined ? startNum : null;
|
||||
for (var i = fromLineIndex; i < lines.length; i++) {
|
||||
var m = lines[i].match(re);
|
||||
if (!m) break;
|
||||
if (num === null) num = parseInt(m[1], 10);
|
||||
var newNumStr = String(num);
|
||||
if (m[1] !== newNumStr) {
|
||||
lines[i] = indent + newNumStr + '. ' + lines[i].slice(m[0].length);
|
||||
}
|
||||
num++;
|
||||
}
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
function handleEnterKey(text, cursorPos) {
|
||||
var before = text.slice(0, cursorPos);
|
||||
var after = text.slice(cursorPos);
|
||||
var lineStart = before.lastIndexOf('\n') + 1;
|
||||
var lineEnd = text.indexOf('\n', cursorPos);
|
||||
if (lineEnd === -1) lineEnd = text.length;
|
||||
var fullLine = text.slice(lineStart, lineEnd);
|
||||
var info = detectListPrefix(fullLine);
|
||||
if (!info) return null;
|
||||
|
||||
var contentAfterPrefix = fullLine.slice(info.indent.length + info.prefix.length);
|
||||
if (contentAfterPrefix.trim() === '') {
|
||||
var newText = text.slice(0, lineStart) + '\n' + after;
|
||||
var newCursor = lineStart + 1;
|
||||
if (info.type === 'ordered') {
|
||||
var lineIndex = text.slice(0, lineStart).split('\n').length;
|
||||
newText = renumberOrderedList(newText, lineIndex, info.indent);
|
||||
}
|
||||
return { text: newText, cursor: newCursor };
|
||||
}
|
||||
|
||||
var cont = continuationPrefix(info);
|
||||
var newText = before + '\n' + cont + after;
|
||||
var newCursor = cursorPos + 1 + cont.length;
|
||||
if (info.type === 'ordered') {
|
||||
var insertedLineIndex = before.split('\n').length;
|
||||
newText = renumberOrderedList(newText, insertedLineIndex, info.indent);
|
||||
}
|
||||
return { text: newText, cursor: newCursor };
|
||||
}
|
||||
|
||||
function deleteOrderedLine(text, cursorPos) {
|
||||
var lineStart = text.lastIndexOf('\n', cursorPos - 1) + 1;
|
||||
var lineEnd = text.indexOf('\n', cursorPos);
|
||||
if (lineEnd === -1) lineEnd = text.length;
|
||||
var fullLine = text.slice(lineStart, lineEnd);
|
||||
var info = detectListPrefix(fullLine);
|
||||
if (!info || info.type !== 'ordered') return null;
|
||||
|
||||
var newText = text.slice(0, lineStart) + text.slice(lineEnd === text.length ? lineEnd : lineEnd + 1);
|
||||
var newCursor = lineStart;
|
||||
var fromLineIndex = text.slice(0, lineStart).split('\n').length - 1;
|
||||
newText = renumberOrderedList(newText, fromLineIndex, info.indent, info.num);
|
||||
return { text: newText, cursor: Math.min(newCursor, newText.length) };
|
||||
}
|
||||
|
||||
return { handleEnterKey: handleEnterKey, deleteOrderedLine: deleteOrderedLine };
|
||||
})();
|
||||
274
assets/editor/tables.js
Normal file
274
assets/editor/tables.js
Normal file
@@ -0,0 +1,274 @@
|
||||
window.EditorTables = (function () {
|
||||
|
||||
function repeat(ch, n) {
|
||||
var s = '';
|
||||
for (var i = 0; i < n; i++) s += ch;
|
||||
return s;
|
||||
}
|
||||
|
||||
function padLeft(s, w) {
|
||||
while (s.length < w) s = ' ' + s;
|
||||
return s;
|
||||
}
|
||||
|
||||
function padRight(s, w) {
|
||||
while (s.length < w) s += ' ';
|
||||
return s;
|
||||
}
|
||||
|
||||
function padCenter(s, w) {
|
||||
while (s.length < w) {
|
||||
s = s + ' ';
|
||||
if (s.length < w) s = ' ' + s;
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
function parseTableRow(line) {
|
||||
var trimmed = line.trim();
|
||||
if (trimmed.charAt(0) === '|') trimmed = trimmed.slice(1);
|
||||
if (trimmed.charAt(trimmed.length - 1) === '|') trimmed = trimmed.slice(0, -1);
|
||||
return trimmed.split('|').map(function (c) { return c.trim(); });
|
||||
}
|
||||
|
||||
function isSeparatorRow(cells) {
|
||||
return cells.every(function (c) { return /^:?-+:?$/.test(c); });
|
||||
}
|
||||
|
||||
function parseAlignment(cell) {
|
||||
var left = cell.charAt(0) === ':';
|
||||
var right = cell.charAt(cell.length - 1) === ':';
|
||||
if (left && right) return 'center';
|
||||
if (left) return 'left';
|
||||
if (right) return 'right';
|
||||
return null;
|
||||
}
|
||||
|
||||
function makeSepCell(width, align) {
|
||||
if (align === 'left') return ':' + repeat('-', width - 1);
|
||||
if (align === 'center') return ':' + repeat('-', width - 2) + ':';
|
||||
if (align === 'right') return repeat('-', width - 1) + ':';
|
||||
return repeat('-', width);
|
||||
}
|
||||
|
||||
function findTableRange(text, cursorPos) {
|
||||
var lines = text.split('\n');
|
||||
var charCount = 0;
|
||||
var cursorLine = 0;
|
||||
for (var i = 0; i < lines.length; i++) {
|
||||
if (charCount + lines[i].length >= cursorPos && charCount <= cursorPos) {
|
||||
cursorLine = i;
|
||||
}
|
||||
charCount += lines[i].length + 1;
|
||||
}
|
||||
if (!/^\|.*\|$/.test(lines[cursorLine].trim())) return null;
|
||||
var start = cursorLine;
|
||||
while (start > 0 && /^\|.*\|$/.test(lines[start - 1].trim())) start--;
|
||||
var end = cursorLine;
|
||||
while (end < lines.length - 1 && /^\|.*\|$/.test(lines[end + 1].trim())) end++;
|
||||
if (end - start < 1) return null;
|
||||
return { start: start, end: end, lines: lines, cursorLine: cursorLine };
|
||||
}
|
||||
|
||||
function formatTableText(text, cursorPos) {
|
||||
var range = findTableRange(text, cursorPos);
|
||||
if (!range) return null;
|
||||
|
||||
var rows = [];
|
||||
var sepIndex = -1;
|
||||
var alignments = [];
|
||||
for (var i = range.start; i <= range.end; i++) {
|
||||
var cells = parseTableRow(range.lines[i]);
|
||||
if (sepIndex === -1 && isSeparatorRow(cells)) {
|
||||
sepIndex = rows.length;
|
||||
alignments = cells.map(function (c) { return parseAlignment(c); });
|
||||
}
|
||||
rows.push(cells);
|
||||
}
|
||||
if (sepIndex === -1) return null;
|
||||
|
||||
var colCount = 0;
|
||||
rows.forEach(function (r) { if (r.length > colCount) colCount = r.length; });
|
||||
while (alignments.length < colCount) alignments.push(null);
|
||||
|
||||
rows = rows.map(function (r) {
|
||||
while (r.length < colCount) r.push('');
|
||||
return r;
|
||||
});
|
||||
|
||||
var widths = [];
|
||||
for (var c = 0; c < colCount; c++) {
|
||||
var max = 3;
|
||||
for (var r = 0; r < rows.length; r++) {
|
||||
if (r === sepIndex) continue;
|
||||
if (rows[r][c].length > max) max = rows[r][c].length;
|
||||
}
|
||||
widths.push(max);
|
||||
}
|
||||
|
||||
var formatted = rows.map(function (row, ri) {
|
||||
var cells = row.map(function (cell, ci) {
|
||||
var w = widths[ci];
|
||||
if (ri === sepIndex) return makeSepCell(w, alignments[ci]);
|
||||
var align = alignments[ci];
|
||||
if (align === 'right') return padLeft(cell, w);
|
||||
if (align === 'center') return padCenter(cell, w);
|
||||
return padRight(cell, w);
|
||||
});
|
||||
return '| ' + cells.join(' | ') + ' |';
|
||||
});
|
||||
|
||||
var beforeTable = range.lines.slice(0, range.start).join('\n');
|
||||
var afterTable = range.lines.slice(range.end + 1).join('\n');
|
||||
var parts = [];
|
||||
if (beforeTable) parts.push(beforeTable);
|
||||
parts.push(formatted.join('\n'));
|
||||
if (afterTable) parts.push(afterTable);
|
||||
var newText = parts.join('\n');
|
||||
|
||||
var oldBeforeLen = 0;
|
||||
for (var i = 0; i < range.start; i++) oldBeforeLen += range.lines[i].length + 1;
|
||||
var cursorInTable = cursorPos - oldBeforeLen;
|
||||
var newTableText = formatted.join('\n');
|
||||
var newCursor = (beforeTable ? beforeTable.length + 1 : 0) + Math.min(cursorInTable, newTableText.length);
|
||||
|
||||
return { text: newText, cursor: newCursor };
|
||||
}
|
||||
|
||||
function getCursorColumn(text, cursorPos) {
|
||||
var range = findTableRange(text, cursorPos);
|
||||
if (!range) return null;
|
||||
var lines = text.split('\n');
|
||||
var charCount = 0;
|
||||
for (var i = 0; i < range.cursorLine; i++) charCount += lines[i].length + 1;
|
||||
var lineOffset = cursorPos - charCount;
|
||||
var line = lines[range.cursorLine];
|
||||
var col = -1;
|
||||
for (var c = 0; c < line.length; c++) {
|
||||
if (line.charAt(c) === '|') {
|
||||
col++;
|
||||
if (c >= lineOffset) return Math.max(col - 1, 0);
|
||||
}
|
||||
}
|
||||
return Math.max(col, 0);
|
||||
}
|
||||
|
||||
function setColumnAlignment(text, cursorPos, align) {
|
||||
var range = findTableRange(text, cursorPos);
|
||||
if (!range) return null;
|
||||
var colIdx = getCursorColumn(text, cursorPos);
|
||||
if (colIdx === null) return null;
|
||||
|
||||
var rows = [];
|
||||
var sepIndex = -1;
|
||||
for (var i = range.start; i <= range.end; i++) {
|
||||
var cells = parseTableRow(range.lines[i]);
|
||||
if (sepIndex === -1 && isSeparatorRow(cells)) sepIndex = rows.length;
|
||||
rows.push(cells);
|
||||
}
|
||||
if (sepIndex === -1 || colIdx >= rows[sepIndex].length) return null;
|
||||
|
||||
var cell = rows[sepIndex][colIdx].replace(/:/g, '-');
|
||||
if (align === 'left') cell = ':' + cell.slice(1);
|
||||
else if (align === 'center') cell = ':' + cell.slice(1, -1) + ':';
|
||||
else if (align === 'right') cell = cell.slice(0, -1) + ':';
|
||||
rows[sepIndex][colIdx] = cell;
|
||||
|
||||
var newLines = range.lines.slice();
|
||||
for (var i = 0; i < rows.length; i++) {
|
||||
newLines[range.start + i] = '| ' + rows[i].join(' | ') + ' |';
|
||||
}
|
||||
return formatTableText(newLines.join('\n'), cursorPos);
|
||||
}
|
||||
|
||||
function insertColumn(text, cursorPos) {
|
||||
var range = findTableRange(text, cursorPos);
|
||||
if (!range) return null;
|
||||
var colIdx = getCursorColumn(text, cursorPos);
|
||||
if (colIdx === null) return null;
|
||||
|
||||
var newLines = range.lines.slice();
|
||||
var sepIndex = -1;
|
||||
for (var i = range.start; i <= range.end; i++) {
|
||||
var cells = parseTableRow(range.lines[i]);
|
||||
var isSep = sepIndex === -1 && isSeparatorRow(cells);
|
||||
if (isSep) sepIndex = i;
|
||||
cells.splice(colIdx, 0, isSep ? '---' : '');
|
||||
newLines[i] = '| ' + cells.join(' | ') + ' |';
|
||||
}
|
||||
return formatTableText(newLines.join('\n'), cursorPos);
|
||||
}
|
||||
|
||||
function deleteColumn(text, cursorPos) {
|
||||
var range = findTableRange(text, cursorPos);
|
||||
if (!range) return null;
|
||||
var colIdx = getCursorColumn(text, cursorPos);
|
||||
if (colIdx === null) return null;
|
||||
|
||||
var newLines = range.lines.slice();
|
||||
for (var i = range.start; i <= range.end; i++) {
|
||||
var cells = parseTableRow(range.lines[i]);
|
||||
if (cells.length <= 1) return null;
|
||||
cells.splice(colIdx, 1);
|
||||
newLines[i] = '| ' + cells.join(' | ') + ' |';
|
||||
}
|
||||
return formatTableText(newLines.join('\n'), cursorPos);
|
||||
}
|
||||
|
||||
function insertRow(text, cursorPos) {
|
||||
var range = findTableRange(text, cursorPos);
|
||||
if (!range) return null;
|
||||
var colCount = 0;
|
||||
for (var i = range.start; i <= range.end; i++) {
|
||||
var cells = parseTableRow(range.lines[i]);
|
||||
if (cells.length > colCount) colCount = cells.length;
|
||||
}
|
||||
var emptyCells = [];
|
||||
for (var c = 0; c < colCount; c++) emptyCells.push('');
|
||||
var newLines = range.lines.slice();
|
||||
newLines.splice(range.cursorLine + 1, 0, '| ' + emptyCells.join(' | ') + ' |');
|
||||
return formatTableText(newLines.join('\n'), cursorPos);
|
||||
}
|
||||
|
||||
function insertRowBelow(text, cursorPos) {
|
||||
var range = findTableRange(text, cursorPos);
|
||||
if (!range) return null;
|
||||
var result = insertRow(text, cursorPos);
|
||||
if (!result) return null;
|
||||
var lines = result.text.split('\n');
|
||||
var cursor = 0;
|
||||
for (var i = 0; i <= range.cursorLine; i++) cursor += lines[i].length + 1;
|
||||
cursor += 2; // skip leading '| '
|
||||
return { text: result.text, cursor: cursor };
|
||||
}
|
||||
|
||||
function deleteRow(text, cursorPos) {
|
||||
var range = findTableRange(text, cursorPos);
|
||||
if (!range) return null;
|
||||
var sepIndex = -1;
|
||||
for (var i = range.start; i <= range.end; i++) {
|
||||
if (isSeparatorRow(parseTableRow(range.lines[i]))) { sepIndex = i; break; }
|
||||
}
|
||||
if (range.cursorLine === range.start || range.cursorLine === sepIndex) return null;
|
||||
|
||||
var newLines = range.lines.slice();
|
||||
newLines.splice(range.cursorLine, 1);
|
||||
var newCursor = cursorPos;
|
||||
if (range.cursorLine < newLines.length) {
|
||||
var charCount = 0;
|
||||
for (var i = 0; i < range.cursorLine; i++) charCount += newLines[i].length + 1;
|
||||
newCursor = charCount;
|
||||
}
|
||||
return formatTableText(newLines.join('\n'), Math.min(newCursor, newLines.join('\n').length));
|
||||
}
|
||||
|
||||
return {
|
||||
formatTableText: formatTableText,
|
||||
setColumnAlignment: setColumnAlignment,
|
||||
insertColumn: insertColumn,
|
||||
deleteColumn: deleteColumn,
|
||||
insertRow: insertRow,
|
||||
insertRowBelow: insertRowBelow,
|
||||
deleteRow: deleteRow,
|
||||
};
|
||||
})();
|
||||
@@ -42,9 +42,15 @@
|
||||
<button type="button" class="btn btn-tool" data-action="ul" data-key="U" title="Unordered list (U)">-</button>
|
||||
<button type="button" class="btn btn-tool" data-action="ol" data-key="O" title="Ordered list (O)">1.</button>
|
||||
<button type="button" class="btn btn-tool" data-action="hr" data-key="R" title="Horizontal rule (R)">---</button>
|
||||
<span class="toolbar-sep"></span>
|
||||
<button type="button" class="btn btn-tool toolbar-dropdown" data-action="tbldrop" title="Table (T)">T▾</button>
|
||||
<button type="button" class="btn btn-tool toolbar-dropdown" data-action="datedrop" title="Insert date (D/W)">D▾</button>
|
||||
</div>
|
||||
<textarea name="content" id="editor" autofocus>{{.RawContent}}</textarea>
|
||||
</form>
|
||||
<script src="/_/editor/lists.js"></script>
|
||||
<script src="/_/editor/tables.js"></script>
|
||||
<script src="/_/editor/dates.js"></script>
|
||||
<script src="/_/editor.js"></script>
|
||||
{{else}}
|
||||
{{if .Content}}
|
||||
|
||||
@@ -274,6 +274,35 @@ main {
|
||||
align-self: stretch;
|
||||
}
|
||||
|
||||
/* === Toolbar dropdowns === */
|
||||
.toolbar-dropdown {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.toolbar-dropdown-menu {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
display: none;
|
||||
z-index: 100;
|
||||
background: #001a00;
|
||||
border: 1px solid #0a0;
|
||||
min-width: 9rem;
|
||||
}
|
||||
|
||||
.toolbar-dropdown-menu.is-open {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.toolbar-dropdown-item {
|
||||
display: block;
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
border: none;
|
||||
border-radius: 0;
|
||||
padding: 0.2rem 0.5rem;
|
||||
}
|
||||
|
||||
/* === Edit form === */
|
||||
.edit-form {
|
||||
display: flex;
|
||||
|
||||
Reference in New Issue
Block a user