您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Displays a Markdown toolbar for textboxes on OzBargain and ChoiceCheapies
// ==UserScript== // @name OzBargain Markdown Toolbar // @namespace nategasm // @version 1.25 // @description Displays a Markdown toolbar for textboxes on OzBargain and ChoiceCheapies // @author wOxxOm, darkred, nategasm // @license MIT // @include https://www.ozbargain.com.au/deals/submit // @include https://www.ozbargain.com.au/node/* // @include https://www.ozbargain.com.au/comment/edit/* // @include https://www.ozbargain.com.au/privatemsg/* // @include https://www.cheapies.nz/deals/submit // @include https://www.cheapies.nz/node/* // @include https://www.cheapies.nz/comment/edit/* // @include https://www.cheapies.nz/privatemsg/* // @icon https://www.ozbargain.com.au/favicon.ico // @run-at document-end // @grant GM_addStyle // ==/UserScript== //Add button styles GM_addStyle(` .mdBtn { display: inline-block; cursor: pointer; margin: 0 0.5px 4px 0.5px; padding: 4px; background: var(--input-bg); border: 1px solid #999; border-radius: 2px; white-space: pre; box-shadow: 0px 1px 0px #FFF inset, 0px -1px 2px #BBB inset; color: var(--page-fg); user-select: none; } .mdBtn:hover { background: var(--shade3-bg) !important; color: #fff !important; } .mdBtn svg { vertical-align: middle; pointer-events: none; } .qtBtn { font-weight: bold; position: fixed; display: none; line-height: 100%; padding: 3px 5px; border: var(--shade3-bg) solid 2px; opacity: 85%; user-select: none; } .qtBtn:hover { opacity: 100%; } `); //Add toolbar to main preloaded textboxes const textarea = document.querySelector('textarea'); if (textarea) { addFeatures(textarea.parentNode); } else { return; } //Add more features to nodes if (location.href.indexOf('/node/') > 0) { //Observe and add toolbar to expanded reply boxes let targetNode = document.querySelectorAll(".comment.level0"); //Need All to capture pinned comments let callback = (mutationList, observer) => { for (const mutation of mutationList) { if (mutation.addedNodes.length > 0 && mutation.addedNodes[0].className === "comment" && mutation.addedNodes[0].nodeName === "FORM") { addFeatures(mutation.addedNodes[0].querySelector('textarea').parentNode); } } }; let observer = new MutationObserver(callback); for (let i = 0; i < targetNode.length; i++) { observer.observe(targetNode[i], {attributes: false, childList: true, subtree: true}); } //Add quote button to node content let node = document.querySelector('.node:not(.messages)'); if (node) { let a = document.createElement('a'); a.className = 'btn qtBtn'; a.innerHTML = 'Quote Selection'; a.addEventListener('click', function(e){edPrefixTag('>', true, edInit(e.target,'>'));}); a.addEventListener('mousedown',function(e){event.preventDefault()}); node.textAreaNode = textarea; node.appendChild(a); ['mouseup', 'touchend'].forEach(function(e) { node.addEventListener(e, (event) => { let selectedText = getSelectionText().trim(); if (selectedText.length > 0) { const range = window.getSelection().getRangeAt(0); const rect = range.getBoundingClientRect(); //Position the button near the selection a.style.top = `${event.clientY + 20}px`; a.style.left = `${event.clientX - 65}px`; a.style.display = 'inline-block'; } else { a.style.display = 'none'; } }); }); document.addEventListener("selectionchange", () => { //Remove button if unselected if (getSelectionText().length === 0) { a.style.display = 'none'; } }); } } function addFeatures(n) { n.textAreaNode = n.querySelector('textarea'); const ctrlKey = isMacOS() ? 'Cmd' : 'Ctrl'; //Add buttons btnMake(n, '<b>B</b>', 'Bold ('+ctrlKey+'+B)', '**', '', false, false, false, 'b', 'M4 2h4.5a3.501 3.501 0 0 1 2.852 5.53A3.499 3.499 0 0 1 9.5 14H4a1 1 0 0 1-1-1V3a1 1 0 0 1 1-1Zm1 7v3h4.5a1.5 1.5 0 0 0 0-3Zm3.5-2a1.5 1.5 0 0 0 0-3H5v3Z'); btnMake(n, '<i>I </i>', 'Italic ('+ctrlKey+'+I)', '*', '', false, false, false, 'i', 'M6 2.75A.75.75 0 0 1 6.75 2h6.5a.75.75 0 0 1 0 1.5h-2.505l-3.858 9H9.25a.75.75 0 0 1 0 1.5h-6.5a.75.75 0 0 1 0-1.5h2.505l3.858-9H6.75A.75.75 0 0 1 6 2.75Z'); btnMake(n, '<s>S</s>', 'Strikethrough ('+ctrlKey+'+`)', '~~', '', false, false, false, '`', 'M6.333 5.686c0 .31.083.581.27.814H5.166a2.8 2.8 0 0 1-.099-.76c0-1.627 1.436-2.768 3.48-2.768 1.969 0 3.39 1.175 3.445 2.85h-1.23c-.11-1.08-.964-1.743-2.25-1.743-1.23 0-2.18.602-2.18 1.607zm2.194 7.478c-2.153 0-3.589-1.107-3.705-2.81h1.23c.144 1.06 1.129 1.703 2.544 1.703 1.34 0 2.31-.705 2.31-1.675 0-.827-.547-1.374-1.914-1.675L8.046 8.5H1v-1h14v1h-3.504c.468.437.675.994.675 1.697 0 1.826-1.436 2.967-3.644 2.967'); btnMake(n, 'H', 'Add Heading ('+ctrlKey+'+3)','#', '', false, true, true, '3', 'M3.75 2a.75.75 0 0 1 .75.75V7h7V2.75a.75.75 0 0 1 1.5 0v10.5a.75.75 0 0 1-1.5 0V8.5h-7v4.75a.75.75 0 0 1-1.5 0V2.75A.75.75 0 0 1 3.75 2Z'); btnMake(n, '---', 'Horizontal rule ('+ctrlKey+'+-)', '\n\n---\n\n', '', true, false, false, '-', 'M2 8a.5.5 0 0 1 .5-.5h11a.5.5 0 0 1 0 1h-11A.5.5 0 0 1 2 8'); btnMake(n, 'Quote', 'Quote text ('+ctrlKey+'+.)','>', '', false, true, true, '.', 'M12 12a1 1 0 0 0 1-1V8.558a1 1 0 0 0-1-1h-1.388q0-.527.062-1.054.093-.558.31-.992t.559-.683q.34-.279.868-.279V3q-.868 0-1.52.372a3.3 3.3 0 0 0-1.085.992 4.9 4.9 0 0 0-.62 1.458A7.7 7.7 0 0 0 9 7.558V11a1 1 0 0 0 1 1zm-6 0a1 1 0 0 0 1-1V8.558a1 1 0 0 0-1-1H4.612q0-.527.062-1.054.094-.558.31-.992.217-.434.559-.683.34-.279.868-.279V3q-.868 0-1.52.372a3.3 3.3 0 0 0-1.085.992 4.9 4.9 0 0 0-.62 1.458A7.7 7.7 0 0 0 3 7.558V11a1 1 0 0 0 1 1z'); btnMake(n, '• List', 'Unordered list ('+ctrlKey+'+8)', function(e) { try {edList('* ', edInit(e.target,'* '));} catch(e) {} }, '', false, false, false, '8', 'M5.75 2.5h8.5a.75.75 0 0 1 0 1.5h-8.5a.75.75 0 0 1 0-1.5Zm0 5h8.5a.75.75 0 0 1 0 1.5h-8.5a.75.75 0 0 1 0-1.5Zm0 5h8.5a.75.75 0 0 1 0 1.5h-8.5a.75.75 0 0 1 0-1.5ZM2 14a1 1 0 1 1 0-2 1 1 0 0 1 0 2Zm1-6a1 1 0 1 1-2 0 1 1 0 0 1 2 0ZM2 4a1 1 0 1 1 0-2 1 1 0 0 1 0 2Z'); btnMake(n, '# List', 'Numbered list ('+ctrlKey+'+/)', function(e) { try {edList('1. ', edInit(e.target,'1. '), true);} catch(e) {} }, '', false, false, false, '/', 'M5 3.25a.75.75 0 0 1 .75-.75h8.5a.75.75 0 0 1 0 1.5h-8.5A.75.75 0 0 1 5 3.25Zm0 5a.75.75 0 0 1 .75-.75h8.5a.75.75 0 0 1 0 1.5h-8.5A.75.75 0 0 1 5 8.25Zm0 5a.75.75 0 0 1 .75-.75h8.5a.75.75 0 0 1 0 1.5h-8.5a.75.75 0 0 1-.75-.75ZM.924 10.32a.5.5 0 0 1-.851-.525l.001-.001.001-.002.002-.004.007-.011c.097-.144.215-.273.348-.384.228-.19.588-.392 1.068-.392.468 0 .858.181 1.126.484.259.294.377.673.377 1.038 0 .987-.686 1.495-1.156 1.845l-.047.035c-.303.225-.522.4-.654.597h1.357a.5.5 0 0 1 0 1H.5a.5.5 0 0 1-.5-.5c0-1.005.692-1.52 1.167-1.875l.035-.025c.531-.396.8-.625.8-1.078a.57.57 0 0 0-.128-.376C1.806 10.068 1.695 10 1.5 10a.658.658 0 0 0-.429.163.835.835 0 0 0-.144.153ZM2.003 2.5V6h.503a.5.5 0 0 1 0 1H.5a.5.5 0 0 1 0-1h.503V3.308l-.28.14a.5.5 0 0 1-.446-.895l1.003-.5a.5.5 0 0 1 .723.447Z'); btnMake(n, 'URL', 'Add URL ('+ctrlKey+'+K)', function(e) { try {edWrapInTag('[', '](URL)', edInit(e.target,'['));} catch(e) {} }, '', false, false, false, 'k', 'm7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z'); btnMake(n, 'Table', 'Insert table template', function(e) { try { const columns = parseInt(prompt('Enter number of columns:'), 10); const rows = parseInt(prompt('Enter number of rows:'), 10); if (isNaN(columns) || isNaN(rows) || columns <= 0 || rows <= 0) { alert('Please enter valid positive numbers'); return; } edInsertText(createMarkdownTable(columns, rows), edInit(e.target,'|')); } catch(e) {} }, '', false, false, false, '', 'M0 2a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2zm15 2h-4v3h4zm0 4h-4v3h4zm0 4h-4v3h3a1 1 0 0 0 1-1zm-5 3v-3H6v3zm-5 0v-3H1v2a1 1 0 0 0 1 1zm-4-4h4V8H1zm0-4h4V4H1zm5-3v3h4V4zm4 4H6v3h4z'); btnMake(n, 'Code', 'Code Block', function(e){ let ed = edInit(e.target,'`'); if (ed.sel.indexOf('\n') < 0) { edWrapInTag('`', '`', ed); } else { edWrapInTag(((ed.sel1==0) || (ed.text.charAt(ed.sel1-1) == '\n') ? '' : '\n') + '~~~' + (ed.sel.charAt(0) == '\n' ? '' : '\n'), (ed.sel.substr(-1) == '\n' ? '' : '\n') + '~~~' + (ed.text.substr(ed.sel2,1) == '\n' ? '' : '\n'), ed); } }, '', false, false, false, '', 'M5.854 4.854a.5.5 0 1 0-.708-.708l-3.5 3.5a.5.5 0 0 0 0 .708l3.5 3.5a.5.5 0 0 0 .708-.708L2.707 8zm4.292 0a.5.5 0 0 1 .708-.708l3.5 3.5a.5.5 0 0 1 0 .708l-3.5 3.5a.5.5 0 0 1-.708-.708L13.293 8z'); bindCtrlKeyToTextareaAction(n.textAreaNode); //Update hover text for Preview button shortcut let preview = n.parentNode.querySelector('input.form-submit[value="Preview"]'); if (preview) { preview.title = ctrlKey+'+P'} } function btnMake(afterNode, label, title, tag1, tag2, noWrap, prefix, gap, shortcut, svgPath) { let a = document.createElement('a'); a.className = 'mdBtn'; if (!svgPath) { a.innerHTML = label }; a.title = title; a.dataset.key = shortcut; a.addEventListener('click', typeof(tag1) === 'function' ? tag1 : noWrap ? function(e){ edInsertText(tag1, edInit(e.target,tag1)); } : prefix ? function(e){ edPrefixTag(tag1, gap, edInit(e.target,tag1)); } : function(e){ edWrapInTag(tag1, tag2, edInit(e.target,tag1)); }); if (prefix) { a.addEventListener('mousedown',function(e){ event.preventDefault() }) } if (label === 'Quote') { document.addEventListener("selectionchange", () => { //Highlight quote button if text selected outside of textbox if (getSelectionText().length > 0 && document.activeElement.tagName !== 'TEXTAREA') { a.style.background = 'var(--shade3-bg)'; } else { a.style.background = 'var(--input-bg)'; } }); } a.textAreaNode = afterNode.textAreaNode; let newA = afterNode.insertBefore(a, afterNode.textAreaNode); //Insert button SVG if (svgPath) { const svgNS = "http://www.w3.org/2000/svg"; const svg = document.createElementNS(svgNS, "svg"); svg.setAttribute("width", "18"); svg.setAttribute("height", "18"); if(['<s>S</s>','---','Code','Quote'].includes(label)) { svg.setAttribute("viewBox", "1 1 14 14"); } else { svg.setAttribute("viewBox", "0 0 16 16"); } const path = document.createElementNS(svgNS, "path"); path.setAttribute("d", svgPath); path.setAttribute("fill", "currentColor"); svg.appendChild(path); newA.appendChild(svg); } } function edInit(btn, tag) { let ed = { node: btn.parentNode.textAreaNode }; ed.sel1 = ed.node.selectionStart; ed.sel2 = ed.node.selectionEnd; //Trim Whitespace and line breaks from start/end of selection, also improves gap detection trimSelection(ed, tag); ed.text = ed.node.value; ed.sel = ed.text.substring(ed.sel1, ed.sel2); return ed; } function trimSelection(ed, tag) { while (ed.sel1 < ed.sel2) { const startChar = ed.node.value.charAt(ed.sel1); const endChar = ed.node.value.charAt(ed.sel2 - 1); const isListTag = ['* ', '1. ', '>'].includes(tag); const trimStart = (!isListTag && /\s/.test(startChar)) || (isListTag && startChar === '\n'); const trimEnd = (!isListTag && /\s/.test(endChar)) || (isListTag && endChar === '\n'); if (!trimStart && !trimEnd) break; if (trimStart) ed.sel1++; if (trimEnd) ed.sel2--; } } function detectGapBefore(selStart, text, tag) { if (selStart === 0 || selStart - tag.length === 0 || //Start of textbox text.substring(selStart - 2, selStart - 1) === '\n' || //Line break exists before selection text.substring(selStart - tag.length - 1, selStart - tag.length) === tag) { //Tag exists before selection return true; } } function detectGapAfter(selEnd, text, tag) { if (text.substring(selEnd + 1, selEnd + 2) === '\n' || //Line break exists after selection (!tag && selEnd === text.length)) { //End of textbox return true; } } function edWrapInTag(tag1, tag2, ed) { if (ed.sel.startsWith(tag1) && ed.sel.endsWith(tag2?tag2:tag1)) { // Remove the syntax if it's already wrapped ed.node.value = ed.text.substr(0, ed.sel1) + ed.sel.slice(tag1.length, (tag2?-tag2.length:-tag1.length)) + ed.text.substr(ed.sel2); ed.node.setSelectionRange(ed.sel1, ed.sel1 + ed.sel.length - tag1.length - (tag2?tag2.length:tag1.length)); } else { // Wrap syntax if (ed.sel.length === 0) { replaceTextWithUndo(ed, tag1 + (tag2?tag2:tag1), true); } else { if (tag2 === '](URL)' && ed.sel.substr(0,4) === 'http'){ //Check if URL was selected tag1 = '[Label]('; tag2 = ')'; } replaceTextWithUndo(ed, ed.text.substr(0, ed.sel1) + tag1 + ed.sel + (tag2?tag2:tag1) + ed.text.substr(ed.sel2)); } //If URL tag select the URL or Label if (tag2 === '](URL)' && ed.sel.length > 0) { ed.node.setSelectionRange(ed.sel1 + tag1.length + ed.sel.length + 2, ed.sel1 + tag1.length + ed.sel.length + 5); } else if (tag1 === '[Label](' && ed.sel.length > 0) { ed.node.setSelectionRange(ed.sel1 + 1, ed.sel1 + 6); } else { ed.node.setSelectionRange(ed.sel1 + tag1.length, ed.sel1 + tag1.length + ed.sel.length); } } ed.node.focus(); } function edInsertText(text, ed) { //Insert at cursor replaceTextWithUndo(ed, text, true); ed.node.setSelectionRange(ed.sel2 + text.length, ed.sel2 + text.length); ed.node.focus(); } function edPrefixTag(tag, gap, ed) { if (ed.sel.startsWith(tag)) { //Remove the syntax if it's already prefixed const selection = removeCharAtStartOfLines(ed.sel, tag); ed.node.value = ed.text.substr(0, ed.sel1) + selection + ed.text.substr(ed.sel2); ed.node.setSelectionRange(ed.sel1, ed.sel1 + selection.length); } else { const selPage = (getSelectionText().length > 0 && document.activeElement.tagName !== 'TEXTAREA' && tag === '>'); let selection; //Prefix syntax - Note Firefox and Chrome handle getSelection() differently for textboxes if (selPage || ed.sel.length > 0) { //Text selected in page or textarea const selTrimmed = trimWhitespaceFromLines(selPage?getSelectionText():ed.sel); const selectionObj = insertAtStartOfLines(selTrimmed.trimmedString, {charToInsert: tag, numberLines: false, skipEmptyLines: true}); selection = markdownGapDetection(ed, selectionObj, tag, selTrimmed.trimmedCount, selPage?selTrimmed.trimmedString.length:0, selPage); } else { //Add tag to the start of the line const lineInfo = getLineInfo(ed, tag); ed.sel1 = lineInfo.startOfLine; ed.sel2 = lineInfo.endOfLine; trimSelection(ed, tag); ed.sel = ed.text.substring(ed.sel1, ed.sel2); const selTrimmed = trimWhitespaceFromLines(ed.sel); selection = markdownGapDetection(ed, {text: (tag + selTrimmed.trimmedString), tagCount: tag.length}, tag, selTrimmed.trimmedCount, 0); } //Set the full textarea replaceTextWithUndo(ed, ed.text.substr(0, ed.sel1) + (selPage?ed.sel:'') + (selection.result??selection) + ed.text.substr(ed.sel2)); if (selPage) { //Exclude first tag from selection so that tag can continously be added for nested markdown like blockquote ed.node.setSelectionRange(ed.sel1 + (selPage?ed.sel.length:0) + selection.result.length, ed.sel1 + (selPage?ed.sel.length:0) + selection.result.length); } else if (ed.sel.length === 0) { ed.node.setSelectionRange(ed.sel2 + tag.length, ed.sel2 + tag.length); } else { ed.node.setSelectionRange(ed.sel1 + tag.length + selection.iBefore, ed.sel1 + selection.result.length - selection.iAfter); } } ed.node.focus(); } function edList(tag, ed, ordered) { //Add what to do when no selection let selection; if (ed.sel.startsWith(tag)) { // Remove the syntax if it's already prefixed selection = trimWhitespaceFromLines(removeCharAtStartOfLines(ed.sel, tag, ordered)).trimmedString; ed.node.value = ed.text.substr(0, ed.sel1) + selection + ed.text.substr(ed.sel2); ed.node.setSelectionRange(ed.sel1, ed.sel1 + selection.length); } else { if (ed.sel.length > 0) { // Wrap syntax const selTrimmed = trimWhitespaceFromLines(ed.sel); const selectionObj = insertAtStartOfLines(selTrimmed.trimmedString, {charToInsert: tag, numberLines: ordered, skipEmptyLines: true}); selection = markdownGapDetection(ed, selectionObj, tag, selTrimmed.trimmedCount, 0); } else { //Nothing selected, just add tag const lineInfo = getLineInfo(ed, tag); ed.sel1 = lineInfo.startOfLine; ed.sel2 = lineInfo.endOfLine; trimSelection(ed, tag); ed.sel = ed.text.substring(ed.sel1, ed.sel2); let selTrimmed = trimWhitespaceFromLines(ed.sel); selection = markdownGapDetection(ed, {text: (tag + selTrimmed.trimmedString), tagCount: tag.length}, tag, selTrimmed.trimmedCount, 0); } replaceTextWithUndo(ed, (ed.text.substr(0, ed.sel1) + (selection.result??selection) + ed.text.substr(ed.sel2))); if (ed.sel.length === 0){ //Don't select anything if only tag was inserted ed.node.setSelectionRange(ed.sel2 + tag.length, ed.sel2 + tag.length); } else { ed.node.setSelectionRange(ed.sel1 + selection.iBefore, ed.sel1 + selection.result.length - selection.iAfter); } } ed.node.focus(); } function markdownGapDetection(ed, selectionObj, tag, trimCount, addedLength, selPage) { let iBefore; let iAfter; let selection = selectionObj.text; //Check and add up to 2 line breaks before the selection for (iBefore = 0;selection !== tag && iBefore < 2 && !detectGapBefore(ed.sel1 + iBefore, ed.text.substr(0, ed.sel1) + selection, tag);iBefore++) { selection = '\n' + selection; } //Check and add up to 2 line breaks after the selection for (iAfter = 0;selection !== tag && iAfter < 2 && (ed.sel2 !== ed.text.length || selPage) && !detectGapAfter(ed.sel2 + selectionObj.tagCount + iBefore - (selPage?0:trimCount) + addedLength, ed.text.substr(0, ed.sel1) + selection + ed.text.substr(ed.sel2), tag);iAfter++) { selection = selection + '\n'; } return { result: selection, iBefore: iBefore, iAfter: iAfter } } function getSelectionText() { if (window.getSelection) { return window.getSelection().toString(); } } function createMarkdownTable(columns, rows) { const cellWidth = 9; //Define uniform cell width for better alignment function pad(text) { //Helper to pad text to fixed width return text.padEnd(cellWidth, ' '); } const headers = Array.from({ length: columns }, (_, i) => `Head${i + 1}`); const headerRow = `| ${headers.join(' | ')} |`; const separatorRow = `| ${headers.map(() => '-'.repeat(cellWidth)).join(' | ')} |`; const dataRows = []; for (let r = 0; r < rows; r++) { const row = Array.from({ length: columns }, () => pad(' Cell')); dataRows.push(`| ${row.join(' | ')} |`); } return [headerRow, separatorRow, ...dataRows].join('\n'); } function getLineInfo(ed, tag) { if (!ed || typeof ed.text !== 'string' || typeof ed.sel1 !== 'number') { return { startOfLine: ed?.sel1 ?? 0, endOfLine: ed?.sel1 ?? 0, line: '' }; } const startOfLine = ed.text.lastIndexOf('\n', ed.sel1 - 1) + 1; let endOfLine = ed.text.indexOf('\n', ed.sel1); if (endOfLine === -1) endOfLine = ed.text.length; const line = ed.text.slice(startOfLine, endOfLine).trim(); //If tag is given and the trimmed line is made up of repeated tags if (typeof tag === 'string' && tag.length > 0) { const repeated = tag.repeat(Math.ceil(line.length / tag.length)); const trimmedRepeated = repeated.slice(0, line.length); if (line === trimmedRepeated) { return { startOfLine: endOfLine, endOfLine: endOfLine, line: ed.text.slice(startOfLine, endOfLine) //Preserve original line (untrimmed) }; } } return { startOfLine, endOfLine, line: ed.text.slice(startOfLine, endOfLine) }; } function getStartOfLine(ed) { if (!ed || typeof ed.text !== 'string' || typeof ed.sel1 !== 'number') { return ed?.sel1; } const beforeSel = ed.text.substring(0, ed.sel1); const lastNewline = beforeSel.lastIndexOf('\n'); return lastNewline + 1; } function insertAtStartOfLines(inputString, options = {}) { const { charToInsert = '', numberLines = false, skipEmptyLines = true } = options; let lineNumber = 1; let count = 0; let result = inputString .split('\n') .map(line => { if (skipEmptyLines && line.trim() === '') { return line; } if (numberLines) { count = count + lineNumber.toString().length + 2; return `${lineNumber++}. ${line}`; } else { count = count + charToInsert.length; return charToInsert + line; } }) .join('\n'); return { text: result, tagCount: count } } function removeCharAtStartOfLines(inputString, charToRemove, removeNumbers) { return inputString .split('\n') .map(line => { let newLine = line; //Remove the specific character if needed if (charToRemove && newLine.startsWith(charToRemove) && !removeNumbers) { newLine = newLine.slice(1); } //Remove leading numbers (and optional dot/space after) if needed if (removeNumbers) { newLine = newLine.replace(/^\d+\.?\s*/, ''); } return newLine; }) .join('\n'); } function trimWhitespaceFromLines(inputString) { let trimmedCount = 0; const trimmedLines = inputString.split('\n').map(line => { const originalLength = line.length; const trimmedLine = line.trim(); trimmedCount += originalLength - trimmedLine.length; return trimmedLine; }); return { trimmedString: trimmedLines.join('\n'), trimmedCount }; } function replaceTextWithUndo(ed, newText, insert) { ed.node.focus(); ed.node.setSelectionRange(insert?ed.sel2:0, insert?ed.sel2:ed.node.value.length); try { //Note: execCommand is needed for Undo functionality but is deprecated and could fail in the future document.execCommand('insertText', false, newText); } catch (err) { //Fallback if execCommand fails ed.node.setRangeText(newText, 0, ed.node.value.length, 'start'); ed.node.dispatchEvent(new Event('input', { bubbles: true })); } } function bindCtrlKeyToTextareaAction(textareaEl) { if (!textareaEl) return; function handleKeydown(e) { const ctrlKey = isMacOS() ? e.metaKey : e.ctrlKey; if (!ctrlKey || e.altKey || e.shiftKey) return; const key = e.key.toLowerCase(); let button; if (key === 'p') { //Hardcode Preview shortcut (Ctrl+P) button = textareaEl.parentNode.parentNode.querySelector('input.form-submit[value="Preview"]'); } else { button = textareaEl.parentNode.querySelector(`a[data-key="${key}"]`); } if (button) { e.preventDefault(); button.click(); } } textareaEl.addEventListener('keydown', handleKeydown); } function isMacOS() { const platform = navigator.platform || ""; const userAgent = navigator.userAgent || ""; return platform.includes("Mac") || userAgent.includes("Macintosh"); }
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址