adapt-authoring-ui 0.0.1

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.
Files changed (414) hide show
  1. package/.eslintignore +2 -0
  2. package/.eslintrc +14 -0
  3. package/.github/ISSUE_TEMPLATE/bug_report.yml +55 -0
  4. package/.github/ISSUE_TEMPLATE/config.yml +1 -0
  5. package/.github/ISSUE_TEMPLATE/feature_request.yml +22 -0
  6. package/.github/dependabot.yml +11 -0
  7. package/.github/pull_request_template.md +25 -0
  8. package/.github/workflows/labelled_prs.yml +16 -0
  9. package/.github/workflows/new.yml +19 -0
  10. package/adapt-authoring.json +11 -0
  11. package/app/core/app.js +43 -0
  12. package/app/core/assets/adapt-learning-logo-outline.png +0 -0
  13. package/app/core/assets/adapt-learning-logo-white.png +0 -0
  14. package/app/core/assets/ajax-loader.gif +0 -0
  15. package/app/core/assets/favicon.png +0 -0
  16. package/app/core/assets/transparency-light.png +0 -0
  17. package/app/core/browserStorage.js +45 -0
  18. package/app/core/collections/apiCollection.js +74 -0
  19. package/app/core/collections/contentCollection.js +33 -0
  20. package/app/core/collections/contentPluginCollection.js +24 -0
  21. package/app/core/collections/tagsCollection.js +4 -0
  22. package/app/core/constants.js +10 -0
  23. package/app/core/helpers.js +375 -0
  24. package/app/core/index.hbs +192 -0
  25. package/app/core/l10n.js +33 -0
  26. package/app/core/less/_reset.less +43 -0
  27. package/app/core/less/app.less +100 -0
  28. package/app/core/less/buttons.less +268 -0
  29. package/app/core/less/colourLabels.less +14 -0
  30. package/app/core/less/colours.less +132 -0
  31. package/app/core/less/columns.less +190 -0
  32. package/app/core/less/fonts.less +30 -0
  33. package/app/core/less/forms.less +77 -0
  34. package/app/core/less/sharedStyles.less +73 -0
  35. package/app/core/less/tables.less +29 -0
  36. package/app/core/less/tags.less +194 -0
  37. package/app/core/loading.hbs +27 -0
  38. package/app/core/models/articleModel.js +19 -0
  39. package/app/core/models/blockModel.js +25 -0
  40. package/app/core/models/clipboardModel.js +10 -0
  41. package/app/core/models/componentModel.js +18 -0
  42. package/app/core/models/configModel.js +22 -0
  43. package/app/core/models/contentModel.js +26 -0
  44. package/app/core/models/contentObjectModel.js +19 -0
  45. package/app/core/models/contentPluginModel.js +10 -0
  46. package/app/core/models/courseModel.js +24 -0
  47. package/app/core/models/sessionModel.js +86 -0
  48. package/app/core/origin.js +189 -0
  49. package/app/core/router.js +135 -0
  50. package/app/core/views/originView.js +92 -0
  51. package/app/libraries/babel-polyfill.min.js +1 -0
  52. package/app/libraries/backbone-forms-lists.js +669 -0
  53. package/app/libraries/backbone-forms.js +2604 -0
  54. package/app/libraries/backbone.js +1581 -0
  55. package/app/libraries/ckeditor.js +6 -0
  56. package/app/libraries/handlebars.js +4902 -0
  57. package/app/libraries/imageReady.js +2 -0
  58. package/app/libraries/inview.js +140 -0
  59. package/app/libraries/jquery-ui.min.js +6 -0
  60. package/app/libraries/jquery.form.js +1277 -0
  61. package/app/libraries/jquery.js +10346 -0
  62. package/app/libraries/jquery.tagsinput.min.js +1 -0
  63. package/app/libraries/moment.min.js +7 -0
  64. package/app/libraries/polyglot.min.js +2922 -0
  65. package/app/libraries/require.js +36 -0
  66. package/app/libraries/scrollTo.js +11 -0
  67. package/app/libraries/selectize/css/selectize.less +403 -0
  68. package/app/libraries/selectize/js/selectize.js +4500 -0
  69. package/app/libraries/spectrum/spectrum.js +2323 -0
  70. package/app/libraries/spectrum/spectrum.less +507 -0
  71. package/app/libraries/underscore.js +1692 -0
  72. package/app/libraries/velocity.js +4773 -0
  73. package/app/modules/appHeader/assets/top-bar-1080.jpg +0 -0
  74. package/app/modules/appHeader/assets/top-bar-2560.jpg +0 -0
  75. package/app/modules/appHeader/index.js +5 -0
  76. package/app/modules/appHeader/less/appHeader.less +72 -0
  77. package/app/modules/appHeader/templates/appHeader.hbs +17 -0
  78. package/app/modules/appHeader/views/appHeaderView.js +63 -0
  79. package/app/modules/assetManagement/collections/assetCollection.js +16 -0
  80. package/app/modules/assetManagement/index.js +65 -0
  81. package/app/modules/assetManagement/less/asset.less +229 -0
  82. package/app/modules/assetManagement/less/assetManagementModalTags.less +34 -0
  83. package/app/modules/assetManagement/less/assetManagementNew.less +60 -0
  84. package/app/modules/assetManagement/models/assetModel.js +11 -0
  85. package/app/modules/assetManagement/templates/assetManagement.hbs +12 -0
  86. package/app/modules/assetManagement/templates/assetManagementCollection.hbs +2 -0
  87. package/app/modules/assetManagement/templates/assetManagementEditAsset.hbs +9 -0
  88. package/app/modules/assetManagement/templates/assetManagementEditAssetSidebar.hbs +10 -0
  89. package/app/modules/assetManagement/templates/assetManagementListItem.hbs +35 -0
  90. package/app/modules/assetManagement/templates/assetManagementModalAutofill.hbs +1 -0
  91. package/app/modules/assetManagement/templates/assetManagementModalEditAsset.hbs +11 -0
  92. package/app/modules/assetManagement/templates/assetManagementModalFilters.hbs +16 -0
  93. package/app/modules/assetManagement/templates/assetManagementModalTags.hbs +26 -0
  94. package/app/modules/assetManagement/templates/assetManagementPreview.hbs +53 -0
  95. package/app/modules/assetManagement/templates/assetManagementSidebar.hbs +93 -0
  96. package/app/modules/assetManagement/templates/assetPicker.hbs +45 -0
  97. package/app/modules/assetManagement/views/assetManagementCollectionView.js +216 -0
  98. package/app/modules/assetManagement/views/assetManagementEditAssetSidebarView.js +25 -0
  99. package/app/modules/assetManagement/views/assetManagementEditAssetView.js +123 -0
  100. package/app/modules/assetManagement/views/assetManagementItemView.js +68 -0
  101. package/app/modules/assetManagement/views/assetManagementModalAutofillView.js +51 -0
  102. package/app/modules/assetManagement/views/assetManagementModalEditAssetView.js +38 -0
  103. package/app/modules/assetManagement/views/assetManagementModalFiltersView.js +132 -0
  104. package/app/modules/assetManagement/views/assetManagementModalTagsView.js +169 -0
  105. package/app/modules/assetManagement/views/assetManagementModalView.js +62 -0
  106. package/app/modules/assetManagement/views/assetManagementPreviewView.js +69 -0
  107. package/app/modules/assetManagement/views/assetManagementSidebarView.js +126 -0
  108. package/app/modules/assetManagement/views/assetManagementView.js +43 -0
  109. package/app/modules/colorLabel/index.js +48 -0
  110. package/app/modules/colorLabel/less/colorLabels.less +102 -0
  111. package/app/modules/colorLabel/models/colors.js +31 -0
  112. package/app/modules/colorLabel/templates/colorLabelPopUpView.hbs +21 -0
  113. package/app/modules/colorLabel/views/colorLabelPopupView.js +75 -0
  114. package/app/modules/contentHeader/index.js +6 -0
  115. package/app/modules/contentHeader/less/contentHeader.less +152 -0
  116. package/app/modules/contentHeader/less/options.less +51 -0
  117. package/app/modules/contentHeader/templates/actionsButton.hbs +3 -0
  118. package/app/modules/contentHeader/templates/contentHeader.hbs +38 -0
  119. package/app/modules/contentHeader/templates/contentHeaderToggleButton.hbs +11 -0
  120. package/app/modules/contentHeader/templates/filterItem.hbs +23 -0
  121. package/app/modules/contentHeader/templates/options.hbs +14 -0
  122. package/app/modules/contentHeader/templates/partials/part_headerButtonContent.hbs +2 -0
  123. package/app/modules/contentHeader/templates/sortItem.hbs +9 -0
  124. package/app/modules/contentHeader/views/actionsButtonView.js +14 -0
  125. package/app/modules/contentHeader/views/contentHeaderButtonView.js +42 -0
  126. package/app/modules/contentHeader/views/contentHeaderToggleButtonView.js +52 -0
  127. package/app/modules/contentHeader/views/contentHeaderView.js +96 -0
  128. package/app/modules/contentHeader/views/filtersButtonView.js +31 -0
  129. package/app/modules/contentHeader/views/optionsView.js +88 -0
  130. package/app/modules/contentHeader/views/sortsButtonView.js +45 -0
  131. package/app/modules/contentPane/index.js +20 -0
  132. package/app/modules/contentPane/less/contentPane.less +8 -0
  133. package/app/modules/contentPane/templates/contentPane.hbs +2 -0
  134. package/app/modules/contentPane/views/contentPaneView.js +77 -0
  135. package/app/modules/contextMenu/index.js +85 -0
  136. package/app/modules/contextMenu/less/contextMenu.less +54 -0
  137. package/app/modules/contextMenu/templates/contextMenu.hbs +6 -0
  138. package/app/modules/contextMenu/templates/contextMenuItem.hbs +8 -0
  139. package/app/modules/contextMenu/views/contextMenuItemView.js +37 -0
  140. package/app/modules/contextMenu/views/contextMenuView.js +76 -0
  141. package/app/modules/editor/article/index.js +18 -0
  142. package/app/modules/editor/article/templates/editorArticleEdit.hbs +2 -0
  143. package/app/modules/editor/article/templates/editorArticleEditSidebar.hbs +19 -0
  144. package/app/modules/editor/article/views/editorArticleEditSidebarView.js +32 -0
  145. package/app/modules/editor/article/views/editorArticleEditView.js +19 -0
  146. package/app/modules/editor/block/index.js +18 -0
  147. package/app/modules/editor/block/templates/editorBlockEdit.hbs +2 -0
  148. package/app/modules/editor/block/templates/editorBlockEditSidebar.hbs +19 -0
  149. package/app/modules/editor/block/views/editorBlockEditSidebarView.js +28 -0
  150. package/app/modules/editor/block/views/editorBlockEditView.js +19 -0
  151. package/app/modules/editor/component/index.js +22 -0
  152. package/app/modules/editor/component/templates/editorComponentEdit.hbs +2 -0
  153. package/app/modules/editor/component/templates/editorComponentEditSidebar.hbs +19 -0
  154. package/app/modules/editor/component/views/editorComponentEditSidebarView.js +32 -0
  155. package/app/modules/editor/component/views/editorComponentEditView.js +23 -0
  156. package/app/modules/editor/config/index.js +15 -0
  157. package/app/modules/editor/config/templates/editorConfigEdit.hbs +1 -0
  158. package/app/modules/editor/config/templates/editorConfigEditSidebar.hbs +10 -0
  159. package/app/modules/editor/config/views/editorConfigEditSidebarView.js +28 -0
  160. package/app/modules/editor/config/views/editorConfigEditView.js +28 -0
  161. package/app/modules/editor/contentObject/assets/component-icons/icon-accordion.png +0 -0
  162. package/app/modules/editor/contentObject/assets/component-icons/icon-assessment.png +0 -0
  163. package/app/modules/editor/contentObject/assets/component-icons/icon-blank.png +0 -0
  164. package/app/modules/editor/contentObject/assets/component-icons/icon-default.png +0 -0
  165. package/app/modules/editor/contentObject/assets/component-icons/icon-gmcq.png +0 -0
  166. package/app/modules/editor/contentObject/assets/component-icons/icon-graphic.png +0 -0
  167. package/app/modules/editor/contentObject/assets/component-icons/icon-hot-graphic.png +0 -0
  168. package/app/modules/editor/contentObject/assets/component-icons/icon-matching.png +0 -0
  169. package/app/modules/editor/contentObject/assets/component-icons/icon-mcq.png +0 -0
  170. package/app/modules/editor/contentObject/assets/component-icons/icon-media.png +0 -0
  171. package/app/modules/editor/contentObject/assets/component-icons/icon-narrative.png +0 -0
  172. package/app/modules/editor/contentObject/assets/component-icons/icon-slider.png +0 -0
  173. package/app/modules/editor/contentObject/assets/component-icons/icon-text.png +0 -0
  174. package/app/modules/editor/contentObject/index.js +68 -0
  175. package/app/modules/editor/contentObject/less/editing-overlay-component-list.less +60 -0
  176. package/app/modules/editor/contentObject/less/editing-overlay-panel.less +25 -0
  177. package/app/modules/editor/contentObject/less/editorMenu.less +84 -0
  178. package/app/modules/editor/contentObject/less/editorMenuItem.less +114 -0
  179. package/app/modules/editor/contentObject/less/editorMenuLayer.less +70 -0
  180. package/app/modules/editor/contentObject/less/editorPage.less +162 -0
  181. package/app/modules/editor/contentObject/less/editorPageArticle.less +40 -0
  182. package/app/modules/editor/contentObject/less/editorPageBlock.less +33 -0
  183. package/app/modules/editor/contentObject/less/editorPageComponent.less +47 -0
  184. package/app/modules/editor/contentObject/less/editorPageComponentList.less +234 -0
  185. package/app/modules/editor/contentObject/templates/editorMenu.hbs +1 -0
  186. package/app/modules/editor/contentObject/templates/editorMenuItem.hbs +23 -0
  187. package/app/modules/editor/contentObject/templates/editorMenuLayer.hbs +12 -0
  188. package/app/modules/editor/contentObject/templates/editorMenuSidebar.hbs +1 -0
  189. package/app/modules/editor/contentObject/templates/editorPage.hbs +15 -0
  190. package/app/modules/editor/contentObject/templates/editorPageArticle.hbs +15 -0
  191. package/app/modules/editor/contentObject/templates/editorPageBlock.hbs +14 -0
  192. package/app/modules/editor/contentObject/templates/editorPageComponent.hbs +9 -0
  193. package/app/modules/editor/contentObject/templates/editorPageComponentList.hbs +33 -0
  194. package/app/modules/editor/contentObject/templates/editorPageComponentListItem.hbs +24 -0
  195. package/app/modules/editor/contentObject/templates/editorPageComponentPasteZone.hbs +4 -0
  196. package/app/modules/editor/contentObject/templates/editorPageEdit.hbs +1 -0
  197. package/app/modules/editor/contentObject/templates/editorPageEditSidebar.hbs +10 -0
  198. package/app/modules/editor/contentObject/templates/editorPageSidebar.hbs +1 -0
  199. package/app/modules/editor/contentObject/views/editorMenuItemView.js +174 -0
  200. package/app/modules/editor/contentObject/views/editorMenuLayerView.js +212 -0
  201. package/app/modules/editor/contentObject/views/editorMenuSidebarView.js +13 -0
  202. package/app/modules/editor/contentObject/views/editorMenuView.js +211 -0
  203. package/app/modules/editor/contentObject/views/editorPageArticleView.js +278 -0
  204. package/app/modules/editor/contentObject/views/editorPageBlockView.js +280 -0
  205. package/app/modules/editor/contentObject/views/editorPageComponentListItemView.js +80 -0
  206. package/app/modules/editor/contentObject/views/editorPageComponentListView.js +131 -0
  207. package/app/modules/editor/contentObject/views/editorPageComponentPasteZoneView.js +47 -0
  208. package/app/modules/editor/contentObject/views/editorPageComponentView.js +202 -0
  209. package/app/modules/editor/contentObject/views/editorPageEditSidebarView.js +28 -0
  210. package/app/modules/editor/contentObject/views/editorPageEditView.js +19 -0
  211. package/app/modules/editor/contentObject/views/editorPageSidebarView.js +13 -0
  212. package/app/modules/editor/contentObject/views/editorPageView.js +183 -0
  213. package/app/modules/editor/course/index.js +26 -0
  214. package/app/modules/editor/course/templates/editorCourseEdit.hbs +2 -0
  215. package/app/modules/editor/course/templates/editorCourseEditSidebar.hbs +19 -0
  216. package/app/modules/editor/course/views/editorCourseEditSidebarView.js +28 -0
  217. package/app/modules/editor/course/views/editorCourseEditView.js +56 -0
  218. package/app/modules/editor/extensions/index.js +26 -0
  219. package/app/modules/editor/extensions/less/extensions.less +22 -0
  220. package/app/modules/editor/extensions/templates/editorExtensionsEdit.hbs +85 -0
  221. package/app/modules/editor/extensions/templates/editorExtensionsEditSidebar.hbs +1 -0
  222. package/app/modules/editor/extensions/views/editorExtensionsEditSidebarView.js +10 -0
  223. package/app/modules/editor/extensions/views/editorExtensionsEditView.js +85 -0
  224. package/app/modules/editor/global/collections/editorCollection.js +24 -0
  225. package/app/modules/editor/global/editorDataLoader.js +91 -0
  226. package/app/modules/editor/global/helpers.js +99 -0
  227. package/app/modules/editor/global/less/colorLabels.less +55 -0
  228. package/app/modules/editor/global/less/editor.less +204 -0
  229. package/app/modules/editor/global/templates/editor.hbs +1 -0
  230. package/app/modules/editor/global/templates/editorPasteZone.hbs +4 -0
  231. package/app/modules/editor/global/templates/partials/part_editorCommon.hbs +70 -0
  232. package/app/modules/editor/global/templates/partials/part_editorItemSidebar.hbs +40 -0
  233. package/app/modules/editor/global/templates/partials/part_editorMenu.hbs +4 -0
  234. package/app/modules/editor/global/templates/partials/part_settingsGeneral.hbs +27 -0
  235. package/app/modules/editor/global/views/editorOriginView.js +255 -0
  236. package/app/modules/editor/global/views/editorPasteZoneView.js +69 -0
  237. package/app/modules/editor/global/views/editorView.js +288 -0
  238. package/app/modules/editor/index.js +52 -0
  239. package/app/modules/editor/menuSettings/index.js +27 -0
  240. package/app/modules/editor/menuSettings/less/menusettings.less +55 -0
  241. package/app/modules/editor/menuSettings/templates/editorMenuSettingsEdit.hbs +1 -0
  242. package/app/modules/editor/menuSettings/templates/editorMenuSettingsEditSidebar.hbs +5 -0
  243. package/app/modules/editor/menuSettings/templates/editorMenuSettingsItem.hbs +4 -0
  244. package/app/modules/editor/menuSettings/views/editorMenuSettingsEditSidebarView.js +21 -0
  245. package/app/modules/editor/menuSettings/views/editorMenuSettingsEditView.js +71 -0
  246. package/app/modules/editor/menuSettings/views/editorMenuSettingsView.js +44 -0
  247. package/app/modules/editor/themeEditor/collections/editorPresetCollection.js +13 -0
  248. package/app/modules/editor/themeEditor/index.js +15 -0
  249. package/app/modules/editor/themeEditor/less/editorPresetEdit.less +78 -0
  250. package/app/modules/editor/themeEditor/less/editorTheming.less +73 -0
  251. package/app/modules/editor/themeEditor/models/editorModel.js +62 -0
  252. package/app/modules/editor/themeEditor/models/editorPresetModel.js +6 -0
  253. package/app/modules/editor/themeEditor/templates/editorPresetEdit.hbs +31 -0
  254. package/app/modules/editor/themeEditor/templates/editorTheming.hbs +35 -0
  255. package/app/modules/editor/themeEditor/templates/editorThemingSidebar.hbs +23 -0
  256. package/app/modules/editor/themeEditor/views/editorPresetEditView.js +98 -0
  257. package/app/modules/editor/themeEditor/views/editorThemingSidebarView.js +62 -0
  258. package/app/modules/editor/themeEditor/views/editorThemingView.js +304 -0
  259. package/app/modules/frameworkImport/index.js +32 -0
  260. package/app/modules/frameworkImport/less/frameworkImport.less +101 -0
  261. package/app/modules/frameworkImport/templates/frameworkImport.hbs +58 -0
  262. package/app/modules/frameworkImport/templates/frameworkImportSidebar.hbs +12 -0
  263. package/app/modules/frameworkImport/templates/frameworkImportSummary.hbs +40 -0
  264. package/app/modules/frameworkImport/templates/partials/part_frameworkImportButton.hbs +3 -0
  265. package/app/modules/frameworkImport/templates/partials/part_frameworkImportStatusMessages.hbs +22 -0
  266. package/app/modules/frameworkImport/views/frameworkImportSidebarView.js +31 -0
  267. package/app/modules/frameworkImport/views/frameworkImportView.js +109 -0
  268. package/app/modules/globalMenu/index.js +86 -0
  269. package/app/modules/globalMenu/less/globalMenu.less +117 -0
  270. package/app/modules/globalMenu/templates/globalMenu.hbs +6 -0
  271. package/app/modules/globalMenu/templates/globalMenuItem.hbs +10 -0
  272. package/app/modules/globalMenu/templates/partials/part_globalMenuButton.hbs +7 -0
  273. package/app/modules/globalMenu/views/globalMenuItemView.js +108 -0
  274. package/app/modules/globalMenu/views/globalMenuView.js +45 -0
  275. package/app/modules/limiteduser/index.js +17 -0
  276. package/app/modules/limiteduser/less/limitedUser.less +29 -0
  277. package/app/modules/limiteduser/templates/limitedUser.hbs +8 -0
  278. package/app/modules/limiteduser/views/limitedUserView.js +12 -0
  279. package/app/modules/modal/index.js +13 -0
  280. package/app/modules/modal/less/modal.less +61 -0
  281. package/app/modules/modal/models/modalModel.js +14 -0
  282. package/app/modules/modal/templates/modal.hbs +34 -0
  283. package/app/modules/modal/views/modalView.js +102 -0
  284. package/app/modules/notify/index.js +32 -0
  285. package/app/modules/notify/plugins/alert/assets/sweetalert.css +932 -0
  286. package/app/modules/notify/plugins/alert/index.js +117 -0
  287. package/app/modules/notify/plugins/alert/less/alert.less +74 -0
  288. package/app/modules/notify/plugins/alert/sweetalert2-11.1.7.all.min.js +2 -0
  289. package/app/modules/notify/plugins/console/index.js +18 -0
  290. package/app/modules/notify/plugins/snackbar/index.js +69 -0
  291. package/app/modules/notify/plugins/snackbar/less/snackbar.less +40 -0
  292. package/app/modules/notify/plugins/toast/index.js +70 -0
  293. package/app/modules/notify/plugins/toast/less/toast.less +62 -0
  294. package/app/modules/pluginManagement/index.js +36 -0
  295. package/app/modules/pluginManagement/less/pluginManagement.less +23 -0
  296. package/app/modules/pluginManagement/less/pluginType.less +25 -0
  297. package/app/modules/pluginManagement/templates/pluginManagement.hbs +43 -0
  298. package/app/modules/pluginManagement/templates/pluginManagementSidebar.hbs +40 -0
  299. package/app/modules/pluginManagement/templates/pluginManagementUpload.hbs +12 -0
  300. package/app/modules/pluginManagement/templates/pluginManagementUploadSidebar.hbs +13 -0
  301. package/app/modules/pluginManagement/templates/pluginType.hbs +43 -0
  302. package/app/modules/pluginManagement/views/pluginManagementSidebarView.js +48 -0
  303. package/app/modules/pluginManagement/views/pluginManagementUploadSidebarView.js +25 -0
  304. package/app/modules/pluginManagement/views/pluginManagementUploadView.js +52 -0
  305. package/app/modules/pluginManagement/views/pluginManagementView.js +104 -0
  306. package/app/modules/pluginManagement/views/pluginTypeView.js +105 -0
  307. package/app/modules/projects/assets/origami-project.jpg +0 -0
  308. package/app/modules/projects/index.js +82 -0
  309. package/app/modules/projects/less/projects.less +152 -0
  310. package/app/modules/projects/less/qproject.less +143 -0
  311. package/app/modules/projects/templates/project.hbs +63 -0
  312. package/app/modules/projects/templates/projects.hbs +6 -0
  313. package/app/modules/projects/templates/projectsSidebar.hbs +42 -0
  314. package/app/modules/projects/views/projectView.js +167 -0
  315. package/app/modules/projects/views/projectsSidebarView.js +172 -0
  316. package/app/modules/projects/views/projectsView.js +271 -0
  317. package/app/modules/scaffold/backboneFormsOverrides.js +284 -0
  318. package/app/modules/scaffold/index.js +342 -0
  319. package/app/modules/scaffold/lang/en.json +9 -0
  320. package/app/modules/scaffold/less/codeEditor.less +6 -0
  321. package/app/modules/scaffold/less/colourPicker.less +141 -0
  322. package/app/modules/scaffold/less/displayTitle.less +17 -0
  323. package/app/modules/scaffold/less/forms.less +174 -0
  324. package/app/modules/scaffold/less/list.less +35 -0
  325. package/app/modules/scaffold/less/modal.less +45 -0
  326. package/app/modules/scaffold/less/scaffoldAsset.less +23 -0
  327. package/app/modules/scaffold/less/scaffoldAssetItem.less +50 -0
  328. package/app/modules/scaffold/less/selectize.less +29 -0
  329. package/app/modules/scaffold/less/textArea.less +9 -0
  330. package/app/modules/scaffold/less/users.less +6 -0
  331. package/app/modules/scaffold/templates/field.hbs +25 -0
  332. package/app/modules/scaffold/templates/fieldset.hbs +10 -0
  333. package/app/modules/scaffold/templates/form.hbs +6 -0
  334. package/app/modules/scaffold/templates/list.hbs +4 -0
  335. package/app/modules/scaffold/templates/listItem.hbs +5 -0
  336. package/app/modules/scaffold/templates/scaffoldAsset.hbs +65 -0
  337. package/app/modules/scaffold/templates/scaffoldAssetItem.hbs +35 -0
  338. package/app/modules/scaffold/templates/scaffoldDisplayTitle.hbs +8 -0
  339. package/app/modules/scaffold/templates/scaffoldEditHtml.hbs +1 -0
  340. package/app/modules/scaffold/templates/scaffoldFile.hbs +1 -0
  341. package/app/modules/scaffold/templates/scaffoldItemsModal.hbs +14 -0
  342. package/app/modules/scaffold/templates/scaffoldModalOverlay.hbs +1 -0
  343. package/app/modules/scaffold/templates/scaffoldUsersOption.hbs +8 -0
  344. package/app/modules/scaffold/views/scaffoldAssetItemView.js +203 -0
  345. package/app/modules/scaffold/views/scaffoldAssetView.js +157 -0
  346. package/app/modules/scaffold/views/scaffoldCodeEditorView.js +64 -0
  347. package/app/modules/scaffold/views/scaffoldColourPickerView.js +70 -0
  348. package/app/modules/scaffold/views/scaffoldDisplayTitleView.js +112 -0
  349. package/app/modules/scaffold/views/scaffoldFileView.js +18 -0
  350. package/app/modules/scaffold/views/scaffoldItemsModalView.js +100 -0
  351. package/app/modules/scaffold/views/scaffoldListView.js +158 -0
  352. package/app/modules/scaffold/views/scaffoldTagsView.js +71 -0
  353. package/app/modules/scaffold/views/scaffoldUsersView.js +89 -0
  354. package/app/modules/sidebar/index.js +22 -0
  355. package/app/modules/sidebar/less/sidebar.less +298 -0
  356. package/app/modules/sidebar/less/sidebarFilter.less +91 -0
  357. package/app/modules/sidebar/templates/sidebar.hbs +7 -0
  358. package/app/modules/sidebar/templates/sidebarBreadcrumb.hbs +3 -0
  359. package/app/modules/sidebar/templates/sidebarDivide.hbs +5 -0
  360. package/app/modules/sidebar/templates/sidebarFieldsetFilter.hbs +6 -0
  361. package/app/modules/sidebar/templates/sidebarFilter.hbs +30 -0
  362. package/app/modules/sidebar/templates/sidebarRowFilter.hbs +4 -0
  363. package/app/modules/sidebar/templates/sidebarUpdateButton.hbs +3 -0
  364. package/app/modules/sidebar/views/sidebarFieldsetFilterView.js +43 -0
  365. package/app/modules/sidebar/views/sidebarFilterView.js +132 -0
  366. package/app/modules/sidebar/views/sidebarItemView.js +172 -0
  367. package/app/modules/sidebar/views/sidebarView.js +71 -0
  368. package/app/modules/user/assets/adapt-learning-logo.png +0 -0
  369. package/app/modules/user/assets/adapt-logo.png +0 -0
  370. package/app/modules/user/assets/login_bg.jpg +0 -0
  371. package/app/modules/user/index.js +75 -0
  372. package/app/modules/user/less/forgotPassword.less +14 -0
  373. package/app/modules/user/less/login.less +138 -0
  374. package/app/modules/user/less/logout.less +3 -0
  375. package/app/modules/user/less/resetPassword.less +7 -0
  376. package/app/modules/user/less/userProfile.less +49 -0
  377. package/app/modules/user/models/userProfileModel.js +32 -0
  378. package/app/modules/user/templates/forgotPassword.hbs +36 -0
  379. package/app/modules/user/templates/login.hbs +29 -0
  380. package/app/modules/user/templates/resetPassword.hbs +29 -0
  381. package/app/modules/user/templates/userProfile.hbs +71 -0
  382. package/app/modules/user/templates/userProfileSidebar.hbs +16 -0
  383. package/app/modules/user/views/forgotPasswordView.js +63 -0
  384. package/app/modules/user/views/loginView.js +93 -0
  385. package/app/modules/user/views/resetPasswordView.js +78 -0
  386. package/app/modules/user/views/userProfileSidebarView.js +32 -0
  387. package/app/modules/user/views/userProfileView.js +107 -0
  388. package/app/modules/userManagement/collections/userCollection.js +65 -0
  389. package/app/modules/userManagement/helpers.js +46 -0
  390. package/app/modules/userManagement/index.js +67 -0
  391. package/app/modules/userManagement/less/userManagement.less +268 -0
  392. package/app/modules/userManagement/models/userModel.js +34 -0
  393. package/app/modules/userManagement/templates/addUser.hbs +35 -0
  394. package/app/modules/userManagement/templates/addUserSidebar.hbs +9 -0
  395. package/app/modules/userManagement/templates/user.hbs +126 -0
  396. package/app/modules/userManagement/templates/userManagement.hbs +49 -0
  397. package/app/modules/userManagement/templates/userManagementFilter.hbs +19 -0
  398. package/app/modules/userManagement/templates/userManagementSidebar.hbs +13 -0
  399. package/app/modules/userManagement/views/addUserSidebarView.js +25 -0
  400. package/app/modules/userManagement/views/addUserView.js +85 -0
  401. package/app/modules/userManagement/views/filterView.js +49 -0
  402. package/app/modules/userManagement/views/userManagementSidebarView.js +33 -0
  403. package/app/modules/userManagement/views/userManagementView.js +104 -0
  404. package/app/modules/userManagement/views/userView.js +324 -0
  405. package/conf/config.schema.json +18 -0
  406. package/docs/migrating-from-legacy.md +19 -0
  407. package/docs/plugins/index-ui.md +17 -0
  408. package/docs/plugins/uidocs.js +54 -0
  409. package/docs/ui-extensions.md +15 -0
  410. package/index.js +1 -0
  411. package/lib/UiBuild.js +391 -0
  412. package/lib/UiModule.js +158 -0
  413. package/npm_hooks/postinstall.js +4 -0
  414. package/package.json +27 -0
