Files: 26ec4bc385c250f6b9b1d0eb534ceaff0f876083 / app / page / blogNew.js
6698 bytesRaw
1 | const nest = require('depnest') |
2 | const { h, Struct, Value } = 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 -channel', [ |
59 | h('div.label', strings.channel), |
60 | channelInput |
61 | ]), |
62 | h('div.field -title', [ |
63 | h('div.label', strings.blogNew.field.title), |
64 | h('input', { |
65 | 'ev-input': e => meta.title.set(e.target.value), |
66 | placeholder: strings.blogNew.field.title |
67 | }) |
68 | ]), |
69 | h('div.field -summary', [ |
70 | h('div.label', strings.blogNew.field.summary), |
71 | h('input', { |
72 | 'ev-input': e => meta.summary.set(e.target.value), |
73 | placeholder: strings.blogNew.field.summary |
74 | }) |
75 | ]), |
76 | mediumComposer, |
77 | AddFileButton({ api, filesById, meta, textArea: mediumComposer }), |
78 | composer |
79 | ]) |
80 | ]) |
81 | ]) |
82 | |
83 | initialiseMedium({ page, el: mediumComposer, meta }) |
84 | initialiseChannelSuggests({ input: channelInput, suggester: getChannelSuggestions, meta }) |
85 | |
86 | return page |
87 | } |
88 | } |
89 | |
90 | function AddFileButton ({ api, filesById, meta, textArea }) { |
91 | // var textRaw = meta.text |
92 | |
93 | const fileInput = api.blob.html.input(file => { |
94 | filesById[file.link] = file |
95 | |
96 | const isImage = file.type.match(/^image/) |
97 | const imgPrefix = isImage ? '!' : '' |
98 | const spacer = isImage ? '\n' : ' ' |
99 | const insertLink = spacer + imgPrefix + '[' + file.name + ']' + '(' + file.link + ')' + spacer |
100 | |
101 | const pos = textArea.selectionStart |
102 | // var newText = textRaw().slice(0, pos) + insertLink + textRaw().slice(pos) |
103 | // textArea.value = newText |
104 | // textRaw.set(newText) |
105 | |
106 | // TODO pivot on image to insert link, or image |
107 | const img = h('p', [ |
108 | h('img', { |
109 | src: `http://localhost:8989/blobs/get/${encodeURIComponent(file.link)}`, |
110 | alt: file.name |
111 | }) |
112 | ]) |
113 | // TODO - insert where the mouse is yo |
114 | textArea.appendChild(img) |
115 | |
116 | console.log('added:', file) |
117 | }) |
118 | |
119 | return fileInput |
120 | } |
121 | |
122 | function initialiseDummyComposer ({ meta, api, filesById }) { |
123 | return api.message.html.compose( |
124 | { |
125 | meta, |
126 | // placeholder: strings.blogNew.actions.writeBlog, |
127 | shrink: false, |
128 | filesById, |
129 | prepublish: function (content, cb) { |
130 | var m = /\!\[[^]+\]\(([^\)]+)\)/.exec(marksum.image(content.text)) |
131 | content.thumbnail = m && m[1] |
132 | // content.summary = marksum.summary(content.text) // TODO Need a summary which doesn't trim the start |
133 | |
134 | var stream = pull.values([content.text]) |
135 | api.sbot.async.addBlob(stream, function (err, hash) { |
136 | if (err) return cb(err) |
137 | if (!hash) throw new Error('missing hash') |
138 | content.blog = hash |
139 | delete content.text |
140 | cb(null, content) |
141 | }) |
142 | } |
143 | }, |
144 | (err, msg) => api.history.sync.push(err || { page: 'blogIndex' }) |
145 | ) |
146 | } |
147 | |
148 | function initialiseChannelSuggests ({ input, suggester, meta }) { |
149 | addSuggest(input, (inputText, cb) => { |
150 | inputText = inputText.replace(/^#/, '') |
151 | var suggestions = suggester(inputText) |
152 | .map(s => { |
153 | s.value = s.value.replace(/^#/, '') // strip the defualt # prefix here |
154 | return s |
155 | }) |
156 | .map(s => { |
157 | if (s.subtitle === 'subscribed') { s.subtitle = h('i.fa.fa-heart') } // TODO - translation-friendly subscribed |
158 | return s |
159 | }) |
160 | |
161 | // HACK add the input text if it's not an option already |
162 | if (!suggestions.some(s => s.title === inputText)) { |
163 | suggestions.push({ |
164 | title: inputText, |
165 | subtitle: h('i.fa.fa-plus-circle'), |
166 | value: inputText |
167 | }) |
168 | } |
169 | |
170 | cb(null, suggestions) |
171 | }, {cls: 'PatchSuggest.-channelHorizontal'}) // WARNING hacking suggest-box cls |
172 | |
173 | input.addEventListener('suggestselect', (e) => { |
174 | meta.channel.set(e.detail.value) |
175 | }) |
176 | } |
177 | |
178 | function initialiseMedium ({ page, el, meta }) { |
179 | var editor = new MediumEditor(el, { |
180 | elementsContainer: page, |
181 | toolbar: { |
182 | allowMultiParagraphSelection: true, |
183 | buttons: ['bold', 'italic', 'underline', 'anchor', 'h2', 'h3', 'quote'], |
184 | diffLeft: 0, |
185 | diffTop: -10, |
186 | firstButtonClass: 'medium-editor-button-first', |
187 | lastButtonClass: 'medium-editor-button-last', |
188 | relativeContainer: null, |
189 | standardizeSelectionStart: false, |
190 | static: false, |
191 | /* options which only apply when static is true */ |
192 | align: 'center', |
193 | sticky: false, |
194 | updateOnEmptySelection: false |
195 | }, |
196 | extensions: { |
197 | markdown: new MediumToMD( |
198 | { |
199 | toMarkdownOptions: { |
200 | converters: [{ |
201 | filter: 'img', |
202 | replacement: (content, node) => { |
203 | var blob = decodeURIComponent(node.src.replace('http://localhost:8989/blobs/get/', '')) |
204 | return `![${node.alt}](${blob})` |
205 | } |
206 | }] |
207 | }, |
208 | events: ['input', 'change', 'DOMNodeInserted'] |
209 | }, |
210 | md => meta.text.set(md) |
211 | ) |
212 | } |
213 | }) |
214 | |
215 | return editor |
216 | } |
217 |
Built with git-ssb-web