Files: 3ad00d6029fdc254261c83d438be1fe0dd4977bf / app / page / blogNew.js
7733 bytesRaw
1 | const nest = require('depnest') |
2 | const { h, Struct, Value, when } = require('mutant') |
3 | const addSuggest = require('suggest-box') |
4 | const pull = require('pull-stream') |
5 | const marksum = require('markdown-summary') |
6 | const MediumEditor = require('medium-editor').MediumEditor |
7 | const MediumToMD = require('medium-editor-markdown') |
8 | // const CustomHtml = require('medium-editor-custom-async') |
9 | |
10 | exports.gives = nest('app.page.blogNew') |
11 | |
12 | exports.needs = nest({ |
13 | 'app.html.sideNav': 'first', |
14 | 'blob.html.input': 'first', |
15 | 'channel.async.suggest': 'first', |
16 | 'history.sync.push': 'first', |
17 | 'message.html.compose': 'first', |
18 | 'translations.sync.strings': 'first', |
19 | 'sbot.async.addBlob': 'first' |
20 | }) |
21 | |
22 | exports.create = (api) => { |
23 | return nest('app.page.blogNew', blogNew) |
24 | |
25 | function blogNew (location) { |
26 | const strings = api.translations.sync.strings() |
27 | const getChannelSuggestions = api.channel.async.suggest() |
28 | |
29 | const meta = Struct({ |
30 | type: 'blog', |
31 | channel: Value(), |
32 | title: Value(), |
33 | summary: Value(), |
34 | text: Value('') |
35 | }) |
36 | |
37 | const mediumComposer = h('Markdown.editor') |
38 | var filesById = {} |
39 | const composer = initialiseDummyComposer({ filesById, meta, api }) |
40 | // NOTE we are bootstrapping off the message.html.compose logic |
41 | // - the mediumComposer feeds content into the composer, which the provides publishing |
42 | // - this works, but should be refactorer after more thought about generalised composer |
43 | // componentes have been done |
44 | |
45 | const channelInput = h('input', { |
46 | 'ev-input': e => meta.channel.set(e.target.value), |
47 | value: meta.channel, |
48 | placeholder: strings.channel |
49 | }) |
50 | const updateTitle = e => { |
51 | if (e.target.childElementCount) { |
52 | // catch people pasting html in here! |
53 | e.target.innerHTML = e.target.innerText |
54 | } |
55 | meta.title.set(e.target.innerText) |
56 | } |
57 | |
58 | var page = h('Page -blogNew', [ |
59 | api.app.html.sideNav(location), |
60 | h('div.content', [ |
61 | h('div.container', [ |
62 | h('div.field -title', [ |
63 | h('h1.input', { |
64 | attributes: { |
65 | contenteditable: true, |
66 | 'data-placeholder': strings.blogNew.field.title |
67 | }, |
68 | 'ev-input': updateTitle, |
69 | className: when(meta.title, '', '-empty') |
70 | }) |
71 | ]), |
72 | mediumComposer, |
73 | h('div.field -attachment', |
74 | AddFileButton({ api, filesById, meta, textArea: mediumComposer }) |
75 | ), |
76 | h('div.field -channel', [ |
77 | h('div.label', strings.channel), |
78 | channelInput |
79 | ]), |
80 | h('div.field -summary', [ |
81 | h('div.label', strings.blogNew.field.summary), |
82 | h('input', { |
83 | 'ev-input': e => meta.summary.set(e.target.value), |
84 | placeholder: strings.blogNew.field.summary |
85 | }) |
86 | ]), |
87 | composer |
88 | ]) |
89 | ]) |
90 | ]) |
91 | |
92 | initialiseMedium({ page, el: mediumComposer, meta }) |
93 | initialiseChannelSuggests({ input: channelInput, suggester: getChannelSuggestions, meta }) |
94 | |
95 | return page |
96 | } |
97 | } |
98 | |
99 | function AddFileButton ({ api, filesById, meta, textArea }) { |
100 | // var textRaw = meta.text |
101 | // |
102 | |
103 | const fileInput = api.blob.html.input(file => { |
104 | filesById[file.link] = file |
105 | |
106 | const isImage = file.type.match(/^image/) |
107 | |
108 | var content |
109 | |
110 | if (isImage) { |
111 | content = h('img', { |
112 | src: `http://localhost:8989/blobs/get/${encodeURIComponent(file.link)}`, |
113 | alt: file.name |
114 | }) |
115 | } else { |
116 | content = h('a', { href: file.link }, file.name) |
117 | } |
118 | // TODO - insert where the mouse is yo |
119 | var editor = MediumEditor.getEditorFromElement(textArea) |
120 | textArea.insertBefore( |
121 | h('p', content), |
122 | editor.currentEl || null |
123 | ) |
124 | |
125 | console.log('added:', file) |
126 | }) |
127 | |
128 | return fileInput |
129 | } |
130 | |
131 | function initialiseDummyComposer ({ meta, api, filesById }) { |
132 | return api.message.html.compose( |
133 | { |
134 | meta, |
135 | // placeholder: strings.blogNew.actions.writeBlog, |
136 | shrink: false, |
137 | canAttach: false, |
138 | canPreview: false, |
139 | publishString: api.translations.sync.strings().publishBlog, |
140 | filesById, |
141 | prepublish: function (content, cb) { |
142 | var m = /\!\[[^]+\]\(([^\)]+)\)/.exec(marksum.image(content.text)) |
143 | content.thumbnail = m && m[1] |
144 | // content.summary = marksum.summary(content.text) // TODO Need a summary which doesn't trim the start |
145 | |
146 | var stream = pull.values([content.text]) |
147 | api.sbot.async.addBlob(stream, function (err, hash) { |
148 | if (err) return cb(err) |
149 | if (!hash) throw new Error('missing hash') |
150 | content.blog = hash |
151 | delete content.text |
152 | cb(null, content) |
153 | }) |
154 | } |
155 | }, |
156 | (err, msg) => api.history.sync.push(err || { page: 'blogIndex' }) |
157 | ) |
158 | } |
159 | |
160 | function initialiseChannelSuggests ({ input, suggester, meta }) { |
161 | addSuggest(input, (inputText, cb) => { |
162 | inputText = inputText.replace(/^#/, '') |
163 | var suggestions = suggester(inputText) |
164 | .map(s => { |
165 | s.value = s.value.replace(/^#/, '') // strip the defualt # prefix here |
166 | return s |
167 | }) |
168 | .map(s => { |
169 | if (s.subtitle === 'subscribed') { s.subtitle = h('i.fa.fa-heart') } // TODO - translation-friendly subscribed |
170 | return s |
171 | }) |
172 | |
173 | // HACK add the input text if it's not an option already |
174 | if (!suggestions.some(s => s.title === inputText)) { |
175 | suggestions.push({ |
176 | title: inputText, |
177 | subtitle: h('i.fa.fa-plus-circle'), |
178 | value: inputText |
179 | }) |
180 | } |
181 | |
182 | cb(null, suggestions) |
183 | }, {cls: 'PatchSuggest.-channelHorizontal'}) // WARNING hacking suggest-box cls |
184 | |
185 | input.addEventListener('suggestselect', (e) => { |
186 | meta.channel.set(e.detail.value) |
187 | }) |
188 | } |
189 | |
190 | function initialiseMedium ({ page, el, meta }) { |
191 | var editor = new MediumEditor(el, { |
192 | elementsContainer: page, |
193 | // autoLink: true, |
194 | buttonLabels: 'fontawesome', |
195 | imageDragging: true, |
196 | toolbar: { |
197 | allowMultiParagraphSelection: true, |
198 | buttons: [ |
199 | 'bold', |
200 | 'italic', |
201 | 'anchor', |
202 | { |
203 | name: 'h2', |
204 | contentFA: '<i class="fa fa-header" />', |
205 | classList: ['custom-button-h2'] |
206 | }, |
207 | { |
208 | name: 'h3', |
209 | contentFA: '<i class="fa fa-header" />', |
210 | classList: ['custom-button-h3'] |
211 | }, |
212 | 'quote' |
213 | ], |
214 | diffLeft: 0, |
215 | diffTop: 10, |
216 | firstButtonClass: 'medium-editor-button-first', |
217 | lastButtonClass: 'medium-editor-button-last', |
218 | relativeContainer: null, |
219 | standardizeSelectionStart: false, |
220 | static: false, |
221 | /* options which only apply when static is true */ |
222 | align: 'center', |
223 | sticky: false, |
224 | updateOnEmptySelection: false |
225 | }, |
226 | extensions: { |
227 | markdown: new MediumToMD( |
228 | { |
229 | toMarkdownOptions: { |
230 | converters: [{ |
231 | filter: 'img', |
232 | replacement: (content, node) => { |
233 | var blob = decodeURIComponent(node.src.replace('http://localhost:8989/blobs/get/', '')) |
234 | return `![${node.alt}](${blob})` |
235 | } |
236 | }, { |
237 | filter: 'span', |
238 | replacement: (content, node) => content |
239 | }] |
240 | }, |
241 | events: ['input', 'change', 'DOMNodeInserted'] |
242 | }, |
243 | md => meta.text.set(md) |
244 | ) |
245 | } |
246 | }) |
247 | |
248 | editor.on(el, 'keyup', setCurrentEl) |
249 | editor.on(el, 'click', setCurrentEl) |
250 | |
251 | function setCurrentEl (ev) { |
252 | var sel = window.getSelection() |
253 | var container = sel.getRangeAt(0).commonAncestorContainer |
254 | editor.currentEl = container.textContent === '' // NOTE this could be a brittle check |
255 | ? container |
256 | : container.parentElement |
257 | } |
258 | |
259 | return editor |
260 | } |
261 |
Built with git-ssb-web