A clean, Markdown-based publishing platform made for writers. Write together, and build a community. https://writefreely.org
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

422 lines
15 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. {{if .CustomCSS}}<link rel="stylesheet" type="text/css" href="/local/custom.css" />{{end}}
  7. <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  8. <meta name="google" value="notranslate">
  9. </head>
  10. <body id="pad" class="light">
  11. <div id="overlay"></div>
  12. <textarea dir="auto" id="writer" placeholder="Write..." class="{{.Post.Font}}" autofocus>{{if .Post.Title}}# {{.Post.Title}}
  13. {{end}}{{.Post.Content}}</textarea>
  14. <div class="alert success hidden" id="edited-elsewhere">This post has been updated elsewhere since you last published! <a href="#" id="erase-edit">Delete draft and reload</a>.</div>
  15. <header id="tools">
  16. <div id="clip">
  17. {{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}}
  18. <nav id="target" {{if .SingleUser}}style="margin-left:0"{{end}}><ul>
  19. {{if .Editing}}<li>{{if .EditCollection}}<a href="{{.EditCollection.CanonicalURL}}">{{.EditCollection.Title}}</a>{{else}}<a>Draft</a>{{end}}</li>
  20. {{else}}<li class="has-submenu"><a href="#" id="publish-to" onclick="return false"><span id="target-name">Draft</span> <img class="ic-18dp" src="/img/ic_down_arrow_dark@2x.png" /></a>
  21. <ul>
  22. <li class="menu-heading">Publish to...</li>
  23. {{if .Blogs}}{{range $idx, $el := .Blogs}}
  24. <li class="target{{if eq $idx 0}} selected{{end}}" id="blog-{{$el.Alias}}"><a href="#{{$el.Alias}}"><i class="material-icons md-18">public</i> {{if $el.Title}}{{$el.Title}}{{else}}{{$el.Alias}}{{end}}</a></li>
  25. {{end}}{{end}}
  26. <li class="target" id="blog-anonymous"><a href="#anonymous"><i class="material-icons md-18">description</i> <em>Draft</em></a></li>
  27. <li id="user-separator" class="separator"><hr /></li>
  28. {{ if .SingleUser }}
  29. <li><a href="/"><i class="material-icons md-18">launch</i> View Blog</a></li>
  30. <li><a href="/me/c/{{.Username}}"><i class="material-icons md-18">palette</i> Customize</a></li>
  31. <li><a href="/me/c/{{.Username}}/stats"><i class="material-icons md-18">trending_up</i> Stats</a></li>
  32. {{ else }}
  33. <li><a href="/me/c/"><i class="material-icons md-18">library_books</i> View Blogs</a></li>
  34. {{ end }}
  35. <li><a href="/me/posts/"><i class="material-icons md-18">view_list</i> View Drafts</a></li>
  36. <li><a href="/me/logout"><i class="material-icons md-18">power_settings_new</i> Log out</a></li>
  37. </ul>
  38. </li>{{end}}
  39. </ul></nav>
  40. <nav id="font-picker" class="if-room room-3 hidden" style="margin-left:-1em"><ul>
  41. <li class="has-submenu"><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>
  42. <ul style="text-align: center">
  43. <li class="menu-heading">Font</li>
  44. <li class="selected"><a class="font norm" href="#norm">Serif</a></li>
  45. <li><a class="font sans" href="#sans">Sans-serif</a></li>
  46. <li><a class="font wrap" href="#wrap">Monospace</a></li>
  47. </ul>
  48. </li>
  49. </ul></nav>
  50. <span id="wc" class="hidden if-room room-4">0 words</span>
  51. </div>
  52. <noscript style="margin-left: 2em;"><strong>NOTE</strong>: for now, you'll need JavaScript enabled to post.</noscript>
  53. <div id="belt">
  54. {{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}}
  55. <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>
  56. <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>
  57. <div class="tool"><a href="#publish" title="Publish" id="publish"><img class="ic-24dp" src="/img/ic_send_dark@2x.png" /></a></div>
  58. </div>
  59. </header>
  60. <script src="/js/h.js"></script>
  61. <script type="text/javascript" src="/js/menu.js"></script>
  62. <script>
  63. function toggleTheme() {
  64. if (document.body.classList.contains('light')) {
  65. setTheme('dark');
  66. } else {
  67. setTheme('light');
  68. }
  69. H.set('padTheme', newTheme);
  70. }
  71. function setTheme(newTheme) {
  72. document.body.classList.remove('light');
  73. document.body.classList.remove('dark');
  74. document.body.classList.add(newTheme);
  75. var btns = Array.prototype.slice.call(document.getElementById('tools').querySelectorAll('a img'));
  76. if (newTheme == 'light') {
  77. // check if current theme is dark otherwise we'll get `_dark_dark@2x.png`
  78. if (H.get('padTheme', 'auto') == 'dark'){
  79. for (var i=0; i<btns.length; i++) {
  80. btns[i].src = btns[i].src.replace('@2x.png', '_dark@2x.png');
  81. }
  82. }
  83. } else {
  84. for (var i=0; i<btns.length; i++) {
  85. btns[i].src = btns[i].src.replace('_dark@2x.png', '@2x.png');
  86. }
  87. }
  88. H.set('padTheme', newTheme);
  89. }
  90. if (H.get('padTheme', 'auto') == 'light') {
  91. setTheme('light');
  92. } else if (H.get('padTheme', 'auto') == 'dark') {
  93. setTheme('dark');
  94. } else {
  95. const isDarkMode = window.matchMedia("(prefers-color-scheme: dark)").matches
  96. if (isDarkMode) {
  97. setTheme('dark');
  98. } else {
  99. setTheme('light');
  100. }
  101. }
  102. var $writer = H.getEl('writer');
  103. var $btnPublish = H.getEl('publish');
  104. var $btnEraseEdit = H.getEl('edited-elsewhere');
  105. var $wc = H.getEl("wc");
  106. var updateWordCount = function() {
  107. var words = 0;
  108. var val = $writer.el.value.trim();
  109. if (val != '') {
  110. words = $writer.el.value.trim().replace(/\s+/gi, ' ').split(' ').length;
  111. }
  112. $wc.el.innerText = words + " word" + (words != 1 ? "s" : "");
  113. };
  114. var setButtonStates = function() {
  115. if (!canPublish) {
  116. $btnPublish.el.className = 'disabled';
  117. return;
  118. }
  119. if ($writer.el.value.length === 0 || (draftDoc != 'lastDoc' && $writer.el.value == origDoc)) {
  120. $btnPublish.el.className = 'disabled';
  121. } else {
  122. $btnPublish.el.className = '';
  123. }
  124. };
  125. {{if .Post.Id}}var draftDoc = 'draft{{.Post.Id}}';
  126. var origDoc = '{{.Post.Content}}';{{else}}var draftDoc = 'lastDoc';{{end}}
  127. var updatedStr = '{{.Post.Updated8601}}';
  128. var updated = null;
  129. if (updatedStr != '') {
  130. updated = new Date(updatedStr);
  131. }
  132. var ok = H.load($writer, draftDoc, true, updated);
  133. if (!ok) {
  134. // Show "edited elsewhere" warning
  135. $btnEraseEdit.el.classList.remove('hidden');
  136. }
  137. var defaultTimeSet = false;
  138. updateWordCount();
  139. var typingTimer;
  140. var doneTypingInterval = 200;
  141. var posts;
  142. {{if and .Post.Id (not .Post.Slug)}}
  143. var token = null;
  144. var curPostIdx;
  145. posts = JSON.parse(H.get('posts', '[]'));
  146. for (var i=0; i<posts.length; i++) {
  147. if (posts[i].id == "{{.Post.Id}}") {
  148. token = posts[i].token;
  149. break;
  150. }
  151. }
  152. var canPublish = token != null;
  153. {{else}}var canPublish = true;{{end}}
  154. var publishing = false;
  155. var justPublished = false;
  156. var silenced = {{.Silenced}};
  157. var publish = function(content, font) {
  158. if (silenced === true) {
  159. alert("Your account is silenced, so you can't publish or update posts.");
  160. return;
  161. }
  162. {{if and (and .Post.Id (not .Post.Slug)) (not .User)}}
  163. if (!token) {
  164. alert("You don't have permission to update this post.");
  165. return;
  166. }
  167. if ($btnPublish.el.className == 'disabled') {
  168. return;
  169. }
  170. {{end}}
  171. $btnPublish.el.children[0].textContent = 'more_horiz';
  172. publishing = true;
  173. var xpostTarg = H.get('crosspostTarget', '[]');
  174. var http = new XMLHttpRequest();
  175. var post = H.getTitleStrict(content);
  176. var params = {
  177. body: post.content,
  178. title: post.title,
  179. font: font
  180. };
  181. {{ if .Post.Slug }}
  182. var url = "/api/collections/{{.EditCollection.Alias}}/posts/{{.Post.Id}}";
  183. {{ else if .Post.Id }}
  184. var url = "/api/posts/{{.Post.Id}}";
  185. if (typeof token === 'undefined' || !token) {
  186. token = "";
  187. }
  188. params.token = token;
  189. {{ else }}
  190. var lang = navigator.languages ? navigator.languages[0] : (navigator.language || navigator.userLanguage);
  191. lang = lang.substring(0, 2);
  192. params.lang = lang;
  193. var url = "/api/posts";
  194. var postTarget = H.get('postTarget', 'anonymous');
  195. if (postTarget != 'anonymous') {
  196. url = "/api/collections/" + postTarget + "/posts";
  197. }
  198. params.crosspost = JSON.parse(xpostTarg);
  199. {{ end }}
  200. http.open("POST", url, true);
  201. // Send the proper header information along with the request
  202. http.setRequestHeader("Content-type", "application/json");
  203. http.onreadystatechange = function() {
  204. if (http.readyState == 4) {
  205. publishing = false;
  206. if (http.status == 200 || http.status == 201) {
  207. data = JSON.parse(http.responseText);
  208. id = data.data.id;
  209. nextURL = '{{if .SingleUser}}/d{{end}}/'+id;
  210. localStorage.setItem('draft'+id+'-published', new Date().toISOString());
  211. {{ if not .Post.Id }}
  212. // Post created
  213. if (postTarget != 'anonymous') {
  214. nextURL = {{if not .SingleUser}}'/'+postTarget+{{end}}'/'+data.data.slug;
  215. }
  216. editToken = data.data.token;
  217. {{ if not .User }}if (postTarget == 'anonymous') {
  218. // Save the data
  219. var posts = JSON.parse(H.get('posts', '[]'));
  220. {{if .Post.Id}}var newPost = H.createPost("{{.Post.Id}}", token, content);
  221. for (var i=0; i<posts.length; i++) {
  222. if (posts[i].id == "{{.Post.Id}}") {
  223. posts[i].title = newPost.title;
  224. posts[i].summary = newPost.summary;
  225. break;
  226. }
  227. }
  228. nextURL = "/pad/posts";{{else}}posts.push(H.createPost(id, editToken, content));{{end}}
  229. H.set('posts', JSON.stringify(posts));
  230. }
  231. {{ end }}
  232. {{ end }}
  233. justPublished = true;
  234. if (draftDoc != 'lastDoc') {
  235. H.remove(draftDoc);
  236. {{if .Editing}}H.remove('draft{{.Post.Id}}font');{{end}}
  237. } else {
  238. H.set(draftDoc, '');
  239. }
  240. {{if .EditCollection}}
  241. window.location = '{{.EditCollection.CanonicalURL}}{{.Post.Slug}}';
  242. {{else}}
  243. window.location = nextURL;
  244. {{end}}
  245. } else {
  246. $btnPublish.el.children[0].textContent = 'send';
  247. alert("Failed to post. Please try again.");
  248. }
  249. }
  250. }
  251. http.send(JSON.stringify(params));
  252. };
  253. setButtonStates();
  254. $writer.on('keyup input', function() {
  255. setButtonStates();
  256. clearTimeout(typingTimer);
  257. typingTimer = setTimeout(doneTyping, doneTypingInterval);
  258. }, false);
  259. $writer.on('keydown', function(e) {
  260. clearTimeout(typingTimer);
  261. if (e.keyCode == 13 && (e.metaKey || e.ctrlKey)) {
  262. $btnPublish.el.click();
  263. }
  264. });
  265. $btnPublish.on('click', function(e) {
  266. e.preventDefault();
  267. if (!publishing && $writer.el.value) {
  268. var content = $writer.el.value;
  269. publish(content, selectedFont);
  270. }
  271. });
  272. H.getEl('erase-edit').on('click', function(e) {
  273. e.preventDefault();
  274. H.remove(draftDoc);
  275. H.remove(draftDoc+'-published');
  276. justPublished = true; // Block auto-save
  277. location.reload();
  278. });
  279. H.getEl('toggle-theme').on('click', function(e) {
  280. e.preventDefault();
  281. var newTheme = 'light';
  282. if (document.body.className == 'light') {
  283. newTheme = 'dark';
  284. }
  285. toggleTheme();
  286. });
  287. var targets = document.querySelectorAll('#target li.target a');
  288. for (var i=0; i<targets.length; i++) {
  289. targets[i].addEventListener('click', function(e) {
  290. e.preventDefault();
  291. var targetName = this.href.substring(this.href.indexOf('#')+1);
  292. H.set('postTarget', targetName);
  293. document.querySelector('#target li.target.selected').classList.remove('selected');
  294. this.parentElement.classList.add('selected');
  295. var newText = this.innerText.split(' ');
  296. newText.shift();
  297. document.getElementById('target-name').innerText = newText.join(' ');
  298. });
  299. }
  300. var postTarget = H.get('postTarget', '{{if .Blogs}}{{$blog := index .Blogs 0}}{{$blog.Alias}}{{else}}anonymous{{end}}');
  301. if (location.hash != '') {
  302. postTarget = location.hash.substring(1);
  303. // TODO: pushState to /pad (or whatever the URL is) so we live on a clean URL
  304. location.hash = '';
  305. }
  306. var pte = document.querySelector('#target li.target#blog-'+postTarget+' a');
  307. if (pte != null) {
  308. pte.click();
  309. } else {
  310. postTarget = 'anonymous';
  311. H.set('postTarget', postTarget);
  312. }
  313. var sansLoaded = false;
  314. WebFontConfig = {
  315. custom: { families: [ 'Lora:400,700:latin' ], urls: [ '/css/fonts.css' ] }
  316. };
  317. var loadSans = function() {
  318. if (sansLoaded) return;
  319. sansLoaded = true;
  320. WebFontConfig.custom.families.push('Open+Sans:400,700:latin');
  321. try {
  322. (function() {
  323. var wf=document.createElement('script');
  324. wf.src = '/js/webfont.js';
  325. wf.type='text/javascript';
  326. wf.async='true';
  327. var s=document.getElementsByTagName('script')[0];
  328. s.parentNode.insertBefore(wf, s);
  329. })();
  330. } catch (e) {}
  331. };
  332. var fonts = document.querySelectorAll('nav#font-picker a.font');
  333. for (var i=0; i<fonts.length; i++) {
  334. fonts[i].addEventListener('click', function(e) {
  335. e.preventDefault();
  336. selectedFont = this.href.substring(this.href.indexOf('#')+1);
  337. $writer.el.className = selectedFont;
  338. document.querySelector('nav#font-picker li.selected').classList.remove('selected');
  339. this.parentElement.classList.add('selected');
  340. H.set('{{if .Editing}}draft{{.Post.Id}}font{{else}}padFont{{end}}', selectedFont);
  341. if (selectedFont == 'sans') {
  342. loadSans();
  343. }
  344. });
  345. }
  346. var selectedFont = H.get('{{if .Editing}}draft{{.Post.Id}}font{{else}}padFont{{end}}', '{{.Post.Font}}');
  347. var sfe = document.querySelector('nav#font-picker a.font.'+selectedFont);
  348. if (sfe != null) {
  349. sfe.click();
  350. }
  351. var doneTyping = function() {
  352. if (draftDoc == 'lastDoc' || $writer.el.value != origDoc) {
  353. H.save($writer, draftDoc);
  354. if (!defaultTimeSet) {
  355. var lastLocalPublishStr = localStorage.getItem(draftDoc+'-published');
  356. if (lastLocalPublishStr == null || lastLocalPublishStr == '') {
  357. localStorage.setItem(draftDoc+'-published', updatedStr);
  358. }
  359. defaultTimeSet = true;
  360. }
  361. updateWordCount();
  362. }
  363. };
  364. window.addEventListener('beforeunload', function(e) {
  365. if (draftDoc != 'lastDoc' && $writer.el.value == origDoc) {
  366. H.remove(draftDoc);
  367. H.remove(draftDoc+'-published');
  368. } else if (!justPublished) {
  369. doneTyping();
  370. }
  371. });
  372. try {
  373. (function() {
  374. var wf=document.createElement('script');
  375. wf.src = '/js/webfont.js';
  376. wf.type='text/javascript';
  377. wf.async='true';
  378. var s=document.getElementsByTagName('script')[0];
  379. s.parentNode.insertBefore(wf, s);
  380. })();
  381. } catch (e) {
  382. // whatevs
  383. }
  384. </script>
  385. <link href="/css/icons.css" rel="stylesheet">
  386. </body>
  387. </html>{{end}}