glib-web 2.3.0 → 2.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,8 +1,8 @@
1
1
  <template>
2
2
  <div :style="$styles()" :class="$classes()">
3
3
  <v-tabs v-model="mode" fixed-tabs>
4
- <v-tab>Editor</v-tab>
5
- <v-tab>Code</v-tab>
4
+ <v-tab @click="onRichTextClicked">Editor</v-tab>
5
+ <v-tab @click="onRawTextClicked">Code</v-tab>
6
6
  </v-tabs>
7
7
 
8
8
  <v-progress-linear v-if="showProgress" v-model="progress.value" />
@@ -11,22 +11,22 @@
11
11
  <VueEditor
12
12
  v-if="!rawMode"
13
13
  id="rich-editor"
14
- v-model="htmlValue"
14
+ v-model="richEditorValue"
15
15
  :editor-toolbar="customToolbar"
16
16
  use-custom-image-handler
17
- @text-change="onEditorChange"
17
+ @text-change="onRichTextEditorChanged"
18
18
  @image-added="uploadImage"
19
+ :editorOptions="editorSettings"
19
20
  />
20
21
  <!-- Hide these fields but don't remove them because these are the values that will get submitted. -->
21
22
  <div :style="{ display: rawMode ? 'block' : 'none' }">
22
23
  <v-textarea
23
24
  id="raw-editor"
24
- v-model="producedValue"
25
- :name="spec.name"
25
+ v-model="rawEditorValue"
26
26
  :style="$styles()"
27
27
  :class="$classes()"
28
28
  :outlined="$classes().includes('outlined')"
29
- @input="onCodeChange"
29
+ @input="onRawTextEditorChanged"
30
30
  ></v-textarea>
31
31
  <v-text-field
32
32
  v-for="(imageKey, index) in imageKeys"
@@ -39,6 +39,7 @@
39
39
  :value="images[imageKey]"
40
40
  />
41
41
  </div>
42
+ <input type="hidden" :name="spec.name" :value="producedValue" />
42
43
  </div>
43
44
  </template>
44
45
 
@@ -47,29 +48,156 @@ import Uploader from "../../utils/uploader";
47
48
  import { VueEditor, Quill } from "vue2-editor";
48
49
  import TurndownService from "turndown";
49
50
  import { gfm } from "turndown-plugin-gfm";
