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.
 
 
 
 
 

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