| | @@ -1,50 +1,158 @@ |
| 1 | 1 | (function(F/*the fossil object*/){ |
| 2 | 2 | "use strict"; |
| 3 | 3 | /** |
| 4 | 4 | Client-side implementation of the /pikchrshow app. Requires that |
| 5 | | - the fossil JS bootstrapping is complete and that these fossil |
| 6 | | - JS APIs have been installed: fossil.fetch, fossil.dom |
| 5 | + the fossil JS bootstrapping is complete and that these fossil JS |
| 6 | + APIs have been installed: fossil.fetch, fossil.dom, |
| 7 | + fossil.copybutton |
| 7 | 8 | */ |
| 8 | 9 | const E = (s)=>document.querySelector(s), |
| 9 | 10 | D = F.dom, |
| 10 | 11 | P = F.page; |
| 12 | + |
| 13 | + P.previewMode = 0 /*0==rendered SVG, 1==pikchr text markdown, |
| 14 | + 2==pikchr text fossil, 3==raw SVG. */ |
| 15 | + P.response = {/*stashed state for the server's preview response*/ |
| 16 | + isError: false, |
| 17 | + inputText: undefined /* value of the editor field at render-time */, |
| 18 | + raw: undefined /* raw response text/HTML from server */ |
| 19 | + }; |
| 11 | 20 | F.onPageLoad(function() { |
| 12 | 21 | document.body.classList.add('pikchrshow'); |
| 13 | 22 | P.e = { /* various DOM elements we work with... */ |
| 14 | 23 | previewTarget: E('#pikchrshow-output'), |
| 24 | + previewModeLabel: E('#pikchrshow-output-wrapper > legend'), |
| 25 | + btnCopy: E('#pikchrshow-output-wrapper > legend > .copy-button'), |
| 15 | 26 | btnSubmit: E('#pikchr-submit-preview'), |
| 16 | 27 | cbDarkMode: E('#flipcolors-wrapper > input[type=checkbox]'), |
| 17 | | - taContent: E('#content') |
| 28 | + taContent: E('#content'), |
| 29 | + taPreviewText: D.attr(D.textarea(), 'rows', 20, 'cols', 60, |
| 30 | + 'readonly', true), |
| 31 | + divControls: E('#pikchrshow-controls'), |
| 32 | + btnTogglePreviewMode: D.button("Preview mode"), |
| 33 | + selectMarkupAlignment: D.select() |
| 18 | 34 | }; |
| 35 | + D.append(P.e.divControls, P.e.btnTogglePreviewMode); |
| 36 | + |
| 37 | + // Setup markup alignment selection... |
| 38 | + D.append(P.e.divControls, P.e.selectMarkupAlignment); |
| 39 | + D.disable(D.option(P.e.selectMarkupAlignment, '', 'Markup Alignment')); |
| 40 | + ['left', 'center'].forEach(function(val,ndx){ |
| 41 | + D.option(P.e.selectMarkupAlignment, ndx ? val : '', val); |
| 42 | + }); |
| 43 | + |
| 44 | + // Setup clipboard-copy of markup/SVG... |
| 45 | + F.copyButton(P.e.btnCopy, {copyFromElement: P.e.taPreviewText}); |
| 46 | + P.e.btnCopy.addEventListener('text-copied',function(ev){ |
| 47 | + D.flashOnce(ev.target); |
| 48 | + },false); |
| 19 | 49 | |
| 50 | + // Set up dark mode simulator... |
| 20 | 51 | P.e.cbDarkMode.addEventListener('change', function(ev){ |
| 21 | 52 | if(ev.target.checked) D.addClass(P.e.previewTarget, 'dark-mode'); |
| 22 | 53 | else D.removeClass(P.e.previewTarget, 'dark-mode'); |
| 23 | 54 | }, false); |
| 24 | 55 | if(P.e.cbDarkMode.checked) D.addClass(P.e.previewTarget, 'dark-mode'); |
| 25 | 56 | |
| 26 | | - P.e.btnSubmit.addEventListener('click', function(){ |
| 27 | | - P.preview(); |
| 57 | + // Set up preview update and preview mode toggle... |
| 58 | + P.e.btnSubmit.addEventListener('click', ()=>P.preview(), false); |
| 59 | + P.e.btnTogglePreviewMode.addEventListener('click', function(){ |
| 60 | + /* Rotate through the 4 available preview modes */ |
| 61 | + P.previewMode = ++P.previewMode % 4; |
| 62 | + P.renderPreview(); |
| 63 | + }, false); |
| 64 | + P.e.selectMarkupAlignment.addEventListener('change', function(ev){ |
| 65 | + /* Update markdown/fossil wiki preview if it's active */ |
| 66 | + if(P.previewMode==1 || P.previewMode==2){ |
| 67 | + P.renderPreview(); |
| 68 | + } |
| 28 | 69 | }, false); |
| 70 | + |
| 71 | + if(P.e.taContent.value/*was pre-filled server-side*/){ |
| 72 | + /* Fill our "response" state so that renderPreview() can work */ |
| 73 | + P.response.inputText = P.e.taContent.value; |
| 74 | + P.response.raw = P.e.previewTarget.innerHTML; |
| 75 | + P.renderPreview()/*not strictly necessary, but gets all |
| 76 | + labels/headers in alignment.*/; |
| 77 | + } |
| 29 | 78 | }/*F.onPageLoad()*/); |
| 30 | 79 | |
| 80 | + /** |
| 81 | + Updates the preview view based on the current preview mode and |
| 82 | + error state. |
| 83 | + */ |
| 84 | + P.renderPreview = function f(){ |
| 85 | + if(!f.hasOwnProperty('rxNonce')){ |
| 86 | + f.rxNonce = /<!--.+-->\r?\n?/g /*nonce comments*/; |
| 87 | + } |
| 88 | + const preTgt = this.e.previewTarget; |
| 89 | + if(this.response.isError){ |
| 90 | + preTgt.innerHTML = this.response.raw; |
| 91 | + D.addClass(preTgt, 'error'); |
| 92 | + this.e.previewModeLabel.innerText = "Error"; |
| 93 | + return; |
| 94 | + } |
| 95 | + D.removeClass(preTgt, 'error'); |
| 96 | + D.removeClass(this.e.btnTogglePreviewMode, 'hidden'); |
| 97 | + let label; |
| 98 | + switch(this.previewMode){ |
| 99 | + case 0: |
| 100 | + label = "Rendered SVG"; |
| 101 | + preTgt.innerHTML = this.response.raw; |
| 102 | + this.e.taPreviewText.value = this.response.raw.replace(f.rxNonce, '')/*for copy button*/; |
| 103 | + break; |
| 104 | + case 1: |
| 105 | + label = "Markdown"; |
| 106 | + this.e.taPreviewText.value = [ |
| 107 | + '```pikchr'+(this.e.selectMarkupAlignment.value |
| 108 | + ? ' '+this.e.selectMarkupAlignment.value : ''), |
| 109 | + this.response.inputText, '```' |
| 110 | + ].join('\n'); |
| 111 | + D.append(D.clearElement(preTgt), this.e.taPreviewText); |
| 112 | + break; |
| 113 | + case 2: |
| 114 | + label = "Fossil wiki"; |
| 115 | + this.e.taPreviewText.value = [ |
| 116 | + '<verbatim type="pikchr', |
| 117 | + this.e.selectMarkupAlignment.value ? ' '+this.e.selectMarkupAlignment.value : '', |
| 118 | + '">', this.response.inputText, '</verbatim>' |
| 119 | + ].join(''); |
| 120 | + D.append(D.clearElement(preTgt), this.e.taPreviewText); |
| 121 | + break; |
| 122 | + case 3: |
| 123 | + label = "Raw SVG"; |
| 124 | + this.e.taPreviewText.value = this.response.raw.replace(f.rxNonce, ''); |
| 125 | + D.append(D.clearElement(preTgt), this.e.taPreviewText); |
| 126 | + break; |
| 127 | + } |
| 128 | + D.append(D.clearElement(this.e.previewModeLabel), |
| 129 | + label, this.e.btnCopy); |
| 130 | + }; |
| 131 | + |
| 132 | + /** Fetches the preview from the server and updates the preview to |
| 133 | + the rendered SVG content or error report. */ |
| 31 | 134 | P.preview = function fp(){ |
| 32 | 135 | if(!fp.hasOwnProperty('toDisable')){ |
| 33 | | - fp.toDisable = [ |
| 34 | | - P.e.btnSubmit, P.e.taContent |
| 136 | + fp.toDisable = [ /* elements to disable during ajax operations */ |
| 137 | + this.e.btnSubmit, this.e.taContent, |
| 138 | + this.e.btnTogglePreviewMode, this.e.selectMarkupAlignment, |
| 35 | 139 | ]; |
| 36 | | - fp.target = P.e.previewTarget; |
| 140 | + fp.target = this.e.previewTarget; |
| 37 | 141 | fp.updateView = function(c,isError){ |
| 142 | + P.previewMode = 0; |
| 143 | + P.response.raw = c; |
| 144 | + P.response.isError = isError; |
| 38 | 145 | D.enable(fp.toDisable); |
| 39 | | - fp.target.innerHTML = c || ''; |
| 40 | | - if(isError) D.addClass(fp.target, 'error'); |
| 41 | | - else D.removeClass(fp.target, 'error'); |
| 146 | + P.renderPreview(); |
| 42 | 147 | }; |
| 43 | 148 | } |
| 44 | 149 | D.disable(fp.toDisable); |
| 45 | | - const content = this.e.taContent.value; |
| 150 | + D.addClass(this.e.btnTogglePreviewMode, 'hidden'); |
| 151 | + const content = this.e.taContent.value.trim(); |
| 152 | + this.response.raw = undefined; |
| 153 | + this.response.inputText = content; |
| 46 | 154 | if(!content){ |
| 47 | 155 | fp.updateView("No pikchr content!",true); |
| 48 | 156 | return this; |
| 49 | 157 | } |
| 50 | 158 | const self = this; |
| 51 | 159 | |