@@ -0,0 +1,2604 @@
1
+ /**
2
+ * Backbone Forms v0.14.0
3
+ *
4
+ * NOTE:
5
+ * This version is for use with RequireJS
6
+ * If using regular <script> tags to include your files, use backbone-forms.min.js
7
+ *
8
+ * Copyright (c) 2013 Charles Davison, Pow Media Ltd
9
+ *
10
+ * License and more information at:
11
+ * http://github.com/powmedia/backbone-forms
12
+ */
13
+ define(['jquery', 'underscore', 'backbone'], function($, _, Backbone) {
14
+
15
+ //==================================================================================================
16
+ //FORM
17
+ //==================================================================================================
18
+
19
+ var Form = Backbone.View.extend({
20
+
21
+ events: {
22
+ 'submit': function(event) {
23
+ this.trigger('submit', event);
24
+ }
25
+ },
26
+
27
+ /**
28
+ * Constructor
29
+ *
30
+ * @param {Object} [options.schema]
31
+ * @param {Backbone.Model} [options.model]
32
+ * @param {Object} [options.data]
33
+ * @param {String[]|Object[]} [options.fieldsets]
34
+ * @param {String[]} [options.fields]
35
+ * @param {String} [options.idPrefix]
36
+ * @param {Form.Field} [options.Field]
37
+ * @param {Form.Fieldset} [options.Fieldset]
38
+ * @param {Function} [options.template]
39
+ * @param {Boolean|String} [options.submitButton]
40
+ */
41
+ initialize: function(options) {
42
+ var self = this;
43
+
44
+ //Merge default options
45
+ options = this.options = _.extend({
46
+ submitButton: false
47
+ }, options);
48
+
49
+ //Find the schema to use
50
+ var schema = this.schema = (function() {
51
+ //Prefer schema from options
52
+ if (options.schema) return _.result(options, 'schema');
53
+
54
+ //Then schema on model
55
+ var model = options.model;
56
+ if (model && model.schema) return _.result(model, 'schema');
57
+
58
+ //Then built-in schema
59
+ if (self.schema) return _.result(self, 'schema');
60
+
61
+ //Fallback to empty schema
62
+ return {};
63
+ })();
64
+
65
+ //Store important data
66
+ _.extend(this, _.pick(options, 'model', 'data', 'idPrefix', 'templateData'));
67
+
68
+ //Override defaults
69
+ var constructor = this.constructor;
70
+ this.template = options.template || this.template || constructor.template;
71
+ this.Fieldset = options.Fieldset || this.Fieldset || constructor.Fieldset;
72
+ this.Field = options.Field || this.Field || constructor.Field;
73
+ this.NestedField = options.NestedField || this.NestedField || constructor.NestedField;
74
+
75
+ //Check which fields will be included (defaults to all)
76
+ var selectedFields = this.selectedFields = options.fields || _.keys(schema);
77
+
78
+ //Create fields
79
+ var fields = this.fields = {};
80
+
81
+ _.each(selectedFields, function(key) {
82
+ var fieldSchema = schema[key];
83
+ fields[key] = this.createField(key, fieldSchema);
84
+ }, this);
85
+
86
+ //Create fieldsets
87
+ var fieldsetSchema = options.fieldsets || _.result(this, 'fieldsets') || _.result(this.model, 'fieldsets') || [selectedFields],
88
+ fieldsets = this.fieldsets = [];
89
+
90
+ _.each(fieldsetSchema, function(itemSchema) {
91
+ this.fieldsets.push(this.createFieldset(itemSchema));
92
+ }, this);
93
+ },
94
+
95
+ /**
96
+ * Creates a Fieldset instance
97
+ *
98
+ * @param {String[]|Object[]} schema Fieldset schema
99
+ *
100
+ * @return {Form.Fieldset}
101
+ */
102
+ createFieldset: function(schema) {
103
+ var options = {
104
+ schema: schema,
105
+ fields: this.fields,
106
+ legend: schema.legend || null
107
+ };
108
+
109
+ return new this.Fieldset(options);
110
+ },
111
+
112
+ /**
113
+ * Creates a Field instance
114
+ *
115
+ * @param {String} key
116
+ * @param {Object} schema Field schema
117
+ *
118
+ * @return {Form.Field}
119
+ */
120
+ createField: function(key, schema) {
121
+ var options = {
122
+ form: this,
123
+ key: key,
124
+ schema: schema,
125
+ idPrefix: this.idPrefix
126
+ };
127
+
128
+ if (this.model) {
129
+ options.model = this.model;
130
+ } else if (this.data) {
131
+ options.value = this.data[key];
132
+ } else {
133
+ options.value = undefined;
134
+ }
135
+
136
+ var field = new this.Field(options);
137
+
138
+ this.listenTo(field.editor, 'all', this.handleEditorEvent);
139
+
140
+ return field;
141
+ },
142
+
143
+ /**
144
+ * Callback for when an editor event is fired.
145
+ * Re-triggers events on the form as key:event and triggers additional form-level events
146
+ *
147
+ * @param {String} event
148
+ * @param {Editor} editor
149
+ */
150
+ handleEditorEvent: function(event, editor) {
151
+ //Re-trigger editor events on the form
152
+ var formEvent = editor.key+':'+event;
153
+
154
+ this.trigger.call(this, formEvent, this, editor, Array.prototype.slice.call(arguments, 2));
155
+
156
+ //Trigger additional events
157
+ switch (event) {
158
+ case 'change':
159
+ this.trigger('change', this);
160
+ break;
161
+
162
+ case 'focus':
163
+ if (!this.hasFocus) this.trigger('focus', this);
164
+ break;
165
+
166
+ case 'blur':
167
+ if (this.hasFocus) {
168
+ //TODO: Is the timeout etc needed?
169
+ var self = this;
170
+ setTimeout(function() {
171
+ var focusedField = _.find(self.fields, function(field) {
172
+ return field.editor.hasFocus;
173
+ });
174
+
175
+ if (!focusedField) self.trigger('blur', self);
176
+ }, 0);
177
+ }
178
+ break;
179
+ }
180
+ },
181
+
182
+ templateData: function() {
183
+ var options = this.options;
184
+
185
+ return {
186
+ submitButton: options.submitButton
187
+ }
188
+ },
189
+
190
+ render: function() {
191
+ var self = this,
192
+ fields = this.fields,
193
+ $ = Backbone.$;
194
+
195
+ //Render form
196
+ var $form = $($.trim(this.template(_.result(this, 'templateData'))));
197
+
198
+ //Render standalone editors
199
+ $form.find('[data-editors]').add($form).each(function(i, el) {
200
+ var $container = $(el),
201
+ selection = $container.attr('data-editors');
202
+
203
+ if (_.isUndefined(selection)) return;
204
+
205
+ //Work out which fields to include
206
+ var keys = (selection == '*')
207
+ ? self.selectedFields || _.keys(fields)
208
+ : selection.split(',');
209
+
210
+ //Add them
211
+ _.each(keys, function(key) {
212
+ var field = fields[key];
213
+
214
+ $container.append(field.editor.render().el);
215
+ });
216
+ });
217
+
218
+ //Render standalone fields
219
+ $form.find('[data-fields]').add($form).each(function(i, el) {
220
+ var $container = $(el),
221
+ selection = $container.attr('data-fields');
222
+
223
+ if (_.isUndefined(selection)) return;
224
+
225
+ //Work out which fields to include
226
+ var keys = (selection == '*')
227
+ ? self.selectedFields || _.keys(fields)
228
+ : selection.split(',');
229
+
230
+ //Add them
231
+ _.each(keys, function(key) {
232
+ var field = fields[key];
233
+
234
+ $container.append(field.render().el);
235
+ });
236
+ });
237
+
238
+ //Render fieldsets
239
+ $form.find('[data-fieldsets]').add($form).each(function(i, el) {
240
+ var $container = $(el),
241
+ selection = $container.attr('data-fieldsets');
242
+
243
+ if (_.isUndefined(selection)) return;
244
+
245
+ _.each(self.fieldsets, function(fieldset) {
246
+ $container.append(fieldset.render().el);
247
+ });
248
+ });
249
+
250
+ //Set the main element
251
+ this.setElement($form);
252
+
253
+ //Set class
254
+ $form.addClass(this.className);
255
+
256
+ //Set attributes
257
+ if (this.attributes) {
258
+ $form.attr(this.attributes)
259
+ }
260
+
261
+ return this;
262
+ },
263
+
264
+ /**
265
+ * Validate the data
266
+ *
267
+ * @return {Object} Validation errors
268
+ */
269
+ validate: function(options) {
270
+ var self = this,
271
+ fields = this.fields,
272
+ model = this.model,
273
+ errors = {};
274
+
275
+ options = options || {};
276
+
277
+ //Collect errors from schema validation
278
+ _.each(fields, function(field) {
279
+ var error = field.validate();
280
+ if (error) {
281
+ errors[field.key] = error;
282
+ }
283
+ });
284
+
285
+ //Get errors from default Backbone model validator
286
+ if (!options.skipModelValidate && model && model.validate) {
287
+ var modelErrors = model.validate(this.getValue());
288
+
289
+ if (modelErrors) {
290
+ var isDictionary = _.isObject(modelErrors) && !_.isArray(modelErrors);
291
+
292
+ //If errors are not in object form then just store on the error object
293
+ if (!isDictionary) {
294
+ errors._others = errors._others || [];
295
+ errors._others.push(modelErrors);
296
+ }
297
+
298
+ //Merge programmatic errors (requires model.validate() to return an object e.g. { fieldKey: 'error' })
299
+ if (isDictionary) {
300
+ _.each(modelErrors, function(val, key) {
301
+ //Set error on field if there isn't one already
302
+ if (fields[key] && !errors[key]) {
303
+ fields[key].setError(val);
304
+ errors[key] = val;
305
+ }
306
+
307
+ else {
308
+ //Otherwise add to '_others' key
309
+ errors._others = errors._others || [];
310
+ var tmpErr = {};
311
+ tmpErr[key] = val;
312
+ errors._others.push(tmpErr);
313
+ }
314
+ });
315
+ }
316
+ }
317
+ }
318
+
319
+ return _.isEmpty(errors) ? null : errors;
320
+ },
321
+
322
+ /**
323
+ * Update the model with all latest values.
324
+ *
325
+ * @param {Object} [options] Options to pass to Model#set (e.g. { silent: true })
326
+ *
327
+ * @return {Object} Validation errors
328
+ */
329
+ commit: function(options) {
330
+ //Validate
331
+ options = options || {};
332
+
333
+ var validateOptions = {
334
+ skipModelValidate: !options.validate
335
+ };
336
+
337
+ var errors = this.validate(validateOptions);
338
+ if (errors) return errors;
339
+
340
+ //Commit
341
+ var modelError;
342
+
343
+ var setOptions = _.extend({
344
+ error: function(model, e) {
345
+ modelError = e;
346
+ }
347
+ }, options);
348
+
349
+ this.model.set(this.getValue(), setOptions);
350
+
351
+ if (modelError) return modelError;
352
+ },
353
+
354
+ /**
355
+ * Get all the field values as an object.
356
+ * Use this method when passing data instead of objects
357
+ *
358
+ * @param {String} [key] Specific field value to get
359
+ */
360
+ getValue: function(key) {
361
+ //Return only given key if specified
362
+ if (key) return this.fields[key].getValue();
363
+
364
+ //Otherwise return entire form
365
+ var values = {};
366
+ _.each(this.fields, function(field) {
367
+ values[field.key] = field.getValue();
368
+ });
369
+
370
+ return values;
371
+ },
372
+
373
+ /**
374
+ * Update field values, referenced by key
375
+ *
376
+ * @param {Object|String} key New values to set, or property to set
377
+ * @param val Value to set
378
+ */
379
+ setValue: function(prop, val) {
380
+ var data = {};
381
+ if (typeof prop === 'string') {
382
+ data[prop] = val;
383
+ } else {
384
+ data = prop;
385
+ }
386
+
387
+ var key;
388
+ for (key in this.schema) {
389
+ if (data[key] !== undefined) {
390
+ this.fields[key].setValue(data[key]);
391
+ }
392
+ }
393
+ },
394
+
395
+ /**
396
+ * Returns the editor for a given field key
397
+ *
398
+ * @param {String} key
399
+ *
400
+ * @return {Editor}
401
+ */
402
+ getEditor: function(key) {
403
+ var field = this.fields[key];
404
+ if (!field) throw new Error('Field not found: '+key);
405
+
406
+ return field.editor;
407
+ },
408
+
409
+ /**
410
+ * Gives the first editor in the form focus
411
+ */
412
+ focus: function() {
413
+ if (this.hasFocus) return;
414
+
415
+ //Get the first field
416
+ var fieldset = this.fieldsets[0],
417
+ field = fieldset.getFieldAt(0);
418
+
419
+ if (!field) return;
420
+
421
+ //Set focus
422
+ field.editor.focus();
423
+ },
424
+
425
+ /**
426
+ * Removes focus from the currently focused editor
427
+ */
428
+ blur: function() {
429
+ if (!this.hasFocus) return;
430
+
431
+ var focusedField = _.find(this.fields, function(field) {
432
+ return field.editor.hasFocus;
433
+ });
434
+
435
+ if (focusedField) focusedField.editor.blur();
436
+ },
437
+
438
+ /**
439
+ * Manages the hasFocus property
440
+ *
441
+ * @param {String} event
442
+ */
443
+ trigger: function(event) {
444
+ if (event === 'focus') {
445
+ this.hasFocus = true;
446
+ }
447
+ else if (event === 'blur') {
448
+ this.hasFocus = false;
449
+ }
450
+
451
+ return Backbone.View.prototype.trigger.apply(this, arguments);
452
+ },
453
+
454
+ /**
455
+ * Override default remove function in order to remove embedded views
456
+ *
457
+ * TODO: If editors are included directly with data-editors="x", they need to be removed
458
+ * May be best to use XView to manage adding/removing views
459
+ */
460
+ remove: function() {
461
+ _.each(this.fieldsets, function(fieldset) {
462
+ fieldset.remove();
463
+ });
464
+
465
+ _.each(this.fields, function(field) {
466
+ field.remove();
467
+ });
468
+
469
+ return Backbone.View.prototype.remove.apply(this, arguments);
470
+ }
471
+
472
+ }, {
473
+ editors: {}
474
+
475
+ });
476
+
477
+ //Statics to add on after Form is declared
478
+ Form.templateSettings = {
479
+ evaluate: /<%([\s\S]+?)%>/g,
480
+ interpolate: /<%=([\s\S]+?)%>/g,
481
+ escape: /<%-([\s\S]+?)%>/g
482
+ };
483
+
484
+ Form.template = _.template('\
485
+ <form>\
486
+ <div data-fieldsets></div>\
487
+ <% if (submitButton) { %>\
488
+ <button type="submit"><%= submitButton %></button>\
489
+ <% } %>\
490
+ </form>\
491
+ ', null, Form.templateSettings);
492
+
493
+
494
+ //==================================================================================================
495
+ //VALIDATORS
496
+ //==================================================================================================
497
+
498
+ Form.validators = (function() {
499
+
500
+ var validators = {};
501
+
502
+ validators.errMessages = {
503
+ required: 'Required',
504
+ regexp: 'Invalid',
505
+ number: 'Must be a number',
506
+ range: _.template('Must be a number between <%= min %> and <%= max %>', null, Form.templateSettings),
507
+ email: 'Invalid email address',
508
+ url: 'Invalid URL',
509
+ match: _.template('Must match field "<%= field %>"', null, Form.templateSettings)
510
+ };
511
+
512
+ validators.required = function(options) {
513
+ options = _.extend({
514
+ type: 'required',
515
+ message: this.errMessages.required
516
+ }, options);
517
+
518
+ return function required(value) {
519
+ options.value = value;
520
+
521
+ var err = {
522
+ type: options.type,
523
+ message: _.isFunction(options.message) ? options.message(options) : options.message
524
+ };
525
+
526
+ if (value === null || value === undefined || value === false || value === '' || $.trim(value) === '' ) return err;
527
+ };
528
+ };
529
+
530
+ validators.regexp = function(options) {
531
+ if (!options.regexp) throw new Error('Missing required "regexp" option for "regexp" validator');
532
+
533
+ options = _.extend({
534
+ type: 'regexp',
535
+ match: true,
536
+ message: this.errMessages.regexp
537
+ }, options);
538
+
539
+ return function regexp(value) {
540
+ options.value = value;
541
+
542
+ var err = {
543
+ type: options.type,
544
+ message: _.isFunction(options.message) ? options.message(options) : options.message
545
+ };
546
+
547
+ //Don't check empty values (add a 'required' validator for this)
548
+ if (value === null || value === undefined || value === '') return;
549
+
550
+ //Create RegExp from string if it's valid
551
+ if ('string' === typeof options.regexp) options.regexp = new RegExp(options.regexp, options.flags);
552
+
553
+ if ((options.match) ? !options.regexp.test(value) : options.regexp.test(value)) return err;
554
+ };
555
+ };
556
+
557
+ validators.number = function(options) {
558
+ options = _.extend({
559
+ type: 'number',
560
+ message: this.errMessages.number,
561
+ regexp: /^[-+]?([0-9]*.[0-9]+|[0-9]+)$/
562
+ }, options);
563
+
564
+ return validators.regexp(options);
565
+ };
566
+
567
+ validators.range = function(options) {
568
+ options = _.extend({
569
+ type: 'range',
570
+ message: this.errMessages.range,
571
+ numberMessage: this.errMessages.number,
572
+ min: 0,
573
+ max: 100
574
+ }, options);
575
+
576
+ return function range(value) {
577
+ options.value = value;
578
+ var err = {
579
+ type: options.type,
580
+ message: _.isFunction(options.message) ? options.message(options) : options.message
581
+ };
582
+
583
+ //Don't check empty values (add a 'required' validator for this)
584
+ if (value === null || value === undefined || value === '') return;
585
+
586
+ // check value is a number
587
+ var numberCheck = validators.number({message: options.numberMessage})(value);
588
+ if (numberCheck) return numberCheck;
589
+
590
+ // check value is in range
591
+ var number = parseFloat(options.value);
592
+ if (number < options.min || number > options.max) return err;
593
+ }
594
+ }
595
+
596
+ validators.email = function(options) {
597
+ options = _.extend({
598
+ type: 'email',
599
+ message: this.errMessages.email,
600
+ regexp: /^[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/i
601
+ }, options);
602
+
603
+ return validators.regexp(options);
604
+ };
605
+
606
+ validators.url = function(options) {
607
+ options = _.extend({
608
+ type: 'url',
609
+ message: this.errMessages.url,
610
+ regexp: /^((http|https):\/\/)?(([A-Z0-9][A-Z0-9_\-]*)(\.[A-Z0-9][A-Z0-9_\-]*)+)(:(\d+))?\/?/i
611
+ }, options);
612
+
613
+ return validators.regexp(options);
614
+ };
615
+
616
+ validators.match = function(options) {
617
+ if (!options.field) throw new Error('Missing required "field" options for "match" validator');
618
+
619
+ options = _.extend({
620
+ type: 'match',
621
+ message: this.errMessages.match
622
+ }, options);
623
+
624
+ return function match(value, attrs) {
625
+ options.value = value;
626
+
627
+ var err = {
628
+ type: options.type,
629
+ message: _.isFunction(options.message) ? options.message(options) : options.message
630
+ };
631
+
632
+ //Don't check empty values (add a 'required' validator for this)
633
+ if (value === null || value === undefined || value === '') return;
634
+
635
+ if (value !== attrs[options.field]) return err;
636
+ };
637
+ };
638
+
639
+
640
+ return validators;
641
+
642
+ })();
643
+
644
+
645
+ //==================================================================================================
646
+ //FIELDSET
647
+ //==================================================================================================
648
+
649
+ Form.Fieldset = Backbone.View.extend({
650
+
651
+ /**
652
+ * Constructor
653
+ *
654
+ * Valid fieldset schemas:
655
+ * ['field1', 'field2']
656
+ * { legend: 'Some Fieldset', fields: ['field1', 'field2'] }
657
+ *
658
+ * @param {String[]|Object[]} options.schema Fieldset schema
659
+ * @param {Object} options.fields Form fields
660
+ */
661
+ initialize: function(options) {
662
+ options = options || {};
663
+
664
+ //Create the full fieldset schema, merging defaults etc.
665
+ var schema = this.schema = this.createSchema(options.schema);
666
+
667
+ //Store the fields for this fieldset
668
+ this.fields = _.pick(options.fields, schema.fields);
669
+
670
+ //Override defaults
671
+ this.template = options.template || schema.template || this.template || this.constructor.template;
672
+ },
673
+
674
+ /**
675
+ * Creates the full fieldset schema, normalising, merging defaults etc.
676
+ *
677
+ * @param {String[]|Object[]} schema
678
+ *
679
+ * @return {Object}
680
+ */
681
+ createSchema: function(schema) {
682
+ //Normalise to object
683
+ if (_.isArray(schema)) {
684
+ schema = { fields: schema };
685
+ }
686
+
687
+ //Add null legend to prevent template error
688
+ schema.legend = schema.legend || null;
689
+
690
+ return schema;
691
+ },
692
+
693
+ /**
694
+ * Returns the field for a given index
695
+ *
696
+ * @param {Number} index
697
+ *
698
+ * @return {Field}
699
+ */
700
+ getFieldAt: function(index) {
701
+ var key = this.schema.fields[index];
702
+
703
+ return this.fields[key];
704
+ },
705
+
706
+ /**
707
+ * Returns data to pass to template
708
+ *
709
+ * @return {Object}
710
+ */
711
+ templateData: function() {
712
+ return this.schema;
713
+ },
714
+
715
+ /**
716
+ * Renders the fieldset and fields
717
+ *
718
+ * @return {Fieldset} this
719
+ */
720
+ render: function() {
721
+ var schema = this.schema,
722
+ fields = this.fields,
723
+ $ = Backbone.$;
724
+
725
+ //Render fieldset
726
+ var $fieldset = $($.trim(this.template(_.result(this, 'templateData'))));
727
+
728
+ //Render fields
729
+ $fieldset.find('[data-fields]').add($fieldset).each(function(i, el) {
730
+ var $container = $(el),
731
+ selection = $container.attr('data-fields');
732
+
733
+ if (_.isUndefined(selection)) return;
734
+
735
+ _.each(fields, function(field) {
736
+ $container.append(field.render().el);
737
+ });
738
+ });
739
+
740
+ this.setElement($fieldset);
741
+
742
+ return this;
743
+ },
744
+
745
+ /**
746
+ * Remove embedded views then self
747
+ */
748
+ remove: function() {
749
+ _.each(this.fields, function(field) {
750
+ field.remove();
751
+ });
752
+
753
+ Backbone.View.prototype.remove.call(this);
754
+ }
755
+
756
+ }, {
757
+ //STATICS
758
+
759
+ template: _.template('\
760
+ <fieldset data-fields>\
761
+ <% if (legend) { %>\
762
+ <legend><%= legend %></legend>\
763
+ <% } %>\
764
+ </fieldset>\
765
+ ', null, Form.templateSettings)
766
+
767
+ });
768
+
769
+
770
+ //==================================================================================================
771
+ //FIELD
772
+ //==================================================================================================
773
+
774
+ Form.Field = Backbone.View.extend({
775
+
776
+ /**
777
+ * Constructor
778
+ *
779
+ * @param {Object} options.key
780
+ * @param {Object} options.form
781
+ * @param {Object} [options.schema]
782
+ * @param {Function} [options.schema.template]
783
+ * @param {Backbone.Model} [options.model]
784
+ * @param {Object} [options.value]
785
+ * @param {String} [options.idPrefix]
786
+ * @param {Function} [options.template]
787
+ * @param {Function} [options.errorClassName]
788
+ */
789
+ initialize: function(options) {
790
+ options = options || {};
791
+
792
+ //Store important data
793
+ _.extend(this, _.pick(options, 'form', 'key', 'model', 'value', 'idPrefix'));
794
+
795
+ //Create the full field schema, merging defaults etc.
796
+ var schema = this.schema = this.createSchema(options.schema);
797
+
798
+ //Override defaults
799
+ this.template = options.template || schema.template || this.template || this.constructor.template;
800
+ this.errorClassName = options.errorClassName || schema.errorClassName || this.errorClassName || this.constructor.errorClassName;
801
+
802
+ //Create editor
803
+ this.editor = this.createEditor();
804
+ },
805
+
806
+ /**
807
+ * Creates the full field schema, merging defaults etc.
808
+ *
809
+ * @param {Object|String} schema
810
+ *
811
+ * @return {Object}
812
+ */
813
+ createSchema: function(schema) {
814
+ if (_.isString(schema)) schema = { type: schema };
815
+
816
+ //Set defaults
817
+ schema = _.extend({
818
+ type: 'Text',
819
+ title: this.createTitle()
820
+ }, schema);
821
+
822
+ //Get the real constructor function i.e. if type is a string such as 'Text'
823
+ schema.type = (_.isString(schema.type)) ? Form.editors[schema.type] : schema.type;
824
+
825
+ return schema;
826
+ },
827
+
828
+ /**
829
+ * Creates the editor specified in the schema; either an editor string name or
830
+ * a constructor function
831
+ *
832
+ * @return {View}
833
+ */
834
+ createEditor: function() {
835
+ var options = _.extend(
836
+ _.pick(this, 'schema', 'form', 'key', 'model', 'value'),
837
+ { id: this.createEditorId() }
838
+ );
839
+
840
+ var constructorFn = this.schema.type;
841
+
842
+ return new constructorFn(options);
843
+ },
844
+
845
+ /**
846
+ * Creates the ID that will be assigned to the editor
847
+ *
848
+ * @return {String}
849
+ */
850
+ createEditorId: function() {
851
+ var prefix = this.idPrefix,
852
+ id = this.key;
853
+
854
+ //Replace periods with underscores (e.g. for when using paths)
855
+ id = id.replace(/\./g, '_');
856
+
857
+ //If a specific ID prefix is set, use it
858
+ if (_.isString(prefix) || _.isNumber(prefix)) return prefix + id;
859
+ if (_.isNull(prefix)) return id;
860
+
861
+ //Otherwise, if there is a model use it's CID to avoid conflicts when multiple forms are on the page
862
+ if (this.model) return this.model.cid + '_' + id;
863
+
864
+ return id;
865
+ },
866
+
867
+ /**
868
+ * Create the default field title (label text) from the key name.
869
+ * (Converts 'camelCase' to 'Camel Case')
870
+ *
871
+ * @return {String}
872
+ */
873
+ createTitle: function() {
874
+ var str = this.key;
875
+
876
+ //Add spaces
877
+ str = str.replace(/([A-Z])/g, ' $1');
878
+
879
+ //Uppercase first character
880
+ str = str.replace(/^./, function(str) { return str.toUpperCase(); });
881
+
882
+ return str;
883
+ },
884
+
885
+ /**
886
+ * Returns the data to be passed to the template
887
+ *
888
+ * @return {Object}
889
+ */
890
+ templateData: function() {
891
+ var schema = this.schema;
892
+
893
+ return {
894
+ help: schema.help || '',
895
+ title: schema.title,
896
+ titleHTML: schema.titleHTML,
897
+ fieldAttrs: schema.fieldAttrs,
898
+ editorAttrs: schema.editorAttrs,
899
+ key: this.key,
900
+ editorId: this.editor.id
901
+ };
902
+ },
903
+
904
+ /**
905
+ * Render the field and editor
906
+ *
907
+ * @return {Field} self
908
+ */
909
+ render: function() {
910
+ var schema = this.schema,
911
+ editor = this.editor,
912
+ $ = Backbone.$;
913
+
914
+ //Only render the editor if requested
915
+ if (this.editor.noField === true) {
916
+ return this.setElement(editor.render().el);
917
+ }
918
+
919
+ //Render field
920
+ var $field = $($.trim(this.template(_.result(this, 'templateData'))));
921
+
922
+ if (schema.fieldClass) $field.addClass(schema.fieldClass);
923
+ if (schema.fieldAttrs) $field.attr(schema.fieldAttrs);
924
+
925
+ //Render editor
926
+ $field.find('[data-editor]').add($field).each(function(i, el) {
927
+ var $container = $(el),
928
+ selection = $container.attr('data-editor');
929
+
930
+ if (_.isUndefined(selection)) return;
931
+
932
+ $container.append(editor.render().el);
933
+ });
934
+
935
+ this.setElement($field);
936
+
937
+ return this;
938
+ },
939
+
940
+ /**
941
+ * Disable the field's editor
942
+ * Will call the editor's disable method if it exists
943
+ * Otherwise will add the disabled attribute to all inputs in the editor
944
+ */
945
+ disable: function(){
946
+ if ( _.isFunction(this.editor.disable) ){
947
+ this.editor.disable();
948
+ }
949
+ else {
950
+ $input = this.editor.$el;
951
+ $input = $input.is("input") ? $input : $input.find("input");
952
+ $input.attr("disabled",true);
953
+ }
954
+ },
955
+
956
+ /**
957
+ * Enable the field's editor
958
+ * Will call the editor's disable method if it exists
959
+ * Otherwise will remove the disabled attribute to all inputs in the editor
960
+ */
961
+ enable: function(){
962
+ if ( _.isFunction(this.editor.enable) ){
963
+ this.editor.enable();
964
+ }
965
+ else {
966
+ $input = this.editor.$el;
967
+ $input = $input.is("input") ? $input : $input.find("input");
968
+ $input.attr("disabled",false);
969
+ }
970
+ },
971
+
972
+ /**
973
+ * Check the validity of the field
974
+ *
975
+ * @return {String}
976
+ */
977
+ validate: function() {
978
+ var error = this.editor.validate();
979
+
980
+ if (error) {
981
+ this.setError(error.message);
982
+ } else {
983
+ this.clearError();
984
+ }
985
+
986
+ return error;
987
+ },
988
+
989
+ /**
990
+ * Set the field into an error state, adding the error class and setting the error message
991
+ *
992
+ * @param {String} msg Error message
993
+ */
994
+ setError: function(msg) {
995
+ //Nested form editors (e.g. Object) set their errors internally
996
+ if (this.editor.hasNestedForm) return;
997
+
998
+ //Add error CSS class
999
+ this.$el.addClass(this.errorClassName);
1000
+
1001
+ //Set error message
1002
+ this.$('[data-error]').last().html(msg);
1003
+ },
1004
+
1005
+ /**
1006
+ * Clear the error state and reset the help message
1007
+ */
1008
+ clearError: function() {
1009
+ //Remove error CSS class
1010
+ this.$el.removeClass(this.errorClassName);
1011
+
1012
+ //Clear error message
1013
+ this.$('[data-error]').empty();
1014
+ },
1015
+
1016
+ /**
1017
+ * Update the model with the new value from the editor
1018
+ *
1019
+ * @return {Mixed}
1020
+ */
1021
+ commit: function() {
1022
+ return this.editor.commit();
1023
+ },
1024
+
1025
+ /**
1026
+ * Get the value from the editor
1027
+ *
1028
+ * @return {Mixed}
1029
+ */
1030
+ getValue: function() {
1031
+ return this.editor.getValue();
1032
+ },
1033
+
1034
+ /**
1035
+ * Set/change the value of the editor
1036
+ *
1037
+ * @param {Mixed} value
1038
+ */
1039
+ setValue: function(value) {
1040
+ this.editor.setValue(value);
1041
+ },
1042
+
1043
+ /**
1044
+ * Give the editor focus
1045
+ */
1046
+ focus: function() {
1047
+ this.editor.focus();
1048
+ },
1049
+
1050
+ /**
1051
+ * Remove focus from the editor
1052
+ */
1053
+ blur: function() {
1054
+ this.editor.blur();
1055
+ },
1056
+
1057
+ /**
1058
+ * Remove the field and editor views
1059
+ */
1060
+ remove: function() {
1061
+ this.editor.remove();
1062
+
1063
+ Backbone.View.prototype.remove.call(this);
1064
+ }
1065
+
1066
+ }, {
1067
+ //STATICS
1068
+
1069
+ template: _.template('\
1070
+ <div>\
1071
+ <label for="<%= editorId %>">\
1072
+ <% if (titleHTML){ %><%= titleHTML %>\
1073
+ <% } else { %><%- title %><% } %>\
1074
+ </label>\
1075
+ <div>\
1076
+ <span data-editor></span>\
1077
+ <div data-error></div>\
1078
+ <div><%= help %></div>\
1079
+ </div>\
1080
+ </div>\
1081
+ ', null, Form.templateSettings),
1082
+
1083
+ /**
1084
+ * CSS class name added to the field when there is a validation error
1085
+ */
1086
+ errorClassName: 'error'
1087
+
1088
+ });
1089
+
1090
+
1091
+ //==================================================================================================
1092
+ //NESTEDFIELD
1093
+ //==================================================================================================
1094
+
1095
+ Form.NestedField = Form.Field.extend({
1096
+
1097
+ template: _.template('\
1098
+ <div>\
1099
+ <label for="<%= editorId %>">\
1100
+ <% if (titleHTML){ %><%= titleHTML %>\
1101
+ <% } else { %><%- title %><% } %>\
1102
+ </label>\
1103
+ <div>\
1104
+ <span data-editor></span>\
1105
+ <div class="error-text" data-error></div>\
1106
+ <div class="error-help"><%= help %></div>\
1107
+ </div>\
1108
+ </div>\
1109
+ ', null, Form.templateSettings)
1110
+
1111
+ });
1112
+
1113
+ /**
1114
+ * Base editor (interface). To be extended, not used directly
1115
+ *
1116
+ * @param {Object} options
1117
+ * @param {String} [options.id] Editor ID
1118
+ * @param {Model} [options.model] Use instead of value, and use commit()
1119
+ * @param {String} [options.key] The model attribute key. Required when using 'model'
1120
+ * @param {Mixed} [options.value] When not using a model. If neither provided, defaultValue will be used
1121
+ * @param {Object} [options.schema] Field schema; may be required by some editors
1122
+ * @param {Object} [options.validators] Validators; falls back to those stored on schema
1123
+ * @param {Object} [options.form] The form
1124
+ */
1125
+ Form.Editor = Form.editors.Base = Backbone.View.extend({
1126
+
1127
+ defaultValue: null,
1128
+
1129
+ hasFocus: false,
1130
+
1131
+ initialize: function(options) {
1132
+ var options = options || {};
1133
+
1134
+ //Set initial value
1135
+ if (options.model) {
1136
+ if (!options.key) throw new Error("Missing option: 'key'");
1137
+
1138
+ this.model = options.model;
1139
+
1140
+ this.value = this.model.get(options.key);
1141
+ }
1142
+ else if (options.value !== undefined) {
1143
+ this.value = options.value;
1144
+ }
1145
+
1146
+ if (this.value === undefined) this.value = this.defaultValue;
1147
+
1148
+ //Store important data
1149
+ _.extend(this, _.pick(options, 'key', 'form'));
1150
+
1151
+ var schema = this.schema = options.schema || {};
1152
+
1153
+ this.validators = options.validators || schema.validators;
1154
+
1155
+ //Main attributes
1156
+ this.$el.attr('id', this.id);
1157
+ this.$el.attr('name', this.getName());
1158
+ if (schema.editorClass) this.$el.addClass(schema.editorClass);
1159
+ if (schema.editorAttrs) this.$el.attr(schema.editorAttrs);
1160
+ },
1161
+
1162
+ /**
1163
+ * Get the value for the form input 'name' attribute
1164
+ *
1165
+ * @return {String}
1166
+ *
1167
+ * @api private
1168
+ */
1169
+ getName: function() {
1170
+ var key = this.key || '';
1171
+
1172
+ //Replace periods with underscores (e.g. for when using paths)
1173
+ return key.replace(/\./g, '_');
1174
+ },
1175
+
1176
+ /**
1177
+ * Get editor value
1178
+ * Extend and override this method to reflect changes in the DOM
1179
+ *
1180
+ * @return {Mixed}
1181
+ */
1182
+ getValue: function() {
1183
+ return this.value;
1184
+ },
1185
+
1186
+ /**
1187
+ * Set editor value
1188
+ * Extend and override this method to reflect changes in the DOM
1189
+ *
1190
+ * @param {Mixed} value
1191
+ */
1192
+ setValue: function(value) {
1193
+ this.value = value;
1194
+ },
1195
+
1196
+ /**
1197
+ * Give the editor focus
1198
+ * Extend and override this method
1199
+ */
1200
+ focus: function() {
1201
+ throw new Error('Not implemented');
1202
+ },
1203
+
1204
+ /**
1205
+ * Remove focus from the editor
1206
+ * Extend and override this method
1207
+ */
1208
+ blur: function() {
1209
+ throw new Error('Not implemented');
1210
+ },
1211
+
1212
+ /**
1213
+ * Update the model with the current value
1214
+ *
1215
+ * @param {Object} [options] Options to pass to model.set()
1216
+ * @param {Boolean} [options.validate] Set to true to trigger built-in model validation
1217
+ *
1218
+ * @return {Mixed} error
1219
+ */
1220
+ commit: function(options) {
1221
+ var error = this.validate();
1222
+ if (error) return error;
1223
+
1224
+ this.listenTo(this.model, 'invalid', function(model, e) {
1225
+ error = e;
1226
+ });
1227
+ this.model.set(this.key, this.getValue(), options);
1228
+
1229
+ if (error) return error;
1230
+ },
1231
+
1232
+ /**
1233
+ * Check validity
1234
+ *
1235
+ * @return {Object|Undefined}
1236
+ */
1237
+ validate: function() {
1238
+ var $el = this.$el,
1239
+ error = null,
1240
+ value = this.getValue(),
1241
+ formValues = this.form ? this.form.getValue() : {},
1242
+ validators = this.validators,
1243
+ getValidator = this.getValidator;
1244
+
1245
+ if (validators) {
1246
+ //Run through validators until an error is found
1247
+ _.every(validators, function(validator) {
1248
+ error = getValidator(validator)(value, formValues);
1249
+
1250
+ return error ? false : true;
1251
+ });
1252
+ }
1253
+
1254
+ return error;
1255
+ },
1256
+
1257
+ /**
1258
+ * Set this.hasFocus, or call parent trigger()
1259
+ *
1260
+ * @param {String} event
1261
+ */
1262
+ trigger: function(event) {
1263
+ if (event === 'focus') {
1264
+ this.hasFocus = true;
1265
+ }
1266
+ else if (event === 'blur') {
1267
+ this.hasFocus = false;
1268
+ }
1269
+
1270
+ return Backbone.View.prototype.trigger.apply(this, arguments);
1271
+ },
1272
+
1273
+ /**
1274
+ * Returns a validation function based on the type defined in the schema
1275
+ *
1276
+ * @param {RegExp|String|Function} validator
1277
+ * @return {Function}
1278
+ */
1279
+ getValidator: function(validator) {
1280
+ var validators = Form.validators;
1281
+
1282
+ //Convert regular expressions to validators
1283
+ if (_.isRegExp(validator)) {
1284
+ return validators.regexp({ regexp: validator });
1285
+ }
1286
+
1287
+ //Use a built-in validator if given a string
1288
+ if (_.isString(validator)) {
1289
+ if (!validators[validator]) throw new Error('Validator "'+validator+'" not found');
1290
+
1291
+ return validators[validator]();
1292
+ }
1293
+
1294
+ //Functions can be used directly
1295
+ if (_.isFunction(validator)) return validator;
1296
+
1297
+ //Use a customised built-in validator if given an object
1298
+ if (_.isObject(validator) && validator.type) {
1299
+ var config = validator;
1300
+
1301
+ return validators[config.type](config);
1302
+ }
1303
+
1304
+ //Unkown validator type
1305
+ throw new Error('Invalid validator: ' + validator);
1306
+ }
1307
+ });
1308
+
1309
+ /**
1310
+ * Text
1311
+ *
1312
+ * Text input with focus, blur and change events
1313
+ */
1314
+ Form.editors.Text = Form.Editor.extend({
1315
+
1316
+ tagName: 'input',
1317
+
1318
+ defaultValue: '',
1319
+
1320
+ previousValue: '',
1321
+
1322
+ events: {
1323
+ 'keyup': 'determineChange',
1324
+ 'keypress': function(event) {
1325
+ var self = this;
1326
+ setTimeout(function() {
1327
+ self.determineChange();
1328
+ }, 0);
1329
+ },
1330
+ 'select': function(event) {
1331
+ this.trigger('select', this);
1332
+ },
1333
+ 'focus': function(event) {
1334
+ this.trigger('focus', this);
1335
+ },
1336
+ 'blur': function(event) {
1337
+ this.trigger('blur', this);
1338
+ }
1339
+ },
1340
+
1341
+ initialize: function(options) {
1342
+ Form.editors.Base.prototype.initialize.call(this, options);
1343
+
1344
+ var schema = this.schema;
1345
+
1346
+ //Allow customising text type (email, phone etc.) for HTML5 browsers
1347
+ var type = 'text';
1348
+
1349
+ if (schema && schema.editorAttrs && schema.editorAttrs.type) type = schema.editorAttrs.type;
1350
+ if (schema && schema.dataType) type = schema.dataType;
1351
+
1352
+ this.$el.attr('type', type);
1353
+ },
1354
+
1355
+ /**
1356
+ * Adds the editor to the DOM
1357
+ */
1358
+ render: function() {
1359
+ this.setValue(this.value);
1360
+
1361
+ return this;
1362
+ },
1363
+
1364
+ determineChange: function(event) {
1365
+ var currentValue = this.$el.val();
1366
+ var changed = (currentValue !== this.previousValue);
1367
+
1368
+ if (changed) {
1369
+ this.previousValue = currentValue;
1370
+
1371
+ this.trigger('change', this);
1372
+ }
1373
+ },
1374
+
1375
+ /**
1376
+ * Returns the current editor value
1377
+ * @return {String}
1378
+ */
1379
+ getValue: function() {
1380
+ return this.$el.val();
1381
+ },
1382
+
1383
+ /**
1384
+ * Sets the value of the form element
1385
+ * @param {String}
1386
+ */
1387
+ setValue: function(value) {
1388
+ this.value = value;
1389
+ this.$el.val(value);
1390
+ },
1391
+
1392
+ focus: function() {
1393
+ if (this.hasFocus) return;
1394
+
1395
+ this.$el.focus();
1396
+ },
1397
+
1398
+ blur: function() {
1399
+ if (!this.hasFocus) return;
1400
+
1401
+ this.$el.blur();
1402
+ },
1403
+
1404
+ select: function() {
1405
+ this.$el.select();
1406
+ }
1407
+
1408
+ });
1409
+
1410
+ /**
1411
+ * TextArea editor
1412
+ */
1413
+ Form.editors.TextArea = Form.editors.Text.extend({
1414
+
1415
+ tagName: 'textarea',
1416
+
1417
+ /**
1418
+ * Override Text constructor so type property isn't set (issue #261)
1419
+ */
1420
+ initialize: function(options) {
1421
+ Form.editors.Base.prototype.initialize.call(this, options);
1422
+ }
1423
+
1424
+ });
1425
+
1426
+ /**
1427
+ * Password editor
1428
+ */
1429
+ Form.editors.Password = Form.editors.Text.extend({
1430
+
1431
+ initialize: function(options) {
1432
+ Form.editors.Text.prototype.initialize.call(this, options);
1433
+
1434
+ this.$el.attr('type', 'password');
1435
+ }
1436
+
1437
+ });
1438
+
1439
+ /**
1440
+ * NUMBER
1441
+ *
1442
+ * Normal text input that only allows a number. Letters etc. are not entered.
1443
+ */
1444
+ Form.editors.Number = Form.editors.Text.extend({
1445
+
1446
+ defaultValue: 0,
1447
+
1448
+ events: _.extend({}, Form.editors.Text.prototype.events, {
1449
+ 'keypress': 'onKeyPress',
1450
+ 'change': 'onKeyPress'
1451
+ }),
1452
+
1453
+ initialize: function(options) {
1454
+ Form.editors.Text.prototype.initialize.call(this, options);
1455
+
1456
+ var schema = this.schema;
1457
+
1458
+ this.$el.attr('type', 'number');
1459
+
1460
+ if (!schema || !schema.editorAttrs || !schema.editorAttrs.step) {
1461
+ // provide a default for `step` attr,
1462
+ // but don't overwrite if already specified
1463
+ this.$el.attr('step', 'any');
1464
+ }
1465
+ },
1466
+
1467
+ /**
1468
+ * Check value is numeric
1469
+ */
1470
+ onKeyPress: function(event) {
1471
+ var self = this,
1472
+ delayedDetermineChange = function() {
1473
+ setTimeout(function() {
1474
+ self.determineChange();
1475
+ }, 0);
1476
+ };
1477
+
1478
+ //Allow backspace
1479
+ if (event.charCode === 0) {
1480
+ delayedDetermineChange();
1481
+ return;
1482
+ }
1483
+
1484
+ //Get the whole new value so that we can prevent things like double decimals points etc.
1485
+ var newVal = this.$el.val()
1486
+ if( event.charCode != undefined ) {
1487
+ newVal = newVal + String.fromCharCode(event.charCode);
1488
+ }
1489
+
1490
+ var numeric = /^[0-9]*\.?[0-9]*?$/.test(newVal);
1491
+
1492
+ if (numeric) {
1493
+ delayedDetermineChange();
1494
+ }
1495
+ else {
1496
+ event.preventDefault();
1497
+ }
1498
+ },
1499
+
1500
+ getValue: function() {
1501
+ var value = this.$el.val();
1502
+
1503
+ return value === "" ? null : parseFloat(value, 10);
1504
+ },
1505
+
1506
+ setValue: function(value) {
1507
+ value = (function() {
1508
+ if (_.isNumber(value)) return value;
1509
+
1510
+ if (_.isString(value) && value !== '') return parseFloat(value, 10);
1511
+
1512
+ return null;
1513
+ })();
1514
+
1515
+ if (_.isNaN(value)) value = null;
1516
+ this.value = value;
1517
+ Form.editors.Text.prototype.setValue.call(this, value);
1518
+ }
1519
+
1520
+ });
1521
+
1522
+ /**
1523
+ * Hidden editor
1524
+ */
1525
+ Form.editors.Hidden = Form.editors.Text.extend({
1526
+
1527
+ defaultValue: '',
1528
+
1529
+ noField: true,
1530
+
1531
+ initialize: function(options) {
1532
+ Form.editors.Text.prototype.initialize.call(this, options);
1533
+
1534
+ this.$el.attr('type', 'hidden');
1535
+ },
1536
+
1537
+ focus: function() {
1538
+
1539
+ },
1540
+
1541
+ blur: function() {
1542
+
1543
+ }
1544
+
1545
+ });
1546
+
1547
+ /**
1548
+ * Checkbox editor
1549
+ *
1550
+ * Creates a single checkbox, i.e. boolean value
1551
+ */
1552
+ Form.editors.Checkbox = Form.editors.Base.extend({
1553
+
1554
+ defaultValue: false,
1555
+
1556
+ tagName: 'input',
1557
+
1558
+ events: {
1559
+ 'click': function(event) {
1560
+ this.trigger('change', this);
1561
+ },
1562
+ 'focus': function(event) {
1563
+ this.trigger('focus', this);
1564
+ },
1565
+ 'blur': function(event) {
1566
+ this.trigger('blur', this);
1567
+ }
1568
+ },
1569
+
1570
+ initialize: function(options) {
1571
+ Form.editors.Base.prototype.initialize.call(this, options);
1572
+
1573
+ this.$el.attr('type', 'checkbox');
1574
+ },
1575
+
1576
+ /**
1577
+ * Adds the editor to the DOM
1578
+ */
1579
+ render: function() {
1580
+ this.setValue(this.value);
1581
+
1582
+ return this;
1583
+ },
1584
+
1585
+ getValue: function() {
1586
+ return this.$el.prop('checked');
1587
+ },
1588
+
1589
+ setValue: function(value) {
1590
+ if (value) {
1591
+ this.$el.prop('checked', true);
1592
+ }else{
1593
+ this.$el.prop('checked', false);
1594
+ }
1595
+ this.value = !!value;
1596
+ },
1597
+
1598
+ focus: function() {
1599
+ if (this.hasFocus) return;
1600
+
1601
+ this.$el.focus();
1602
+ },
1603
+
1604
+ blur: function() {
1605
+ if (!this.hasFocus) return;
1606
+
1607
+ this.$el.blur();
1608
+ }
1609
+
1610
+ });
1611
+
1612
+ /**
1613
+ * Select editor
1614
+ *
1615
+ * Renders a <select> with given options
1616
+ *
1617
+ * Requires an 'options' value on the schema.
1618
+ * Can be an array of options, a function that calls back with the array of options, a string of HTML
1619
+ * or a Backbone collection. If a collection, the models must implement a toString() method
1620
+ */
1621
+ Form.editors.Select = Form.editors.Base.extend({
1622
+
1623
+ tagName: 'select',
1624
+
1625
+ previousValue: '',
1626
+
1627
+ events: {
1628
+ 'keyup': 'determineChange',
1629
+ 'keypress': function(event) {
1630
+ var self = this;
1631
+ setTimeout(function() {
1632
+ self.determineChange();
1633
+ }, 0);
1634
+ },
1635
+ 'change': function(event) {
1636
+ this.trigger('change', this);
1637
+ },
1638
+ 'focus': function(event) {
1639
+ this.trigger('focus', this);
1640
+ },
1641
+ 'blur': function(event) {
1642
+ this.trigger('blur', this);
1643
+ }
1644
+ },
1645
+
1646
+ initialize: function(options) {
1647
+ Form.editors.Base.prototype.initialize.call(this, options);
1648
+
1649
+ if (!this.schema || !this.schema.options) throw new Error("Missing required 'schema.options'");
1650
+ },
1651
+
1652
+ render: function() {
1653
+ this.setOptions(this.schema.options);
1654
+
1655
+ return this;
1656
+ },
1657
+
1658
+ /**
1659
+ * Sets the options that populate the <select>
1660
+ *
1661
+ * @param {Mixed} options
1662
+ */
1663
+ setOptions: function(options) {
1664
+ var self = this;
1665
+
1666
+ //If a collection was passed, check if it needs fetching
1667
+ if (options instanceof Backbone.Collection) {
1668
+ var collection = options;
1669
+
1670
+ //Don't do the fetch if it's already populated
1671
+ if (collection.length > 0) {
1672
+ this.renderOptions(options);
1673
+ } else {
1674
+ collection.fetch({
1675
+ success: function(collection) {
1676
+ self.renderOptions(options);
1677
+ }
1678
+ });
1679
+ }
1680
+ }
1681
+
1682
+ //If a function was passed, run it to get the options
1683
+ else if (_.isFunction(options)) {
1684
+ options(function(result) {
1685
+ self.renderOptions(result);
1686
+ }, self);
1687
+ }
1688
+
1689
+ //Otherwise, ready to go straight to renderOptions
1690
+ else {
1691
+ this.renderOptions(options);
1692
+ }
1693
+ },
1694
+
1695
+ /**
1696
+ * Adds the <option> html to the DOM
1697
+ * @param {Mixed} Options as a simple array e.g. ['option1', 'option2']
1698
+ * or as an array of objects e.g. [{val: 543, label: 'Title for object 543'}]
1699
+ * or as a string of <option> HTML to insert into the <select>
1700
+ * or any object
1701
+ */
1702
+ renderOptions: function(options) {
1703
+ var $select = this.$el,
1704
+ html;
1705
+
1706
+ html = this._getOptionsHtml(options);
1707
+
1708
+ //Insert options
1709
+ $select.html(html);
1710
+
1711
+ //Select correct option
1712
+ this.setValue(this.value);
1713
+ },
1714
+
1715
+ _getOptionsHtml: function(options) {
1716
+ var html;
1717
+ //Accept string of HTML
1718
+ if (_.isString(options)) {
1719
+ html = options;
1720
+ }
1721
+
1722
+ //Or array
1723
+ else if (_.isArray(options)) {
1724
+ html = this._arrayToHtml(options);
1725
+ }
1726
+
1727
+ //Or Backbone collection
1728
+ else if (options instanceof Backbone.Collection) {
1729
+ html = this._collectionToHtml(options);
1730
+ }
1731
+
1732
+ else if (_.isFunction(options)) {
1733
+ var newOptions;
1734
+
1735
+ options(function(opts) {
1736
+ newOptions = opts;
1737
+ }, this);
1738
+
1739
+ html = this._getOptionsHtml(newOptions);
1740
+ //Or any object
1741
+ }else{
1742
+ html = this._objectToHtml(options);
1743
+ }
1744
+
1745
+ return html;
1746
+ },
1747
+
1748
+ determineChange: function(event) {
1749
+ var currentValue = this.getValue();
1750
+ var changed = (currentValue !== this.previousValue);
1751
+
1752
+ if (changed) {
1753
+ this.previousValue = currentValue;
1754
+
1755
+ this.trigger('change', this);
1756
+ }
1757
+ },
1758
+
1759
+ getValue: function() {
1760
+ return this.$el.val();
1761
+ },
1762
+
1763
+ setValue: function(value) {
1764
+ this.value = value;
1765
+ this.$el.val(value);
1766
+ },
1767
+
1768
+ focus: function() {
1769
+ if (this.hasFocus) return;
1770
+
1771
+ this.$el.focus();
1772
+ },
1773
+
1774
+ blur: function() {
1775
+ if (!this.hasFocus) return;
1776
+
1777
+ this.$el.blur();
1778
+ },
1779
+
1780
+ /**
1781
+ * Transforms a collection into HTML ready to use in the renderOptions method
1782
+ * @param {Backbone.Collection}
1783
+ * @return {String}
1784
+ */
1785
+ _collectionToHtml: function(collection) {
1786
+ //Convert collection to array first
1787
+ var array = [];
1788
+ collection.each(function(model) {
1789
+ array.push({ val: model.id, label: model.toString() });
1790
+ });
1791
+
1792
+ //Now convert to HTML
1793
+ var html = this._arrayToHtml(array);
1794
+
1795
+ return html;
1796
+ },
1797
+ /**
1798
+ * Transforms an object into HTML ready to use in the renderOptions method
1799
+ * @param {Object}
1800
+ * @return {String}
1801
+ */
1802
+ _objectToHtml: function(obj) {
1803
+ //Convert object to array first
1804
+ var array = [];
1805
+ for(var key in obj){
1806
+ if( obj.hasOwnProperty( key ) ) {
1807
+ array.push({ val: key, label: obj[key] });
1808
+ }
1809
+ }
1810
+
1811
+ //Now convert to HTML
1812
+ var html = this._arrayToHtml(array);
1813
+
1814
+ return html;
1815
+ },
1816
+
1817
+
1818
+
1819
+ /**
1820
+ * Create the <option> HTML
1821
+ * @param {Array} Options as a simple array e.g. ['option1', 'option2']
1822
+ * or as an array of objects e.g. [{val: 543, label: 'Title for object 543'}]
1823
+ * @return {String} HTML
1824
+ */
1825
+ _arrayToHtml: function(array) {
1826
+ var html = $();
1827
+
1828
+ //Generate HTML
1829
+ _.each(array, function(option) {
1830
+ if (_.isObject(option)) {
1831
+ if (option.group) {
1832
+ var optgroup = $("<optgroup>")
1833
+ .attr("label",option.group)
1834
+ .html( this._getOptionsHtml(option.options) );
1835
+ html = html.add(optgroup);
1836
+ } else {
1837
+ var val = (option.val || option.val === 0) ? option.val : '';
1838
+ html = html.add( $('<option>').val(val).text(option.label) );
1839
+ }
1840
+ }
1841
+ else {
1842
+ html = html.add( $('<option>').text(option) );
1843
+ }
1844
+ }, this);
1845
+
1846
+ return html;
1847
+ }
1848
+
1849
+ });
1850
+
1851
+ /**
1852
+ * Radio editor
1853
+ *
1854
+ * Renders a <ul> with given options represented as <li> objects containing radio buttons
1855
+ *
1856
+ * Requires an 'options' value on the schema.
1857
+ * Can be an array of options, a function that calls back with the array of options, a string of HTML
1858
+ * or a Backbone collection. If a collection, the models must implement a toString() method
1859
+ */
1860
+ Form.editors.Radio = Form.editors.Select.extend({
1861
+
1862
+ tagName: 'ul',
1863
+
1864
+ events: {
1865
+ 'change input[type=radio]': function() {
1866
+ this.trigger('change', this);
1867
+ },
1868
+ 'focus input[type=radio]': function() {
1869
+ if (this.hasFocus) return;
1870
+ this.trigger('focus', this);
1871
+ },
1872
+ 'blur input[type=radio]': function() {
1873
+ if (!this.hasFocus) return;
1874
+ var self = this;
1875
+ setTimeout(function() {
1876
+ if (self.$('input[type=radio]:focus')[0]) return;
1877
+ self.trigger('blur', self);
1878
+ }, 0);
1879
+ }
1880
+ },
1881
+
1882
+ /**
1883
+ * Returns the template. Override for custom templates
1884
+ *
1885
+ * @return {Function} Compiled template
1886
+ */
1887
+ getTemplate: function() {
1888
+ return this.schema.template || this.constructor.template;
1889
+ },
1890
+
1891
+ getValue: function() {
1892
+ return this.$('input[type=radio]:checked').val();
1893
+ },
1894
+
1895
+ setValue: function(value) {
1896
+ this.value = value;
1897
+ this.$('input[type=radio]').val([value]);
1898
+ },
1899
+
1900
+ focus: function() {
1901
+ if (this.hasFocus) return;
1902
+
1903
+ var checked = this.$('input[type=radio]:checked');
1904
+ if (checked[0]) {
1905
+ checked.focus();
1906
+ return;
1907
+ }
1908
+
1909
+ this.$('input[type=radio]').first().focus();
1910
+ },
1911
+
1912
+ blur: function() {
1913
+ if (!this.hasFocus) return;
1914
+
1915
+ this.$('input[type=radio]:focus').blur();
1916
+ },
1917
+
1918
+ /**
1919
+ * Create the radio list HTML
1920
+ * @param {Array} Options as a simple array e.g. ['option1', 'option2']
1921
+ * or as an array of objects e.g. [{val: 543, label: 'Title for object 543'}]
1922
+ * @return {String} HTML
1923
+ */
1924
+ _arrayToHtml: function (array) {
1925
+ var self = this;
1926
+
1927
+ var template = this.getTemplate(),
1928
+ name = self.getName(),
1929
+ id = self.id;
1930
+
1931
+ var items = _.map(array, function(option, index) {
1932
+ var item = {
1933
+ name: name,
1934
+ id: id + '-' + index
1935
+ };
1936
+
1937
+ if (_.isObject(option)) {
1938
+ item.value = (option.val || option.val === 0) ? option.val : '';
1939
+ item.label = option.label;
1940
+ item.labelHTML = option.labelHTML;
1941
+ } else {
1942
+ item.value = option;
1943
+ item.label = option;
1944
+ }
1945
+
1946
+ return item;
1947
+ });
1948
+
1949
+ return template({ items: items });
1950
+ }
1951
+
1952
+ }, {
1953
+
1954
+ //STATICS
1955
+ template: _.template('\
1956
+ <% _.each(items, function(item) { %>\
1957
+ <li>\
1958
+ <input type="radio" name="<%= item.name %>" value="<%- item.value %>" id="<%= item.id %>" />\
1959
+ <label for="<%= item.id %>"><% if (item.labelHTML){ %><%= item.labelHTML %><% }else{ %><%- item.label %><% } %></label>\
1960
+ </li>\
1961
+ <% }); %>\
1962
+ ', null, Form.templateSettings)
1963
+
1964
+ });
1965
+
1966
+ /**
1967
+ * Checkboxes editor
1968
+ *
1969
+ * Renders a <ul> with given options represented as <li> objects containing checkboxes
1970
+ *
1971
+ * Requires an 'options' value on the schema.
1972
+ * Can be an array of options, a function that calls back with the array of options, a string of HTML
1973
+ * or a Backbone collection. If a collection, the models must implement a toString() method
1974
+ */
1975
+ Form.editors.Checkboxes = Form.editors.Select.extend({
1976
+
1977
+ tagName: 'ul',
1978
+
1979
+ groupNumber: 0,
1980
+
1981
+ events: {
1982
+ 'click input[type=checkbox]': function() {
1983
+ this.trigger('change', this);
1984
+ },
1985
+ 'focus input[type=checkbox]': function() {
1986
+ if (this.hasFocus) return;
1987
+ this.trigger('focus', this);
1988
+ },
1989
+ 'blur input[type=checkbox]': function() {
1990
+ if (!this.hasFocus) return;
1991
+ var self = this;
1992
+ setTimeout(function() {
1993
+ if (self.$('input[type=checkbox]:focus')[0]) return;
1994
+ self.trigger('blur', self);
1995
+ }, 0);
1996
+ }
1997
+ },
1998
+
1999
+ getValue: function() {
2000
+ var values = [];
2001
+ var self = this;
2002
+ this.$('input[type=checkbox]:checked').each(function() {
2003
+ values.push(self.$(this).val());
2004
+ });
2005
+ return values;
2006
+ },
2007
+
2008
+ setValue: function(values) {
2009
+ if (!_.isArray(values)) values = [values];
2010
+ this.value = values;
2011
+ this.$('input[type=checkbox]').val(values);
2012
+ },
2013
+
2014
+ focus: function() {
2015
+ if (this.hasFocus) return;
2016
+
2017
+ this.$('input[type=checkbox]').first().focus();
2018
+ },
2019
+
2020
+ blur: function() {
2021
+ if (!this.hasFocus) return;
2022
+
2023
+ this.$('input[type=checkbox]:focus').blur();
2024
+ },
2025
+
2026
+ /**
2027
+ * Create the checkbox list HTML
2028
+ * @param {Array} Options as a simple array e.g. ['option1', 'option2']
2029
+ * or as an array of objects e.g. [{val: 543, label: 'Title for object 543'}]
2030
+ * @return {String} HTML
2031
+ */
2032
+ _arrayToHtml: function (array) {
2033
+ var html = $();
2034
+ var self = this;
2035
+
2036
+ _.each(array, function(option, index) {
2037
+ var itemHtml = $('<li>');
2038
+ if (_.isObject(option)) {
2039
+ if (option.group) {
2040
+ var originalId = self.id;
2041
+ self.id += "-" + self.groupNumber++;
2042
+ itemHtml = $('<fieldset class="group">').append( $('<legend>').text(option.group) );
2043
+ itemHtml = itemHtml.append( self._arrayToHtml(option.options) );
2044
+ self.id = originalId;
2045
+ close = false;
2046
+ }else{
2047
+ var val = (option.val || option.val === 0) ? option.val : '';
2048
+ itemHtml.append( $('<input type="checkbox" name="'+self.getName()+'" id="'+self.id+'-'+index+'" />').val(val) );
2049
+ if (option.labelHTML){
2050
+ itemHtml.append( $('<label for="'+self.id+'-'+index+'" />').html(option.labelHTML) );
2051
+ }
2052
+ else {
2053
+ itemHtml.append( $('<label for="'+self.id+'-'+index+'" />').text(option.label) );
2054
+ }
2055
+ }
2056
+ }
2057
+ else {
2058
+ itemHtml.append( $('<input type="checkbox" name="'+self.getName()+'" id="'+self.id+'-'+index+'" />').val(option) );
2059
+ itemHtml.append( $('<label for="'+self.id+'-'+index+'" />').text(option) );
2060
+ }
2061
+ html = html.add(itemHtml);
2062
+ });
2063
+
2064
+ return html;
2065
+ }
2066
+
2067
+ });
2068
+
2069
+ /**
2070
+ * Object editor
2071
+ *
2072
+ * Creates a child form. For editing Javascript objects
2073
+ *
2074
+ * @param {Object} options
2075
+ * @param {Form} options.form The form this editor belongs to; used to determine the constructor for the nested form
2076
+ * @param {Object} options.schema The schema for the object
2077
+ * @param {Object} options.schema.subSchema The schema for the nested form
2078
+ */
2079
+ Form.editors.Object = Form.editors.Base.extend({
2080
+ //Prevent error classes being set on the main control; they are internally on the individual fields
2081
+ hasNestedForm: true,
2082
+
2083
+ initialize: function(options) {
2084
+ //Set default value for the instance so it's not a shared object
2085
+ this.value = {};
2086
+
2087
+ //Init
2088
+ Form.editors.Base.prototype.initialize.call(this, options);
2089
+
2090
+ //Check required options
2091
+ if (!this.form) throw new Error('Missing required option "form"');
2092
+ if (!this.schema.subSchema) throw new Error("Missing required 'schema.subSchema' option for Object editor");
2093
+ },
2094
+
2095
+ render: function() {
2096
+ //Get the constructor for creating the nested form; i.e. the same constructor as used by the parent form
2097
+ var NestedForm = this.form.constructor;
2098
+
2099
+ //Create the nested form
2100
+ this.nestedForm = new NestedForm({
2101
+ schema: this.schema.subSchema,
2102
+ data: this.value,
2103
+ idPrefix: this.id + '_',
2104
+ Field: NestedForm.NestedField
2105
+ });
2106
+
2107
+ this._observeFormEvents();
2108
+
2109
+ this.$el.html(this.nestedForm.render().el);
2110
+
2111
+ if (this.hasFocus) this.trigger('blur', this);
2112
+
2113
+ return this;
2114
+ },
2115
+
2116
+ getValue: function() {
2117
+ if (this.nestedForm) return this.nestedForm.getValue();
2118
+
2119
+ return this.value;
2120
+ },
2121
+
2122
+ setValue: function(value) {
2123
+ this.value = value;
2124
+
2125
+ this.render();
2126
+ },
2127
+
2128
+ focus: function() {
2129
+ if (this.hasFocus) return;
2130
+
2131
+ this.nestedForm.focus();
2132
+ },
2133
+
2134
+ blur: function() {
2135
+ if (!this.hasFocus) return;
2136
+
2137
+ this.nestedForm.blur();
2138
+ },
2139
+
2140
+ remove: function() {
2141
+ this.nestedForm.remove();
2142
+
2143
+ Backbone.View.prototype.remove.call(this);
2144
+ },
2145
+
2146
+ validate: function() {
2147
+ var errors = _.extend({},
2148
+ Form.editors.Base.prototype.validate.call(this),
2149
+ this.nestedForm.validate()
2150
+ );
2151
+ return _.isEmpty(errors)?false:errors;
2152
+ },
2153
+
2154
+ _observeFormEvents: function() {
2155
+ if (!this.nestedForm) return;
2156
+
2157
+ this.nestedForm.on('all', function() {
2158
+ // args = ["key:change", form, fieldEditor]
2159
+ var args = _.toArray(arguments);
2160
+ args[1] = this;
2161
+ // args = ["key:change", this=objectEditor, fieldEditor]
2162
+
2163
+ this.trigger.apply(this, args);
2164
+ }, this);
2165
+ }
2166
+
2167
+ });
2168
+
2169
+ /**
2170
+ * NestedModel editor
2171
+ *
2172
+ * Creates a child form. For editing nested Backbone models
2173
+ *
2174
+ * Special options:
2175
+ * schema.model: Embedded model constructor
2176
+ */
2177
+ Form.editors.NestedModel = Form.editors.Object.extend({
2178
+ initialize: function(options) {
2179
+ Form.editors.Base.prototype.initialize.call(this, options);
2180
+
2181
+ if (!this.form) throw new Error('Missing required option "form"');
2182
+ if (!options.schema.model) throw new Error('Missing required "schema.model" option for NestedModel editor');
2183
+ },
2184
+
2185
+ render: function() {
2186
+ //Get the constructor for creating the nested form; i.e. the same constructor as used by the parent form
2187
+ var NestedForm = this.form.constructor;
2188
+
2189
+ var data = this.value || {},
2190
+ key = this.key,
2191
+ nestedModel = this.schema.model;
2192
+
2193
+ //Wrap the data in a model if it isn't already a model instance
2194
+ var modelInstance = (data.constructor === nestedModel) ? data : new nestedModel(data);
2195
+
2196
+ this.nestedForm = new NestedForm({
2197
+ model: modelInstance,
2198
+ idPrefix: this.id + '_',
2199
+ fieldTemplate: 'nestedField'
2200
+ });
2201
+
2202
+ this._observeFormEvents();
2203
+
2204
+ //Render form
2205
+ this.$el.html(this.nestedForm.render().el);
2206
+
2207
+ if (this.hasFocus) this.trigger('blur', this);
2208
+
2209
+ return this;
2210
+ },
2211
+
2212
+ /**
2213
+ * Update the embedded model, checking for nested validation errors and pass them up
2214
+ * Then update the main model if all OK
2215
+ *
2216
+ * @return {Error|null} Validation error or null
2217
+ */
2218
+ commit: function() {
2219
+ var error = this.nestedForm.commit();
2220
+ if (error) {
2221
+ this.$el.addClass('error');
2222
+ return error;
2223
+ }
2224
+
2225
+ return Form.editors.Object.prototype.commit.call(this);
2226
+ }
2227
+
2228
+ });
2229
+
2230
+ /**
2231
+ * Date editor
2232
+ *
2233
+ * Schema options
2234
+ * @param {Number|String} [options.schema.yearStart] First year in list. Default: 100 years ago
2235
+ * @param {Number|String} [options.schema.yearEnd] Last year in list. Default: current year
2236
+ *
2237
+ * Config options (if not set, defaults to options stored on the main Date class)
2238
+ * @param {Boolean} [options.showMonthNames] Use month names instead of numbers. Default: true
2239
+ * @param {String[]} [options.monthNames] Month names. Default: Full English names
2240
+ */
2241
+ Form.editors.Date = Form.editors.Base.extend({
2242
+
2243
+ events: {
2244
+ 'change select': function() {
2245
+ this.updateHidden();
2246
+ this.trigger('change', this);
2247
+ },
2248
+ 'focus select': function() {
2249
+ if (this.hasFocus) return;
2250
+ this.trigger('focus', this);
2251
+ },
2252
+ 'blur select': function() {
2253
+ if (!this.hasFocus) return;
2254
+ var self = this;
2255
+ setTimeout(function() {
2256
+ if (self.$('select:focus')[0]) return;
2257
+ self.trigger('blur', self);
2258
+ }, 0);
2259
+ }
2260
+ },
2261
+
2262
+ initialize: function(options) {
2263
+ options = options || {};
2264
+
2265
+ Form.editors.Base.prototype.initialize.call(this, options);
2266
+
2267
+ var Self = Form.editors.Date,
2268
+ today = new Date();
2269
+
2270
+ //Option defaults
2271
+ this.options = _.extend({
2272
+ monthNames: Self.monthNames,
2273
+ showMonthNames: Self.showMonthNames
2274
+ }, options);
2275
+
2276
+ //Schema defaults
2277
+ this.schema = _.extend({
2278
+ yearStart: today.getFullYear() - 100,
2279
+ yearEnd: today.getFullYear()
2280
+ }, options.schema || {});
2281
+
2282
+ //Cast to Date
2283
+ if (this.value && !_.isDate(this.value)) {
2284
+ this.value = new Date(this.value);
2285
+ }
2286
+
2287
+ //Set default date
2288
+ if (!this.value) {
2289
+ var date = new Date();
2290
+ date.setSeconds(0);
2291
+ date.setMilliseconds(0);
2292
+
2293
+ this.value = date;
2294
+ }
2295
+
2296
+ //Template
2297
+ this.template = options.template || this.constructor.template;
2298
+ },
2299
+
2300
+ render: function() {
2301
+ var options = this.options,
2302
+ schema = this.schema,
2303
+ $ = Backbone.$;
2304
+
2305
+ var datesOptions = _.map(_.range(1, 32), function(date) {
2306
+ return '<option value="'+date+'">' + date + '</option>';
2307
+ });
2308
+
2309
+ var monthsOptions = _.map(_.range(0, 12), function(month) {
2310
+ var value = (options.showMonthNames)
2311
+ ? options.monthNames[month]
2312
+ : (month + 1);
2313
+
2314
+ return '<option value="'+month+'">' + value + '</option>';
2315
+ });
2316
+
2317
+ var yearRange = (schema.yearStart < schema.yearEnd)
2318
+ ? _.range(schema.yearStart, schema.yearEnd + 1)
2319
+ : _.range(schema.yearStart, schema.yearEnd - 1, -1);
2320
+
2321
+ var yearsOptions = _.map(yearRange, function(year) {
2322
+ return '<option value="'+year+'">' + year + '</option>';
2323
+ });
2324
+
2325
+ //Render the selects
2326
+ var $el = $($.trim(this.template({
2327
+ dates: datesOptions.join(''),
2328
+ months: monthsOptions.join(''),
2329
+ years: yearsOptions.join('')
2330
+ })));
2331
+
2332
+ //Store references to selects
2333
+ this.$date = $el.find('[data-type="date"]');
2334
+ this.$month = $el.find('[data-type="month"]');
2335
+ this.$year = $el.find('[data-type="year"]');
2336
+
2337
+ //Create the hidden field to store values in case POSTed to server
2338
+ this.$hidden = $('<input type="hidden" name="'+this.key+'" />');
2339
+ $el.append(this.$hidden);
2340
+
2341
+ //Set value on this and hidden field
2342
+ this.setValue(this.value);
2343
+
2344
+ //Remove the wrapper tag
2345
+ this.setElement($el);
2346
+ this.$el.attr('id', this.id);
2347
+ this.$el.attr('name', this.getName());
2348
+
2349
+ if (this.hasFocus) this.trigger('blur', this);
2350
+
2351
+ return this;
2352
+ },
2353
+
2354
+ /**
2355
+ * @return {Date} Selected date
2356
+ */
2357
+ getValue: function() {
2358
+ var year = this.$year.val(),
2359
+ month = this.$month.val(),
2360
+ date = this.$date.val();
2361
+
2362
+ if (!year || !month || !date) return null;
2363
+
2364
+ return new Date(year, month, date);
2365
+ },
2366
+
2367
+ /**
2368
+ * @param {Date} date
2369
+ */
2370
+ setValue: function(date) {
2371
+ this.value = date;
2372
+ this.$date.val(date.getDate());
2373
+ this.$month.val(date.getMonth());
2374
+ this.$year.val(date.getFullYear());
2375
+
2376
+ this.updateHidden();
2377
+ },
2378
+
2379
+ focus: function() {
2380
+ if (this.hasFocus) return;
2381
+
2382
+ this.$('select').first().focus();
2383
+ },
2384
+
2385
+ blur: function() {
2386
+ if (!this.hasFocus) return;
2387
+
2388
+ this.$('select:focus').blur();
2389
+ },
2390
+
2391
+ /**
2392
+ * Update the hidden input which is maintained for when submitting a form
2393
+ * via a normal browser POST
2394
+ */
2395
+ updateHidden: function() {
2396
+ var val = this.getValue();
2397
+
2398
+ if (_.isDate(val)) val = val.toISOString();
2399
+
2400
+ this.$hidden.val(val);
2401
+ }
2402
+
2403
+ }, {
2404
+ //STATICS
2405
+ template: _.template('\
2406
+ <div>\
2407
+ <select data-type="date"><%= dates %></select>\
2408
+ <select data-type="month"><%= months %></select>\
2409
+ <select data-type="year"><%= years %></select>\
2410
+ </div>\
2411
+ ', null, Form.templateSettings),
2412
+
2413
+ //Whether to show month names instead of numbers
2414
+ showMonthNames: true,
2415
+
2416
+ //Month names to use if showMonthNames is true
2417
+ //Replace for localisation, e.g. Form.editors.Date.monthNames = ['Janvier', 'Fevrier'...]
2418
+ monthNames: ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December']
2419
+ });
2420
+
2421
+ /**
2422
+ * DateTime editor
2423
+ *
2424
+ * @param {Editor} [options.DateEditor] Date editor view to use (not definition)
2425
+ * @param {Number} [options.schema.minsInterval] Interval between minutes. Default: 15
2426
+ */
2427
+ Form.editors.DateTime = Form.editors.Base.extend({
2428
+
2429
+ events: {
2430
+ 'change select': function() {
2431
+ this.updateHidden();
2432
+ this.trigger('change', this);
2433
+ },
2434
+ 'focus select': function() {
2435
+ if (this.hasFocus) return;
2436
+ this.trigger('focus', this);
2437
+ },
2438
+ 'blur select': function() {
2439
+ if (!this.hasFocus) return;
2440
+ var self = this;
2441
+ setTimeout(function() {
2442
+ if (self.$('select:focus')[0]) return;
2443
+ self.trigger('blur', self);
2444
+ }, 0);
2445
+ }
2446
+ },
2447
+
2448
+ initialize: function(options) {
2449
+ options = options || {};
2450
+
2451
+ Form.editors.Base.prototype.initialize.call(this, options);
2452
+
2453
+ //Option defaults
2454
+ this.options = _.extend({
2455
+ DateEditor: Form.editors.DateTime.DateEditor
2456
+ }, options);
2457
+
2458
+ //Schema defaults
2459
+ this.schema = _.extend({
2460
+ minsInterval: 15
2461
+ }, options.schema || {});
2462
+
2463
+ //Create embedded date editor
2464
+ this.dateEditor = new this.options.DateEditor(options);
2465
+
2466
+ this.value = this.dateEditor.value;
2467
+
2468
+ //Template
2469
+ this.template = options.template || this.constructor.template;
2470
+ },
2471
+
2472
+ render: function() {
2473
+ function pad(n) {
2474
+ return n < 10 ? '0' + n : n;
2475
+ }
2476
+
2477
+ var schema = this.schema,
2478
+ $ = Backbone.$;
2479
+
2480
+ //Create options
2481
+ var hoursOptions = _.map(_.range(0, 24), function(hour) {
2482
+ return '<option value="'+hour+'">' + pad(hour) + '</option>';
2483
+ });
2484
+
2485
+ var minsOptions = _.map(_.range(0, 60, schema.minsInterval), function(min) {
2486
+ return '<option value="'+min+'">' + pad(min) + '</option>';
2487
+ });
2488
+
2489
+ //Render time selects
2490
+ var $el = $($.trim(this.template({
2491
+ hours: hoursOptions.join(),
2492
+ mins: minsOptions.join()
2493
+ })));
2494
+
2495
+ //Include the date editor
2496
+ $el.find('[data-date]').append(this.dateEditor.render().el);
2497
+
2498
+ //Store references to selects
2499
+ this.$hour = $el.find('select[data-type="hour"]');
2500
+ this.$min = $el.find('select[data-type="min"]');
2501
+
2502
+ //Get the hidden date field to store values in case POSTed to server
2503
+ this.$hidden = $el.find('input[type="hidden"]');
2504
+
2505
+ //Set time
2506
+ this.setValue(this.value);
2507
+
2508
+ this.setElement($el);
2509
+ this.$el.attr('id', this.id);
2510
+ this.$el.attr('name', this.getName());
2511
+
2512
+ if (this.hasFocus) this.trigger('blur', this);
2513
+
2514
+ return this;
2515
+ },
2516
+
2517
+ /**
2518
+ * @return {Date} Selected datetime
2519
+ */
2520
+ getValue: function() {
2521
+ var date = this.dateEditor.getValue();
2522
+
2523
+ var hour = this.$hour.val(),
2524
+ min = this.$min.val();
2525
+
2526
+ if (!date || !hour || !min) return null;
2527
+
2528
+ date.setHours(hour);
2529
+ date.setMinutes(min);
2530
+
2531
+ return date;
2532
+ },
2533
+
2534
+ /**
2535
+ * @param {Date}
2536
+ */
2537
+ setValue: function(date) {
2538
+ if (!_.isDate(date)) date = new Date(date);
2539
+ this.value = date;
2540
+ this.dateEditor.setValue(date);
2541
+
2542
+ this.$hour.val(date.getHours());
2543
+ this.$min.val(date.getMinutes());
2544
+
2545
+ this.updateHidden();
2546
+ },
2547
+
2548
+ focus: function() {
2549
+ if (this.hasFocus) return;
2550
+
2551
+ this.$('select').first().focus();
2552
+ },
2553
+
2554
+ blur: function() {
2555
+ if (!this.hasFocus) return;
2556
+
2557
+ this.$('select:focus').blur();
2558
+ },
2559
+
2560
+ /**
2561
+ * Update the hidden input which is maintained for when submitting a form
2562
+ * via a normal browser POST
2563
+ */
2564
+ updateHidden: function() {
2565
+ var val = this.getValue();
2566
+ if (_.isDate(val)) val = val.toISOString();
2567
+
2568
+ this.$hidden.val(val);
2569
+ },
2570
+
2571
+ /**
2572
+ * Remove the Date editor before removing self
2573
+ */
2574
+ remove: function() {
2575
+ this.dateEditor.remove();
2576
+
2577
+ Form.editors.Base.prototype.remove.call(this);
2578
+ }
2579
+
2580
+ }, {
2581
+ //STATICS
2582
+ template: _.template('\
2583
+ <div class="bbf-datetime">\
2584
+ <div class="bbf-date-container" data-date></div>\
2585
+ <select data-type="hour"><%= hours %></select>\
2586
+ :\
2587
+ <select data-type="min"><%= mins %></select>\
2588
+ </div>\
2589
+ ', null, Form.templateSettings),
2590
+
2591
+ //The date editor to use (constructor function, not instance)
2592
+ DateEditor: Form.editors.Date
2593
+ });
2594
+
2595
+
2596
+
2597
+ //Metadata
2598
+ Form.VERSION = '0.14.0';
2599
+
2600
+ //Exports
2601
+ Backbone.Form = Form;
2602
+
2603
+ return Form;
2604
+ });