Repository: tholman/zenpen
Branch: master
Commit: b39f57bbf505
Files: 11
Total size: 33.3 KB
Directory structure:
gitextract_4jowxg8a/
├── .github/
│ └── FUNDING.yml
├── .gitignore
├── css/
│ ├── fonts.css
│ └── style.css
├── index.html
├── js/
│ ├── default.js
│ ├── editor.js
│ ├── ui.js
│ └── utils.js
├── license.md
└── readme.md
================================================
FILE CONTENTS
================================================
================================================
FILE: .github/FUNDING.yml
================================================
github: [tholman]
custom: ['https://www.buymeacoffee.com/tholman']
================================================
FILE: .gitignore
================================================
.c9revisions
.DS_Store
================================================
FILE: css/fonts.css
================================================
@font-face {
font-family: 'icomoon';
src:url('fonts/icomoon.eot');
src:url('fonts/icomoon.eot?#iefix') format('embedded-opentype'),
url('fonts/icomoon.woff') format('woff'),
url('fonts/icomoon.ttf') format('truetype'),
url('fonts/icomoon.svg#icomoon') format('svg');
font-weight: normal;
font-style: normal;
}
/* Use the following CSS code if you want to use data attributes for inserting your icons */
[data-icon]:before {
font-family: 'icomoon';
content: attr(data-icon);
speak: none;
font-weight: normal;
font-variant: normal;
text-transform: none;
line-height: 1;
-webkit-font-smoothing: antialiased;
}
/* Use the following CSS code if you want to have a class per icon */
/*
Instead of a list of all class selectors,
you can use the generic selector below, but it's slower:
[class*="icon-"] {
*/
.icon-expand, .icon-target, .icon-contrast, .icon-floppy, .icon-contract, .icon-link, .icon-download-alt {
font-family: 'icomoon';
speak: none;
font-style: normal;
font-weight: normal;
font-variant: normal;
text-transform: none;
line-height: 1;
-webkit-font-smoothing: antialiased;
}
.icon-expand:before {
content: "\e000";
}
.icon-target:before {
content: "\e001";
}
.icon-contrast:before {
content: "\e002";
}
.icon-floppy:before {
content: "\e003";
}
.icon-contract:before {
content: "\e004";
}
.icon-link:before {
content: "\e005";
}
.icon-download-alt:before {
content: "\e006";
}
================================================
FILE: css/style.css
================================================
/*!
* ZenPen
* http://www.zenpen.io
* MIT licensed
*
* Copyright (C) Tim Holman, http://tholman.com
*/
/*********************************************
* BASE STYLES
*********************************************/
* {
-moz-box-sizing: border-box;
box-sizing: border-box;
}
*:focus {
outline: none;
}
html {
overflow: hidden;
}
html, body {
font-family: 'Lora', serif;
padding: 0;
margin: 0;
height: 100%;
}
body {
padding-bottom: 40px;
padding-right: 10px;
overflow-y: scroll;
padding-left: 10px;
padding-top: 20px;
min-width: 800px;
width: 100%;
-webkit-transition: all 600ms;
-moz-transition: all 600ms;
-ms-transition: all 600ms;
-o-transition: all 600ms;
transition: all 600ms;
}
section {
max-width: 600px;
height: 100%;
margin: auto;
}
header {
font-weight: bold;
font-size: 38px;
word-wrap: break-word;
}
article {
padding-bottom: 50px;
line-height: 30px;
margin-top: 22px;
min-height: 90%;
font-size: 22px;
display: block;
word-wrap: break-word;
}
blockquote {
border-left: 4px solid deepskyblue;
margin-left: -19px;
padding-left: 15px;
margin-right: 0;
}
.no-overflow {
overflow: hidden;
display: block;
height: 100%;
width: 100%;
}
/* Used by the ui bubble to stop wrapping */
.lengthen {
display: block;
width: 300px;
height: 100%;
}
.useicons {
-webkit-font-smoothing: antialiased;
font-size: 20px !important;
font-family: 'icomoon' !important;
}
.yin {
background: #fdfdfd;
color: #111;
}
.yang {
background-color: #111;
color: #fafafa;
}
.ui {
position: fixed;
padding: 20px;
width: 65px;
bottom: 0;
left: 0;
top: 0;
}
.ui:hover button, .ui:hover .about {
opacity: .4;
}
.ui button:hover, .ui .about:hover {
opacity: 1;
}
.ui button, .text-options button {
-webkit-transition: opacity 400ms;
-moz-transition: opacity 400ms;
-ms-transition: opacity 400ms;
-o-transition: opacity 400ms;
transition: opacity 400ms;
font-family: inherit;
background: none;
cursor: pointer;
font-size: 25px;
color: inherit;
opacity: .1;
padding: 0;
height: 32px;
width: 25px;
border: 0;
}
a {
text-decoration: none;
color: deepskyblue;
}
a:hover {
text-decoration: underline;
}
.overlay {
position: fixed;
display: none;
height: 100%;
width: 100%;
z-index: 3;
left: 0;
top: 0;
}
.quote {
line-height: 60px !important;
font-size: 49px !important;
}
/*********************************************
* MODAL
*********************************************/
.yang .modal {
background-color: rgba(255,255,255,.9);
color: #111;
}
.modal {
background-color: rgba(0,0,0,.9);
margin-left: -200px;
position: absolute;
border-radius: 3px;
height: 101px;
padding: 15px;
display: none;
width: 400px;
bottom: 10px;
color: #fff;
left: 50%;
}
.modal h1 {
text-align: center;
font-size: 20px;
padding: 0;
margin: 0;
}
.modal div {
margin-bottom: 10px;
margin-top: 10px;
}
.modal input[type="number"] {
font-size: 16px;
display: block;
margin: auto;
width: 150px;
padding: 5px;
}
.description {
height: auto;
}
.description p {
margin-bottom: 0;
text-align: center;
}
.saveoverlay {
margin-left: -215px;
margin-top: -100px;
height: 170px;
left: 50%;
top: 50%;
}
.saveoverlay div {
text-align: center;
font-size: 11px;
}
.saveselection {
margin-top: 17px;
text-align:center;
}
.saveselection span {
-webkit-transition: color 250ms, background 250ms;
-moz-transition: color 250ms, background 250ms;
-ms-transition: color 250ms, background 250ms;
-o-transition: color 250ms, background 250ms;
transition: color 250ms, background 250ms;
cursor: pointer;
font-size: 15px;
margin: 5px;
padding: 5px;
border: 2px solid white;
border-radius: 3px;
}
.saveselection span:hover {
background: rgba(255,255,255,.8);
color: black;
}
.savebutton {
-webkit-transition: opacity 250ms;
-moz-transition: opacity 250ms;
-ms-transition: opacity 250ms;
-o-transition: opacity 250ms;
transition: opacity 250ms;
font-size: 30px !important;
margin: 15px auto;
background: none;
cursor: pointer;
display: block;
border: none;
padding: 0;
width: 80px;
color: #fff;
margin-top: -2px;
}
.yang .savebutton {
color: #000;
}
.savebutton:hover {
opacity: .7;
}
.activesave {
background: rgba(255,255,255,.8);
color: black;
}
.hiddentextbox {
opacity:0;
filter:alpha(opacity=0);
position:absolute;
}
/*********************************************
* WORD COUNT
*********************************************/
.wordcount {
margin-left: -150px;
width: 300px;
}
.word-counter {
box-shadow: inset 0 0 9px -2px rgba(0,0,0,.9);
position: fixed;
height: 100%;
right: -6px;
width: 6px;
top: 0;
}
.word-counter.active {
right: 0;
}
.word-counter .progress {
-webkit-transition: all 400ms ease-in-out;
-moz-transition: all 400ms ease-in-out;
-ms-transition: all 400ms ease-in-out;
-o-transition: all 400ms ease-in-out;
transition: all 400ms ease-in-out;
background-color: deepskyblue;
position: absolute;
bottom: 0;
width: 100%;
height: 0%;
}
.progress.complete{
background-color: greenyellow;
}
/*********************************************
* UI BUBBLE
*********************************************/
.text-options {
-webkit-transition: opacity 250ms, margin 250ms;
-moz-transition: opacity 250ms, margin 250ms;
-ms-transition: opacity 250ms, margin 250ms;
-o-transition: opacity 250ms, margin 250ms;
transition: opacity 250ms, margin 250ms;
position: absolute;
left: -999px;
top: -999px;
color: #fff;
height: 0;
width: 0;
z-index: 5;
margin-top: 5px;
opacity: 0;
}
.text-options.fade {
opacity: 0;
margin-top: -5px;
}
.text-options.active {
opacity: 1;
margin-top: 0;
}
.options {
background-color: rgba(0,0,0,.9);
position: absolute;
border-radius: 5px;
margin-left: -63px;
margin-top: -46px;
z-index: 1000;
padding: 5px 4px 5px 5px;
width: 125px;
height: 40px;
-webkit-transition: all 300ms ease-in-out;
-moz-transition: all 300ms ease-in-out;
-ms-transition: all 300ms ease-in-out;
-o-transition: all 300ms ease-in-out;
transition: all 300ms ease-in-out;
}
.options.url-mode {
width: 275px;
margin-left: -137px;
}
.options.url-mode .bold, .options.url-mode .italic, .options.url-mode .quote {
width: 0;
overflow: hidden;
margin-right: 0;
opacity: 0;
}
.options .italic {
font-style: italic;
}
.options button {
transition: all 250ms ease-in-out;
float: left;
width: 28px;
opacity: .7;
height: 30px;
border-radius: 3px;
margin-right: 1px;
font-family: 'Lora', serif;
}
.about {
opacity: 0.4;
transition: opacity 250ms ease-in-out;
}
.options.url-mode input{
border-left: 2px solid transparent;
padding-right: 5px;
padding-left: 5px;
width: 236px;
}
.options input {
-webkit-transition: all 300ms ease-in-out;
-moz-transition: all 300ms ease-in-out;
-ms-transition: all 300ms ease-in-out;
-o-transition: all 300ms ease-in-out;
transition: all 300ms ease-in-out;
border-radius: 3px;
overflow: hidden;
outline: 0;
height: 30px;
padding: 0;
margin: 0;
border: 0;
float: left;
width: 0;
}
.options button.active {
background-color: rgba(255,255,255,.4);
opacity: 1;
}
.yang .options button.active {
background-color: rgba(0,0,0,.3);
}
.options button:hover, .about:hover {
opacity: .95;
}
.options:before {
content: "";
border-top: 5px solid rgba(0,0,0,.9);
border-bottom: 5px solid transparent;
border-right: 5px solid transparent;
border-left: 5px solid transparent;
position: absolute;
margin-left: -5px;
bottom: -15px;
height: 5px;
width: 0;
left: 50%;
}
.yang .options {
background-color: rgba(255,255,255,.9);
color: #000;
}
.yang .options:before {
border-top: 5px solid rgba(255,255,255,.9);
}
.url {
-webkit-font-smoothing: antialiased;
}
.top {
position: absolute;
top: 0;
}
.bottom {
position: absolute;
bottom: 0;
}
.about {
font-size: 28px !important;
filter: grayscale(1);
text-decoration: none !important;
}
.wrapper {
position: relative;
height: 100%;
}
/*********************************************
* PRINT
*********************************************/
@media print {
body {
overflow: visible;
}
section {
color: #111 !important;
}
.text-options, .ui, .word-counter {
display: none;
}
}
================================================
FILE: index.html
================================================
<!DOCTYPE html>
<html>
<head>
<!-- MISC/META -->
<title>ZenPen ~ Minimal Distraction, Maximum Zen</title>
<meta charset="utf-8" />
<meta
name="description"
content="Zenpen - A minimal text editor, made to stay out of the way while you get the words down."
/>
<!-- CSS -->
<link
href="//fonts.googleapis.com/css?family=Lora:400,700,400italic,700italic"
rel="stylesheet"
type="text/css"
/>
<link href="css/style.css" rel="stylesheet" />
<link href="css/fonts.css" rel="stylesheet" />
</head>
<body class="yin">
<div class="overlay">
<div class="wordcount modal">
<h1>Target Word Count</h1>
<div>
<input type="number" name="quantity" value="0" min="0" />
</div>
</div>
<div class="saveoverlay modal">
<h1>Select save format</h1>
<p class="saveselection">
<span data-format="markdown">Markdown</span>
<span data-format="html">HTML</span>
<span data-format="plain">Plain Text</span>
</p>
<button class="savebutton useicons"></button>
<div>
Or select format and press ctrl+c (cmd+c on mac) to copy the text.
</div>
<textarea class="hiddentextbox"></textarea>
</div>
</div>
<div class="text-options">
<div class="options">
<span class="no-overflow">
<span class="lengthen ui-inputs">
<button class="url useicons"></button>
<input
class="url-input"
type="text"
placeholder="Type or Paste URL here"
/>
<button class="bold">b</button>
<button class="italic">i</button>
<button class="quote">”</button>
</span>
</span>
</div>
</div>
<div class="ui">
<div class="wrapper">
<div class="top editing">
<button class="fullscreen useicons" title="Toggle fullscreen">

