Node-Red configuration
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

ui_template.html 22KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437
  1. <script type="text/javascript">
  2. (async function () {
  3. // convert to i18 text
  4. function c_(x) {
  5. return RED._('@flowfuse/node-red-dashboard/ui-template:ui-template.' + x)
  6. }
  7. // TODO: move this to util.js for reuse & unit testing
  8. function hasProperty(obj, prop) {
  9. return Object.prototype.hasOwnProperty.call(obj, prop)
  10. }
  11. const template = []
  12. template.push('<template>')
  13. template.push(' <div>')
  14. template.push(' <h2>Counter</h2>')
  15. template.push(' <p>Current Count: {{ count }}</p>')
  16. template.push(' <p class="my-class">Formatted Count: {{ formattedCount }}</p>')
  17. template.push(' <v-btn @click="increase()">Increment</v-btn>')
  18. template.push(' </div>')
  19. template.push('</template>')
  20. template.push('')
  21. template.push('<script>')
  22. template.push(' export default {')
  23. template.push(' data() {')
  24. template.push(' // define variables available component-wide')
  25. template.push(' // (in <template> and component functions)')
  26. template.push(' return {')
  27. template.push(' count: 0')
  28. template.push(' }')
  29. template.push(' },')
  30. template.push(' watch: {')
  31. template.push(' // watch for any changes of "count"')
  32. template.push(' count: function () {')
  33. template.push(' if (this.count % 5 === 0) {')
  34. template.push(' this.send({payload: \'Multiple of 5\'})')
  35. template.push(' }')
  36. template.push(' }')
  37. template.push(' },')
  38. template.push(' computed: {')
  39. template.push(' // automatically compute this variable')
  40. template.push(' // whenever VueJS deems appropriate')
  41. template.push(' formattedCount: function () {')
  42. template.push(' return this.count + \'Apples\'')
  43. template.push(' }')
  44. template.push(' },')
  45. template.push(' methods: {')
  46. template.push(' // expose a method to our <template> and Vue Application')
  47. template.push(' increase: function () {')
  48. template.push(' this.count++')
  49. template.push(' }')
  50. template.push(' },')
  51. template.push(' mounted() {')
  52. template.push(' // code here when the component is first loaded')
  53. template.push(' },')
  54. template.push(' unmounted() {')
  55. template.push(' // code here when the component is removed from the Dashboard')
  56. template.push(' // i.e. when the user navigates away from the page')
  57. template.push(' }')
  58. template.push(' }')
  59. // eslint complaingin about unterminated string literal - need to do this oddity to get around it
  60. template.push('</' + 'script>')
  61. template.push('<style>')
  62. template.push(' /* define any styles here - supports raw CSS */')
  63. template.push(' .my-class {')
  64. template.push(' color: red;')
  65. template.push(' }')
  66. template.push('</style>')
  67. const defaultTemplate = template.join('\n')
  68. RED.nodes.registerType('ui-template', {
  69. category: RED._('@flowfuse/node-red-dashboard/ui-base:ui-base.label.category'),
  70. color: RED._('@flowfuse/node-red-dashboard/ui-base:ui-base.colors.dark'),
  71. defaults: {
  72. group: {
  73. type: 'ui-group',
  74. validate: function () {
  75. const tempScope = ($('#node-input-templateScope').val() !== undefined) ? $('#node-input-templateScope').val() : this.templateScope
  76. const groupVal = ($('#node-input-group').val() !== undefined) ? $('#node-input-group').val() : this.group
  77. if (tempScope !== 'local') {
  78. return true
  79. }
  80. if (tempScope === 'local') {
  81. if (!!groupVal && groupVal !== '_ADD_') {
  82. return true
  83. }
  84. }
  85. return false
  86. }
  87. }, // for when template is scoped to 'local' (default)
  88. page: {
  89. type: 'ui-page',
  90. validate: function () {
  91. const tempScope = ($('#node-input-templateScope').val() !== undefined) ? $('#node-input-templateScope').val() : this.templateScope
  92. const pageVal = ($('#node-input-page').val() !== undefined) ? $('#node-input-page').val() : this.page
  93. if (tempScope !== 'widget:page' && tempScope !== 'page:style') {
  94. return true
  95. }
  96. if (tempScope === 'widget:page' || tempScope === 'page:style') {
  97. if (!!pageVal && pageVal !== '_ADD_') {
  98. return true
  99. }
  100. }
  101. return false
  102. }
  103. }, // for when template is scoped to 'page'
  104. ui: {
  105. type: 'ui-base',
  106. validate: function () {
  107. const tempScope = ($('#node-input-templateScope').val() !== undefined) ? $('#node-input-templateScope').val() : this.templateScope
  108. const uiVal = ($('#node-input-ui').val() !== undefined) ? $('#node-input-ui').val() : this.ui
  109. if (tempScope !== 'widget:ui' && tempScope !== 'site:style') {
  110. return true
  111. }
  112. if (tempScope === 'widget:ui' || tempScope === 'site:style') {
  113. if (!!uiVal && uiVal !== '_ADD_') {
  114. return true
  115. }
  116. }
  117. return false
  118. }
  119. }, // for when template is scoped to 'site'
  120. name: { value: '' },
  121. order: { value: 0 },
  122. width: {
  123. value: 0,
  124. validate: function (v) {
  125. let valid = true
  126. if (this.templateScope !== 'global') {
  127. const width = v || 0
  128. const currentGroup = $('#node-input-group').val() || this.group
  129. const groupNode = RED.nodes.node(currentGroup)
  130. valid = !groupNode || +width <= +groupNode.width
  131. $('#node-input-size').toggleClass('input-error', !valid)
  132. }
  133. return valid
  134. }
  135. },
  136. height: { value: 0 },
  137. head: { value: '' },
  138. format: { value: defaultTemplate },
  139. storeOutMessages: { value: true },
  140. passthru: { value: true },
  141. resendOnRefresh: { value: true },
  142. templateScope: { value: 'local' },
  143. className: { value: '' }
  144. },
  145. inputs: 1,
  146. outputs: 1,
  147. icon: 'font-awesome/fa-code',
  148. paletteLabel: 'template',
  149. label: function () {
  150. if (this.name) { return this.name }
  151. const knownLabels = {
  152. local: 'template', // widget:group - kept as local for backward compatability
  153. 'widget:page': 'template',
  154. 'widget:ui': 'template',
  155. 'site:style': 'site style',
  156. 'page:style': 'page style'
  157. }
  158. return knownLabels[this.templateScope] || 'template'
  159. },
  160. labelStyle: function () { return this.name ? 'node_label_italic' : '' },
  161. oneditprepare: function () {
  162. if (RED.editor.__debug === true) {
  163. console.log('ui-template: oneditprepare') // useful for locating this code in the browser debugger
  164. }
  165. if (hasProperty(RED.editor, 'editText') && typeof RED.editor.editText === 'function') {
  166. $('#node-template-expand-editor').show()
  167. } else {
  168. $('#node-template-expand-editor').hide()
  169. }
  170. const that = this
  171. const $templateScope = $('#node-input-templateScope')
  172. // if this groups parent is a subflow template, set the node-config-input-width and node-config-input-height up
  173. // as typedInputs and hide the elementSizer (as it doesn't make sense for subflow templates)
  174. if (RED.nodes.subflow(this.z)) {
  175. // change inputs from hidden to text & display them
  176. $('#node-input-width').attr('type', 'text')
  177. $('#node-input-height').attr('type', 'text')
  178. $('div.form-row.nr-db-ui-element-sizer-row').hide()
  179. $('div.form-row.nr-db-ui-manual-size-row').show()
  180. } else {
  181. // not in a subflow, use the elementSizer
  182. $('div.form-row.nr-db-ui-element-sizer-row').show()
  183. $('div.form-row.nr-db-ui-manual-size-row').hide()
  184. $('#node-input-size').elementSizer({
  185. width: '#node-input-width',
  186. height: '#node-input-height',
  187. group: '#node-input-group'
  188. })
  189. }
  190. if (typeof this.storeOutMessages === 'undefined') {
  191. this.storeOutMessages = true
  192. $('#node-input-storeOutMessages').prop('checked', true)
  193. }
  194. if (typeof this.passthru === 'undefined') {
  195. this.passthru = true
  196. $('#node-input-passthru').prop('checked', true)
  197. }
  198. if (typeof this.templateScope === 'undefined') {
  199. this.templateScope = 'local'
  200. $templateScope.val(this.templateScope)
  201. }
  202. $templateScope.on('change', function () {
  203. $('#template-row-group, #template-row-page, #template-row-ui, #template-row-class').hide()
  204. switch ($templateScope.val()) {
  205. case 'site:style':
  206. $('#template-row-ui').show()
  207. that.editor.getSession().setMode('ace/mode/css')
  208. break
  209. case 'page:style':
  210. $('#template-row-page').show()
  211. that.editor.getSession().setMode('ace/mode/css')
  212. break
  213. case 'site:script':
  214. $('#template-row-ui').show()
  215. that.editor.getSession().setMode('ace/mode/javascript')
  216. break
  217. case 'page:script':
  218. $('#template-row-page').show()
  219. that.editor.getSession().setMode('ace/mode/javascript')
  220. break
  221. case 'widget:ui':
  222. $('#template-row-ui').show()
  223. that.editor.getSession().setMode('ace/mode/html')
  224. break
  225. case 'widget:page':
  226. $('#template-row-page').show()
  227. that.editor.getSession().setMode('ace/mode/html')
  228. break
  229. default:
  230. $('#template-row-group, #template-row-class').show()
  231. that.editor.getSession().setMode('ace/mode/html')
  232. break
  233. }
  234. resize.call(that)
  235. })
  236. this.editor = RED.editor.createEditor({
  237. id: 'node-input-format-editor',
  238. mode: 'ace/mode/html',
  239. value: $('#node-input-format').val()
  240. })
  241. RED.library.create({
  242. url: 'uitemplates', // where to get the data from
  243. type: 'ui-template', // the type of object the library is for
  244. editor: this.editor, // the field name the main text body goes to
  245. mode: 'ace/mode/html',
  246. fields: ['name']
  247. })
  248. this.editor.focus()
  249. RED.popover.tooltip($('#node-template-expand-editor'), c_('label.expand'))
  250. $('#node-template-expand-editor').on('click', function (e) {
  251. e.preventDefault()
  252. const value = that.editor.getValue()
  253. RED.editor.editText({
  254. mode: $templateScope.val() === 'global' ? 'css' : 'html',
  255. value,
  256. width: 'Infinity',
  257. cursor: that.editor.getCursorPosition(),
  258. complete: function (v, cursor) {
  259. that.editor.setValue(v, -1)
  260. that.editor.gotoLine(cursor.row + 1, cursor.column, false)
  261. setTimeout(function () { that.editor.focus() }, 300)
  262. }
  263. })
  264. })
  265. // use jQuery UI tooltip to convert the plain old title attribute to a nice tooltip
  266. $('.ui-node-popover-title').tooltip({
  267. show: {
  268. effect: 'slideDown',
  269. delay: 150
  270. }
  271. })
  272. $templateScope.trigger('change') // trigger the change event to hide/show the group/dashboard row
  273. },
  274. oneditsave: function () {
  275. const annot = this.editor.getSession().getAnnotations()
  276. this.noerr = 0
  277. $('#node-input-noerr').val(0)
  278. for (let k = 0; k < annot.length; k++) {
  279. if (annot[k].type === 'error') {
  280. $('#node-input-noerr').val(annot.length)
  281. this.noerr = annot.length
  282. }
  283. }
  284. $('#node-input-format').val(this.editor.getValue())
  285. this.editor.destroy()
  286. delete this.editor
  287. // ensure we only have one of group/page/dashboard defined
  288. const scope = $('#node-input-templateScope').val()
  289. if (scope === 'local') {
  290. $('#node-input-ui').val('_ADD_')
  291. $('#node-input-page').val('_ADD_')
  292. } else if (scope === 'widget:page') {
  293. $('#node-input-ui').val('_ADD_')
  294. $('#node-input-group').val('_ADD_')
  295. } else if (scope === 'widget:ui') {
  296. $('#node-input-page').val('_ADD_')
  297. $('#node-input-group').val('_ADD_')
  298. } else if (scope === 'site:style') {
  299. $('#node-input-page').val('_ADD_')
  300. $('#node-input-group').val('_ADD_')
  301. } else if (scope === 'page:style') {
  302. $('#node-input-ui').val('_ADD_')
  303. $('#node-input-group').val('_ADD_')
  304. }
  305. },
  306. oneditcancel: function () {
  307. this.editor.destroy()
  308. delete this.editor
  309. },
  310. oneditresize: resize.bind(this)
  311. })
  312. function resize(size) {
  313. const rows = $('#dialog-form>div:not(.node-text-editor-row)')
  314. let fixRowsHeight = 0
  315. for (let i = 0; i < rows.size(); i++) {
  316. fixRowsHeight += $(rows[i]).height()
  317. }
  318. const dialogHeight = $('#dialog-form').height()
  319. const height = dialogHeight - fixRowsHeight
  320. $('.node-text-editor').css('height', height + 'px')
  321. if (RED.editor.__debug === true) {
  322. console.log('dialogHeight', dialogHeight, 'fixRowsHeight', fixRowsHeight, 'height', height, 'editorRow')
  323. }
  324. this.editor && this.editor.resize()
  325. }
  326. })()
  327. </script>
  328. <script type="text/html" data-template-name="ui-template">
  329. <div class="form-row">
  330. <label for="node-input-name"><i class="fa fa-tag"></i> <span data-i18n="node-red:common.label.name"></span></label>
  331. <div style="display:inline-block; width:calc(100% - 105px)">
  332. <input type="text" id="node-input-name" data-i18n="[placeholder]node-red:common.label.name">
  333. </div>
  334. </div>
  335. <div class="form-row">
  336. <label for="node-input-temlplateScope"><i class="fa fa-dot-circle-o"></i> <span data-i18n="ui-template.label.scope"></span></label>
  337. <select style="width:76%" id="node-input-templateScope">
  338. <option value="local" data-i18n="ui-template.label.widget-group"></option>
  339. <option value="widget:page" data-i18n="ui-template.label.widget-page"></option>
  340. <option value="widget:ui" data-i18n="ui-template.label.widget-ui"></option>
  341. <option value="site:style" data-i18n="ui-template.label.site-style"></option>
  342. <option value="page:style" data-i18n="ui-template.label.page-style"></option>
  343. <!-- FUTURE? <option value="site:script" data-i18n="ui-template.label.site-script"></option>
  344. <option value="page:script" data-i18n="ui-template.label.page-script"></option> -->
  345. </select>
  346. </div>
  347. <div id="template-row-ui" class="form-row">
  348. <label for="node-input-ui"><i class="fa fa-bookmark"></i> <span data-i18n="ui-template.label.ui"></label>
  349. <input type="text" id="node-input-ui">
  350. </div>
  351. <div id="template-row-page" class="form-row">
  352. <label for="node-input-page"><i class="fa fa-bookmark"></i> <span data-i18n="ui-template.label.page"></label>
  353. <input type="text" id="node-input-page">
  354. </div>
  355. <div id="template-row-group" class="form-row">
  356. <label for="node-input-group"><i class="fa fa-table"></i> <span data-i18n="ui-template.label.group"></label>
  357. <input type="text" id="node-input-group">
  358. </div>
  359. <div class="form-row nr-db-ui-element-sizer-row">
  360. <label><i class="fa fa-object-group"></i> <span data-i18n="ui-template.label.size">Size</label>
  361. <button class="editor-button" id="node-input-size"></button>
  362. </div>
  363. <div class="form-row nr-db-ui-manual-size-row">
  364. <label><i class="fa fa-arrows-h"></i> <span data-i18n="ui-template.label.width">Width</label>
  365. <input type="hidden" id="node-input-width">
  366. </div>
  367. <div class="form-row nr-db-ui-manual-size-row">
  368. <label><i class="fa fa-arrows-v"></i> <span data-i18n="ui-template.label.height">Height</label>
  369. <input type="hidden" id="node-input-height">
  370. </div>
  371. <!--<div class="form-row" id="template-row-size">
  372. <label><i class="fa fa-object-group"></i> <span data-i18n="ui-template.label.size"></span></label>
  373. <input type="hidden" id="node-input-width">
  374. <input type="hidden" id="node-input-height">
  375. <button class="editor-button" id="node-input-size"></button>
  376. </div>-->
  377. <div class="form-row">
  378. <label for="node-input-className"><i class="fa fa-code"></i> <span data-i18n="ui-template.label.className"></label>
  379. <div style="display: inline;">
  380. <input style="width: 70%;" type="text" id="node-input-className" data-i18n="[placeholder]ui-template.label.classNamePlaceholder" style="flex-grow: 1;">
  381. <a
  382. data-html="true"
  383. title="Dynamic Property: Send msg.class to append new classes to this widget. NOTE: classes set at runtime will be applied in addition to any class(es) set in the nodes class field."
  384. class="red-ui-button ui-node-popover-title"
  385. style="margin-left: 4px; cursor: help; font-size: 0.625rem; border-radius: 50%; width: 24px; height: 24px; display: inline-flex; justify-content: center; align-items: center;">
  386. <i style="font-family: ui-serif;">fx</i>
  387. </a>
  388. </div>
  389. </div>
  390. <div class="form-row" style="margin-bottom:0px;">
  391. <label for="node-input-format" style="width:110px;"><i class="fa fa-copy"></i> <span data-i18n="ui-template.label.template"></span></label>
  392. <input type="hidden" id="node-input-format">
  393. <button id="node-template-expand-editor" class="red-ui-button red-ui-button-small" style="float:right"><i class="fa fa-expand"></i></button>
  394. </div>
  395. <div class="form-row node-text-editor-row" style="display: block;">
  396. <div style="height:250px; min-height:100px" class="node-text-editor" id="node-input-format-editor" ></div>
  397. </div>
  398. <div id="template-pass-store">
  399. <div class="form-row" style="margin-bottom:0px;">
  400. <input type="checkbox" id="node-input-passthru" style="display:inline-block; margin-left:8px; width:auto; vertical-align:top;">
  401. <label for="node-input-passthru" style="width:70%;"> <span data-i18n="ui-template.label.pass-through"></span></label>
  402. </div>
  403. </div>
  404. <!--<div class="form-row" style="margin-bottom:0px;">
  405. <input type="checkbox" id="node-input-storeOutMessages" style="display:inline-block; margin-left:8px; width:auto; vertical-align:top;">
  406. <label for="node-input-storeOutMessages" style="width:70%;"> <span data-i18n="ui-template.label.store-state"></span></label>
  407. </div>
  408. <div class="form-row" style="margin-bottom:0px;">
  409. <input type="checkbox" id="node-input-resendOnRefresh" style="display:inline-block; margin-left:8px; width:auto; vertical-align:top;">
  410. <label for="node-input-resendOnRefresh" style="width:70%;"> <span data-i18n="ui-template.label.resend"></span></label>
  411. </div>
  412. </div> -->
  413. </script>