Browse Source

integrated wysiwyg

Skylsmoi 5 years ago
parent
commit
6658ef04d9
4 changed files with 118 additions and 21 deletions
  1. 94 3
      dist/index.html
  2. 1 0
      package.json
  3. 1 0
      src/component/HtmlDocument.jsx
  4. 22 18
      src/container/HtmlDocument.jsx

+ 94 - 3
dist/index.html View File

@@ -15,12 +15,103 @@
15 15
   <script src="./dev/popper-1.12.3.js"></script>
16 16
   <script src="./dev/bootstrap-4.0.0-beta.2.js"></script>
17 17
 
18
-  <div id='content'></div>
18
+  <script type="text/javascript" src="/asset/tinymce/jquery.tinymce.min.js"></script>
19
+  <script type="text/javascript" src="/asset/tinymce/tinymce.min.js"></script>
19 20
 
20
-  <script src='./html-documents.app.dev.js'></script>
21
+  <div id='content'></div>
21 22
 
22 23
   <script type='text/javascript'>
23
-    // appPageHtml.renderApp('content')
24
+    (function () {
25
+      wysiwyg = function (selector, handleOnChange) {
26
+        function base64EncodeAndTinyMceInsert (files) {
27
+          for (var i = 0; i < files.length; i++) {
28
+            if (files[i].size > 1000000)
29
+              files[i].allowed = confirm(files[i].name + " fait plus de 1mo et peut prendre du temps à insérer, voulez-vous continuer ?")
30
+          }
31
+
32
+          for (var i = 0; i < files.length; i++) {
33
+            if (files[i].allowed !== false && files[i].type.match('image.*')) {
34
+              var img = document.createElement('img')
35
+
36
+              var fr = new FileReader()
37
+
38
+              fr.readAsDataURL(files[i])
39
+
40
+              fr.onloadend = function (e) {
41
+                img.src = e.target.result
42
+                tinymce.activeEditor.execCommand('mceInsertContent', false, img.outerHTML)
43
+              }
44
+            }
45
+          }
46
+        }
47
+
48
+        // HACK: The tiny mce source code modal contain a textarea, but we
49
+        // can't edit it (like it's readonly). The following solution
50
+        // solve the bug: https://stackoverflow.com/questions/36952148/tinymce-code-editor-is-readonly-in-jtable-grid
51
+        $(document).on('focusin', function(e) {
52
+          if ($(e.target).closest(".mce-window").length) {
53
+            e.stopImmediatePropagation();
54
+          }
55
+        });
56
+
57
+        tinymce.init({
58
+          selector: selector,
59
+          // height: 130,
60
+          // width: 530,
61
+          menubar: false,
62
+          resize: false,
63
+          plugins: [
64
+            'advlist autolink lists link image charmap print preview anchor textcolor',
65
+            'searchreplace visualblocks code fullscreen',
66
+            'insertdatetime media table contextmenu paste code help'
67
+          ],
68
+          toolbar: 'insert | formatselect | bold italic underline strikethrough forecolor backcolor | link | alignleft aligncenter alignright alignjustify  | numlist bullist outdent indent  | table | code ',
69
+          content_css: [
70
+            '//fonts.googleapis.com/css?family=Lato:300,300i,400,400i',
71
+            '//www.tinymce.com/css/codepen.min.css'
72
+          ],
73
+          setup: function ($editor) {
74
+            $editor.on('change', function(e) {
75
+              handleOnChange({target: {value: $editor.getContent()}}) // target.value to emulate a js event so the react handler can expect one
76
+            })
77
+
78
+            //////////////////////////////////////////////
79
+            // add custom btn to handle image by selecting them with system explorer
80
+            $editor.addButton('customInsertImage', {
81
+              icon: 'mce-ico mce-i-image',
82
+              title: 'Image',
83
+              onclick: function () {
84
+                if ($('#hidden_tinymce_fileinput').length > 0) $('#hidden_tinymce_fileinput').remove()
85
+
86
+                fileTag = document.createElement('input')
87
+                fileTag.id = 'hidden_tinymce_fileinput'
88
+                fileTag.type = 'file'
89
+                $('body').append(fileTag)
90
+
91
+                $('#hidden_tinymce_fileinput').on('change', function () {
92
+                  base64EncodeAndTinyMceInsert($(this)[0].files)
93
+                })
94
+
95
+                $('#hidden_tinymce_fileinput').click()
96
+              }
97
+            })
98
+
99
+            //////////////////////////////////////////////
100
+            // Handle drag & drop image into TinyMce by encoding them in base64 (to avoid uploading them somewhere and keep saving comment in string format)
101
+            $editor
102
+              .on('drag dragstart dragend dragover dragenter dragleave drop', function (e) {
103
+                e.preventDefault()
104
+                e.stopPropagation()
105
+              })
106
+              .on('drop', function(e) {
107
+                base64EncodeAndTinyMceInsert(e.dataTransfer.files)
108
+              })
109
+          }
110
+        })
111
+      }
112
+    })()
24 113
   </script>
