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