</button>
<button class="color-flip useicons" title="Invert colors">

</button>
<button class="target useicons" title="Set target word count">

</button>
<button class="save useicons" title="Save Text">

</button>
</div>
<div class="bottom">
<a
class="about"
href="https://www.buymeacoffee.com/tholman"
target="_blank"
>
☕
</a>
</div>
</div>
</div>
<div class="word-counter">
<span class="progress"></span>
</div>
<section>
<header contenteditable="true" class="header"></header>
<article contenteditable="true" class="content"></article>
</section>
<!-- LIBS -->
<script src="js/libs/FileSaver.min.js"></script>
<script src="js/libs/Blob.min.js"></script>
<script src="js/libs/screenfull.min.js"></script>
<!-- JS -->
<script src="js/default.js"></script>
<script src="js/utils.js"></script>
<script src="js/editor.js"></script>
<script src="js/ui.js"></script>
<script type="text/javascript">
// Initiate ZenPen
ZenPen.editor.init();
ZenPen.ui.init();
</script>
<!-- Global site tag (gtag.js) - Google Analytics -->
<script
async
src="https://www.googletagmanager.com/gtag/js?id=UA-38039699-1"
></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag() {
dataLayer.push(arguments);
}
gtag("js", new Date());
gtag("config", "UA-38039699-1");
</script>
</body>
</html>
================================================
FILE: js/default.js
================================================
var defaultTitle = 'This is ZenPen';
var defaultContent =
'<p>\
A minimalist writing zone, where you can block out all distractions and get to what\'s important. The writing! \
</p>\
<p> \
To get started, all you need to do is delete this text (seriously, just highlight it and hit delete), and fill the page with your own fantastic words. You can even change the title! \
</p> \
<p> \
You can use <b>bold</b>, <i>italics</i>, <b><i>both</i></b> and <a href="http://zenpen.io"> urls </a> just by highlighting the text and selecting them from the tiny options box that appears above it.\
</p>\
<blockquote>\
Quotes are easy to add too!\
</blockquote>\
<p>\
If you\'re using ZenPen, and want to contribute a few dollars, there\'s a small donate button on the bottom left.\
</p>\
<p>Happy Typing! ~ <b>Tim Holman (@twholman)</b></p>';
================================================
FILE: js/editor.js
================================================
// editor
ZenPen = window.ZenPen || {};
ZenPen.editor = (function() {
// Editor elements
var headerField, contentField, lastType, currentNodeList, lastSelection;
// Editor Bubble elements
var textOptions, optionsBox, boldButton, italicButton, quoteButton, urlButton, urlInput;
var composing;
function init() {
composing = false;
bindElements();
createEventBindings();
// Load state if storage is supported
if ( ZenPen.util.supportsHtmlStorage() ) {
loadState();
} else {
loadDefault();
}
// Set cursor position
var range = document.createRange();
var selection = window.getSelection();
range.setStart(headerField, 1);
selection.removeAllRanges();
selection.addRange(range);
}
function createEventBindings() {
// Key up bindings
if ( ZenPen.util.supportsHtmlStorage() ) {
document.onkeyup = function( event ) {
checkTextHighlighting( event );
saveState();
}
} else {
document.onkeyup = checkTextHighlighting;
}
// Mouse bindings
document.onmousedown = checkTextHighlighting;
document.onmouseup = function( event ) {
setTimeout( function() {
checkTextHighlighting( event );
}, 1);
};
// Window bindings
window.addEventListener( 'resize', function( event ) {
updateBubblePosition();
});
document.body.addEventListener( 'scroll', function() {
// TODO: Debounce update bubble position to stop excessive redraws
updateBubblePosition();
});
// Composition bindings. We need them to distinguish
// IME composition from text selection
document.addEventListener( 'compositionstart', onCompositionStart );
document.addEventListener( 'compositionend', onCompositionEnd );
}
function bindElements() {
headerField = document.querySelector( '.header' );
contentField = document.querySelector( '.content' );
textOptions = document.querySelector( '.text-options' );
optionsBox = textOptions.querySelector( '.options' );
boldButton = textOptions.querySelector( '.bold' );
boldButton.onclick = onBoldClick;
italicButton = textOptions.querySelector( '.italic' );
italicButton.onclick = onItalicClick;
quoteButton = textOptions.querySelector( '.quote' );
quoteButton.onclick = onQuoteClick;
urlButton = textOptions.querySelector( '.url' );
urlButton.onmousedown = onUrlClick;
urlInput = textOptions.querySelector( '.url-input' );
urlInput.onblur = onUrlInputBlur;
urlInput.onkeydown = onUrlInputKeyDown;
}
function checkTextHighlighting( event ) {
var selection = window.getSelection();
if ( (event.target.className === "url-input" ||
event.target.classList.contains( "url" ) ||
event.target.parentNode.classList.contains( "ui-inputs" ) ) ) {
currentNodeList = findNodes( selection.focusNode );
updateBubbleStates();
return;
}
// Check selections exist
if ( selection.isCollapsed === true && lastType === false ) {
onSelectorBlur();
}
// Text is selected
if ( selection.isCollapsed === false && composing === false ) {
currentNodeList = findNodes( selection.focusNode );
// Find if highlighting is in the editable area
if ( hasNode( currentNodeList, "ARTICLE") ) {
updateBubbleStates();
updateBubblePosition();
// Show the ui bubble
textOptions.className = "text-options active";
}
}
lastType = selection.isCollapsed;
}
function updateBubblePosition() {
var selection = window.getSelection();
var range = selection.getRangeAt(0);
var boundary = range.getBoundingClientRect();
textOptions.style.top = boundary.top - 5 + window.pageYOffset + "px";
textOptions.style.left = (boundary.left + boundary.right)/2 + "px";
}
function updateBubbleStates() {
// It would be possible to use classList here, but I feel that the
// browser support isn't quite there, and this functionality doesn't
// warrent a shim.
if ( hasNode( currentNodeList, 'B') ) {
boldButton.className = "bold active"
} else {
boldButton.className = "bold"
}
if ( hasNode( currentNodeList, 'I') ) {
italicButton.className = "italic active"
} else {
italicButton.className = "italic"
}
if ( hasNode( currentNodeList, 'BLOCKQUOTE') ) {
quoteButton.className = "quote active"
} else {
quoteButton.className = "quote"
}
if ( hasNode( currentNodeList, 'A') ) {
urlButton.className = "url useicons active"
} else {
urlButton.className = "url useicons"
}
}
function onSelectorBlur() {
textOptions.className = "text-options fade";
setTimeout( function() {
if (textOptions.className == "text-options fade") {
textOptions.className = "text-options";
textOptions.style.top = '-999px';
textOptions.style.left = '-999px';
}
}, 260 )
}
function findNodes( element ) {
var nodeNames = {};
// Internal node?
var selection = window.getSelection();
// if( selection.containsNode( document.querySelector('b'), false ) ) {
// nodeNames[ 'B' ] = true;
// }
while ( element.parentNode ) {
nodeNames[element.nodeName] = true;
element = element.parentNode;
if ( element.nodeName === 'A' ) {
nodeNames.url = element.href;
}
}
return nodeNames;
}
function hasNode( nodeList, name ) {
return !!nodeList[ name ];
}
function saveState( event ) {
localStorage[ 'header' ] = headerField.innerHTML;
localStorage[ 'content' ] = contentField.innerHTML;
}
function loadState() {
if ( localStorage[ 'header' ] ) {
headerField.innerHTML = localStorage[ 'header' ];
} else {
headerField.innerHTML = defaultTitle; // in default.js
}
if ( localStorage[ 'content' ] ) {
contentField.innerHTML = localStorage[ 'content' ];
} else {
loadDefaultContent()
}
}
function loadDefault() {
headerField.innerHTML = defaultTitle; // in default.js
loadDefaultContent();
}
function loadDefaultContent() {
contentField.innerHTML = defaultContent; // in default.js
}
function onBoldClick() {
document.execCommand( 'bold', false );
}
function onItalicClick() {
document.execCommand( 'italic', false );
}
function onQuoteClick() {
var nodeNames = findNodes( window.getSelection().focusNode );
if ( hasNode( nodeNames, 'BLOCKQUOTE' ) ) {
document.execCommand( 'formatBlock', false, 'p' );
document.execCommand( 'outdent' );
} else {
document.execCommand( 'formatBlock', false, 'blockquote' );
}
}
function onUrlClick() {
if ( optionsBox.className == 'options' ) {
optionsBox.className = 'options url-mode';
// Set timeout here to debounce the focus action
setTimeout( function() {
var nodeNames = findNodes( window.getSelection().focusNode );
if ( hasNode( nodeNames , "A" ) ) {
urlInput.value = nodeNames.url;
} else {
// Symbolize text turning into a link, which is temporary, and will never be seen.
document.execCommand( 'createLink', false, '/' );
}
// Since typing in the input box kills the highlighted text we need
// to save this selection, to add the url link if it is provided.
lastSelection = window.getSelection().getRangeAt(0);
lastType = false;
urlInput.focus();
}, 100);
} else {
optionsBox.className = 'options';
}
}
function onUrlInputKeyDown( event ) {
if ( event.keyCode === 13 ) {
event.preventDefault();
applyURL( urlInput.value );
urlInput.blur();
}
}
function onUrlInputBlur( event ) {
optionsBox.className = 'options';
applyURL( urlInput.value );
urlInput.value = '';
currentNodeList = findNodes( window.getSelection().focusNode );
updateBubbleStates();
}
function applyURL( url ) {
rehighlightLastSelection();
// Unlink any current links
document.execCommand( 'unlink', false );
if (url !== "") {
// Insert HTTP if it doesn't exist.
if ( !url.match("^(http|https)://") ) {
url = "http://" + url;
}
document.execCommand( 'createLink', false, url );
}
}
function rehighlightLastSelection() {
var selection = window.getSelection();
if (selection.rangeCount > 0) {
selection.removeAllRanges();
}
selection.addRange( lastSelection );
}
function getWordCount() {
var text = ZenPen.util.getText( contentField );
if ( text === "" ) {
return 0
} else {
return text.split(/\s+/).length;
}
}
function onCompositionStart ( event ) {
composing = true;
}
function onCompositionEnd (event) {
composing = false;
}
return {
init: init,
saveState: saveState,
getWordCount: getWordCount
}
})();
================================================
FILE: js/ui.js
================================================
// ui functions
ZenPen = window.ZenPen || {};
ZenPen.ui = (function() {
// Base elements
var body, article, uiContainer, overlay, header;
// Buttons
var screenSizeElement, colorLayoutElement, targetElement, saveElement;
// Word Counter
var wordCountValue, wordCountBox, wordCountElement, wordCounter, wordCounterProgress;
//save support
var supportsSave, saveFormat, textToWrite;
var expandScreenIcon = '';
var shrinkScreenIcon = '';
var darkLayout = false;
function init() {
supportsSave = !!new Blob()?true:false;
bindElements();
wordCountActive = false;
if ( ZenPen.util.supportsHtmlStorage() ) {
loadState();
}
console.log( "Checkin under the hood eh? We've probably got a lot in common. You should totally check out ZenPen on github! (https://github.com/tholman/zenpen)." );
}
function loadState() {
// Activate word counter
if ( localStorage['wordCount'] && localStorage['wordCount'] !== "0") {
wordCountValue = parseInt(localStorage['wordCount']);
wordCountElement.value = localStorage['wordCount'];
wordCounter.className = "word-counter active";
updateWordCount();
}
// Activate color switch
if ( localStorage['darkLayout'] === 'true' ) {
if ( darkLayout === false ) {
document.body.className = 'yang';
} else {
document.body.className = 'yin';
}
darkLayout = !darkLayout;
}
}
function saveState() {
if ( ZenPen.util.supportsHtmlStorage() ) {
localStorage[ 'darkLayout' ] = darkLayout;
localStorage[ 'wordCount' ] = wordCountElement.value;
}
}
function bindElements() {
// Body element for light/dark styles
body = document.body;
uiContainer = document.querySelector( '.ui' );
// UI element for color flip
colorLayoutElement = document.querySelector( '.color-flip' );
colorLayoutElement.onclick = onColorLayoutClick;
// UI element for full screen
screenSizeElement = document.querySelector( '.fullscreen' );
screenSizeElement.onclick = onScreenSizeClick;
targetElement = document.querySelector( '.target ');
targetElement.onclick = onTargetClick;
//init event listeners only if browser can save
if (supportsSave) {
saveElement = document.querySelector( '.save' );
saveElement.onclick = onSaveClick;
var formatSelectors = document.querySelectorAll( '.saveselection span' );
for( var i in formatSelectors ) {
formatSelectors[i].onclick = selectFormat;
}
document.querySelector('.savebutton').onclick = saveText;
} else {
document.querySelector('.save.useicons').style.display = "none";
}
// Overlay when modals are active
overlay = document.querySelector( '.overlay' );
overlay.onclick = onOverlayClick;
article = document.querySelector( '.content' );
article.onkeyup = onArticleKeyUp;
wordCountBox = overlay.querySelector( '.wordcount' );
wordCountElement = wordCountBox.querySelector( 'input' );
wordCountElement.onchange = onWordCountChange;
wordCountElement.onkeyup = onWordCountKeyUp;
saveModal = overlay.querySelector('.saveoverlay');
wordCounter = document.querySelector( '.word-counter' );
wordCounterProgress = wordCounter.querySelector( '.progress' );
header = document.querySelector( '.header' );
header.onkeypress = onHeaderKeyPress;
}
function onScreenSizeClick( event ) {
screenfull.toggle();
if ( screenfull.enabled ) {
document.addEventListener( screenfull.raw.fullscreenchange, function () {
if ( screenfull.isFullscreen ) {
screenSizeElement.innerHTML = shrinkScreenIcon;
} else {
screenSizeElement.innerHTML = expandScreenIcon;
}
});
}
};
function onColorLayoutClick( event ) {
if ( darkLayout === false ) {
document.body.className = 'yang';
} else {
document.body.className = 'yin';
}
darkLayout = !darkLayout;
saveState();
}
function onTargetClick( event ) {
overlay.style.display = "block";
wordCountBox.style.display = "block";
wordCountElement.focus();
}
function onSaveClick( event ) {
overlay.style.display = "block";
saveModal.style.display = "block";
}
function saveText( event ) {
if (typeof saveFormat != 'undefined' && saveFormat != '') {
var blob = new Blob([textToWrite], {type: "text/plain;charset=utf-8"});
/* remove tabs and line breaks from header */
var headerText = header.innerHTML.replace(/(\t|\n|\r)/gm,"");
if (headerText === "") {
headerText = "ZenPen";
}
saveAs(blob, headerText + '.txt');
} else {
document.querySelector('.saveoverlay h1').style.color = '#FC1E1E';
}
}
/* Allows the user to press enter to tab from the title */
function onHeaderKeyPress( event ) {
if ( event.keyCode === 13 ) {
event.preventDefault();
article.focus();
}
}
/* Allows the user to press enter to tab from the word count modal */
function onWordCountKeyUp( event ) {
if ( event.keyCode === 13 ) {
event.preventDefault();
setWordCount( parseInt(this.value) );
removeOverlay();
article.focus();
}
}
function onWordCountChange( event ) {
setWordCount( parseInt(this.value) );
}
function setWordCount( count ) {
// Set wordcount ui to active
if ( count > 0) {
wordCountValue = count;
wordCounter.className = "word-counter active";
updateWordCount();
} else {
wordCountValue = 0;
wordCounter.className = "word-counter";
}
saveState();
}
function onArticleKeyUp( event ) {
if ( wordCountValue > 0 ) {
updateWordCount();
}
}
function updateWordCount() {
var wordCount = ZenPen.editor.getWordCount();
var percentageComplete = wordCount / wordCountValue;
wordCounterProgress.style.height = percentageComplete * 100 + '%';
if ( percentageComplete >= 1 ) {
wordCounterProgress.className = "progress complete";
} else {
wordCounterProgress.className = "progress";
}
}
function selectFormat( e ) {
if ( document.querySelectorAll('span.activesave').length > 0 ) {
document.querySelector('span.activesave').className = '';
}
document.querySelector('.saveoverlay h1').style.cssText = '';
var targ;
if (!e) var e = window.event;
if (e.target) targ = e.target;
else if (e.srcElement) targ = e.srcElement;
// defeat Safari bug
if (targ.nodeType == 3) {
targ = targ.parentNode;
}
targ.className ='activesave';
saveFormat = targ.getAttribute('data-format');
var header = document.querySelector('header.header');
var headerText = header.innerHTML.replace(/(\r\n|\n|\r)/gm,"") + "\n";
var body = document.querySelector('article.content');
var bodyText = body.innerHTML;
textToWrite = formatText(saveFormat,headerText,bodyText);
var textArea = document.querySelector('.hiddentextbox');
textArea.value = textToWrite;
textArea.focus();
textArea.select();
}
function formatText( type, header, body ) {
var text;
switch( type ) {
case 'html':
header = "<h1>" + header + "</h1>";
text = header + body;
text = text.replace(/\t/g, '');
break;
case 'markdown':
header = header.replace(/\t/g, '');
header = header.replace(/\n$/, '');
header = "#" + header + "#";
text = body.replace(/\t/g, '');
text = text.replace(/<b>|<\/b>/g,"**")
.replace(/\r\n+|\r+|\n+|\t+/ig,"")
.replace(/<i>|<\/i>/g,"_")
.replace(/<blockquote>/g,"> ")
.replace(/<\/blockquote>/g,"")
.replace(/<p>|<\/p>/gi,"\n")
.replace(/<br>/g,"\n");
var links = text.match(/<a href="(.+)">(.+)<\/a>/gi);
if (links !== null) {
for ( var i = 0; i<links.length; i++ ) {
var tmpparent = document.createElement('div');
tmpparent.innerHTML = links[i];
var tmp = tmpparent.firstChild;
var href = tmp.getAttribute('href');
var linktext = tmp.textContent || tmp.innerText || "";
text = text.replace(links[i],'['+linktext+']('+href+')');
}
}
text = header +"\n\n"+ text;
break;
case 'plain':
header = header.replace(/\t/g, '');
var tmp = document.createElement('div');
tmp.innerHTML = body;
text = tmp.textContent || tmp.innerText || "";
text = text.replace(/\t/g, '')
.replace(/\n{3}/g,"\n")
.replace(/\n/,""); //replace the opening line break
text = header + text;
break;
default:
break;
}
return text;
}
function onOverlayClick( event ) {
if ( event.target.className === "overlay" ) {
removeOverlay();
}
}
function removeOverlay() {
overlay.style.display = "none";
wordCountBox.style.display = "none";
descriptionModal.style.display = "none";
saveModal.style.display = "none";
if ( document.querySelectorAll('span.activesave' ).length > 0) {
document.querySelector('span.activesave').className = '';
}
saveFormat='';
}
return {
init: init
}
})();
================================================
FILE: js/utils.js
================================================
// Utility functions
ZenPen = window.ZenPen || {};
ZenPen.util = (function() {
function supportsHtmlStorage() {
try {
return 'localStorage' in window && window['localStorage'] !== null;
} catch (e) {
return false;
}
};
function getText(el) {
var ret = " ";
var length = el.childNodes.length;
for(var i = 0; i < length; i++) {
var node = el.childNodes[i];
if(node.nodeType != 8) {
if ( node.nodeType != 1 ) {
// Strip white space.
ret += node.nodeValue;
} else {
ret += getText( node );
}
}
}
return ZenPen.util.trim(ret);
};
function trim(string) {
return string.replace(/^\s+|\s+$/g, '');
};
return {
trim: trim,
getText: getText,
supportsHtmlStorage: supportsHtmlStorage
}
})()
================================================
FILE: license.md
================================================
## The MIT License (MIT)
Copyright (c) 2025 ~ Tim Holman - @twholman
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
================================================
FILE: readme.md
================================================
## ZenPen - A minimalist writing zone.
Zenpen (http://zenpen.io) is a web app for writing minimally, and getting into the Zone.
All information is persistent locally, using HTML5 local storage.
### ZenPen's minimal interface

### Text styling

### Saving

### License
The MIT License
Copyright (C) 2016 ~ [Tim Holman](http://tholman.com) ~ timothy.w.holman@gmail.com
gitextract_4jowxg8a/ ├── .github/ │ └── FUNDING.yml ├── .gitignore ├── css/ │ ├── fonts.css │ └── style.css ├── index.html ├── js/ │ ├── default.js │ ├── editor.js │ ├── ui.js │ └── utils.js ├── license.md └── readme.md
SYMBOL INDEX (46 symbols across 3 files)
FILE: js/editor.js
function init (line 13) | function init() {
function createEventBindings (line 35) | function createEventBindings() {
function bindElements (line 77) | function bindElements() {
function checkTextHighlighting (line 102) | function checkTextHighlighting( event ) {
function updateBubblePosition (line 140) | function updateBubblePosition() {
function updateBubbleStates (line 149) | function updateBubbleStates() {
function onSelectorBlur (line 180) | function onSelectorBlur() {
function findNodes (line 194) | function findNodes( element ) {
function hasNode (line 218) | function hasNode( nodeList, name ) {
function saveState (line 223) | function saveState( event ) {
function loadState (line 229) | function loadState() {
function loadDefault (line 244) | function loadDefault() {
function loadDefaultContent (line 249) | function loadDefaultContent() {
function onBoldClick (line 253) | function onBoldClick() {
function onItalicClick (line 257) | function onItalicClick() {
function onQuoteClick (line 261) | function onQuoteClick() {
function onUrlClick (line 273) | function onUrlClick() {
function onUrlInputKeyDown (line 306) | function onUrlInputKeyDown( event ) {
function onUrlInputBlur (line 315) | function onUrlInputBlur( event ) {
function applyURL (line 325) | function applyURL( url ) {
function rehighlightLastSelection (line 344) | function rehighlightLastSelection() {
function getWordCount (line 352) | function getWordCount() {
function onCompositionStart (line 363) | function onCompositionStart ( event ) {
function onCompositionEnd (line 367) | function onCompositionEnd (event) {
FILE: js/ui.js
function init (line 22) | function init() {
function loadState (line 37) | function loadState() {
function saveState (line 59) | function saveState() {
function bindElements (line 67) | function bindElements() {
function onScreenSizeClick (line 122) | function onScreenSizeClick( event ) {
function onColorLayoutClick (line 136) | function onColorLayoutClick( event ) {
function onTargetClick (line 147) | function onTargetClick( event ) {
function onSaveClick (line 153) | function onSaveClick( event ) {
function saveText (line 158) | function saveText( event ) {
function onHeaderKeyPress (line 174) | function onHeaderKeyPress( event ) {
function onWordCountKeyUp (line 183) | function onWordCountKeyUp( event ) {
function onWordCountChange (line 196) | function onWordCountChange( event ) {
function setWordCount (line 201) | function setWordCount( count ) {
function onArticleKeyUp (line 219) | function onArticleKeyUp( event ) {
function updateWordCount (line 226) | function updateWordCount() {
function selectFormat (line 239) | function selectFormat( e ) {
function formatText (line 276) | function formatText( type, header, body ) {
function onOverlayClick (line 341) | function onOverlayClick( event ) {
function removeOverlay (line 348) | function removeOverlay() {
FILE: js/utils.js
function supportsHtmlStorage (line 5) | function supportsHtmlStorage() {
function getText (line 13) | function getText(el) {
function trim (line 31) | function trim(string) {
Condensed preview — 11 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (39K chars).
[
{
"path": ".github/FUNDING.yml",
"chars": 67,
"preview": "github: [tholman]\ncustom: ['https://www.buymeacoffee.com/tholman']\n"
},
{
"path": ".gitignore",
"chars": 22,
"preview": ".c9revisions\n.DS_Store"
},
{
"path": "css/fonts.css",
"chars": 1422,
"preview": "@font-face {\n\tfont-family: 'icomoon';\n\tsrc:url('fonts/icomoon.eot');\n\tsrc:url('fonts/icomoon.eot?#iefix') format('embedd"
},
{
"path": "css/style.css",
"chars": 8216,
"preview": "/*!\n * ZenPen\n * http://www.zenpen.io\n * MIT licensed\n *\n * Copyright (C) Tim Holman, http://tholman.com\n */\n\n\n/********"
},
{
"path": "index.html",
"chars": 3324,
"preview": "<!DOCTYPE html>\n<html>\n\t<head>\n\t\t<!-- MISC/META -->\n\t\t<title>ZenPen ~ Minimal Distraction, Maximum Zen</title>\n\t\t<meta c"
},
{
"path": "js/default.js",
"chars": 839,
"preview": "var defaultTitle = 'This is ZenPen';\nvar defaultContent = \n'<p>\\\nA minimalist writing zone, where you can block out all "
},
{
"path": "js/editor.js",
"chars": 8497,
"preview": "// editor\nZenPen = window.ZenPen || {};\nZenPen.editor = (function() {\n\n\t// Editor elements\n\tvar headerField, contentFiel"
},
{
"path": "js/ui.js",
"chars": 9378,
"preview": "// ui functions\nZenPen = window.ZenPen || {};\nZenPen.ui = (function() {\n\n\t// Base elements\n\tvar body, article, uiContain"
},
{
"path": "js/utils.js",
"chars": 779,
"preview": "// Utility functions\nZenPen = window.ZenPen || {};\nZenPen.util = (function() {\n\n\tfunction supportsHtmlStorage() {\n\t\ttry "
},
{
"path": "license.md",
"chars": 1094,
"preview": "## The MIT License (MIT)\n\nCopyright (c) 2025 ~ Tim Holman - @twholman\n\nPermission is hereby granted, free of charge, to "
},
{
"path": "readme.md",
"chars": 507,
"preview": "## ZenPen - A minimalist writing zone.\n\nZenpen (http://zenpen.io) is a web app for writing minimally, and getting into t"
}
]
About this extraction
This page contains the full source code of the tholman/zenpen GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 11 files (33.3 KB), approximately 9.5k tokens, and a symbol index with 46 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.