git ssb

2+

mixmix / ticktack



Tree: 69fecb32f29fb953037e1e0b67db4d89bb1cab8b

Files: 69fecb32f29fb953037e1e0b67db4d89bb1cab8b / app / page / blogNew.js

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

Built with git-ssb-web