您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Add a Preview as Markdown below textarea fields in a ServiceNow form.
// ==UserScript== // @name ServiceNow - ITSM - Add Markdown Preview // @namespace https://www.fruitionpartners.nl/ // @version 0.4 // @description Add a Preview as Markdown below textarea fields in a ServiceNow form. // @author Ricardo Constantino <[email protected]> // @match https://*.service-now.com/incident.do?* // @match https://*.service-now.com/change_request.do?* // @match https://*.service-now.com/rm_story.do?* // @match https://*.service-now.com/sys_update_set.do?* // @require https://cdnjs.cloudflare.com/ajax/libs/showdown/1.9.1/showdown.min.js#sha256=jl1+DOsSs9uABTKppOJ2GF8kXoc3XQzBtFFyS0i9Xoo // @require https://cdnjs.cloudflare.com/ajax/libs/highlight.js/10.1.1/highlight.min.js#sha256=Uv3H6lx7dJmRfRvH8TH6kJD1TSK1aFcwgx+mdg3epi8= // @resource css https://cdnjs.cloudflare.com/ajax/libs/highlight.js/10.1.1/styles/default.min.css // @grant GM_addStyle // @grant GM_getResourceText // ==/UserScript== (function() { 'use strict'; if (typeof g_form === 'undefined') return; // True if Preview should be shown instead of the normal textarea field on page load const DEFAULT_ENABLED = true; // Fields to add Markdown Preview to. // Supports '<fieldName>' as well as '<tableName>.<fieldName>' syntax so it only adds a Preview for that field in a specific table's form. const FIELDS_MAP = [ 'description', 'incident.u_steps_to_reproduce', 'comments_and_work_notes' ]; GM_addStyle(GM_getResourceText("css")); GM_addStyle(` .__preview { border: 1px solid #bbb; margin: 0em 0; padding: 0em 0.5em; border-radius: 0.3em; background: #eee; min-height: 64px; max-height: 435px; overflow: auto; } html.compact .__preview { min-height: 48px; } .__preview > * { max-width: 720px } .__md_button { margin-right: 0.3em; color: green; } `); showdown.extension('snownumber', function() { return [ { type: 'lang', regex: /(?<!\[.+)STRY(0*)(\d+)(?!.*\])/, replace: '[$&](/rm_story.do?sysparm_query=number=$&^ORnumberCONTAINS$1$2^ORnumberENDSWITH$2)' }, { type: 'lang', regex: /(?<!\[.+)INC(0*)(\d+)(?!.*\])/g, replace: '[$&](/incident.do?sysparm_query=number=$&^ORnumberCONTAINS$1$2^ORnumberENDSWITH$2)' }, { type: 'lang', regex: /(?<!\[.+)CHG(0*)(\d+)(?!.*\])/g, replace: '[$&](/change_request.do?sysparm_query=number=$&^ORnumberCONTAINS$1$2^ORnumberENDSWITH$2)' }, { type: 'lang', regex: /(?<!\[.+)PRJ(0*)(\d+)(?!.*\])/g, replace: '[$&](/pm_project.do?sysparm_query=number=$&^ORnumberCONTAINS$1$2^ORnumberENDSWITH$2)' }, { type: 'lang', regex: /(?<!\[.+)PRJTASK(0*)(\d+)(?!.*\])/g, replace: '[$&](/pm_project_task.do?sysparm_query=number=$&^ORnumberCONTAINS$1$2^ORnumberENDSWITH$2)' } ]; }); showdown.extension('codehighlight', function() { function htmlunencode(text) { return ( text .replace(/&/g, '&') .replace(/</g, '<') .replace(/>/g, '>') ); } return [ { type: 'output', filter: function (text, converter, options) { // use new shodown's regexp engine to conditionally parse codeblocks var left = '<pre><code\\b[^>]*>', right = '</code></pre>', flags = 'g', replacement = function (wholeMatch, match, left, right) { // unescape match to prevent double escaping match = htmlunencode(match); return left + hljs.highlightAuto(match).value + right; }; return showdown.helper.replaceRecursiveRegExp(text, replacement, left, right, flags); } } ]; }); const showdownOpts = { omitExtraWLInCodeBlocks: true, simplifiedAutoLink: true, literalMidWordUnderscores: true, strikethrough: true, tables: true, simpleLineBreaks: true, underline: true, tasklists: true, disableForced4SpacesIndentedSublists: true, extensions: ['codehighlight', 'snownumber'], }; let sd = new showdown.Converter(showdownOpts); FIELDS_MAP.forEach(createPreview); function createPreview(fieldName) { if (fieldName === 'comments_and_work_notes') return replaceActivityBody(); if (!g_form.hasField(fieldName)) return; // Support 'table.field' syntax as well as 'field'. fieldName = fieldName.split('.').pop(); let element = g_form.getElement(fieldName); // field is not a textarea if (element.type !== 'textarea') return; // If Preview for this field doesn't exist yet, create it if (!document.querySelector('#preview_'+fieldName)) { let previewElement = document.createElement('div'); previewElement.id = 'preview_'+fieldName; previewElement.classList.add('__preview'); if (!DEFAULT_ENABLED && !element.readOnly) { previewElement.hide(); } else { element.hide(); } element.insertAdjacentElement('afterend', previewElement); element.addEventListener('input', updatePreview); let button = document.createElement('button'); button.id = 'button_'+fieldName; button.addEventListener('click', showHidePreview); button.setAttribute('class', 'btn btn-default icon-script __md_button'); g_form.getLabel(fieldName).insertAdjacentElement('afterbegin', button); } updatePreview({target: g_form.getElement(fieldName)}); } function replaceActivityBody() { document.querySelectorAll('span.sn-widget-textblock-body.sn-widget-textblock-body_formatted').forEach(element => { element.innerHTML = sd.makeHtml(element.innerText); }); } function updatePreview(event) { if (!event || !event.target) return; let element = event.target; let fieldName = typeof element.id === 'string' ? element.id.split('.')[1] : ''; if (!fieldName) return; let previewElement = document.querySelector('#preview_'+fieldName); previewElement.innerHTML = sd.makeHtml(element.value); } function showHidePreview (event) { if (!event || !event.target) return; let element = event.target; let fieldName = typeof element.id === 'string' ? element.id.split('button_')[1] : ''; if (!fieldName) return; let formElement = g_form.getElement(fieldName); let previewElement = document.querySelector('#preview_'+fieldName); if (!previewElement) return; formElement.toggle(); previewElement.toggle(); } })();