├── .gitignore ├── README.md ├── data └── .htaccess ├── index.php ├── lib ├── .htaccess ├── ext │ ├── HTML_To_Markdown.php │ ├── Markdown.php │ ├── MarkdownExtra.php │ └── yoslogin.lib.php ├── jotter.class.php ├── login.class.php └── utils.class.php └── tpl ├── config.tpl.php ├── error.tpl.php ├── footer.tpl.php ├── header.tpl.php ├── img ├── ajax-loader.gif ├── arbo-parent-closed.png ├── arbo-parent-open.png ├── chain--minus.png ├── chain--plus.png ├── disk--exclamation.png ├── disk-black.png ├── disk.png ├── document--minus.png ├── document--pencil.png ├── document--plus.png ├── document-export.png ├── document-import.png ├── document.png ├── door-open-out.png ├── edit-alignment-center.png ├── edit-alignment-justify.png ├── edit-alignment-right.png ├── edit-alignment.png ├── edit-bold.png ├── edit-code.png ├── edit-heading-1.png ├── edit-heading-2.png ├── edit-heading-3.png ├── edit-heading-4.png ├── edit-heading-5.png ├── edit-heading-6.png ├── edit-heading-minus.png ├── edit-heading.png ├── edit-indent.png ├── edit-italic.png ├── edit-list-order.png ├── edit-list.png ├── edit-markdown.png ├── edit-outdent.png ├── edit-strike.png ├── edit-underline.png ├── eye.png ├── folder--arrow.png ├── folder--minus.png ├── folder--pencil.png ├── folder--plus.png ├── folder-export.png ├── folder-import.png ├── folder-open.png ├── folder-tree.png ├── folder.png ├── folders-stack-minus.png ├── folders-stack.png ├── image.png ├── jotter-icon-16.png ├── jotter.png └── wrench-screwdriver.png ├── itemDelete.tpl.php ├── itemForm.tpl.php ├── js ├── editor-wysiwyg.js ├── editor.js ├── ext │ ├── bootstrap-wysiwyg.js │ ├── bootstrap.min.js │ ├── jquery-2.0.3.min.js │ └── jquery.hotkeys.js └── main.js ├── loginForm.tpl.php ├── markdown.tpl.php ├── note.tpl.php ├── notebook.tpl.php ├── notebookForm.tpl.php ├── notebooks.tpl.php └── style.css /.gitignore: -------------------------------------------------------------------------------- 1 | cache/* 2 | data/* 3 | errors.log 4 | !data/.htaccess 5 | *.sublime-project 6 | *.sublime-workspace 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Jotter 2 | 3 | ![Jotter](http://www.yosko.net/data/images/jotter.png) 4 | 5 | Jotter is a lightweight, no database, powerful web notebook that lets you create and manage notes online safely, quickly & easily. 6 | 7 | See [the demo](http://tools.yosko.net/demos/jotter/) or install it yourself! 8 | 9 | ## Features 10 | 11 | - Markdown and WYSIWYG (What You See Is What You Get) editors 12 | - organize notes hierarchically 13 | - manage as many notebooks as you want 14 | - multi-user support (currently, notebooks can't be shared between users) 15 | - no DBMS needed. Everything is stored in flat files (JSON & Markdown) 16 | 17 | ![Jotter screenshot](http://www.yosko.net/data/images/jotter-v0.2.png) 18 | 19 | ## Requirements 20 | 21 | - PHP 5.3 or above 22 | - write access to the sub-directory `data/` 23 | 24 | ## Install 25 | 26 | ### New install 27 | 28 | 1. Upload it (or `git clone` it) on your server (let's say in `/var/www/jotter`) 29 | 2. Go to the corresponding URL (lets say `http://www.example.com/jotter`) 30 | 31 | ### Update 32 | 33 | Currently the update is quite easy: 34 | 1. (optional) backup your `data/` directory up 35 | 2. just overwrite app files with latest version ((or `git pull` it) 36 | 3. That's all, folks! 37 | 38 | ## TODO 39 | 40 | - Next version: 41 | - remember folded/unfolded folder (will change the save format) 42 | - Following ones: 43 | - Trash bin for deleted notes 44 | - Keep last N versions of each note and restore it on demand 45 | - Option to make some notes/notebooks publicly accessible 46 | - Not sure if possible: 47 | - Sync API (à la Simplenotes?) for desktop/mobile apps 48 | - Share notebooks between users & handle concurrent edit (à la Etherpad?) 49 | - Patch the WYSIWYG and Markdown libraries to enhance behavior and avoid most common rendering problems 50 | 51 | ## Version History 52 | 53 | - v0.4 (2014-03-20) 54 | - major Javascript rewrite 55 | - introduced markdown editor (with help page) 56 | - fixed wysiyg editor issues 57 | - display version number 58 | - dropdown for changing notebook now always accessible 59 | - add note/directory now accessible from contextual menu 60 | - fixed save shortcut not always working in Firefox 61 | - v0.3 (2013-11-28) 62 | - drag & drop to move notes/directories within a notebook 63 | - v0.2 (2013-11-22) 64 | - fold/unfold directories (not yet saved on server) 65 | - moved/changed some buttons for better ergonomics 66 | - always keep toolbar visible 67 | - change notebook without returning to homepage 68 | - interactive source code display (whitout base64 code) 69 | - image button implemented 70 | - prefill link with 'http://' 71 | - FIX random sort order 72 | - other minor fixes and tweaks 73 | - v0.1 (2013-11-18) 74 | - initial version 75 | 76 | ## License 77 | 78 | Jotter is a work by [Yosko](http://www.yosko.net), all rights reserved. 79 | 80 | It is licensed under the [GNU LGPL](http://www.gnu.org/licenses/lgpl.html). 81 | 82 | ## Dependencies 83 | 84 | Everything you need to make Jotter work is already on this repository. It includes: 85 | 86 | - [PHP Markdown](https://github.com/michelf/php-markdown/) 87 | - [HTML To Markdown for PHP](https://github.com/nickcernis/html-to-markdown), under the MIT license. 88 | - [YosLogin](https://github.com/yosko/yoslogin), under the GNU LGPL license. This library also includes: 89 | - [Secure-random-bytes-in-PHP](https://github.com/GeorgeArgyros/Secure-random-bytes-in-PHP/), under the New BSD license. 90 | - [bootstrap-wysiwyg](http://github.com/mindmup/bootstrap-wysiwyg), under the MIT license. This library also includes: 91 | - [jQuery Hotkeys](http://github.com/tzuryby/jquery.hotkeys), under the MIT & GPL2 licenses. 92 | - [jQuery](jquery.org), under the MIT license. 93 | - [Bootstrap.js](http://twitter.github.com/bootstrap/), under Apache License v2.0. 94 | -------------------------------------------------------------------------------- /data/.htaccess: -------------------------------------------------------------------------------- 1 | Order deny,allow 2 | Deny from all -------------------------------------------------------------------------------- /index.php: -------------------------------------------------------------------------------- 1 | 7 | * @version v0.4 8 | * @link https://github.com/yosko/jotter 9 | */ 10 | define( 'VERSION', '0.4' ); 11 | define( 'ROOT', __DIR__.'/' ); 12 | define( 'DIR_DATA', ROOT.'data/' ); 13 | define( 'DIR_TPL', ROOT.'tpl/' ); 14 | define( 'URL', 15 | (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] != 'off'?'https':'http') 16 | .'://' 17 | .$_SERVER['HTTP_HOST'] 18 | .rtrim(dirname($_SERVER['SCRIPT_NAME']),'/') 19 | .'/' 20 | ); 21 | define( 'URL_TPL', URL.'tpl/' ); 22 | 23 | define( 'ENV_DEMO', 'demo' ); 24 | define( 'ENV_DEV', 'dev' ); 25 | define( 'ENV_PROD', 'prod' ); 26 | define( 'ENV_CURRENT', ENV_DEV ); 27 | 28 | //display errors & warnings 29 | if (ENV_CURRENT == ENV_DEV) { 30 | error_reporting(E_ALL | E_STRICT); 31 | ini_set('display_errors','On'); 32 | ini_set('log_errors', 'On'); 33 | ini_set('error_log', ROOT.'errors.log'); 34 | } else { 35 | ini_set('display_errors','Off'); 36 | } 37 | 38 | // external libraries 39 | // https://github.com/michelf/php-markdown/ 40 | require_once( ROOT.'lib/ext/Markdown.php'); 41 | require_once( ROOT.'lib/ext/MarkdownExtra.php'); 42 | // https://github.com/nickcernis/html-to-markdown 43 | require_once( ROOT.'lib/ext/HTML_To_Markdown.php'); 44 | // https://github.com/yosko/yoslogin/ 45 | require_once( ROOT.'lib/ext/yoslogin.lib.php'); 46 | 47 | //Jotter libraries 48 | require_once( ROOT.'lib/utils.class.php'); 49 | require_once( ROOT.'lib/jotter.class.php'); 50 | require_once( ROOT.'lib/login.class.php'); 51 | 52 | $jotter = new Jotter(); 53 | $errors = array(); 54 | $isNote = false; 55 | $isConfigMode = false; 56 | $isEditMode = false; 57 | $isWysiwyg = false; 58 | $isDir = false; 59 | $appInstalled = file_exists(DIR_DATA.'users.json'); 60 | 61 | //check if user is logged in 62 | $logger = new Login( 'jotter' ); 63 | 64 | //user is trying to log in 65 | if( !empty($_POST['submitLoginForm']) ) { 66 | //install app and create first user 67 | if(!$appInstalled) { 68 | $logger->createUser( 69 | htmlspecialchars(trim($_POST['login'])), 70 | htmlspecialchars(trim($_POST['password'])) 71 | ); 72 | } 73 | 74 | $user = $logger->logIn( 75 | htmlspecialchars(trim($_POST['login'])), 76 | htmlspecialchars(trim($_POST['password'])), 77 | isset($_POST['remember']) 78 | ); 79 | 80 | //logging out 81 | } elseif( !empty($_GET['action']) && $_GET['action'] == 'logout' ) { 82 | $logger->logOut(); 83 | } else { 84 | $user = $logger->authUser(); 85 | } 86 | 87 | 88 | // ajax calls 89 | if(!empty($_GET['action']) && $_GET['action'] == 'ajax') { 90 | $data = false; 91 | if (ENV_CURRENT == ENV_DEMO) { 92 | $data = true; 93 | } 94 | 95 | // always return false if user is not authenticated 96 | if($user['isLoggedIn'] && ENV_CURRENT != ENV_DEMO) { 97 | $option = isset($_GET['option'])?$_GET['option']:false; 98 | $notebookName = isset($_GET['nb'])?urlencode($_GET['nb']):false; 99 | $itemPath = isset($_GET['item'])?$_GET['item']:false; 100 | 101 | //load the complete list of notebooks 102 | $notebooks = $jotter->loadNotebooks(); 103 | $notebook = ($notebookName !== false)?$jotter->loadNotebook($notebookName, $user['login']):false; 104 | 105 | //move an item into another directory 106 | if($option == 'moveItem') { 107 | $sourcePath = $_GET['source']; 108 | $destPath = $_GET['destination']; 109 | 110 | if(!is_dir(DIR_DATA.$user['login'].'/'.$notebookName.'/'.$destPath)) { 111 | $destPath = dirname($destPath); 112 | } 113 | if($sourcePath == '.') { $sourcePath = ''; } 114 | if($destPath == '.') { $destPath = ''; } 115 | 116 | //TODO: check if source parent of destination 117 | //TODO: check if source & destination are "identical" 118 | 119 | //make sure the requested move is possible and safe 120 | $error = strpos($notebookName, '..') !== false 121 | || strpos($sourcePath, '..') !== false 122 | || strpos($destPath, '..') !== false 123 | || !isset($notebooks[$user['login']][$notebookName]) 124 | || !file_exists(DIR_DATA.$user['login'].'/'.$notebookName.'/'.$sourcePath) 125 | || !file_exists(DIR_DATA.$user['login'].'/'.$notebookName.'/'.$destPath); 126 | 127 | if(!$error) { 128 | $notebook = $jotter->loadNotebook($notebookName, $user['login']); 129 | $error = !$jotter->moveItem($sourcePath, $destPath); 130 | } 131 | 132 | $data = !$error; 133 | 134 | // save current note 135 | } elseif($option == 'save') { 136 | //only load notebook if it is owned by current user 137 | if(isset($notebooks[$user['login']][$notebookName])) { 138 | $itemData = Utils::getArrayItem($notebook['tree'], $itemPath); 139 | $isNote = $itemData === true; 140 | 141 | if($isNote && isset($_POST['text'])) { 142 | if (ENV_CURRENT != ENV_DEMO) { 143 | //save the note 144 | $data = $jotter->setNoteText($itemPath, $_POST['text']); 145 | } else { 146 | $data = true; 147 | } 148 | } 149 | } 150 | 151 | // preview a markdown note in HTML 152 | } elseif($option == 'preview') { 153 | $data = $jotter->loadNote($itemPath, true); 154 | } 155 | } 156 | 157 | header('Content-type: application/json'); 158 | echo json_encode($data); 159 | exit; 160 | 161 | //login form 162 | } elseif(!$user['isLoggedIn']) { 163 | //display form as an installation process 164 | if(!$appInstalled) { 165 | $phpMinVersion = '5.3'; 166 | $phpIsMinVersion = (version_compare(PHP_VERSION, $phpMinVersion) >= 0); 167 | $isWritable = is_writable(DIR_DATA) && is_writable(ROOT.'cache/') 168 | || !file_exists(DIR_DATA) && !file_exists(ROOT.'cache/') 169 | && is_writable(dirname(DIR_DATA)); 170 | } 171 | 172 | include( DIR_TPL.'loginForm.tpl.php' ); 173 | 174 | //notebook pages 175 | } elseif( !empty($_GET['nb']) ) { 176 | $itemPath = ''; 177 | $notebook = false; 178 | $notebookName = urlencode($_GET['nb']); 179 | 180 | //load the complete list of notebooks 181 | $notebooks = $jotter->loadNotebooks(); 182 | 183 | //only load notebook if it is owned by current user 184 | if(isset($notebooks[$user['login']][$notebookName])) { 185 | $notebook = $jotter->loadNotebook($notebookName, $user['login']); 186 | } 187 | 188 | // notebook wasn't loaded 189 | if($notebook == false) { 190 | include( DIR_TPL.'error.tpl.php' ); 191 | 192 | // rename current notebook 193 | } elseif( !empty($_GET['action']) && $_GET['action'] == 'edit' && empty($_GET['item']) ) { 194 | d('edit notebook'); 195 | 196 | // delete current notebook 197 | } elseif( !empty($_GET['action']) && $_GET['action'] == 'delete' && empty($_GET['item']) ) { 198 | //confirmation was sent 199 | if(isset($_POST['delete'])) { 200 | if (ENV_CURRENT != ENV_DEMO) { 201 | $jotter->unsetNotebook($notebookName, $user['login']); 202 | } 203 | 204 | header('Location: '.URL); 205 | exit; 206 | } 207 | 208 | include( DIR_TPL.'itemDelete.tpl.php' ); 209 | 210 | // add a subdirectory or a note to the current directory 211 | } elseif( !empty($_GET['action']) && ($_GET['action'] == 'adddir' || $_GET['action'] == 'addnote') ) { 212 | if(isset($_POST['name'])) { 213 | $item['name'] = $_POST['name']; 214 | $path = $item['name']; 215 | 216 | if(!empty($_GET['item'])) { 217 | if(!is_dir(DIR_DATA.$user['login'].'/'.$notebookName.'/'.$_GET['item'])) { 218 | if(dirname($_GET['item']) != '.') { 219 | $path = dirname($_GET['item']).'/'.$path; 220 | } 221 | } else { 222 | if(!empty($_GET['item'])) { 223 | $path = $_GET['item'].'/'.$path; 224 | } 225 | } 226 | } 227 | 228 | $errors['empty'] = empty($item['name']); 229 | $errors['alreadyExists'] = !is_null(Utils::getArrayItem($notebook['tree'], $path)); 230 | if(!in_array(true, $errors)) { 231 | if (ENV_CURRENT != ENV_DEMO) { 232 | if($_GET['action'] == 'addnote') { 233 | $path .= '.md'; 234 | $jotter->setNote($path); 235 | } 236 | else { 237 | $jotter->setDirectory($path); 238 | } 239 | } 240 | 241 | header('Location: '.URL.'?nb='.$notebookName.'&item='.$path); 242 | exit; 243 | } 244 | } 245 | 246 | include( DIR_TPL.'itemForm.tpl.php' ); 247 | 248 | // notebook item 249 | } elseif( !empty($_GET['item']) && strpos($itemPath, '..') === false ) { 250 | $itemPath = $_GET['item']; 251 | 252 | $itemData = Utils::getArrayItem($notebook['tree'], $itemPath); 253 | $isNote = $itemData === true; 254 | if(!$isNote) { 255 | $dirPath = DIR_DATA.$user['login'].'/'.$notebookName.'/'.$itemPath; 256 | $isDir = file_exists($dirPath) && is_dir($dirPath); 257 | } 258 | 259 | //item not found: show notebook root 260 | if(!$isDir && !$isNote) { 261 | include( DIR_TPL.'notebook.tpl.php' ); 262 | 263 | // rename current item 264 | } elseif( !empty($_GET['action']) && $_GET['action'] == 'edit' ) { 265 | //confirmation was sent 266 | if(isset($_POST['name'])) { 267 | $item['name'] = $_POST['name']; 268 | $path = $item['name']; 269 | $path = (dirname($itemPath)!='.'?dirname($itemPath).'/':'').$path; 270 | 271 | $errors['empty'] = empty($item['name']); 272 | $errors['sameName'] = $itemPath == $path.'.md'; 273 | $errors['alreadyExists'] = !is_null(Utils::getArrayItem($notebook['tree'], $path)); 274 | 275 | if(!in_array(true, $errors)) { 276 | if (ENV_CURRENT != ENV_DEMO) { 277 | if($isNote) { 278 | $path .= '.md'; 279 | $item['name'] .= '.md'; 280 | $jotter->setNote($itemPath, $item['name']); 281 | } 282 | elseif($isDir) { 283 | $jotter->setDirectory($itemPath, $item['name']); 284 | } 285 | } 286 | 287 | header('Location: '.URL.'?nb='.$notebookName.'&item='.$path); 288 | exit; 289 | } 290 | } 291 | include( DIR_TPL.'itemForm.tpl.php' ); 292 | 293 | // delete current item 294 | } elseif( !empty($_GET['action']) && $_GET['action'] == 'delete' ) { 295 | //confirmation was sent 296 | if(isset($_POST['delete'])) { 297 | if (ENV_CURRENT != ENV_DEMO) { 298 | if($isNote) { 299 | $jotter->unsetNote($itemPath); 300 | } elseif($isDir) { 301 | $jotter->unsetDirectory($itemPath); 302 | } 303 | } 304 | 305 | header('Location: '.URL.'?nb='.$notebookName.'&item='.(dirname($itemPath)!='.'?dirname($itemPath):'')); 306 | exit; 307 | } 308 | 309 | include( DIR_TPL.'itemDelete.tpl.php' ); 310 | 311 | //show item 312 | } else { 313 | if($isNote) { 314 | //we are dealing with a note: load it 315 | $note = $jotter->loadNote($_GET['item']); 316 | 317 | // show editor toolbar 318 | $isEditMode = true; 319 | $isWysiwyg = !isset($notebook['editor']) || $notebook['editor'] == 'wysiwyg'; 320 | 321 | include( DIR_TPL.'note.tpl.php' ); 322 | } elseif($isDir) { 323 | //for a directory, just show the notebook's "hompage" 324 | include( DIR_TPL.'notebook.tpl.php' ); 325 | } else { 326 | //TODO: show error 327 | } 328 | } 329 | 330 | //default: show notebook root 331 | } else { 332 | include( DIR_TPL.'notebook.tpl.php' ); 333 | } 334 | 335 | //add a notebook 336 | } elseif( !empty($_GET['action']) && $_GET['action'] == 'add' ) { 337 | // user wants to make a new notebook 338 | if(isset($_POST['name'])) { 339 | $notebook = array( 340 | 'name' => urlencode($_POST['name']), 341 | 'user' => $user['login'], 342 | 'editor' => (isset($_POST['editor']) && $_POST['editor'] == 'wysiwyg')?$_POST['editor']:'markdown', 343 | 'safe' => isset($_POST['safe-wysiwyg']) 344 | ); 345 | 346 | $errors['empty'] = empty($notebook['name']); 347 | $errors['alreadyExists'] = isset($notebooks[$user['login']][$notebook['name']]); 348 | if(!in_array(true, $errors)) { 349 | if (ENV_CURRENT != ENV_DEMO) { 350 | $notebooks = $jotter->setNotebook($notebook['name'], $notebook['user'], $notebook['editor'], $notebook['safe']); 351 | } 352 | 353 | header('Location: '.URL.'?nb='.$notebook['name']); 354 | exit; 355 | } 356 | } 357 | 358 | include( DIR_TPL.'notebookForm.tpl.php' ); 359 | 360 | //configuration page 361 | } elseif( !empty($_GET['action']) && $_GET['action'] == 'config' ) { 362 | $isConfigMode = true; 363 | $users = $logger->getUsers(); 364 | $option = isset($_GET['option'])?$_GET['option']:false; 365 | 366 | if($option == 'myPassword') { 367 | if (isset($_POST['password'])) { 368 | $password = htmlspecialchars(trim($_POST['password'])); 369 | $errors['emptyPassword'] = (!isset($_POST['password']) || trim($_POST['password']) == ""); 370 | 371 | if(!in_array(true, $errors)) { 372 | if (ENV_CURRENT != ENV_DEMO) { 373 | //save password 374 | $errors['save'] = !$logger->setUser($user['login'], $password); 375 | } 376 | 377 | header('Location: '.URL.'?action=config&option=myPassword'); 378 | exit; 379 | } 380 | } 381 | 382 | } elseif($option == 'addUser') { 383 | if (isset($_POST['login']) && isset($_POST['password'])) { 384 | $login = htmlspecialchars(trim($_POST['login'])); 385 | $password = htmlspecialchars(trim($_POST['password'])); 386 | 387 | $errors['emptyLogin'] = $login == ''; 388 | $errors['emptyPassword'] = $password == ''; 389 | $errors['notAvailable'] = false; 390 | foreach ($users as $key => $value) { 391 | if($value['login'] == $login) 392 | $errors['notAvailable'] = true; 393 | } 394 | 395 | if(!in_array(true, $errors)) { 396 | if (ENV_CURRENT != ENV_DEMO) { 397 | $logger->createUser($login, $password); 398 | } 399 | 400 | header('Location: '.URL.'?action=config'); 401 | exit; 402 | } 403 | } 404 | 405 | } elseif($option == 'deleteUser') { 406 | $login = htmlspecialchars(trim($_GET['user'])); 407 | 408 | if(isset($_POST['deleteUserSubmit'])) { 409 | //delete user's notebooks 410 | $notebooks = $jotter->loadNotebooks(); 411 | foreach($notebooks[$user['login']] as $key => $value) { 412 | if($value['user'] == $login && ENV_CURRENT != ENV_DEMO) { 413 | $jotter->unsetNotebook($key); 414 | } 415 | } 416 | 417 | //delete user 418 | $logger->deleteUser($login, $password); 419 | 420 | header('Location: '.URL.'?action=config'); 421 | exit; 422 | } 423 | 424 | } else { 425 | 426 | } 427 | 428 | include( DIR_TPL.'config.tpl.php' ); 429 | 430 | //markdown syntax page 431 | } elseif( !empty($_GET['action']) && $_GET['action'] == 'markdown' ) { 432 | include( DIR_TPL.'markdown.tpl.php' ); 433 | 434 | //homepage: notebooks list 435 | } else { 436 | $notebooks = $jotter->loadNotebooks(); 437 | include( DIR_TPL.'notebooks.tpl.php' ); 438 | } 439 | 440 | ?> -------------------------------------------------------------------------------- /lib/.htaccess: -------------------------------------------------------------------------------- 1 | Order deny,allow 2 | Deny from all -------------------------------------------------------------------------------- /lib/ext/HTML_To_Markdown.php: -------------------------------------------------------------------------------- 1 | 9 | * @link https://github.com/nickcernis/html2markdown/ Latest version on GitHub. 10 | * @link http://twitter.com/nickcernis Nick on twitter. 11 | * @license http://www.opensource.org/licenses/mit-license.php MIT 12 | */ 13 | class HTML_To_Markdown 14 | { 15 | /** 16 | * @var DOMDocument The root of the document tree that holds our HTML. 17 | */ 18 | private $document; 19 | 20 | /** 21 | * @var string|boolean The Markdown version of the original HTML, or false if conversion failed 22 | */ 23 | private $output; 24 | 25 | /** 26 | * @var array Class-wide options users can override. 27 | */ 28 | private $options = array( 29 | 'header_style' => 'setext', // Set to "atx" to output H1 and H2 headers as # Header1 and ## Header2 30 | 'suppress_errors' => true, // Set to false to show warnings when loading malformed HTML 31 | 'strip_tags' => false, // Set to true to strip tags that don't have markdown equivalents. N.B. Strips tags, not their content. Useful to clean MS Word HTML output. 32 | 'bold_style' => '**', // Set to '__' if you prefer the underlined style 33 | 'italic_style' => '*', // Set to '_' if you prefer the underlined style 34 | ); 35 | 36 | 37 | /** 38 | * Constructor 39 | * 40 | * Set up a new DOMDocument from the supplied HTML, convert it to Markdown, and store it in $this->$output. 41 | * 42 | * @param string $html The HTML to convert to Markdown. 43 | * @param array $overrides [optional] List of style and error display overrides. 44 | */ 45 | public function __construct($html = null, $overrides = null) 46 | { 47 | if ($overrides) 48 | $this->options = array_merge($this->options, $overrides); 49 | 50 | if ($html) 51 | $this->convert($html); 52 | } 53 | 54 | 55 | /** 56 | * Setter for conversion options 57 | * 58 | * @param $name 59 | * @param $value 60 | */ 61 | public function set_option($name, $value) 62 | { 63 | $this->options[$name] = $value; 64 | } 65 | 66 | 67 | /** 68 | * Convert 69 | * 70 | * Loads HTML and passes to get_markdown() 71 | * 72 | * @param $html 73 | * @return string The Markdown version of the html 74 | */ 75 | public function convert($html) 76 | { 77 | $html = preg_replace('~>\s+<~', '><', $html); // Strip white space between tags to prevent creation of empty #text nodes 78 | 79 | $this->document = new DOMDocument(); 80 | 81 | if ($this->options['suppress_errors']) 82 | libxml_use_internal_errors(true); // Suppress conversion errors (from http://bit.ly/pCCRSX ) 83 | 84 | $this->document->loadHTML('' . $html); // Hack to load utf-8 HTML (from http://bit.ly/pVDyCt ) 85 | $this->document->encoding = 'UTF-8'; 86 | 87 | if ($this->options['suppress_errors']) 88 | libxml_clear_errors(); 89 | 90 | return $this->get_markdown($html); 91 | } 92 | 93 | 94 | /** 95 | * Is Child Of? 96 | * 97 | * Is the node a child of the given parent tag? 98 | * 99 | * @param $parent_name string The name of the parent node to search for (e.g. 'code') 100 | * @param $node 101 | * @return bool 102 | */ 103 | private static function is_child_of($parent_name, $node) 104 | { 105 | for ($p = $node->parentNode; $p != false; $p = $p->parentNode) { 106 | if (is_null($p)) 107 | return false; 108 | 109 | if ($p->nodeName == $parent_name) 110 | return true; 111 | } 112 | return false; 113 | } 114 | 115 | 116 | /** 117 | * Convert Children 118 | * 119 | * Recursive function to drill into the DOM and convert each node into Markdown from the inside out. 120 | * 121 | * Finds children of each node and convert those to #text nodes containing their Markdown equivalent, 122 | * starting with the innermost element and working up to the outermost element. 123 | * 124 | * @param $node 125 | */ 126 | private function convert_children($node) 127 | { 128 | // Don't convert HTML code inside blocks to Markdown - that should stay as HTML 129 | if (self::is_child_of('code', $node)) 130 | return; 131 | 132 | // If the node has children, convert those to Markdown first 133 | if ($node->hasChildNodes()) { 134 | $length = $node->childNodes->length; 135 | 136 | for ($i = 0; $i < $length; $i++) { 137 | $child = $node->childNodes->item($i); 138 | $this->convert_children($child); 139 | } 140 | } 141 | 142 | // Now that child nodes have been converted, convert the original node 143 | $this->convert_to_markdown($node); 144 | } 145 | 146 | 147 | /** 148 | * Get Markdown 149 | * 150 | * Sends the body node to convert_children() to change inner nodes to Markdown #text nodes, then saves and 151 | * returns the resulting converted document as a string in Markdown format. 152 | * 153 | * @return string|boolean The converted HTML as Markdown, or false if conversion failed 154 | */ 155 | private function get_markdown() 156 | { 157 | // Use the body tag as our root element 158 | $body = $this->document->getElementsByTagName("body")->item(0); 159 | 160 | // Try the head tag if there's no body tag (e.g. the user's passed a single 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 25 | 26 | 27 | 28 |
29 |
30 | 53 |
54 |
55 | 134 | 135 | 139 | 176 | 177 | 178 |
179 |
180 |
181 | 322 |
323 | 324 | 325 |
326 | 327 | -------------------------------------------------------------------------------- /tpl/img/ajax-loader.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yosko/jotter/72caab70ddafb198f75db1de38952a45f92fb8c3/tpl/img/ajax-loader.gif -------------------------------------------------------------------------------- /tpl/img/arbo-parent-closed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yosko/jotter/72caab70ddafb198f75db1de38952a45f92fb8c3/tpl/img/arbo-parent-closed.png -------------------------------------------------------------------------------- /tpl/img/arbo-parent-open.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yosko/jotter/72caab70ddafb198f75db1de38952a45f92fb8c3/tpl/img/arbo-parent-open.png -------------------------------------------------------------------------------- /tpl/img/chain--minus.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yosko/jotter/72caab70ddafb198f75db1de38952a45f92fb8c3/tpl/img/chain--minus.png -------------------------------------------------------------------------------- /tpl/img/chain--plus.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yosko/jotter/72caab70ddafb198f75db1de38952a45f92fb8c3/tpl/img/chain--plus.png -------------------------------------------------------------------------------- /tpl/img/disk--exclamation.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yosko/jotter/72caab70ddafb198f75db1de38952a45f92fb8c3/tpl/img/disk--exclamation.png -------------------------------------------------------------------------------- /tpl/img/disk-black.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yosko/jotter/72caab70ddafb198f75db1de38952a45f92fb8c3/tpl/img/disk-black.png -------------------------------------------------------------------------------- /tpl/img/disk.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yosko/jotter/72caab70ddafb198f75db1de38952a45f92fb8c3/tpl/img/disk.png -------------------------------------------------------------------------------- /tpl/img/document--minus.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yosko/jotter/72caab70ddafb198f75db1de38952a45f92fb8c3/tpl/img/document--minus.png -------------------------------------------------------------------------------- /tpl/img/document--pencil.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yosko/jotter/72caab70ddafb198f75db1de38952a45f92fb8c3/tpl/img/document--pencil.png -------------------------------------------------------------------------------- /tpl/img/document--plus.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yosko/jotter/72caab70ddafb198f75db1de38952a45f92fb8c3/tpl/img/document--plus.png -------------------------------------------------------------------------------- /tpl/img/document-export.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yosko/jotter/72caab70ddafb198f75db1de38952a45f92fb8c3/tpl/img/document-export.png -------------------------------------------------------------------------------- /tpl/img/document-import.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yosko/jotter/72caab70ddafb198f75db1de38952a45f92fb8c3/tpl/img/document-import.png -------------------------------------------------------------------------------- /tpl/img/document.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yosko/jotter/72caab70ddafb198f75db1de38952a45f92fb8c3/tpl/img/document.png -------------------------------------------------------------------------------- /tpl/img/door-open-out.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yosko/jotter/72caab70ddafb198f75db1de38952a45f92fb8c3/tpl/img/door-open-out.png -------------------------------------------------------------------------------- /tpl/img/edit-alignment-center.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yosko/jotter/72caab70ddafb198f75db1de38952a45f92fb8c3/tpl/img/edit-alignment-center.png -------------------------------------------------------------------------------- /tpl/img/edit-alignment-justify.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yosko/jotter/72caab70ddafb198f75db1de38952a45f92fb8c3/tpl/img/edit-alignment-justify.png -------------------------------------------------------------------------------- /tpl/img/edit-alignment-right.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yosko/jotter/72caab70ddafb198f75db1de38952a45f92fb8c3/tpl/img/edit-alignment-right.png -------------------------------------------------------------------------------- /tpl/img/edit-alignment.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yosko/jotter/72caab70ddafb198f75db1de38952a45f92fb8c3/tpl/img/edit-alignment.png -------------------------------------------------------------------------------- /tpl/img/edit-bold.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yosko/jotter/72caab70ddafb198f75db1de38952a45f92fb8c3/tpl/img/edit-bold.png -------------------------------------------------------------------------------- /tpl/img/edit-code.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yosko/jotter/72caab70ddafb198f75db1de38952a45f92fb8c3/tpl/img/edit-code.png -------------------------------------------------------------------------------- /tpl/img/edit-heading-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yosko/jotter/72caab70ddafb198f75db1de38952a45f92fb8c3/tpl/img/edit-heading-1.png -------------------------------------------------------------------------------- /tpl/img/edit-heading-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yosko/jotter/72caab70ddafb198f75db1de38952a45f92fb8c3/tpl/img/edit-heading-2.png -------------------------------------------------------------------------------- /tpl/img/edit-heading-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yosko/jotter/72caab70ddafb198f75db1de38952a45f92fb8c3/tpl/img/edit-heading-3.png -------------------------------------------------------------------------------- /tpl/img/edit-heading-4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yosko/jotter/72caab70ddafb198f75db1de38952a45f92fb8c3/tpl/img/edit-heading-4.png -------------------------------------------------------------------------------- /tpl/img/edit-heading-5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yosko/jotter/72caab70ddafb198f75db1de38952a45f92fb8c3/tpl/img/edit-heading-5.png -------------------------------------------------------------------------------- /tpl/img/edit-heading-6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yosko/jotter/72caab70ddafb198f75db1de38952a45f92fb8c3/tpl/img/edit-heading-6.png -------------------------------------------------------------------------------- /tpl/img/edit-heading-minus.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yosko/jotter/72caab70ddafb198f75db1de38952a45f92fb8c3/tpl/img/edit-heading-minus.png -------------------------------------------------------------------------------- /tpl/img/edit-heading.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yosko/jotter/72caab70ddafb198f75db1de38952a45f92fb8c3/tpl/img/edit-heading.png -------------------------------------------------------------------------------- /tpl/img/edit-indent.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yosko/jotter/72caab70ddafb198f75db1de38952a45f92fb8c3/tpl/img/edit-indent.png -------------------------------------------------------------------------------- /tpl/img/edit-italic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yosko/jotter/72caab70ddafb198f75db1de38952a45f92fb8c3/tpl/img/edit-italic.png -------------------------------------------------------------------------------- /tpl/img/edit-list-order.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yosko/jotter/72caab70ddafb198f75db1de38952a45f92fb8c3/tpl/img/edit-list-order.png -------------------------------------------------------------------------------- /tpl/img/edit-list.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yosko/jotter/72caab70ddafb198f75db1de38952a45f92fb8c3/tpl/img/edit-list.png -------------------------------------------------------------------------------- /tpl/img/edit-markdown.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yosko/jotter/72caab70ddafb198f75db1de38952a45f92fb8c3/tpl/img/edit-markdown.png -------------------------------------------------------------------------------- /tpl/img/edit-outdent.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yosko/jotter/72caab70ddafb198f75db1de38952a45f92fb8c3/tpl/img/edit-outdent.png -------------------------------------------------------------------------------- /tpl/img/edit-strike.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yosko/jotter/72caab70ddafb198f75db1de38952a45f92fb8c3/tpl/img/edit-strike.png -------------------------------------------------------------------------------- /tpl/img/edit-underline.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yosko/jotter/72caab70ddafb198f75db1de38952a45f92fb8c3/tpl/img/edit-underline.png -------------------------------------------------------------------------------- /tpl/img/eye.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yosko/jotter/72caab70ddafb198f75db1de38952a45f92fb8c3/tpl/img/eye.png -------------------------------------------------------------------------------- /tpl/img/folder--arrow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yosko/jotter/72caab70ddafb198f75db1de38952a45f92fb8c3/tpl/img/folder--arrow.png -------------------------------------------------------------------------------- /tpl/img/folder--minus.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yosko/jotter/72caab70ddafb198f75db1de38952a45f92fb8c3/tpl/img/folder--minus.png -------------------------------------------------------------------------------- /tpl/img/folder--pencil.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yosko/jotter/72caab70ddafb198f75db1de38952a45f92fb8c3/tpl/img/folder--pencil.png -------------------------------------------------------------------------------- /tpl/img/folder--plus.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yosko/jotter/72caab70ddafb198f75db1de38952a45f92fb8c3/tpl/img/folder--plus.png -------------------------------------------------------------------------------- /tpl/img/folder-export.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yosko/jotter/72caab70ddafb198f75db1de38952a45f92fb8c3/tpl/img/folder-export.png -------------------------------------------------------------------------------- /tpl/img/folder-import.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yosko/jotter/72caab70ddafb198f75db1de38952a45f92fb8c3/tpl/img/folder-import.png -------------------------------------------------------------------------------- /tpl/img/folder-open.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yosko/jotter/72caab70ddafb198f75db1de38952a45f92fb8c3/tpl/img/folder-open.png -------------------------------------------------------------------------------- /tpl/img/folder-tree.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yosko/jotter/72caab70ddafb198f75db1de38952a45f92fb8c3/tpl/img/folder-tree.png -------------------------------------------------------------------------------- /tpl/img/folder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yosko/jotter/72caab70ddafb198f75db1de38952a45f92fb8c3/tpl/img/folder.png -------------------------------------------------------------------------------- /tpl/img/folders-stack-minus.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yosko/jotter/72caab70ddafb198f75db1de38952a45f92fb8c3/tpl/img/folders-stack-minus.png -------------------------------------------------------------------------------- /tpl/img/folders-stack.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yosko/jotter/72caab70ddafb198f75db1de38952a45f92fb8c3/tpl/img/folders-stack.png -------------------------------------------------------------------------------- /tpl/img/image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yosko/jotter/72caab70ddafb198f75db1de38952a45f92fb8c3/tpl/img/image.png -------------------------------------------------------------------------------- /tpl/img/jotter-icon-16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yosko/jotter/72caab70ddafb198f75db1de38952a45f92fb8c3/tpl/img/jotter-icon-16.png -------------------------------------------------------------------------------- /tpl/img/jotter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yosko/jotter/72caab70ddafb198f75db1de38952a45f92fb8c3/tpl/img/jotter.png -------------------------------------------------------------------------------- /tpl/img/wrench-screwdriver.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yosko/jotter/72caab70ddafb198f75db1de38952a45f92fb8c3/tpl/img/wrench-screwdriver.png -------------------------------------------------------------------------------- /tpl/itemDelete.tpl.php: -------------------------------------------------------------------------------- 1 | 2 |