51
+ import eventFiltering from "../../utils/eventFiltering";
52
+ import QuillImageDropAndPaste from "quill-image-drop-and-paste";
53
+ import bus from "../../utils/eventBus";
54
+
55
+ Quill.register("modules/imageDropAndPaste", QuillImageDropAndPaste);
56
+
57
+ var ImageBlot = Quill.import("formats/image");
58
+ ImageBlot.sanitize = function (url) {
59
+ return url;
60
+ };
61
+
62
+ class Parser {
63
+ static markdownToHtml(data) {
64
+ return Utils.format.markdownForEditor(data);
65
+ }
66
+
67
+ static htmlToMarkdown(data) {
68
+ const turndownService = new TurndownService({ headingStyle: "atx" });
69
+ turndownService.use(gfm);
70
+ turndownService.addRule("strikethrough", {
71
+ filter: ["del", "s", "strike"],
72
+ replacement: function (content) {
73
+ return "~~" + content + "~~";
74
+ },
75
+ });
76
+
77
+ turndownService.addRule("codeblock", {
78
+ filter: ["pre"],
79
+ replacement: function (content) {
80
+ return "```\n" + content + "```";
81
+ },
82
+ });
83
+ return turndownService.turndown(data);
84
+ }
85
+ }
86
+
87
+ class TextEditor {
88
+ constructor(context) {
89
+ this.context = context;
90
+ }
91
+
92
+ // input and output should be either 'html' or 'markdown'
93
+ producedValue(data, input, output) {
94
+ if (output == "markdown") {
95
+ return this.markdownValue(this.replaceWithFakeImage(data), input);
96
+ } else if (output == "html") {
97
+ return this.replaceWithFakeImage(this.htmlValue(data, input));
98
+ }
99
+ }
100
+
101
+ markdownValue(data, input = "html") {
102
+ if (input == "html") {
103
+ return Parser.htmlToMarkdown(data);
104
+ } else if (input == "markdown") {
105
+ return data;
106
+ }
107
+ }
108
+
109
+ htmlValue(data, input) {
110
+ if (input == "html") {
111
+ return data;
112
+ } else if (input == "markdown") {
113
+ return Parser.markdownToHtml(data);
114
+ }
115
+ }
116
+
117
+ markdownValueWithRealImage(data, input) {
118
+ return this.markdownValue(this.replaceWithRealImage(data), input);
119
+ }
120
+
121
+ htmlValueWithRealImage(data, input) {
122
+ return this.htmlValue(this.replaceWithRealImage(data), input);
123
+ }
124
+
125
+ // replace {{image1}} to real image1 url
126
+ replaceWithRealImage(html) {
127
+ const vm = this.context;
128
+ return html.replace(/\{\{image([0-9]+)\}\}/g, function (_, index) {
129
+ const image = vm.spec.images[index - 1];
130
+ if (
131
+ image &&
132
+ vm.$type.isString(image.value) &&
133
+ vm.$type.isString(image.fileUrl)
134
+ ) {
135
+ const url = image.fileUrl;
136
+ const key = url.hashCode().toString();
137
+ vm.images[key] = image.value;
138
+ return url;
139
+ }
140
+ return "{{IMAGE_NOT_FOUND}}";
141
+ });
142
+ }
143
+
144
+ // replace image1 url with {{image1}}
145
+ replaceWithFakeImage(html) {
146
+ const vm = this.context;
147
+ let index = 0;
148
+ vm.imageKeys.clear();
149
+ return html.replace(/src="([^"]+)"|\!\[\]\((.+)\)/g, function (_, g1, g2) {
150
+ var imageValue = g1 || g2;
151
+ // It seems that quill encodes '&' in the URL to '&amp;' which would screw up key matching.
152
+ var decodedValue = imageValue.replace(/&amp;/g, "&");
153
+ const key = decodedValue.hashCode().toString();
154
+ vm.imageKeys.push(key);
155
+ if (g1) {
156
+ return `src="{{image${++index}}}"`;
157
+ } else {
158
+ return `{{image${++index}}}`;
159
+ }
160
+ });
161
+ }
162
+ }
50
163
 
