A clean, Markdown-based publishing platform made for writers. Write together, and build a community. https://writefreely.org
25개 이상의 토픽을 선택하실 수 없습니다. Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

468 lines
18 KiB

  1. {{define "pad"}}<!DOCTYPE HTML>
  2. <html>
  3. <head>
  4. <title>{{if .Editing}}Editing {{if .Post.Title}}{{.Post.Title}}{{else}}{{.Post.Id}}{{end}}{{else}}New Post{{end}} &mdash; {{.SiteName}}</title>
  5. <link rel="stylesheet" type="text/css" href="/css/write.css" />
  6. <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  7. <meta name="google" value="notranslate">
  8. </head>
  9. <body id="pad" class="light">
  10. <div id="overlay"></div>
  11. <textarea id="writer" placeholder="Write..." class="{{.Post.Font}}" autofocus>{{if .Post.Title}}# {{.Post.Title}}
  12. {{end}}{{.Post.Content}}</textarea>
  13. <header id="tools">
  14. <div id="clip">
  15. {{if not .SingleUser}}<h1><a href="/me/c/" title="View blogs"><img class="ic-24dp" src="/img/ic_blogs_dark@2x.png" /></a></h1>{{end}}
  16. <nav id="target" {{if .SingleUser}}style="margin-left:0"{{end}}><ul>
  17. {{if .Editing}}<li>{{if .EditCollection}}<a href="{{.EditCollection.CanonicalURL}}">{{.EditCollection.Title}}</a>{{else}}<a>Draft</a>{{end}}</li>
  18. {{else}}<li><a id="publish-to"><span id="target-name">Draft</span> <img class="ic-18dp" src="/img/ic_down_arrow_dark@2x.png" /></a>
  19. <ul>
  20. <li class="menu-heading">Publish to...</li>
  21. <li class="target selected" id="anonymous"><a href="#anonymous"><i class="material-icons md-18">description</i> <em>Draft</em></a></li>
  22. {{if .Blogs}}{{range .Blogs}}
  23. <li class="target" id="blog-{{.Alias}}"><a href="#{{.Alias}}"><i class="material-icons md-18">public</i> {{if .Title}}{{.Title}}{{else}}{{.Alias}}{{end}}</a></li>
  24. {{end}}{{end}}
  25. <li id="user-separator" class="separator"><hr /></li>
  26. {{ if .SingleUser }}
  27. <li><a href="/"><i class="material-icons md-18">launch</i> View Blog</a></li>
  28. <li><a href="/me/c/{{.Username}}"><i class="material-icons md-18">palette</i> Customize</a></li>
  29. <li><a href="/me/c/{{.Username}}/stats"><i class="material-icons md-18">trending_up</i> Stats</a></li>
  30. {{ else }}
  31. <li><a href="/me/c/"><i class="material-icons md-18">library_books</i> View Blogs</a></li>
  32. {{ end }}
  33. <li><a href="/me/posts/"><i class="material-icons md-18">view_list</i> View Drafts</a></li>
  34. <li><a href="/me/logout"><i class="material-icons md-18">power_settings_new</i> Log out</a></li>
  35. </ul>
  36. </li>{{end}}
  37. </ul></nav>
  38. <nav id="font-picker" class="if-room room-3 hidden" style="margin-left:-1em"><ul>
  39. <li><a href="#" id="" onclick="return false"><img class="ic-24dp" src="/img/ic_font_dark@2x.png" /> <img class="ic-18dp" src="/img/ic_down_arrow_dark@2x.png" /></a>
  40. <ul style="text-align: center">
  41. <li class="menu-heading">Font</li>
  42. <li class="selected"><a class="font norm" href="#norm">Serif</a></li>
  43. <li><a class="font sans" href="#sans">Sans-serif</a></li>
  44. <li><a class="font wrap" href="#wrap">Monospace</a></li>
  45. </ul>
  46. </li>
  47. </ul></nav>
  48. <span id="wc" class="hidden if-room room-4">0 words</span>
  49. </div>
  50. <noscript style="margin-left: 2em;"><strong>NOTE</strong>: for now, you'll need Javascript enabled to post.</noscript>
  51. <div id="belt">
  52. {{if .Editing}}<div class="tool hidden if-room"><a href="{{if .EditCollection}}{{.EditCollection.CanonicalURL}}{{.Post.Slug}}/edit/meta{{else}}/{{if .SingleUser}}d/{{end}}{{.Post.Id}}/meta{{end}}" title="Edit post metadata" id="edit-meta"><img class="ic-24dp" src="/img/ic_info_dark@2x.png" /></a></div>{{end}}
  53. <div class="tool hidden if-room room-2"><a href="#theme" title="Toggle theme" id="toggle-theme"><img class="ic-24dp" src="/img/ic_brightness_dark@2x.png" /></a></div>
  54. <div class="tool if-room room-1"><a href="{{if not .User}}/pad/posts{{else}}/me/posts/{{end}}" title="View posts" id="view-posts"><img class="ic-24dp" src="/img/ic_list_dark@2x.png" /></a></div>
  55. <div class="tool"><a href="#publish" title="Publish" id="publish"><img class="ic-24dp" src="/img/ic_send_dark@2x.png" /></a></div>
  56. </div>
  57. </header>
  58. <script src="/js/h.js"></script>
  59. <script>
  60. function toggleTheme() {
  61. var btns = Array.prototype.slice.call(document.getElementById('tools').querySelectorAll('a img'));
  62. var newTheme = '';
  63. if (document.body.classList.contains('light')) {
  64. newTheme = 'dark';
  65. document.body.className = document.body.className.replace(/(?:^|\s)light(?!\S)/g, newTheme);
  66. for (var i=0; i<btns.length; i++) {
  67. btns[i].src = btns[i].src.replace('_dark@2x.png', '@2x.png');
  68. }
  69. } else {
  70. newTheme = 'light';
  71. document.body.className = document.body.className.replace(/(?:^|\s)dark(?!\S)/g, newTheme);
  72. for (var i=0; i<btns.length; i++) {
  73. btns[i].src = btns[i].src.replace('@2x.png', '_dark@2x.png');
  74. }
  75. }
  76. H.set('padTheme', newTheme);
  77. }
  78. if (H.get('padTheme', 'light') != 'light') {
  79. toggleTheme();
  80. }
  81. var $writer = H.getEl('writer');
  82. var $btnPublish = H.getEl('publish');
  83. var $wc = H.getEl("wc");
  84. var updateWordCount = function() {
  85. var words = 0;
  86. var val = $writer.el.value.trim();
  87. if (val != '') {
  88. words = $writer.el.value.trim().replace(/\s+/gi, ' ').split(' ').length;
  89. }
  90. $wc.el.innerText = words + " word" + (words != 1 ? "s" : "");
  91. };
  92. var setButtonStates = function() {
  93. if (!canPublish) {
  94. $btnPublish.el.className = 'disabled';
  95. return;
  96. }
  97. if ($writer.el.value.length === 0 || (draftDoc != 'lastDoc' && $writer.el.value == origDoc)) {
  98. $btnPublish.el.className = 'disabled';
  99. } else {
  100. $btnPublish.el.className = '';
  101. }
  102. };
  103. {{if .Post.Id}}var draftDoc = 'draft{{.Post.Id}}';
  104. var origDoc = '{{.Post.Content}}';{{else}}var draftDoc = 'lastDoc';{{end}}
  105. H.load($writer, draftDoc, true);
  106. updateWordCount();
  107. var typingTimer;
  108. var doneTypingInterval = 200;
  109. var posts;
  110. {{if and .Post.Id (not .Post.Slug)}}
  111. var token = null;
  112. var curPostIdx;
  113. posts = JSON.parse(H.get('posts', '[]'));
  114. for (var i=0; i<posts.length; i++) {
  115. if (posts[i].id == "{{.Post.Id}}") {
  116. token = posts[i].token;
  117. break;
  118. }
  119. }
  120. var canPublish = token != null;
  121. {{else}}var canPublish = true;{{end}}
  122. var publishing = false;
  123. var justPublished = false;
  124. var publish = function(content, font) {
  125. {{if and (and .Post.Id (not .Post.Slug)) (not .User)}}
  126. if (!token) {
  127. alert("You don't have permission to update this post.");
  128. return;
  129. }
  130. if ($btnPublish.el.className == 'disabled') {
  131. return;
  132. }
  133. {{end}}
  134. $btnPublish.el.children[0].textContent = 'more_horiz';
  135. publishing = true;
  136. var xpostTarg = H.get('crosspostTarget', '[]');
  137. var http = new XMLHttpRequest();
  138. var lang = navigator.languages ? navigator.languages[0] : (navigator.language || navigator.userLanguage);
  139. lang = lang.substring(0, 2);
  140. var post = H.getTitleStrict(content);
  141. var params = {
  142. body: post.content,
  143. title: post.title,
  144. font: font,
  145. lang: lang
  146. };
  147. {{ if .Post.Slug }}
  148. var url = "/api/collections/{{.EditCollection.Alias}}/posts/{{.Post.Id}}";
  149. {{ else if .Post.Id }}
  150. var url = "/api/posts/{{.Post.Id}}";
  151. if (typeof token === 'undefined' || !token) {
  152. token = "";
  153. }
  154. params.token = token;
  155. {{ else }}
  156. var url = "/api/posts";
  157. var postTarget = H.get('postTarget', 'anonymous');
  158. if (postTarget != 'anonymous') {
  159. url = "/api/collections/" + postTarget + "/posts";
  160. }
  161. params.crosspost = JSON.parse(xpostTarg);
  162. {{ end }}
  163. http.open("POST", url, true);
  164. // Send the proper header information along with the request
  165. http.setRequestHeader("Content-type", "application/json");
  166. http.onreadystatechange = function() {
  167. if (http.readyState == 4) {
  168. publishing = false;
  169. if (http.status == 200 || http.status == 201) {
  170. data = JSON.parse(http.responseText);
  171. id = data.data.id;
  172. nextURL = '{{if .SingleUser}}/d{{end}}/'+id;
  173. {{ if not .Post.Id }}
  174. // Post created
  175. if (postTarget != 'anonymous') {
  176. nextURL = {{if not .SingleUser}}'/'+postTarget+{{end}}'/'+data.data.slug;
  177. }
  178. editToken = data.data.token;
  179. {{ if not .User }}if (postTarget == 'anonymous') {
  180. // Save the data
  181. var posts = JSON.parse(H.get('posts', '[]'));
  182. {{if .Post.Id}}var newPost = H.createPost("{{.Post.Id}}", token, content);
  183. for (var i=0; i<posts.length; i++) {
  184. if (posts[i].id == "{{.Post.Id}}") {
  185. posts[i].title = newPost.title;
  186. posts[i].summary = newPost.summary;
  187. break;
  188. }
  189. }
  190. nextURL = "/pad/posts";{{else}}posts.push(H.createPost(id, editToken, content));{{end}}
  191. H.set('posts', JSON.stringify(posts));
  192. }
  193. {{ end }}
  194. {{ end }}
  195. justPublished = true;
  196. if (draftDoc != 'lastDoc') {
  197. H.remove(draftDoc);
  198. {{if .Editing}}H.remove('draft{{.Post.Id}}font');{{end}}
  199. } else {
  200. H.set(draftDoc, '');
  201. }
  202. {{if .EditCollection}}
  203. window.location = '{{.EditCollection.CanonicalURL}}{{.Post.Slug}}';
  204. {{else}}
  205. window.location = nextURL;
  206. {{end}}
  207. } else {
  208. $btnPublish.el.children[0].textContent = 'send';
  209. alert("Failed to post. Please try again.");
  210. }
  211. }
  212. }
  213. http.send(JSON.stringify(params));
  214. };
  215. setButtonStates();
  216. $writer.on('keyup input', function() {
  217. setButtonStates();
  218. clearTimeout(typingTimer);
  219. typingTimer = setTimeout(doneTyping, doneTypingInterval);
  220. }, false);
  221. $writer.on('keydown', function(e) {
  222. clearTimeout(typingTimer);
  223. if (e.keyCode == 13 && (e.metaKey || e.ctrlKey)) {
  224. $btnPublish.el.click();
  225. }
  226. });
  227. $btnPublish.on('click', function(e) {
  228. e.preventDefault();
  229. if (!publishing && $writer.el.value) {
  230. var content = $writer.el.value;
  231. publish(content, selectedFont);
  232. }
  233. });
  234. H.getEl('toggle-theme').on('click', function(e) {
  235. e.preventDefault();
  236. var newTheme = 'light';
  237. if (document.body.className == 'light') {
  238. newTheme = 'dark';
  239. }
  240. toggleTheme();
  241. });
  242. var targets = document.querySelectorAll('#target li.target a');
  243. for (var i=0; i<targets.length; i++) {
  244. targets[i].addEventListener('click', function(e) {
  245. e.preventDefault();
  246. var targetName = this.href.substring(this.href.indexOf('#')+1);
  247. H.set('postTarget', targetName);
  248. document.querySelector('#target li.target.selected').classList.remove('selected');
  249. this.parentElement.classList.add('selected');
  250. var newText = this.innerText.split(' ');
  251. newText.shift();
  252. document.getElementById('target-name').innerText = newText.join(' ');
  253. });
  254. }
  255. var postTarget = H.get('postTarget', 'anonymous');
  256. if (location.hash != '') {
  257. postTarget = location.hash.substring(1);
  258. // TODO: pushState to /pad (or whatever the URL is) so we live on a clean URL
  259. location.hash = '';
  260. }
  261. var pte = document.querySelector('#target li.target#blog-'+postTarget+' a');
  262. if (pte != null) {
  263. pte.click();
  264. } else {
  265. postTarget = 'anonymous';
  266. H.set('postTarget', postTarget);
  267. }
  268. var sansLoaded = false;
  269. WebFontConfig = {
  270. custom: { families: [ 'Lora:400,700:latin' ], urls: [ '/css/fonts.css' ] }
  271. };
  272. var loadSans = function() {
  273. if (sansLoaded) return;
  274. sansLoaded = true;
  275. WebFontConfig.custom.families.push('Open+Sans:400,700:latin');
  276. try {
  277. (function() {
  278. var wf=document.createElement('script');
  279. wf.src = '/js/webfont.js';
  280. wf.type='text/javascript';
  281. wf.async='true';
  282. var s=document.getElementsByTagName('script')[0];
  283. s.parentNode.insertBefore(wf, s);
  284. })();
  285. } catch (e) {}
  286. };
  287. var fonts = document.querySelectorAll('nav#font-picker a.font');
  288. for (var i=0; i<fonts.length; i++) {
  289. fonts[i].addEventListener('click', function(e) {
  290. e.preventDefault();
  291. selectedFont = this.href.substring(this.href.indexOf('#')+1);
  292. $writer.el.className = selectedFont;
  293. document.querySelector('nav#font-picker li.selected').classList.remove('selected');
  294. this.parentElement.classList.add('selected');
  295. H.set('{{if .Editing}}draft{{.Post.Id}}font{{else}}padFont{{end}}', selectedFont);
  296. if (selectedFont == 'sans') {
  297. loadSans();
  298. }
  299. });
  300. }
  301. var selectedFont = H.get('{{if .Editing}}draft{{.Post.Id}}font{{else}}padFont{{end}}', '{{.Post.Font}}');
  302. var sfe = document.querySelector('nav#font-picker a.font.'+selectedFont);
  303. if (sfe != null) {
  304. sfe.click();
  305. }
  306. var doneTyping = function() {
  307. if (draftDoc == 'lastDoc' || $writer.el.value != origDoc) {
  308. H.save($writer, draftDoc);
  309. updateWordCount();
  310. }
  311. };
  312. window.addEventListener('beforeunload', function(e) {
  313. if (draftDoc != 'lastDoc' && $writer.el.value == origDoc) {
  314. H.remove(draftDoc);
  315. } else if (!justPublished) {
  316. doneTyping();
  317. }
  318. });
  319. try {
  320. (function() {
  321. var wf=document.createElement('script');
  322. wf.src = '/js/webfont.js';
  323. wf.type='text/javascript';
  324. wf.async='true';
  325. var s=document.getElementsByTagName('script')[0];
  326. s.parentNode.insertBefore(wf, s);
  327. })();
  328. } catch (e) {
  329. // whatevs
  330. }
  331. var allowedFileTypes = ["image/png", "image/jpeg"];
  332. // using getElementsByTagName instead of querySelector for
  333. // more backward compatibility (and probably performance)
  334. var dropbox = document.getElementsByTagName("body")[0];
  335. dropbox.addEventListener("drop", drop, true);
  336. dropbox.addEventListener("dragover", function(e){e.preventDefault()}, true)
  337. // firefox needs this too in order to not show the floating cursor on linux
  338. // see [blogpost] for why we don't want this
  339. dropbox.addEventListener("dragenter", function(e){e.preventDefault()}, true)
  340. var textarea = document.getElementsByTagName("textarea")[0];
  341. function drop(e) {
  342. e.preventDefault();
  343. var files = e.dataTransfer.files;
  344. for (var i = 0; i < files.length; i++) {
  345. var file = files[i];
  346. // from https://stackoverflow.com/a/1818400/400257
  347. description = file.name.substr(0, file.name.lastIndexOf('.')) || file.name;
  348. file.identifier = description+"_"+Math.random().toString(36).substring(2, 15);
  349. file.progress = 0;
  350. markdown = " [" + file.identifier + "](00%) ";
  351. // if we drop on a textarea, on firefox, we can get the position of the drop
  352. // by searching for the 'file://' uri that it appends to the textarea by default
  353. insertTextAtCursor(markdown, textarea);
  354. handleFile(file);
  355. }
  356. }
  357. function insertTextAtCursor(text, textarea){
  358. if (textarea.nodeName == "TEXTAREA") {
  359. // save cursor position so that we can set it back after the operation on value
  360. var startingPosition = textarea.selectionStart;
  361. var textBeforeCursor = textarea.value.substring(0, textarea.selectionStart);
  362. var textAfterCursor = textarea.value.substring(textarea.selectionEnd, textarea.value.length);
  363. textarea.value = textBeforeCursor + text + textAfterCursor;
  364. // set the cursor to its original position
  365. textarea.selectionStart = textarea.selectionEnd = startingPosition + text.length;
  366. } else throw "Element is not a textarea"
  367. }
  368. function handleFile(file){
  369. var ok = false;
  370. for (var i in allowedFileTypes){
  371. if (file.type.match(allowedFileTypes[i])) {ok=true; break;}
  372. }
  373. if (!ok) throw "disallowed file type";
  374. // var reader = new FileReader();
  375. // reader.readAsDataURL(file);
  376. var xhr = new XMLHttpRequest();
  377. // progressMgmt(xhr,file,img);
  378. xhr.open("POST", "http://localhost/minimalist-ajax-upload/upload.php");
  379. xhr.setRequestHeader("Cache-Control", "no-cache");
  380. xhr.setRequestHeader("X-Requested-With", "XMLHttpRequest");
  381. xhr.setRequestHeader("X-File-Name", file.name);
  382. xhr.setRequestHeader("X-File-Size", file.size);
  383. xhr.upload.onprogress = function(e) {
  384. if (e.lengthComputable) {
  385. var startingPosition = textarea.selectionStart;
  386. var loaded = Math.ceil((e.loaded / e.total)*100);
  387. if (file.progress + 5 < loaded){
  388. file.progress = loaded;
  389. textarea.value = textarea.value.replace(getMarkdownRegex(file.identifier), "["+ file.identifier +"]("+padLeft(loaded,2)+"%)");
  390. textarea.selectionStart = textarea.selectionEnd = startingPosition;
  391. }
  392. }
  393. }
  394. var formData = new FormData();
  395. formData.append('file', file);
  396. xhr.send(formData);
  397. xhr.onreadystatechange = function(){
  398. if(xhr.readyState == 4){
  399. if (xhr.status == 200){
  400. processResponse(xhr.responseText, file.identifier);
  401. } else {
  402. processFailure(file.identifier);
  403. }
  404. }
  405. }
  406. }
  407. function processResponse(responseText, identifier){
  408. var response = JSON.parse(responseText);
  409. description = response.name.substr(0, response.name.lastIndexOf('.')) || response.name;
  410. textarea.value = textarea.value.replace(getMarkdownRegex(identifier), "["+ description +"]("+response.url+")");
  411. }
  412. function processFailure(identifier){
  413. textarea.value = textarea.value.replace(getMarkdownRegex(identifier), "");
  414. }
  415. function getMarkdownRegex(identifier){
  416. return new RegExp('\\[' + identifier + '\\]\\(.*?\\)');
  417. }
  418. function padLeft(number, padTo, str){
  419. return Array(padTo-String(number).length+1).join(str||'0')+number;
  420. }
  421. </script>
  422. <link href="/css/icons.css" rel="stylesheet">
  423. </body>
  424. </html>{{end}}