@websy/websy-designs 1.10.6 → 1.11.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.
@@ -6,6 +6,7 @@
6
6
  WebsyPubSub
7
7
  WebsyForm
8
8
  MultiForm
9
+ MediaUpload
9
10
  WebsyDatePicker
10
11
  WebsyDropdown
11
12
  WebsyRouter
@@ -1835,6 +1836,7 @@ class WebsyDropdown {
1835
1836
  const headerPos = WebsyUtils.getElementPos(headerEl)
1836
1837
  const contentPos = WebsyUtils.getElementPos(contentEl)
1837
1838
  if (this.options.style === 'plain' && headerPos.width > 0 && headerPos.height > 0) {
1839
+ contentEl.style.left = 'unset'
1838
1840
  contentEl.style.right = `calc(100vw - ${headerPos.right}px)`
1839
1841
  contentEl.style.width = `${Math.max(this.options.minWidth, headerEl.clientWidth)}px`
1840
1842
  if (headerPos.bottom + contentPos.height > window.innerHeight) {
@@ -1847,6 +1849,7 @@ class WebsyDropdown {
1847
1849
  }
1848
1850
  else if (this.options.style === 'plain' && headerPos.width === 0 && headerPos.height === 0) {
1849
1851
  const targetPos = WebsyUtils.getElementPos(event.target)
1852
+ contentEl.style.left = 'unset'
1850
1853
  contentEl.style.right = `calc(100vw - ${targetPos.right}px)`
1851
1854
  contentEl.style.width = `${Math.max(this.options.minWidth, targetPos.width)}px`
1852
1855
  }
@@ -2341,6 +2344,7 @@ class WebsyForm {
2341
2344
  const defaults = {
2342
2345
  submit: { text: 'Save', classes: [] },
2343
2346
  useRecaptcha: false,
2347
+ recaptchaAction: 'submit',
2344
2348
  clearAfterSave: false,
2345
2349
  fields: [],
2346
2350
  mode: 'add',
@@ -2403,6 +2407,23 @@ class WebsyForm {
2403
2407
  resolve(false)
2404
2408
  }
2405
2409
  }
2410
+ else if (this.options.useRecaptchaV3 === true) {
2411
+ grecaptcha.ready(() => {
2412
+ grecaptcha.execute(ENVIRONMENT.RECAPTCHA_KEY, { action: this.options.recaptchaAction }).then(token => {
2413
+ this.apiService.add('google/checkrecaptcha', {grecaptcharesponse: token}).then(response => {
2414
+ if (response.success && response.success === true) {
2415
+ resolve(true)
2416
+ grecaptcha.reset(`${this.elementId}_recaptcha`, {sitekey: ENVIRONMENT.RECAPTCHA_KEY})
2417
+ }
2418
+ else {
2419
+ resolve(false)
2420
+ }
2421
+ })
2422
+ }, err => {
2423
+ console.log(err)
2424
+ })
2425
+ })
2426
+ }
2406
2427
  else {
2407
2428
  resolve(true)
2408
2429
  }
@@ -2672,7 +2693,7 @@ class WebsyForm {
2672
2693
  if (f.component) {
2673
2694
  componentsToProcess.push(f)
2674
2695
  html += `
2675
- ${i > 0 ? '-->' : ''}<div id='${this.elementId}_${f.field}_inputContainer' style='${f.style || ''}' class='websy-input-container ${f.classes ? f.classes.join(' ') : ''}'>
2696
+ ${i > 0 ? '-->' : ''}<div id='${this.elementId}_${f.field}_inputContainer' style='${f.style || ''}' class='websy-input-container ${f.classes ? f.classes.join(' ') : ''} ${f.component === 'MediaUpload' ? 'media-upload' : ''}'>
2676
2697
  ${f.label ? `<label for="${f.field}">${f.label}</label>` : ''}${f.required === true ? '<span class="websy-form-required-value">*</span>' : ''}
2677
2698
  <div id='${this.elementId}_input_${f.field}_component' class='form-component'></div>
2678
2699
  <span id='${this.elementId}_${f.field}_error' class='websy-form-validation-error'></span>
@@ -2692,7 +2713,7 @@ class WebsyForm {
2692
2713
  name="${f.field}"
2693
2714
  ${(f.attributes || []).join(' ')}
2694
2715
  class="websy-input websy-textarea"
2695
- ></textarea>
2716
+ >${f.value || ''}</textarea>
2696
2717
  <span id='${this.elementId}_${f.field}_error' class='websy-form-validation-error'></span>
2697
2718
  </div><!--
2698
2719
  `
@@ -2711,7 +2732,7 @@ class WebsyForm {
2711
2732
  ${(f.attributes || []).join(' ')}
2712
2733
  name="${f.field}"
2713
2734
  placeholder="${f.placeholder || ''}"
2714
- value="${f.value || ''}"
2735
+ value="${f.type === 'date' ? '' : f.value || ''}"
2715
2736
  valueAsDate="${f.type === 'date' ? f.value : ''}"
2716
2737
  oninvalidx="this.setCustomValidity('${f.invalidMessage || 'Please fill in this field.'}')"
2717
2738
  />
@@ -2740,7 +2761,7 @@ class WebsyForm {
2740
2761
  `
2741
2762
  el.innerHTML = html
2742
2763
  this.processComponents(componentsToProcess, () => {
2743
- if (this.options.useRecaptcha === true && typeof grecaptcha !== 'undefined') {
2764
+ if ((this.options.useRecaptcha === true || this.options.useRecaptchaV3 === true) && typeof grecaptcha !== 'undefined') {
2744
2765
  this.recaptchaReady()
2745
2766
  }
2746
2767
  })
@@ -2759,6 +2780,9 @@ class WebsyForm {
2759
2780
  if (this.fieldMap[field].type === 'checkbox') {
2760
2781
  el.checked = value
2761
2782
  }
2783
+ if (this.fieldMap[field].type === 'date') {
2784
+ el.valueAsDate = value
2785
+ }
2762
2786
  }
2763
2787
  else {
2764
2788
  console.error(`Input for ${field} does not exist in form.`)
@@ -2830,6 +2854,9 @@ class WebsyForm {
2830
2854
  if (recaptchErrEl) {
2831
2855
  recaptchErrEl.classList.remove('websy-hidden')
2832
2856
  }
2857
+ if (this.options.submitErr) {
2858
+ this.options.submitErr()
2859
+ }
2833
2860
  }
2834
2861
  })
2835
2862
  }
@@ -2900,10 +2927,12 @@ class MultiForm {
2900
2927
  constructor (elementId, options) {
2901
2928
  this.elementId = elementId
2902
2929
  const DEFAULTS = {
2903
- addButton: `<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40" viewBox="0 0 512 512"><line x1="256" y1="112" x2="256" y2="400" style="fill:none;stroke-linecap:round;stroke-linejoin:round;stroke-width:32px"/><line x1="400" y1="256" x2="112" y2="256" style="fill:none;stroke-linecap:round;stroke-linejoin:round;stroke-width:32px"/></svg>`,
2904
- deleteButton: `<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40" viewBox="0 0 512 512"><line x1="368" y1="368" x2="144" y2="144" style="fill:none;stroke-linecap:round;stroke-linejoin:round;stroke-width:32px"/><line x1="368" y1="144" x2="144" y2="368" style="fill:none;stroke-linecap:round;stroke-linejoin:round;stroke-width:32px"/></svg>`,
2930
+ addIcon: `<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40" viewBox="0 0 512 512"><line x1="256" y1="112" x2="256" y2="400" style="fill:none;stroke-linecap:round;stroke-linejoin:round;stroke-width:32px"/><line x1="400" y1="256" x2="112" y2="256" style="fill:none;stroke-linecap:round;stroke-linejoin:round;stroke-width:32px"/></svg>`,
2931
+ deleteIcon: `<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40" viewBox="0 0 512 512"><line x1="368" y1="368" x2="144" y2="144" style="fill:none;stroke-linecap:round;stroke-linejoin:round;stroke-width:32px"/><line x1="368" y1="144" x2="144" y2="368" style="fill:none;stroke-linecap:round;stroke-linejoin:round;stroke-width:32px"/></svg>`,
2905
2932
  allowAdd: true,
2906
- allowDelete: true
2933
+ allowDelete: true,
2934
+ addLabel: '',
2935
+ deleteLabel: ''
2907
2936
  }
2908
2937
  this.options = Object.assign({}, DEFAULTS, options)
2909
2938
  this.formData = []
@@ -2912,7 +2941,12 @@ class MultiForm {
2912
2941
  const el = document.getElementById(elementId)
2913
2942
  if (el) {
2914
2943
  el.addEventListener('click', this.handleClick.bind(this))
2915
- el.innerHTML = `<div id='${elementId}_container' class='websy-multi-form-container'></div>`
2944
+ el.innerHTML = `
2945
+ <div id='${elementId}_container' class='websy-multi-form-container'></div>
2946
+ <button id='${this.elementId}_addButton' class='websy-multi-form-add'>
2947
+ ${this.options.addIcon}${this.options.addLabel}
2948
+ </button>
2949
+ `
2916
2950
  }
2917
2951
  this.render()
2918
2952
  }
@@ -2921,24 +2955,28 @@ class MultiForm {
2921
2955
  this.render()
2922
2956
  }
2923
2957
  addEntry () {
2924
- const el = document.getElementById(`${this.elementId}_container`)
2925
- let newId = WebsyDesigns.Utils.createIdentity()
2926
- const newFormEl = document.createElement('div')
2927
- newFormEl.id = `${this.elementId}_${newId}_formContainer`
2928
- newFormEl.classList.add('websy-multi-form-form-container')
2929
- newFormEl.innerHTML = `
2930
- <div id='${this.elementId}_${newId}_form' class='websy-multi-form-form'>
2931
- </div>
2932
- <button id='${this.elementId}_${newId}_deleteButton' data-formid='${newId}' class='hidden websy-multi-form-delete'>
2933
- ${this.options.deleteButton}
2934
- </button>
2935
- <button id='${this.elementId}_${newId}_addButton' data-formid='${newId}' class='websy-multi-form-add'>
2936
- ${this.options.addButton}
2937
- </button>
2938
- `
2939
- el.appendChild(newFormEl)
2940
- let formOptions = Object.assign({}, this.options, { fields: [...this.options.fields.map(f => Object.assign({}, f))] })
2941
- this.forms.push(new WebsyDesigns.Form(`${this.elementId}_${newId}_form`, formOptions))
2958
+ const addEl = document.getElementById(`${this.elementId}_addButton`)
2959
+ if (typeof this.options.maxRows === 'undefined' || this.forms.length < this.options.maxRows) {
2960
+ const el = document.getElementById(`${this.elementId}_container`)
2961
+ let newId = WebsyDesigns.Utils.createIdentity()
2962
+ const newFormEl = document.createElement('div')
2963
+ newFormEl.id = `${this.elementId}_${newId}_formContainer`
2964
+ newFormEl.classList.add('websy-multi-form-form-container')
2965
+ let html = `
2966
+ <div id='${this.elementId}_${newId}_form' class='websy-multi-form-form'>
2967
+ </div>
2968
+ <button id='${this.elementId}_${newId}_deleteButton' data-formid='${newId}' class='websy-multi-form-delete'>
2969
+ ${this.options.deleteIcon}${this.options.deleteLabel}
2970
+ </button>
2971
+ `
2972
+ newFormEl.innerHTML = html
2973
+ el.appendChild(newFormEl)
2974
+ let formOptions = Object.assign({}, this.options, { fields: [...this.options.fields.map(f => Object.assign({}, f))] })
2975
+ this.forms.push(new WebsyDesigns.Form(`${this.elementId}_${newId}_form`, formOptions))
2976
+ if (addEl) {
2977
+ addEl.style.display = this.forms.length < this.options.maxRows ? 'flex' : 'none'
2978
+ }
2979
+ }
2942
2980
  }
2943
2981
  clear () {
2944
2982
  this.formData = []
@@ -2950,12 +2988,7 @@ class MultiForm {
2950
2988
  }
2951
2989
  }
2952
2990
  get data () {
2953
- const d = this.forms.map(f => (f.data))
2954
- console.log('forms data', d)
2955
- if (this.options.allowAdd !== false) {
2956
- // we don't return the last form
2957
- d.pop()
2958
- }
2991
+ const d = this.forms.map(f => (f.data))
2959
2992
  return d
2960
2993
  }
2961
2994
  set data (d) {
@@ -2967,16 +3000,6 @@ class MultiForm {
2967
3000
  }
2968
3001
  handleClick (event) {
2969
3002
  if (event.target.classList.contains('websy-multi-form-add')) {
2970
- let id = event.target.getAttribute('data-formid')
2971
- // hide add button and show delete button
2972
- const addButtonEl = document.getElementById(`${this.elementId}_${id}_addButton`)
2973
- if (addButtonEl) {
2974
- addButtonEl.classList.add('hidden')
2975
- }
2976
- const deleteButtonEl = document.getElementById(`${this.elementId}_${id}_deleteButton`)
2977
- if (deleteButtonEl) {
2978
- deleteButtonEl.classList.remove('hidden')
2979
- }
2980
3003
  // add new form
2981
3004
  if (this.options.allowAdd === true) {
2982
3005
  this.addEntry()
@@ -3001,6 +3024,10 @@ class MultiForm {
3001
3024
  if (el) {
3002
3025
  el.remove()
3003
3026
  }
3027
+ const addEl = document.getElementById(`${this.elementId}_addButton`)
3028
+ if (addEl) {
3029
+ addEl.style.display = (typeof this.options.maxRows === 'undefined' || this.forms.length < this.options.maxRows) ? 'flex' : 'none'
3030
+ }
3004
3031
  // delete form element based on id
3005
3032
  }
3006
3033
  }
@@ -3020,7 +3047,7 @@ class MultiForm {
3020
3047
  if (this.options.allowDelete === true) {
3021
3048
  html += `
3022
3049
  <button id='${this.elementId}_${d.formId}_deleteButton' data-formid='${d.formId}' data-rowid='${d.id}' class='websy-multi-form-delete'>
3023
- ${this.options.deleteButton}
3050
+ ${this.options.deleteIcon}${this.options.deleteLabel}
3024
3051
  </button>
3025
3052
  `
3026
3053
  }
@@ -3029,20 +3056,6 @@ class MultiForm {
3029
3056
  `
3030
3057
  })
3031
3058
  let id = WebsyDesigns.Utils.createIdentity()
3032
- if (this.options.allowAdd === true) {
3033
- html += `
3034
- <div id='${this.elementId}_${id}_formContainer' class='websy-multi-form-form-container'>
3035
- <div id='${this.elementId}_${id}_form' class='websy-multi-form-form'>
3036
- </div>
3037
- <button id='${this.elementId}_${id}_deleteButton' data-formid='${id}' class='hidden websy-multi-form-delete'>
3038
- ${this.options.deleteButton}
3039
- </button>
3040
- <button id='${this.elementId}_${id}_addButton' data-formid='${id}' class='websy-multi-form-add'>
3041
- ${this.options.addButton}
3042
- </button>
3043
- </div>
3044
- `
3045
- }
3046
3059
  el.innerHTML = html
3047
3060
  this.forms = new Array(this.formData.length)
3048
3061
  this.formData.forEach((d, i) => {
@@ -3051,11 +3064,15 @@ class MultiForm {
3051
3064
  formObject.data = d
3052
3065
  this.forms[i] = formObject
3053
3066
  })
3054
- if (this.options.allowAdd === true) {
3055
- let formOptions = Object.assign({}, this.options, { fields: [...this.options.fields.map(f => Object.assign({}, f))] })
3056
- let formObject = new WebsyDesigns.Form(`${this.elementId}_${id}_form`, formOptions)
3057
- this.forms.push(formObject)
3058
- }
3067
+ const addEl = document.getElementById(`${this.elementId}_addButton`)
3068
+ if (addEl) {
3069
+ if (this.options.allowAdd === true) {
3070
+ addEl.style.display = (typeof this.options.maxRows === 'undefined' || this.forms.length < this.options.maxRows) ? 'flex' : 'none'
3071
+ }
3072
+ else {
3073
+ addEl.style.display = 'none'
3074
+ }
3075
+ }
3059
3076
  }
3060
3077
  }
3061
3078
  validateForm () {
@@ -3693,6 +3710,181 @@ class WebsyNavigationMenu {
3693
3710
  }
3694
3711
  }
3695
3712
 
3713
+ /*
3714
+ global
3715
+ FormData
3716
+ FileReader
3717
+ Image
3718
+ */
3719
+ class MediaUpload {
3720
+ constructor (elementId, options) {
3721
+ const defaults = {
3722
+ allowMultiple: false,
3723
+ createThumbnail: false,
3724
+ thumbSize: 300,
3725
+ supportedTypes: ['image/png', 'image/jpg']
3726
+ }
3727
+ this.media = []
3728
+ this.options = Object.assign({}, defaults, options)
3729
+ this.elementId = elementId
3730
+ if (!elementId) {
3731
+ console.log('No element Id provided')
3732
+ return
3733
+ }
3734
+ const el = document.getElementById(elementId)
3735
+ if (el) {
3736
+ el.addEventListener('change', this.handleChange.bind(this))
3737
+ el.innerHTML = `
3738
+ <div class='websy-upload-form-container'>
3739
+ <span>Drag and drop a file or click to browse.</span>
3740
+ <form id="${this.elementId}_form" enctype="multipart/form-data">
3741
+ <input id="${this.elementId}_file" type="file" name="${this.options.name || 'media'}" accept="${this.options.supportedTypes.join(' ')}" multiple>
3742
+ </form>
3743
+ </div>
3744
+ <div id='${this.elementId}_uploaded' class='websy-uploaded-media'></div>
3745
+ `
3746
+ this.render()
3747
+ }
3748
+ }
3749
+ set data (d = []) {
3750
+ if (Array.isArray(d)) {
3751
+ this.media = d
3752
+ }
3753
+ else {
3754
+ this.media = [d]
3755
+ }
3756
+ this.render()
3757
+ }
3758
+ get data () {
3759
+ return this.media
3760
+ }
3761
+ createHtml (count) {
3762
+ let html = ''
3763
+ for (let i = 0; i < count; i++) {
3764
+ html += `
3765
+ <div>
3766
+ <img id='${this.elementId}_media_${i}'/>
3767
+ </div>
3768
+ `
3769
+ }
3770
+ return html
3771
+ }
3772
+ getForm () {
3773
+ let formFound = false
3774
+ let el = document.getElementById(`${this.elementId}_file`)
3775
+ if (el) {
3776
+ while (formFound === false && el.tagName !== 'BODY') {
3777
+ el = el.parentElement
3778
+ if (el.tagName === 'FORM') {
3779
+ formFound = true
3780
+ return el
3781
+ }
3782
+ }
3783
+ }
3784
+ return null
3785
+ }
3786
+ handleChange (event) {
3787
+ this.fileList = []
3788
+ let uploadForm = document.getElementById(`${this.elementId}_form`)
3789
+ if (!uploadForm) {
3790
+ uploadForm = this.getForm()
3791
+ }
3792
+ if (!uploadForm) {
3793
+ console.error(`The element ${this.elementId}_file does not belong to a form.`)
3794
+ return
3795
+ }
3796
+ const formData = new FormData(uploadForm)
3797
+ const html = this.createHtml(formData.length)
3798
+ formData.forEach((value, key) => {
3799
+ if (key === (this.options.name || 'media')) {
3800
+ this.fileList.push({ name: key, file: value })
3801
+ }
3802
+ })
3803
+ const resultEl = document.getElementById(`${this.elementId}_uploaded`)
3804
+ resultEl.innerHTML = html
3805
+ this.uploadItem(0, this.fileList)
3806
+ }
3807
+ render () {
3808
+ if (this.media.length > 0) {
3809
+ const resultEl = document.getElementById(`${this.elementId}_uploaded`)
3810
+ resultEl.innerHTML = this.createHtml(this.media.length)
3811
+ this.media.forEach((m, i) => {
3812
+ const imgEl = document.getElementById(`${this.elementId}_media_${i}`)
3813
+ imgEl.setAttribute('src', `data:${m.type};base64,` + m.data)
3814
+ })
3815
+ }
3816
+ }
3817
+ uploadItem (index, items, callbackFn) {
3818
+ if (!items[index]) {
3819
+ callbackFn()
3820
+ }
3821
+ else {
3822
+ const r = new FileReader()
3823
+ const mediaData = {
3824
+ type: items[index].file.type,
3825
+ name: items[index].file.name,
3826
+ size: items[index].file.size
3827
+ }
3828
+ r.onloadend = () => {
3829
+ let imgCanvas = document.createElement('canvas')
3830
+ let imgContext = imgCanvas.getContext('2d')
3831
+ let thumbCanvas = document.createElement('canvas')
3832
+ let thumbContext = thumbCanvas.getContext('2d')
3833
+ let img = new Image()
3834
+ img.onload = () => {
3835
+ let width = img.width
3836
+ let height = img.height
3837
+ if (this.options.resize === true && this.options.imgSize) {
3838
+ let ratio = 1
3839
+ if (width > height) {
3840
+ ratio = width / height
3841
+ width = this.options.imgSize
3842
+ height = this.options.imgSize / ratio
3843
+ }
3844
+ else if (height > width) {
3845
+ ratio = height / width
3846
+ width = this.options.imgSize / ratio
3847
+ height = this.options.imgSize
3848
+ }
3849
+ }
3850
+ const fullResEl = document.getElementById(`${this.elementId}_media_${index}_fullres`)
3851
+ if (fullResEl) {
3852
+ fullResEl.innerHTML = `${width} x ${height}`
3853
+ }
3854
+ imgCanvas.width = width
3855
+ imgCanvas.height = height
3856
+ imgContext.drawImage(img, 0, 0, width, height)
3857
+ let ratio = 1
3858
+ if (width > height) {
3859
+ ratio = width / height
3860
+ thumbCanvas.width = this.options.thumbSize
3861
+ thumbCanvas.height = this.options.thumbSize / ratio
3862
+ }
3863
+ else if (height > width) {
3864
+ ratio = height / width
3865
+ thumbCanvas.width = this.options.thumbSize / ratio
3866
+ thumbCanvas.height = this.options.thumbSize
3867
+ }
3868
+ thumbContext.drawImage(img, 0, 0, thumbCanvas.width, thumbCanvas.height)
3869
+ mediaData.name = items[index].name
3870
+ mediaData.fullWidth = imgCanvas.width
3871
+ mediaData.fullHeight = imgCanvas.height
3872
+ mediaData.thumbWidth = thumbCanvas.width
3873
+ mediaData.thumbHeight = thumbCanvas.height
3874
+ mediaData.data = imgCanvas.toDataURL(mediaData.type).replace(/^data:image\/(png|jpg);base64,/, '')
3875
+ mediaData.thumbData = thumbCanvas.toDataURL(mediaData.type).replace(/^data:image\/(png|jpg);base64,/, '')
3876
+ this.media.push(mediaData)
3877
+ const imgEl = document.getElementById(`${this.elementId}_media_${index}`)
3878
+ imgEl.setAttribute('src', imgCanvas.toDataURL(mediaData.type))
3879
+ this.uploadItem(++index, items, callbackFn)
3880
+ }
3881
+ img.src = r.result
3882
+ }
3883
+ r.readAsDataURL(items[index].file)
3884
+ }
3885
+ }
3886
+ }
3887
+
3696
3888
  /* global WebsyDesigns */
3697
3889
  class Pager {
3698
3890
  constructor (elementId, options) {
@@ -5598,17 +5790,13 @@ class WebsySearch {
5598
5790
  searchLetters.splice(term.position, term.length, {text: term.term, term: term})
5599
5791
  })
5600
5792
  let items = searchLetters.map(d => {
5601
- let html = `
5602
- <div
5603
- `
5793
+ let html = `<div`
5604
5794
  if (d.term && d.term.label) {
5605
5795
  html += `
5606
5796
  data-label="${d.term.label}"
5607
5797
  `
5608
5798
  }
5609
- html += `
5610
- >${d.text.replace(/ /g, '&nbsp;')}</div>
5611
- `
5799
+ html += `>${d.text.replace(/ /g, '&nbsp;')}</div>`
5612
5800
  return html
5613
5801
  })
5614
5802
  const el = document.getElementById(`${this.elementId}_lozenges`)
@@ -11421,6 +11609,7 @@ const WebsyDesigns = {
11421
11609
  DatePicker: WebsyDatePicker,
11422
11610
  WebsyDropdown,
11423
11611
  Dropdown: WebsyDropdown,
11612
+ MediaUpload: MediaUpload,
11424
11613
  WebsyResultList,
11425
11614
  ResultList: WebsyResultList,
11426
11615
  WebsyTemplate,