Delete

3 |

4 | You are about to delete . 5 | There is no turning back! 6 |

7 |
8 | 9 |
10 | -------------------------------------------------------------------------------- /tpl/itemForm.tpl.php: -------------------------------------------------------------------------------- 1 | 7 |

8 |
9 | 10 | 11 | 12 |
Please enter a name for your new item.
13 | 14 |
The item already has that name.
15 | 16 |
An item already exists with this name in this directory. Please enter another one.
17 | 18 |
19 | -------------------------------------------------------------------------------- /tpl/js/editor-wysiwyg.js: -------------------------------------------------------------------------------- 1 | 2 | var WysiwygEditor = function () { 3 | 4 | }; 5 | 6 | WysiwygEditor.prototype = new BaseEditor(); 7 | 8 | WysiwygEditor.prototype.customInit = function () { 9 | var editor = $('#editor'); 10 | 11 | document.addEventListener('input', function (e) { 12 | //when user delete everything inside the editor, make sure there is still a

13 | //TODO: handle without jquery? 14 | this.editorNeverEmpty.call(this); 15 | 16 | var html = $('#html'); 17 | if(html !== null && html.length !== 0) { 18 | html.html( this.getEditorHtmlForDisplay.call(this) ); 19 | } 20 | }.bind(this)); 21 | 22 | //init editor 23 | editor.wysiwyg({ 24 | activeToolbarClass: 'selected' 25 | }).focus(); 26 | 27 | //doesn't seem to work in firefox, which still use
28 | document.execCommand('defaultParagraphSeparator', false, 'p'); 29 | 30 | //if note is empty on load, add a

31 | this.editorNeverEmpty.call(this); 32 | 33 | //display html source 34 | $('#source-button').click(function(e){ 35 | if($('#html').length === 0) { 36 | $('#editor').after( '

'+this.getEditorHtmlForDisplay.call(this)+'
' ); 37 | } else { 38 | $('#html').remove(); 39 | } 40 | e.preventDefault(); 41 | }.bind(this)); 42 | 43 | //insert an em dash 44 | $('#mdash-button').click(function(e){ 45 | document.execCommand('insertHTML', false, ' — '); 46 | e.preventDefault(); 47 | }); 48 | 49 | $('#picture-button').click(function(e) { 50 | $('#hidden-picture-button').click(); 51 | e.preventDefault(); 52 | }); 53 | 54 | //add 'http://' to link input 55 | $('#insertLink input').focus(function(e){ 56 | var input = $(this); 57 | if(input.val().trim() === '') { 58 | input.val(input.attr('placeholder')); 59 | } 60 | }); 61 | 62 | //show/hide subtoolbars 63 | $('#linkDropdown').click(function(e){ 64 | $('#insertLink').toggle(); 65 | e.preventDefault(); 66 | }); 67 | $('#insertLink').focusout(function(e){ 68 | $('#insertLink').hide(); 69 | e.preventDefault(); 70 | }); 71 | 72 | $('#headingDropDown').click(function(e){ 73 | toggleHeadingButtons(); 74 | e.preventDefault(); 75 | }); 76 | $('#headingButtons a').click(function(e){ 77 | toggleHeadingButtons(); 78 | e.preventDefault(); 79 | }); 80 | 81 | function toggleHeadingButtons() { 82 | if( $('#headingButtons').is(':hidden') ) { 83 | $('#headingButtons').show(); 84 | $('#toolbar').height(48); 85 | } else { 86 | $('#headingButtons').hide(); 87 | $('#toolbar').height(24); 88 | } 89 | } 90 | }; 91 | 92 | WysiwygEditor.prototype.getEditorHtmlForDisplay = function () { 93 | //get note code from editor 94 | var html = $('#editor').html(); 95 | //remove base64 code for display 96 | html = html.replace(/src="data:image[^"]*"/g, 'src="..."'); 97 | return this.htmlEncode.call(this, html); 98 | }; 99 | 100 | WysiwygEditor.prototype.htmlEncode = function (value) { 101 | if (value) { 102 | return jQuery('
').text(value).html(); 103 | } else { 104 | return ''; 105 | } 106 | }; 107 | 108 | WysiwygEditor.prototype.htmlDecode = function (value) { 109 | if (value) { 110 | return $('
').html(value).text(); 111 | } else { 112 | return ''; 113 | } 114 | }; 115 | 116 | WysiwygEditor.prototype.editorNeverEmpty = function () { 117 | var content = this.editor.innerHTML.trim(); 118 | var previousState = this.unsavedContent; 119 | if(content === '' || content === '
') { 120 | //make sure it is completely empty 121 | this.editor.innerHTML = ''; 122 | 123 | //now make the paragraph on the cursor position 124 | document.execCommand('formatBlock', false, 'p'); 125 | if(previousState === false) { 126 | this.setUnsavedStatus.call(this, false); 127 | } 128 | } 129 | }; -------------------------------------------------------------------------------- /tpl/js/editor.js: -------------------------------------------------------------------------------- 1 | //constructor 2 | var BaseEditor = function() { 3 | this.saveButton = null; 4 | this.saveImage = null; 5 | this.editor = null; 6 | this.unsavedContent = false; 7 | this.currentlySaving = false; 8 | this.cancelKeypress = false; //workaround for Firefox bug 9 | this.isCtrl = false; 10 | }; 11 | 12 | //prototype 13 | BaseEditor.prototype = { 14 | init: function() { 15 | this.saveButton = document.getElementById('save-button'); 16 | this.saveImage = document.getElementById('save-button').querySelector('img'); //TODO fix this bug: point to the inside ID 'save-button' 17 | this.editor = document.getElementById('editor'); 18 | 19 | /** 20 | * EVENTS 21 | */ 22 | 23 | document.addEventListener('input', function (e) { 24 | this.setUnsavedStatus.call(this, true); 25 | this.textareaFitToContent.call(this); 26 | }.bind(this)); 27 | 28 | document.addEventListener('keydown', function (e) { 29 | if(e.ctrlKey && e.keyCode == 'S'.charCodeAt(0)) { 30 | e.preventDefault(); 31 | if(this.unsavedContent) { 32 | this.cancelKeypress = true; 33 | this.saveNote.call(this); 34 | } 35 | } 36 | }.bind(this)); 37 | 38 | /** 39 | * Workaround for Firefox bug: 40 | * e.preventDefault(); and e.stopPropagation(); won't suffice in the keydown 41 | * event, and Firefox will still propagate to keypress in a specific case 42 | * where some non-basic code is executed during the keydown handler.. 43 | */ 44 | document.addEventListener('keypress', function (e){ 45 | if(this.cancelKeypress === true) { 46 | e.preventDefault(); 47 | this.cancelKeypress = false; 48 | } 49 | }.bind(this)); 50 | 51 | //auto save every 30 seconds 52 | setInterval(function(){ 53 | if(this.unsavedContent && !this.currentlySaving) { 54 | this.saveNote.call(this); 55 | } 56 | }.bind(this), 30000); 57 | 58 | //click on save button 59 | this.saveButton.addEventListener('click', function (e){ 60 | if(this.unsavedContent) 61 | this.saveNote.call(this); 62 | e.preventDefault(); 63 | }.bind(this)); 64 | 65 | //avoid leaving page without saving 66 | window.addEventListener('beforeunload', function (e){ 67 | this.checkIsUnsaved.call(this, e); 68 | }.bind(this)); 69 | 70 | this.customInit.call(this); 71 | }, 72 | customInit: function() { 73 | //markdown editor 74 | this.editor.setAttribute('contenteditable', true); 75 | this.textareaFitToContent.call(this); 76 | 77 | document.getElementById('preview-button').addEventListener('click', function (e){ 78 | var preview = null; 79 | var button = document.getElementById('preview-button'); 80 | e.preventDefault(); 81 | button.parentNode.classList.toggle('active'); 82 | if(button.parentNode.classList.contains('active')) { 83 | //prepare preview container 84 | this.editor.style.display = 'none'; 85 | preview = document.createElement('div'); 86 | preview.setAttribute('id','preview'); 87 | this.editor.parentNode.insertBefore(preview, this.editor.nextSibling); 88 | 89 | //show a loading gif 90 | var loadingGif = document.createElement('img'); 91 | loadingGif.setAttribute('src', 'tpl/img/ajax-loader.gif'); 92 | loadingGif.setAttribute('alt', 'Loading...'); 93 | loadingGif.setAttribute('id', 'loadingGif'); 94 | preview.appendChild(loadingGif); 95 | 96 | //send preview request to server 97 | var request = new XMLHttpRequest(); 98 | var notebook = document.getElementById('notebookTitle').getAttribute('data-name'); 99 | var item = document.getElementById('selected').getAttribute('data-path'); 100 | request.open('GET','?action=ajax&option=preview&nb='+notebook+'&item='+item,false); 101 | request.send(); 102 | response = JSON.parse(request.responseText); 103 | 104 | //replace gif with the parsed note 105 | if(response !== false) { 106 | preview.innerHTML = response; 107 | } 108 | } else { 109 | this.editor.style.display = 'block'; 110 | preview = document.getElementById('preview'); 111 | preview.parentNode.removeChild(preview); 112 | } 113 | }.bind(this)); 114 | 115 | }, 116 | textareaFitToContent: function() { 117 | var lineHeight = window.getComputedStyle(this.editor).lineHeight; 118 | lineHeight = parseInt(lineHeight.substr(0, lineHeight.length-2), 10); 119 | if (this.editor.clientHeight == this.editor.scrollHeight) 120 | this.editor.style.height = (lineHeight*4) + 'px'; 121 | 122 | if ( this.editor.scrollHeight > this.editor.clientHeight ) { 123 | this.editor.style.height = (this.editor.scrollHeight + lineHeight) + "px"; 124 | } 125 | }, 126 | saveNote: function() { 127 | this.currentlySaving = true; 128 | this.saveButton.setAttribute('title', 'Saving...'); 129 | this.changeImageFile.call(this, 'ajax-loader.gif'); 130 | 131 | var text = ''; 132 | if(this.editor.nodeName == 'ARTICLE') { 133 | text = this.editor.innerHTML; 134 | } else { 135 | text = this.editor.value; 136 | } 137 | 138 | var notebook = document.getElementById('notebookTitle').getAttribute('data-name'); 139 | var item = document.getElementById('selected').getAttribute('data-path'); 140 | var data = new FormData(); 141 | data.append('text', text); 142 | 143 | //send save request to server 144 | var request = new XMLHttpRequest(); 145 | request.open('POST','?action=ajax&option=save&nb='+notebook+'&item='+item,false); 146 | request.send(data); 147 | response = JSON.parse(request.responseText); 148 | 149 | //the note was saved 150 | if(response === true) { 151 | this.setUnsavedStatus.call(this, false); 152 | 153 | //error, the note wasn't saved 154 | } else { 155 | this.changeImageFile.call(this, 'disk--exclamation.png'); 156 | this.saveButton.setAttribute('title', 'Error: couldn\'t save this note.'); 157 | } 158 | this.currentlySaving = false; 159 | return false; 160 | }, 161 | checkIsUnsaved: function(e) { 162 | if(this.unsavedContent) { 163 | e.preventDefault(); 164 | return "There is unsaved content. Do you still wish to leave this page?"; 165 | } 166 | }, 167 | setUnsavedStatus: function(status) { 168 | this.unsavedContent = status; 169 | 170 | if(this.unsavedContent) { 171 | 172 | this.unsavedContent = true; 173 | 174 | this.saveButton.classList.remove('disabled'); 175 | this.saveButton.setAttribute('title', 'Save changes'); 176 | this.changeImageFile.call(this, 'disk.png'); 177 | } else { 178 | this.changeImageFile.call(this, 'disk-black.png'); 179 | 180 | this.saveButton.classList.add('disabled'); 181 | this.saveButton.setAttribute('title', 'Nothing to save'); 182 | } 183 | }, 184 | changeImageFile: function(newFileName) { 185 | var dirPath = this.saveImage.getAttribute('src').substring(0,this.saveImage.getAttribute('src').lastIndexOf('/') +1 ); 186 | this.saveImage.setAttribute('src', dirPath+newFileName); 187 | } 188 | }; 189 | -------------------------------------------------------------------------------- /tpl/js/ext/bootstrap-wysiwyg.js: -------------------------------------------------------------------------------- 1 | /* http://github.com/mindmup/bootstrap-wysiwyg */ 2 | /*global jQuery, $, FileReader*/ 3 | /*jslint browser:true*/ 4 | (function ($) { 5 | 'use strict'; 6 | var readFileIntoDataUrl = function (fileInfo) { 7 | var loader = $.Deferred(), 8 | fReader = new FileReader(); 9 | fReader.onload = function (e) { 10 | loader.resolve(e.target.result); 11 | }; 12 | fReader.onerror = loader.reject; 13 | fReader.onprogress = loader.notify; 14 | fReader.readAsDataURL(fileInfo); 15 | return loader.promise(); 16 | }; 17 | $.fn.cleanHtml = function () { 18 | var html = $(this).html(); 19 | return html && html.replace(/(
|\s|

<\/div>| )*$/, ''); 20 | }; 21 | $.fn.wysiwyg = function (userOptions) { 22 | var editor = this, 23 | selectedRange, 24 | options, 25 | toolbarBtnSelector, 26 | updateToolbar = function () { 27 | if (options.activeToolbarClass) { 28 | $(options.toolbarSelector).find(toolbarBtnSelector).each(function () { 29 | var command = $(this).data(options.commandRole); 30 | if (document.queryCommandState(command)) { 31 | $(this).addClass(options.activeToolbarClass); 32 | } else { 33 | $(this).removeClass(options.activeToolbarClass); 34 | } 35 | }); 36 | } 37 | }, 38 | execCommand = function (commandWithArgs, valueArg) { 39 | var commandArr = commandWithArgs.split(' '), 40 | command = commandArr.shift(), 41 | args = commandArr.join(' ') + (valueArg || ''); 42 | document.execCommand(command, 0, args); 43 | updateToolbar(); 44 | }, 45 | bindHotkeys = function (hotKeys) { 46 | $.each(hotKeys, function (hotkey, command) { 47 | editor.keydown(hotkey, function (e) { 48 | if (editor.attr('contenteditable') && editor.is(':visible')) { 49 | e.preventDefault(); 50 | e.stopPropagation(); 51 | execCommand(command); 52 | } 53 | }).keyup(hotkey, function (e) { 54 | if (editor.attr('contenteditable') && editor.is(':visible')) { 55 | e.preventDefault(); 56 | e.stopPropagation(); 57 | } 58 | }); 59 | }); 60 | }, 61 | getCurrentRange = function () { 62 | var sel = window.getSelection(); 63 | if (sel.getRangeAt && sel.rangeCount) { 64 | return sel.getRangeAt(0); 65 | } 66 | }, 67 | saveSelection = function () { 68 | selectedRange = getCurrentRange(); 69 | }, 70 | restoreSelection = function () { 71 | var selection = window.getSelection(); 72 | if (selectedRange) { 73 | try { 74 | selection.removeAllRanges(); 75 | } catch (ex) { 76 | document.body.createTextRange().select(); 77 | document.selection.empty(); 78 | } 79 | 80 | selection.addRange(selectedRange); 81 | } 82 | }, 83 | insertFiles = function (files) { 84 | editor.focus(); 85 | $.each(files, function (idx, fileInfo) { 86 | if (/^image\//.test(fileInfo.type)) { 87 | $.when(readFileIntoDataUrl(fileInfo)).done(function (dataUrl) { 88 | execCommand('insertimage', dataUrl); 89 | }).fail(function (e) { 90 | options.fileUploadError("file-reader", e); 91 | }); 92 | } else { 93 | options.fileUploadError("unsupported-file-type", fileInfo.type); 94 | } 95 | }); 96 | }, 97 | markSelection = function (input, color) { 98 | restoreSelection(); 99 | if (document.queryCommandSupported('hiliteColor')) { 100 | document.execCommand('hiliteColor', 0, color || 'transparent'); 101 | } 102 | saveSelection(); 103 | input.data(options.selectionMarker, color); 104 | }, 105 | bindToolbar = function (toolbar, options) { 106 | toolbar.find(toolbarBtnSelector).click(function (e) { 107 | restoreSelection(); 108 | editor.focus(); 109 | execCommand($(this).data(options.commandRole)); 110 | saveSelection(); 111 | e.preventDefault(); //added by Yosko 2013-11-06 112 | }); 113 | toolbar.find('[data-toggle=dropdown]').click(restoreSelection); 114 | 115 | toolbar.find('input[type=text][data-' + options.commandRole + ']').on('webkitspeechchange change', function () { 116 | var newValue = this.value; /* ugly but prevents fake double-calls due to selection restoration */ 117 | this.value = ''; 118 | restoreSelection(); 119 | if (newValue) { 120 | editor.focus(); 121 | execCommand($(this).data(options.commandRole), newValue); 122 | } 123 | saveSelection(); 124 | }).on('focus', function () { 125 | var input = $(this); 126 | if (!input.data(options.selectionMarker)) { 127 | markSelection(input, options.selectionColor); 128 | input.focus(); 129 | } 130 | }).on('blur', function () { 131 | var input = $(this); 132 | if (input.data(options.selectionMarker)) { 133 | markSelection(input, false); 134 | } 135 | }); 136 | toolbar.find('input[type=file][data-' + options.commandRole + ']').change(function () { 137 | restoreSelection(); 138 | if (this.type === 'file' && this.files && this.files.length > 0) { 139 | insertFiles(this.files); 140 | } 141 | saveSelection(); 142 | this.value = ''; 143 | }); 144 | }, 145 | initFileDrops = function () { 146 | editor.on('dragenter dragover', false) 147 | .on('drop', function (e) { 148 | var dataTransfer = e.originalEvent.dataTransfer; 149 | e.stopPropagation(); 150 | e.preventDefault(); 151 | if (dataTransfer && dataTransfer.files && dataTransfer.files.length > 0) { 152 | insertFiles(dataTransfer.files); 153 | } 154 | }); 155 | }; 156 | options = $.extend({}, $.fn.wysiwyg.defaults, userOptions); 157 | toolbarBtnSelector = 'a[data-' + options.commandRole + '],button[data-' + options.commandRole + '],input[type=button][data-' + options.commandRole + ']'; 158 | bindHotkeys(options.hotKeys); 159 | if (options.dragAndDropImages) { 160 | initFileDrops(); 161 | } 162 | bindToolbar($(options.toolbarSelector), options); 163 | editor.attr('contenteditable', true) 164 | .on('mouseup keyup mouseout', function () { 165 | saveSelection(); 166 | updateToolbar(); 167 | }); 168 | $(window).bind('touchend', function (e) { 169 | var isInside = (editor.is(e.target) || editor.has(e.target).length > 0), 170 | currentRange = getCurrentRange(), 171 | clear = currentRange && (currentRange.startContainer === currentRange.endContainer && currentRange.startOffset === currentRange.endOffset); 172 | if (!clear || isInside) { 173 | saveSelection(); 174 | updateToolbar(); 175 | } 176 | }); 177 | return this; 178 | }; 179 | $.fn.wysiwyg.defaults = { 180 | hotKeys: { 181 | 'ctrl+b meta+b': 'bold', 182 | 'ctrl+i meta+i': 'italic', 183 | 'ctrl+u meta+u': 'underline', 184 | 'ctrl+z meta+z': 'undo', 185 | 'ctrl+y meta+y meta+shift+z': 'redo', 186 | 'ctrl+l meta+l': 'justifyleft', 187 | 'ctrl+r meta+r': 'justifyright', 188 | 'ctrl+e meta+e': 'justifycenter', 189 | 'ctrl+j meta+j': 'justifyfull', 190 | 'shift+tab': 'outdent', 191 | 'tab': 'indent' 192 | }, 193 | toolbarSelector: '[data-role=editor-toolbar]', 194 | commandRole: 'edit', 195 | activeToolbarClass: 'btn-info', 196 | selectionMarker: 'edit-focus-marker', 197 | selectionColor: 'darkgrey', 198 | dragAndDropImages: true, 199 | fileUploadError: function (reason, detail) { console.log("File upload error", reason, detail); } 200 | }; 201 | }(window.jQuery)); 202 | -------------------------------------------------------------------------------- /tpl/js/ext/bootstrap.min.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap.js by @fat & @mdo 3 | * Copyright 2013 Twitter, Inc. 4 | * http://www.apache.org/licenses/LICENSE-2.0.txt 5 | */ 6 | !function(e){"use strict";e(function(){e.support.transition=function(){var e=function(){var e=document.createElement("bootstrap"),t={WebkitTransition:"webkitTransitionEnd",MozTransition:"transitionend",OTransition:"oTransitionEnd otransitionend",transition:"transitionend"},n;for(n in t)if(e.style[n]!==undefined)return t[n]}();return e&&{end:e}}()})}(window.jQuery),!function(e){"use strict";var t='[data-dismiss="alert"]',n=function(n){e(n).on("click",t,this.close)};n.prototype.close=function(t){function s(){i.trigger("closed").remove()}var n=e(this),r=n.attr("data-target"),i;r||(r=n.attr("href"),r=r&&r.replace(/.*(?=#[^\s]*$)/,"")),i=e(r),t&&t.preventDefault(),i.length||(i=n.hasClass("alert")?n:n.parent()),i.trigger(t=e.Event("close"));if(t.isDefaultPrevented())return;i.removeClass("in"),e.support.transition&&i.hasClass("fade")?i.on(e.support.transition.end,s):s()};var r=e.fn.alert;e.fn.alert=function(t){return this.each(function(){var r=e(this),i=r.data("alert");i||r.data("alert",i=new n(this)),typeof t=="string"&&i[t].call(r)})},e.fn.alert.Constructor=n,e.fn.alert.noConflict=function(){return e.fn.alert=r,this},e(document).on("click.alert.data-api",t,n.prototype.close)}(window.jQuery),!function(e){"use strict";var t=function(t,n){this.$element=e(t),this.options=e.extend({},e.fn.button.defaults,n)};t.prototype.setState=function(e){var t="disabled",n=this.$element,r=n.data(),i=n.is("input")?"val":"html";e+="Text",r.resetText||n.data("resetText",n[i]()),n[i](r[e]||this.options[e]),setTimeout(function(){e=="loadingText"?n.addClass(t).attr(t,t):n.removeClass(t).removeAttr(t)},0)},t.prototype.toggle=function(){var e=this.$element.closest('[data-toggle="buttons-radio"]');e&&e.find(".active").removeClass("active"),this.$element.toggleClass("active")};var n=e.fn.button;e.fn.button=function(n){return this.each(function(){var r=e(this),i=r.data("button"),s=typeof n=="object"&&n;i||r.data("button",i=new t(this,s)),n=="toggle"?i.toggle():n&&i.setState(n)})},e.fn.button.defaults={loadingText:"loading..."},e.fn.button.Constructor=t,e.fn.button.noConflict=function(){return e.fn.button=n,this},e(document).on("click.button.data-api","[data-toggle^=button]",function(t){var n=e(t.target);n.hasClass("btn")||(n=n.closest(".btn")),n.button("toggle")})}(window.jQuery),!function(e){"use strict";var t=function(t,n){this.$element=e(t),this.$indicators=this.$element.find(".carousel-indicators"),this.options=n,this.options.pause=="hover"&&this.$element.on("mouseenter",e.proxy(this.pause,this)).on("mouseleave",e.proxy(this.cycle,this))};t.prototype={cycle:function(t){return t||(this.paused=!1),this.interval&&clearInterval(this.interval),this.options.interval&&!this.paused&&(this.interval=setInterval(e.proxy(this.next,this),this.options.interval)),this},getActiveIndex:function(){return this.$active=this.$element.find(".item.active"),this.$items=this.$active.parent().children(),this.$items.index(this.$active)},to:function(t){var n=this.getActiveIndex(),r=this;if(t>this.$items.length-1||t<0)return;return this.sliding?this.$element.one("slid",function(){r.to(t)}):n==t?this.pause().cycle():this.slide(t>n?"next":"prev",e(this.$items[t]))},pause:function(t){return t||(this.paused=!0),this.$element.find(".next, .prev").length&&e.support.transition.end&&(this.$element.trigger(e.support.transition.end),this.cycle(!0)),clearInterval(this.interval),this.interval=null,this},next:function(){if(this.sliding)return;return this.slide("next")},prev:function(){if(this.sliding)return;return this.slide("prev")},slide:function(t,n){var r=this.$element.find(".item.active"),i=n||r[t](),s=this.interval,o=t=="next"?"left":"right",u=t=="next"?"first":"last",a=this,f;this.sliding=!0,s&&this.pause(),i=i.length?i:this.$element.find(".item")[u](),f=e.Event("slide",{relatedTarget:i[0],direction:o});if(i.hasClass("active"))return;this.$indicators.length&&(this.$indicators.find(".active").removeClass("active"),this.$element.one("slid",function(){var t=e(a.$indicators.children()[a.getActiveIndex()]);t&&t.addClass("active")}));if(e.support.transition&&this.$element.hasClass("slide")){this.$element.trigger(f);if(f.isDefaultPrevented())return;i.addClass(t),i[0].offsetWidth,r.addClass(o),i.addClass(o),this.$element.one(e.support.transition.end,function(){i.removeClass([t,o].join(" ")).addClass("active"),r.removeClass(["active",o].join(" ")),a.sliding=!1,setTimeout(function(){a.$element.trigger("slid")},0)})}else{this.$element.trigger(f);if(f.isDefaultPrevented())return;r.removeClass("active"),i.addClass("active"),this.sliding=!1,this.$element.trigger("slid")}return s&&this.cycle(),this}};var n=e.fn.carousel;e.fn.carousel=function(n){return this.each(function(){var r=e(this),i=r.data("carousel"),s=e.extend({},e.fn.carousel.defaults,typeof n=="object"&&n),o=typeof n=="string"?n:s.slide;i||r.data("carousel",i=new t(this,s)),typeof n=="number"?i.to(n):o?i[o]():s.interval&&i.pause().cycle()})},e.fn.carousel.defaults={interval:5e3,pause:"hover"},e.fn.carousel.Constructor=t,e.fn.carousel.noConflict=function(){return e.fn.carousel=n,this},e(document).on("click.carousel.data-api","[data-slide], [data-slide-to]",function(t){var n=e(this),r,i=e(n.attr("data-target")||(r=n.attr("href"))&&r.replace(/.*(?=#[^\s]+$)/,"")),s=e.extend({},i.data(),n.data()),o;i.carousel(s),(o=n.attr("data-slide-to"))&&i.data("carousel").pause().to(o).cycle(),t.preventDefault()})}(window.jQuery),!function(e){"use strict";var t=function(t,n){this.$element=e(t),this.options=e.extend({},e.fn.collapse.defaults,n),this.options.parent&&(this.$parent=e(this.options.parent)),this.options.toggle&&this.toggle()};t.prototype={constructor:t,dimension:function(){var e=this.$element.hasClass("width");return e?"width":"height"},show:function(){var t,n,r,i;if(this.transitioning||this.$element.hasClass("in"))return;t=this.dimension(),n=e.camelCase(["scroll",t].join("-")),r=this.$parent&&this.$parent.find("> .accordion-group > .in");if(r&&r.length){i=r.data("collapse");if(i&&i.transitioning)return;r.collapse("hide"),i||r.data("collapse",null)}this.$element[t](0),this.transition("addClass",e.Event("show"),"shown"),e.support.transition&&this.$element[t](this.$element[0][n])},hide:function(){var t;if(this.transitioning||!this.$element.hasClass("in"))return;t=this.dimension(),this.reset(this.$element[t]()),this.transition("removeClass",e.Event("hide"),"hidden"),this.$element[t](0)},reset:function(e){var t=this.dimension();return this.$element.removeClass("collapse")[t](e||"auto")[0].offsetWidth,this.$element[e!==null?"addClass":"removeClass"]("collapse"),this},transition:function(t,n,r){var i=this,s=function(){n.type=="show"&&i.reset(),i.transitioning=0,i.$element.trigger(r)};this.$element.trigger(n);if(n.isDefaultPrevented())return;this.transitioning=1,this.$element[t]("in"),e.support.transition&&this.$element.hasClass("collapse")?this.$element.one(e.support.transition.end,s):s()},toggle:function(){this[this.$element.hasClass("in")?"hide":"show"]()}};var n=e.fn.collapse;e.fn.collapse=function(n){return this.each(function(){var r=e(this),i=r.data("collapse"),s=e.extend({},e.fn.collapse.defaults,r.data(),typeof n=="object"&&n);i||r.data("collapse",i=new t(this,s)),typeof n=="string"&&i[n]()})},e.fn.collapse.defaults={toggle:!0},e.fn.collapse.Constructor=t,e.fn.collapse.noConflict=function(){return e.fn.collapse=n,this},e(document).on("click.collapse.data-api","[data-toggle=collapse]",function(t){var n=e(this),r,i=n.attr("data-target")||t.preventDefault()||(r=n.attr("href"))&&r.replace(/.*(?=#[^\s]+$)/,""),s=e(i).data("collapse")?"toggle":n.data();n[e(i).hasClass("in")?"addClass":"removeClass"]("collapsed"),e(i).collapse(s)})}(window.jQuery),!function(e){"use strict";function r(){e(".dropdown-backdrop").remove(),e(t).each(function(){i(e(this)).removeClass("open")})}function i(t){var n=t.attr("data-target"),r;n||(n=t.attr("href"),n=n&&/#/.test(n)&&n.replace(/.*(?=#[^\s]*$)/,"")),r=n&&e(n);if(!r||!r.length)r=t.parent();return r}var t="[data-toggle=dropdown]",n=function(t){var n=e(t).on("click.dropdown.data-api",this.toggle);e("html").on("click.dropdown.data-api",function(){n.parent().removeClass("open")})};n.prototype={constructor:n,toggle:function(t){var n=e(this),s,o;if(n.is(".disabled, :disabled"))return;return s=i(n),o=s.hasClass("open"),r(),o||("ontouchstart"in document.documentElement&&e('