51
164
  export default {
52
165
  components: { VueEditor },
53
166
  props: {
54
- spec: { type: Object, required: true }
167
+ spec: { type: Object, required: true },
55
168
  },
56
169
  data: () => ({
57
170
  customToolbar: [
58
171
  ["bold", "italic", "strike"],
59
172
  [{ header: 1 }, { header: 2 }, { header: 3 }],
60
173
  [{ list: "ordered" }, { list: "bullet" }],
61
- ["image", "link"]
174
+ ["image", "link"],
62
175
  ],
63
- htmlValue: "",
64
- cleanValue: null,
176
+ editorSettings: {
177
+ modules: {
178
+ imageDropAndPaste: {
179
+ // add an custom image handler
180
+ handler: function (imageDataUrl, type, imageData) {
181
+ bus.$emit("richText/dropOrPaste", {
182
+ file: imageData.toFile(),
183
+ editor: this.quill,
184
+ cursorLocation: this.getIndex(),
185
+ });
186
+ },
187
+ },
188
+ },
189
+ },
190
+ richEditorValue: "",
191
+ rawEditorValue: "",
65
192
  producedValue: "",
66
193
  images: {},
67
194
  imageKeys: [],
68
195
  progress: { value: -1 },
69
196
  imageUploader: {},
70
- produce: null,
197
+ textEditor: null,
71
198
  mode: null,
72
- turndownService: new TurndownService({ headingStyle: "atx" })
199
+ produce: null,
200
+ accept: null,
73
201
  }),
74
202
  computed: {
75
203
  showProgress() {
@@ -77,88 +205,37 @@ export default {
77
205
  },
78
206
  rawMode() {
79
207
  return this.mode == 1;
80
- }
81
- },
82
- watch: {
83
- cleanValue(val, oldVal) {
84
- if (oldVal == null) {
85
- // Don't update `producedValue` if this is first-time initialization to preserve the original value.
86
- return;
87
- }
88
- switch (this.produce) {
89
- case "html":
90
- this.producedValue = val;
91
- break;
92
- case "markdown":
93
- this.producedValue = this.turndownService.turndown(val);
94
- break;
95
- default:
96
- console.log(`Unsupported format: ${this.produce}`);
97
- }
98
208
  },
99
- producedValue(val) {
100
- switch (this.produce) {
101
- case "html":
102
- this.htmlValue = val;
103
- break;
104
- case "markdown":
105
- this.htmlValue = this.toHtmlValue(val);
106
- break;
107
- default:
108
- console.log(`Unsupported format: ${this.produce}`);
109
- }
110
- }
111
209
  },
210
+ watch: {},
112
211
  mounted() {
113
- this.registerScrollEvent();
212
+ bus.$on("richText/dropOrPaste", ({ file, editor, cursorLocation }) =>
213
+ this.uploadImage(file, editor, cursorLocation)
214
+ );
114
215
  },
115
216
  methods: {
116
217
  $ready() {
117
218
  this.produce = this.spec.produce || "markdown";
219
+ this.accept = this.spec.accept || "markdown";
118
220
 
119
- this.turndownService.use(gfm);
120
- this.turndownService.addRule("strikethrough", {
121
- filter: ["del", "s", "strike"],
122
- replacement: function(content) {
123
- return "~~" + content + "~~";
124
- }
125
- });
126
-
127
- this.turndownService.addRule("codeblock", {
128
- filter: ["pre"],
129
- replacement: function(content) {
130
- return "```\n" + content + "```";
131
- }
132
- });
133
-
221
+ this.textEditor = new TextEditor(this);
134
222
  this.imageUploader = this.spec.imageUploader;
135
223
 
136
- // Convert initial markdown value to html for displaying.
137
- this.producedValue = this.spec.value || "";
138
- this.htmlValue = this.toHtmlValue(this.producedValue);
139
- },
140
- toHtmlValue(producedValue) {
141
- const vm = this;
142
- var value = producedValue.replace(/\{\{image([0-9]+)\}\}/g, function(
143
- _,
144
- index
145
- ) {
146
- const image = vm.spec.images[index - 1];
147
- if (
148
- image &&
149
- vm.$type.isString(image.value) &&
150
- vm.$type.isString(image.fileUrl)
151
- ) {
152
- const url = image.fileUrl;
153
- const key = url.hashCode().toString();
154
- vm.images[key] = image.value;
155
- return url;
156
- }
157
- return "{{IMAGE_NOT_FOUND}}";
158
- });
159
- return this.produce == "markdown" ? Utils.format.markdown(value) : value;
224
+ this.richEditorValue = this.textEditor.htmlValueWithRealImage(
225
+ this.spec.value,
226
+ this.accept
227
+ );
228
+ this.rawEditorValue = this.textEditor.markdownValueWithRealImage(
229
+ this.spec.value,
230
+ this.accept
231
+ );
232
+ this.producedValue = this.textEditor.producedValue(
233
+ this.spec.value,
234
+ this.accept,
235
+ this.produce
236
+ );
160
237
  },
161
- uploadImage: function(file, editor, cursorLocation) {
238
+ uploadImage: function (file, editor, cursorLocation) {
162
239
  let vm = this;
163
240
  const uploaderSpec = this.imageUploader;
164
241
  // const input = this.$refs.directUploadFile
@@ -175,6 +252,7 @@ export default {
175
252
  upload.start((error, blob) => {
176
253
  if (error) {
177
254
  // Handle the error
255
+ console.error(error);
178
256
  } else {
179
257
  vm.insertImage(file, editor, cursorLocation, blob);
180
258
  }
@@ -184,17 +262,43 @@ export default {
184
262
  });
185
263
  }
186
264
  },
187
- onEditorChange() {
188
- this.separateOutImages();
189
- this.onCodeChange();
265
+ onRichTextClicked() {
266
+ this.richEditorValue = this.textEditor.htmlValue(
267
+ this.rawEditorValue,
268
+ "markdown"
269
+ );
270
+ },
271
+ onRawTextClicked() {
272
+ this.rawEditorValue = this.textEditor.markdownValue(
273
+ this.richEditorValue,
274
+ "html"
275
+ );
190
276
  },
191
- onCodeChange() {
192
- Utils.type.ifObject(this.spec.onChange, onChange => {
277
+ onRichTextEditorChanged: eventFiltering.debounce(function () {
278
+ this.producedValue = this.textEditor.producedValue(
279
+ this.richEditorValue,
280
+ "html",
281
+ this.produce
282
+ );
283
+
284
+ this.onChange(this.producedValue);
285
+ }),
286
+ onRawTextEditorChanged: eventFiltering.debounce(function () {
287
+ this.producedValue = this.textEditor.producedValue(
288
+ this.rawEditorValue,
289
+ "markdown",
290
+ this.produce
291
+ );
292
+
293
+ this.onChange(this.producedValue);
294
+ }),
295
+ onChange(producedValue) {
296
+ Utils.type.ifObject(this.spec.onChange, (onChange) => {
193
297
  this.$nextTick(() => {
194
298
  const params = {
195
299
  [this.spec.paramNameForFormData || "formData"]: {
196
- [this.fieldName]: this.fieldModel
197
- }
300
+ [this.fieldName]: producedValue,
301
+ },
198
302
  };
199
303
 
200
304
  const data = Object.assign({}, onChange, params);
@@ -202,32 +306,16 @@ export default {
202
306
  });
203
307
  });
204
308
  },
205
- separateOutImages: function() {
206
- const vm = this;
207
- var index = 0;
208
- vm.imageKeys.clear();
209
- // TODO: Fix to avoid replacing <video src="">
210
- this.cleanValue = this.htmlValue.replace(/src="([^"]+)"/g, function(
211
- _,
212
- imageValue
213
- ) {
214
- // It seems that quill encodes '&' in the URL to '&amp;' which would screw up key matching.
215
- var decodedValue = imageValue.replace(/&amp;/g, "&");
216
- const key = decodedValue.hashCode().toString();
217
- vm.imageKeys.push(key);
218
- return `src="{{image${++index}}}"`;
219
- });
220
- },
221
- insertImage: function(file, Editor, cursorLocation, blob) {
309
+ insertImage: function (file, Editor, cursorLocation, blob) {
222
310
  let vm = this;
223
311
  var reader = new FileReader();
224
- reader.onload = function(e) {
312
+ reader.onload = function (e) {
225
313
  // vm.fileUrl = e.target.result;
226
314
  if (file.type.indexOf("image") !== -1) {
227
315
  var image = new Image();
228
- image.src = e.target.result;
316
+ image.src = URL.createObjectURL(file);
229
317
 
230
- image.onload = function() {
318
+ image.onload = function () {
231
319
  Editor.insertEmbed(cursorLocation, "image", image.src);
232
320
  };
233
321
 
@@ -238,7 +326,7 @@ export default {
238
326
  reader.readAsDataURL(file);
239
327
  vm.progress.value = -1;
240
328
  },
241
- updateToolbar: function(wrapper, toolbar, container) {
329
+ updateToolbar: function (wrapper, toolbar, container) {
242
330
  if (wrapper.scrollTop >= container.offsetTop) {
243
331
  toolbar.classList.add("sticky");
244
332
  toolbar.style.top = `${wrapper.offsetTop}px`;
@@ -251,7 +339,7 @@ export default {
251
339
  toolbar.style.width = "auto";
252
340
  }
253
341
  },
254
- registerScrollEvent: function() {
342
+ registerScrollEvent: function () {
255
343
  const wrapper = document.querySelector(".v-dialog") || window;
256
344
  const toolbar = this.$el.querySelector(".ql-toolbar");
257
345
  const container = this.$el.querySelector(".ql-container");
@@ -264,8 +352,8 @@ export default {
264
352
  this.updateToolbar(wrapper, toolbar, container)
265
353
  );
266
354
  }
267
- }
268
- }
355
+ },
356
+ },
269
357
  };
270
358
  </script>
271
359
 
@@ -24,7 +24,7 @@ export default {
24
24
  },
25
25
  action_merge(mergedSpec) {
26
26
  Object.assign(this.updatedSpec, mergedSpec);
27
- this.$refs.delegate.updateData();
27
+ this.$refs.delegate.updateData(true);
28
28
  }
29
29
  }
30
30
  };
package/components/h4.vue CHANGED
@@ -17,6 +17,11 @@ export default {
17
17
  mixins: [textMixin],
18
18
  props: {
19
19
  spec: { type: Object, required: true }
20
+ },
21
+ methods: {
22
+ action_merge(mergedSpec) {
23
+ Object.assign(this.spec, mergedSpec);
24
+ }
20
25
  }
21
26
  };
22
27
  </script>
@@ -177,6 +177,20 @@ export default {
177
177
  this._typingTimer = null;
178
178
  GLib.action.execute(this.spec.onTypeEnd, this);
179
179
  }, duration);
180
+ },
181
+ $executeOnChange(newValue) {
182
+ Utils.type.ifObject(this.spec.onChange, onChange => {
183
+ this.$nextTick(() => {
184
+ const value = newValue || this.fieldModel;
185
+ const params = {
186
+ [this.spec.paramNameForFormData || "formData"]: {
187
+ [this.fieldName]: value
188
+ }
189
+ };
190
+ const data = Object.assign({}, onChange, params);
191
+ GLib.action.execute(data, this);
192
+ });
193
+ });
180
194
  }
181
195
  }
182
196
  };
@@ -5,7 +5,7 @@ export default {
5
5
  while (parent != null) {
6
6
  if (
7
7
  Utils.type.isObject(parent.spec) &&
8
- Utils.app.vueName(parent) == name
8
+ GLib.component.vueName(parent) == name
9
9
  ) {
10
10
  return parent;
11
11
  }
@@ -191,7 +191,7 @@ export default {
191
191
  },
192
192
  _linkFieldModels() {
193
193
  const hasCondition = this.spec && this.spec.showIf;
194
- const name = Utils.app.vueName(this);
194
+ const name = GLib.component.vueName(this);
195
195
  const isField = name && name.startsWith("fields-");
196
196
  if (hasCondition || isField) {
197
197
  const form = this.$closest("panels-form");
@@ -205,8 +205,11 @@ export default {
205
205
  this.fieldModel = this._sanitizeValue(this.spec.value);
206
206
  }
207
207
  },
208
- $classes(spec) {
209
- const properties = spec || this.spec;
208
+ $classes(spec, defaultViewName) {
209
+ const properties = Object.assign(
210
+ { view: defaultViewName },
211
+ spec || this.spec
212
+ );
210
213
  const componentName = Utils.app.componentName(properties.view);
211
214
  const classes = (properties.styleClasses || []).concat(componentName);
212
215
 
@@ -6,7 +6,7 @@
6
6
  :href="$href()"
7
7
  @click="$onClick()"
8
8
  >
9
- <template v-for="(item, index) in spec.childViews">
9
+ <template v-for="(item, index) in childViews">
10
10
  <glib-component :key="index" :spec="item" />
11
11
  </template>
12
12
  </component>
@@ -17,6 +17,11 @@ export default {
17
17
  props: {
18
18
  spec: { type: Object, required: true }
19
19
  },
20
+ data() {
21
+ return {
22
+ childViews: null
23
+ };
24
+ },
20
25
  computed: {
21
26
  cssClasses: function() {
22
27
  const classes = this.$classes().concat("layouts-horizontal");
@@ -58,8 +63,19 @@ export default {
58
63
  }
59
64
  },
60
65
  methods: {
66
+ $ready() {
67
+ this.childViews = this.spec.childViews;
68
+ console.log("C1", this.childViews);
69
+ },
61
70
  $displayValue() {
62
71
  return "flex";
72
+ },
73
+ action_merge(mergedSpec) {
74
+ console.log("mergedSpec1", this.spec, this.spec.childViews);
75
+ Object.assign(this.spec, mergedSpec);
76
+ console.log("mergedSpec2", this.spec, this.spec.childViews);
77
+ // this.$refs.delegate.updateData();
78
+ this.$ready();
63
79
  }
64
80
  }
65
81
  };
@@ -1,35 +1,46 @@
1
1
  <template>
2
- <v-container :class="$classes()">
2
+ <!-- Paddings cannot be applied to v-timeline directly -->
3
+ <div :style="$styles()" :class="$classes()">
3
4
  <v-timeline v-if="events" dense align-top>
4
5
  <v-timeline-item
5
6
  v-for="(item, index) in events"
6
7
  :key="index"
7
- color="white"
8
- fill-dot
9
- :small="$classes().includes('small')"
10
- :large="$classes().includes('large')"
11
- class="my-6"
8
+ :color="item.backgroundColor || 'white'"
9
+ :small="itemClasses(item).includes('small')"
10
+ :large="itemClasses(item).includes('large')"
12
11
  :hide-dot="item.hide_dot"
12
+ fill-dot
13
13
  >
14
14
  <template v-slot:icon>
15
- <div :class="$classes().includes('outlined') ? 'outlined-dots' : ''">
16
- <div v-if="item.label" class="number-circle">
17
- {{ item.label }}
15
+ <div
16
+ :class="
17
+ itemClasses(item).includes('outlined') ? 'outlined-dots' : ''
18
+ "
19
+ >
20
+ <div
21
+ v-if="item.text"
22
+ class="number-circle"
23
+ :style="{ color: item.color }"
24
+ >
25
+ {{ item.text }}
18
26
  </div>
19
27
  <div v-else class="icon">
20
28
  <common-icon
21
29
  :spec="{
22
- material: { name: item.icon, size: iconSize() },
30
+ material: { name: item.icon, size: iconSize(item) },
23
31
  color: item.color
24
32
  }"
25
33
  />
26
34
  </div>
27
35
  </div>
28
36
  </template>
29
- <panels-responsive :spec="{ childViews: [childViews[index]] }" />
37
+ <panels-responsive
38
+ v-if="childViews"
39
+ :spec="{ childViews: [childViews[index]] }"
40
+ />
30
41
  </v-timeline-item>
31
42
  </v-timeline>
32
- </v-container>
43
+ </div>
33
44
  </template>
34
45
 
35
46
  <script>
@@ -48,19 +59,38 @@ export default {
48
59
  this.events = this.spec.events;
49
60
  this.childViews = this.spec.childViews;
50
61
  },
51
- iconSize() {
52
- return this.$classes().includes("small") ? 16 : 24;
62
+ iconSize(item) {
63
+ return this.itemClasses(item).includes("small") ? 20 : 24;
64
+ },
65
+ itemClasses(item) {
66
+ return item.styleClasses || [];
67
+ },
68
+ action_merge(mergedSpec) {
69
+ Object.assign(this.spec, mergedSpec);
70
+ this.$ready();
53
71
  }
54
72
  }
55
73
  };
56
74
  </script>
57
75
 
76
+ <style lang="scss">
77
+ .v-timeline-item__dot {
78
+ height: 36px;
79
+ width: 36px;
80
+ }
81
+ .v-timeline-item__dot--small {
82
+ height: 20px;
83
+ width: 20px;
84
+ }
85
+ </style>
86
+
58
87
  <style lang="scss" scoped>
59
88
  .v-timeline {
60
89
  padding-top: 0px;
61
90
  }
62
91
  .v-timeline-item {
63
- padding-bottom: 0px !important;
92
+ // Minimum line distance
93
+ padding-bottom: 10px;
64
94
  }
65
95
  .container {
66
96
  padding-top: 0px;
@@ -73,7 +103,8 @@ export default {
73
103
  margin: auto;
74
104
  text-align: center;
75
105
  font-weight: bold;
76
- font-size: 22px;
106
+ padding: 4px 0;
107
+ // font-size: 22px;
77
108
  }
78
109
  .outlined .v-timeline-item {
79
110
  ::v-deep .v-timeline-item__dot {
@@ -22,3 +22,7 @@ Array.prototype.first = function() {
22
22
  Array.prototype.last = function() {
23
23
  return this[this.length - 1];
24
24
  };
25
+
26
+ Array.prototype.random = function() {
27
+ return this[Math.floor(Math.random() * this.length)];
28
+ };
@@ -19,3 +19,7 @@ String.prototype.contains = function(substr) {
19
19
  String.prototype.presence = function() {
20
20
  return this.length > 0 ? this.toString() : null;
21
21
  };
22
+
23
+ String.prototype.truncate = function(n) {
24
+ return this.length > n ? this.slice(0, n - 1) + "..." : this.toString();
25
+ };