114
+
115
+  <script src='./html-documents.app.dev.js'></script>
25 116
 </body>
26 117
 </html>

+ 1 - 0
package.json View File

@@ -51,6 +51,7 @@
51 51
       "history",
52 52
       "btoa",
53 53
       "wysiwyg",
54
+      "tinymce",
54 55
       "GLOBAL_renderApp",
55 56
       "GLOBAL_unmountApp",
56 57
       "GLOBAL_dispatchEvent"

+ 1 - 0
src/component/HtmlDocument.jsx View File

@@ -14,6 +14,7 @@ const HtmlDocument = props => {
14 14
 
15 15
       {props.mode === MODE.EDIT &&
16 16
         <TextAreaApp
17
+          id={props.wysiwygNewVersion}
17 18
           customClass={'html-documents__editionmode'}
18 19
           onClickCancelBtn={props.onClickCloseEditMode}
19 20
           onClickValidateBtn={props.onClickValidateBtn}

+ 22 - 18
src/container/HtmlDocument.jsx View File

@@ -22,6 +22,7 @@ class HtmlDocument extends React.Component {
22 22
       content: props.data ? props.data.content : debug.content,
23 23
       timeline: props.data ? [] : [], // debug.timeline,
24 24
       newComment: '',
25
+      timelineWysiwyg: false,
25 26
       mode: MODE.VIEW
26 27
     }
27 28
 
@@ -43,21 +44,22 @@ class HtmlDocument extends React.Component {
43 44
 
44 45
   componentDidMount () {
45 46
     console.log('HtmlDocument did mount')
46
-    if (this.state.content.content_id === -1) return // debug case
47 47
 
48 48
     this.loadContent()
49
-    wysiwyg()
50 49
   }
51 50
 
52 51
   componentDidUpdate (prevProps, prevState) {
53
-    console.log('HtmlDocument did update', prevState, this.state)
54
-    if (!prevState.content || !this.state.content) return
52
+    const { state } = this
55 53
 
56
-    if (prevState.content.content_id !== this.state.content.content_id) {
57
-      this.loadContent()
58
-    }
54
+    console.log('HtmlDocument did update', prevState, state)
55
+    if (!prevState.content || !state.content) return
56
+
57
+    if (prevState.content.content_id !== state.content.content_id) this.loadContent()
59 58
 
60
-    if (prevState.mode === MODE.VIEW && this.state.mode === MODE.EDIT) wysiwyg()
59
+    if (state.mode === MODE.EDIT) wysiwyg('#wysiwygNewVersion', this.handleChangeText)
60
+
61
+    if (!prevState.timelineWysiwyg && state.timelineWysiwyg) wysiwyg('#wysiwygTimelineComment', this.handleChangeNewComment)
62
+    else if (prevState.timelineWysiwyg && !state.timelineWysiwyg) tinymce.remove('#wysiwygTimelineComment')
61 63
   }
62 64
 
63 65
   loadContent = async () => {
@@ -120,10 +122,7 @@ class HtmlDocument extends React.Component {
120 122
     fetch(`${this.state.config.apiUrl}/workspaces/${this.state.content.workspace_id}/html-documents/${this.state.content.content_id}`, {
121 123
       ...FETCH_CONFIG,
122 124
       method: 'PUT',
123
-      body: JSON.stringify({
124
-        label: label,
125
-        raw_content: rawContent
126
-      })
125
+      body: JSON.stringify({label: label, raw_content: rawContent})
127 126
     })
128 127
 
129 128
   handleClickBtnCloseApp = () => {
@@ -141,11 +140,10 @@ class HtmlDocument extends React.Component {
141 140
       })
142 141
   }
143 142
 
144
-  handleClickNewVersion = () => {
145
-    this.setState({ mode: MODE.EDIT })
146
-  }
143
+  handleClickNewVersion = () => this.setState({ mode: MODE.EDIT })
147 144
 
148 145
   handleCloseNewVersion = () => {
146
+    tinymce.remove('#wysiwygNewVersion')
149 147
     this.setState({ mode: MODE.VIEW })
150 148
   }
151 149
 
@@ -166,7 +164,7 @@ class HtmlDocument extends React.Component {
166 164
   }
167 165
 
168 166
   handleChangeText = e => {
169
-    const newText = e.target.value // because SyntheticEvent is pooled (react specificity
167
+    const newText = e.target.value // because SyntheticEvent is pooled (react specificity)
170 168
     this.setState(prev => ({content: {...prev.content, raw_content: newText}}))
171 169
   }
172 170
 
@@ -190,6 +188,7 @@ class HtmlDocument extends React.Component {
190 188
       .then(resSave => {
191 189
         if (resSave.apiResponse.status === 200) {
192 190
           this.setState({newComment: ''})
191
+          if (this.state.timelineWysiwyg) tinymce.get('wysiwygTimelineComment').setContent('')
193 192
           this.loadContent()
194 193
         } else {
195 194
           console.warn('Error saving html-document comment. Result:', resSave, 'content:', content, 'config:', config)
@@ -197,6 +196,8 @@ class HtmlDocument extends React.Component {
197 196
       })
198 197
   }
199 198
 
199
+  handleToggleWysiwyg = () => this.setState(prev => ({timelineWysiwyg: !prev.timelineWysiwyg}))
200
+
200 201
   handleChangeStatus = async newStatus => {
201 202
     const { config, content } = this.state
202 203
 
@@ -237,7 +238,7 @@ class HtmlDocument extends React.Component {
237 238
   }
238 239
 
239 240
   render () {
240
-    const { isVisible, loggedUser, content, timeline, newComment, config } = this.state
241
+    const { isVisible, loggedUser, content, timeline, newComment, timelineWysiwyg, config } = this.state
241 242
 
242 243
     if (!isVisible) return null
243 244
 
@@ -256,7 +257,7 @@ class HtmlDocument extends React.Component {
256 257
           availableStatus={config.availableStatuses}
257 258
           onClickNewVersionBtn={this.handleClickNewVersion}
258 259
           onChangeStatus={this.handleChangeStatus}
259
-          selectedStatus={config.availableStatuses.find(s => s.slug === content.status)} // peut être vide avant que api reponde
260
+          selectedStatus={config.availableStatuses.find(s => s.slug === content.status)} // might be empty while api hasn't responded yet
260 261
           onClickArchive={this.handleClickArchive}
261 262
           onClickDelete={this.handleClickDelete}
262 263
           i18n={i18n}
@@ -265,6 +266,7 @@ class HtmlDocument extends React.Component {
265 266
         <PopinFixedContent customClass={`${config.slug}__contentpage`}>
266 267
           <HtmlDocumentComponent
267 268
             mode={this.state.mode}
269
+            wysiwygNewVersion={'wysiwygNewVersion'}
268 270
             onClickCloseEditMode={this.handleCloseNewVersion}
269 271
             onClickValidateBtn={this.handleSaveHtmlDocument}
270 272
             version={timeline.filter(t => t.timelineType === 'revision').length}
@@ -278,8 +280,10 @@ class HtmlDocument extends React.Component {
278 280
             loggedUser={loggedUser}
279 281
             timelineData={timeline}
280 282
             newComment={newComment}
283
+            wysiwyg={timelineWysiwyg}
281 284
             onChangeNewComment={this.handleChangeNewComment}
282 285
             onClickValidateNewCommentBtn={this.handleClickValidateNewCommentBtn}
286
+            onClickWysiwygBtn={this.handleToggleWysiwyg}
283 287
           />
284 288
         </PopinFixedContent>
285 289
       </PopinFixed>