A bookmarklet script that adds a custom CodeMirror-based Markdown view to the Personal Notes editor of toolbox. A product of boredom and a personal lesson in bookmarklets, dynamic script loading, and (on a note unrelated to the final product) the limitations of Chromebooks.
- Custom font sizes for headings 1 through 3, and bold/underline/italic for headings 4-6
- When a list item wraps to a second line, include a hanging indent to align the wrapped portion of the line with the start of the text (unordered lists only at the moment)
- Status bar with cursor position indicator and very simple settings pane, including an option to enable and disable line numbers
- Roughly compatible with the Reddit redesign
Click the bookmarklet once while you have a note open to start the editor. Click the bookmarklet a second time to disable the editor.
This script can be installed in one of two ways: As a traditional bookmarklet that lives in your browser's bookmarks bar, or as a shortcut link in toolbox's built-in modbar. Using the latter method styles the link there as a toggle button which shows whether or no the module is enabled (as in the screenshot below), but otherwise the functionality is the same.
Simply copy the javascript:
URL below into the URL field of a new bookmark, then move the bookmark to the bookmarks bar. The name of the bookmark doesn't matter. If you're unsure how to create a bookmark, consult your browser's help documentation.
Go to toolbox settings > Modbar > Shortcuts, click the plus button, and paste the URL below into the "url" field in the new table row. The name of the shortcut doesn't matter, but you may wish to call it "Markdown Mode" as that's what the name will be replaced with when you activate it.
javascript:/*$ Personal Notes Markdown Mode $*/(function personalNotesMarkdownMode(){function main(){const $button=$('#tb-toolbarshortcuts a[href^=\'javascript:/*$ Personal Notes Markdown Mode $*/\']').toggleClass('pnmm-trigger',!0);const $defaultEditArea=$('#tb-personal-notes-editarea');if($defaultEditArea.length&&!$('#tb-personal-notes-landing').length){let editor=window.pnmmEditor;if(!editor){editor=CodeMirror.fromTextArea($defaultEditArea[0],{lineNumbers:!1,mode:'gfm',lineWrapping:!0,tabSize:2,indentWithTabs:!1,matchBrackets:!0,autoCloseBrackets:!0});const $editorStyle=$(`<style name='pnmmEditorStyle'>.mod-toolbox #tb-personal-notes-content .pnmmEditor{box-sizing:border-box;width:450px;height:300px;font-size:12px;padding-bottom:21px}.pnmmEditor .cm-header-1{font-size:2em}.pnmmEditor .cm-header-2{font-size:1.5em}.pnmmEditor .cm-header-3{font-size:1.33em}.pnmmEditor .cm-header-5{font-weight:400;text-decoration:underline}.pnmmEditor .cm-header-6{font-weight:400;font-style:italic}.pnmm-statusbar{position:absolute;bottom:0;right:0;background:#F7F7F7;height:21px;left:0;z-index:4;box-sizing:border-box;border-top:1px solid #DDD;display:flex;justify-content:space-between}.pnmm-statusbar-left,.pnmm-statusbar-right,.pnmm-statusbar-item{line-height:20px;margin:0 5px}.pnmm-settingspane{position:absolute;bottom:21px;right:5px;border:1px solid #DDD;background:#F7F7F7;z-index:4;border-bottom:0;padding:4px 0}.pnmm-setting{margin:0 5px;display:flex;justify-content:space-between;white-space:pre}.pnmm-setting-label:after{content:':\\A0'}.pnmm-setting-input[type=checkbox]{all:inherit;margin:0!important}.pnmm-setting-input[type=checkbox]::after{content:'Off';color:red}.pnmm-setting-input[type=checkbox]:checked::after{content:'\\A0 On';color:limegreen}.pnmm-setting.enabled .pnmm-setting-value{color:green!important}</style>`);const $statusBar=$(`<div class='pnmm-statusbar' />`);const $statusBarLeft=$(`<div class='pnmm-statusbar-left' />`).appendTo($statusBar);const $statusBarRight=$(`<div class='pnmm-statusbar-right' />`).appendTo($statusBar);const $settingsPane=$(`<div class='pnmm-settingspane' style='display:none'/>`);const $settingsButton=$(`<a href='javascript:;' class='pnmm-statusbar-item pnmm-settings'>Settings</a>`).on('click',function(){$settingsPane.toggle()}).appendTo($statusBarRight);const unorderedListLineRegExp=/^\s*[-+*]\s+/;editor.on('renderLine',function(cm,line,elt){const result=unorderedListLineRegExp.exec(line.text);if(!result)return;var off=editor.defaultCharWidth()*result[0].length;elt.style.textIndent='-'+off+'px';elt.style.paddingLeft=4+off+'px'});editor.setOption('extraKeys',{Tab:function(cm){cm.execCommand('insertSoftTab')}});$settingsPane.append($(`<div class='pnmm-setting pnmm-linenos' />`).append(`<span class='pnmm-setting-label'>Line Numbers</span>`).append($(`<input type=checkbox class='pnmm-setting-input'>`).on('change',function(){const $this=$(this);const $setting=$this.closest('.pnmm-setting');if($this.is(':checked')){$setting.toggleClass('enabled',!0);editor.setOption('lineNumbers',!0)}else{$setting.toggleClass('enabled',!1);editor.setOption('lineNumbers',!1)}})));const $ruler=$(`<span class='pnmm-statusbar-item pnmm-ruler'>1:1</span>`);$statusBarRight.prepend($ruler);editor.on('cursorActivity',function(){const{line,ch}=editor.getDoc().getCursor();$ruler.text(`${line + 1}:${ch + 1}`)});editor.on('change',function(){$defaultEditArea.val(editor.getDoc().getValue())});$('.tb-popup.personal-notes-popup').on('click','.tb-personal-note-link, .tb-personal-note-delete, #create-personal-note',function listener(){$('.tb-popup.personal-notes-popup').off('click',listener);$defaultEditArea.val(editor.getDoc().getValue());$('.pnmm-control, .pnmmEditor').remove();$button.html('Markdown Mode <span style=\'color:red\'>Disabled</span>').toggleClass('enabled',!1);window.pnmmEditor=null});$(editor.getWrapperElement()).toggleClass('pnmmEditor').append($editorStyle,$statusBar,$settingsPane).hide();window.pnmmEditor=editor}const $codeMirrorWrapper=$(editor.getWrapperElement());if($codeMirrorWrapper.is(':visible')){$codeMirrorWrapper.hide();$defaultEditArea.show();$button.html('Markdown Mode <span style=\'color:red\'>Disabled</span>').toggleClass('enabled',!1)}else{$defaultEditArea.hide();$codeMirrorWrapper.show();editor.setValue($defaultEditArea.val());editor.refresh();$button.html('Markdown Mode <span style=\'color:green\'>Enabled</span>').toggleClass('enabled',!0)}}else{window.alert('You don\'t have a note open! Please open a note before trying this.')}}function getScripts(arr,callback,index=0){if(!arr[index])return callback();let script=document.createElement('SCRIPT');script.src=arr[index];script.type='text/javascript';script.onload=getScripts.bind(this,arr,callback,index+1);document.getElementsByTagName('head')[0].appendChild(script)}let scripts=[];if(!window.jQuery){scripts.push('//code.jquery.com/jquery-3.2.1.min.js')}if(!window.CodeMirror){scripts.push('//cdnjs.cloudflare.com/ajax/libs/codemirror/5.29.0/codemirror.min.js','//cdnjs.cloudflare.com/ajax/libs/codemirror/5.29.0/addon/mode/overlay.min.js','//cdnjs.cloudflare.com/ajax/libs/codemirror/5.29.0/mode/markdown/markdown.min.js','//cdnjs.cloudflare.com/ajax/libs/codemirror/5.29.0/mode/gfm/gfm.min.js','//cdnjs.cloudflare.com/ajax/libs/codemirror/5.29.0/addon/edit/matchbrackets.min.js','//cdnjs.cloudflare.com/ajax/libs/codemirror/5.29.0/addon/edit/closebrackets.min.js')}getScripts(scripts,main)})();undefined
This bookmarklet adds a custom editor to the Personal Notes editor if one is present on the page. Triggering the bookmarklet a second time will disable the custom editor and restore the native textarea. It uses CodeMirror and the gfm
(GitHub-flavored Markdown) editor mode along with some custom styles to display Markdown styles inline within the editor, allowing you to visualize how the note would be displayed if it were rendered as Markdown. This means that you can write in Markdown without having to have a second pane to view the rendered content - the markdown syntax is simply styled within the editor. It also includes bracket matching, because why not.
When the bookmarklet is triggered, it starts by checking for the existence of CodeMirror on the page. If it doesn't detect this, it will load the CodeMirror base script and the addons it depends on with calls to $.getScript()
. After these scripts are loaded, or if CodeMirror was already loaded, it makes sure the Personal Notes editor is open. It then checks to see if a CodeMirror editor instance exists, and if not, creates one via calling CodeMirror.fromTextArea
, passing the native editor as the target. It also configures the newly-created editor with modified behavior, custom styles, and extra elements to enable the features of the final editor.
Once the editor is known to exist, the script simply toggles its state. If the original editor is visible, it hides it, shows the custom editor, and copies the value of the original editor to the custom one. If the original editor is hidden, it hides the custom editor and shows the original one; the value of the custom editor does not need to be transferred at this time because it is actively updated whenever the custom editor's value changes.
The bookmarklet is intended to be added to the Toolbox modbar shortcuts area. When it is, a comment at the beginning of the bookmarklet's code allows the code to identify the clicked link. It then updates this link each time it's clicked with the status of the bookmarklet - either "On" if the custom editor is shown, or "Off" if it is hidden.
The style of the source file is reflective of its nature as a bookmarklet. The primary method of testing this script was by copying it and typing javascript:
into the Chrome address bar before pasting the entire script in and then pressing Enter. On paste, the newlines in the script are turned into spaces, so all comments in the source are block comments to prevent them from interfering from the code in this way. Additionally, since semicolons are required without newlines, this file is required to contain them to work with this method of testing. Finally, since the final product can be stored in HTML within double quotes as part of the toolbox modbar markup, the entire script does not use double quotes, opting instead for single quotes and template literals.