437 lines
22 KiB
HTML

<script type="text/javascript">
(async function () {
// convert to i18 text
function c_(x) {
return RED._('@flowfuse/node-red-dashboard/ui-template:ui-template.' + x)
}
// TODO: move this to util.js for reuse & unit testing
function hasProperty(obj, prop) {
return Object.prototype.hasOwnProperty.call(obj, prop)
}
const template = []
template.push('<template>')
template.push(' <div>')
template.push(' <h2>Counter</h2>')
template.push(' <p>Current Count: {{ count }}</p>')
template.push(' <p class="my-class">Formatted Count: {{ formattedCount }}</p>')
template.push(' <v-btn @click="increase()">Increment</v-btn>')
template.push(' </div>')
template.push('</template>')
template.push('')
template.push('<script>')
template.push(' export default {')
template.push(' data() {')
template.push(' // define variables available component-wide')
template.push(' // (in <template> and component functions)')
template.push(' return {')
template.push(' count: 0')
template.push(' }')
template.push(' },')
template.push(' watch: {')
template.push(' // watch for any changes of "count"')
template.push(' count: function () {')
template.push(' if (this.count % 5 === 0) {')
template.push(' this.send({payload: \'Multiple of 5\'})')
template.push(' }')
template.push(' }')
template.push(' },')
template.push(' computed: {')
template.push(' // automatically compute this variable')
template.push(' // whenever VueJS deems appropriate')
template.push(' formattedCount: function () {')
template.push(' return this.count + \'Apples\'')
template.push(' }')
template.push(' },')
template.push(' methods: {')
template.push(' // expose a method to our <template> and Vue Application')
template.push(' increase: function () {')
template.push(' this.count++')
template.push(' }')
template.push(' },')
template.push(' mounted() {')
template.push(' // code here when the component is first loaded')
template.push(' },')
template.push(' unmounted() {')
template.push(' // code here when the component is removed from the Dashboard')
template.push(' // i.e. when the user navigates away from the page')
template.push(' }')
template.push(' }')
// eslint complaingin about unterminated string literal - need to do this oddity to get around it
template.push('</' + 'script>')
template.push('<style>')
template.push(' /* define any styles here - supports raw CSS */')
template.push(' .my-class {')
template.push(' color: red;')
template.push(' }')
template.push('</style>')
const defaultTemplate = template.join('\n')
RED.nodes.registerType('ui-template', {
category: RED._('@flowfuse/node-red-dashboard/ui-base:ui-base.label.category'),
color: RED._('@flowfuse/node-red-dashboard/ui-base:ui-base.colors.dark'),
defaults: {
group: {
type: 'ui-group',
validate: function () {
const tempScope = ($('#node-input-templateScope').val() !== undefined) ? $('#node-input-templateScope').val() : this.templateScope
const groupVal = ($('#node-input-group').val() !== undefined) ? $('#node-input-group').val() : this.group
if (tempScope !== 'local') {
return true
}
if (tempScope === 'local') {
if (!!groupVal && groupVal !== '_ADD_') {
return true
}
}
return false
}
}, // for when template is scoped to 'local' (default)
page: {
type: 'ui-page',
validate: function () {
const tempScope = ($('#node-input-templateScope').val() !== undefined) ? $('#node-input-templateScope').val() : this.templateScope
const pageVal = ($('#node-input-page').val() !== undefined) ? $('#node-input-page').val() : this.page
if (tempScope !== 'widget:page' && tempScope !== 'page:style') {
return true
}
if (tempScope === 'widget:page' || tempScope === 'page:style') {
if (!!pageVal && pageVal !== '_ADD_') {
return true
}
}
return false
}
}, // for when template is scoped to 'page'
ui: {
type: 'ui-base',
validate: function () {
const tempScope = ($('#node-input-templateScope').val() !== undefined) ? $('#node-input-templateScope').val() : this.templateScope
const uiVal = ($('#node-input-ui').val() !== undefined) ? $('#node-input-ui').val() : this.ui
if (tempScope !== 'widget:ui' && tempScope !== 'site:style') {
return true
}
if (tempScope === 'widget:ui' || tempScope === 'site:style') {
if (!!uiVal && uiVal !== '_ADD_') {
return true
}
}
return false
}
}, // for when template is scoped to 'site'
name: { value: '' },
order: { value: 0 },
width: {
value: 0,
validate: function (v) {
let valid = true
if (this.templateScope !== 'global') {
const width = v || 0
const currentGroup = $('#node-input-group').val() || this.group
const groupNode = RED.nodes.node(currentGroup)
valid = !groupNode || +width <= +groupNode.width
$('#node-input-size').toggleClass('input-error', !valid)
}
return valid
}
},
height: { value: 0 },
head: { value: '' },
format: { value: defaultTemplate },
storeOutMessages: { value: true },
passthru: { value: true },
resendOnRefresh: { value: true },
templateScope: { value: 'local' },
className: { value: '' }
},
inputs: 1,
outputs: 1,
icon: 'font-awesome/fa-code',
paletteLabel: 'template',
label: function () {
if (this.name) { return this.name }
const knownLabels = {
local: 'template', // widget:group - kept as local for backward compatability
'widget:page': 'template',
'widget:ui': 'template',
'site:style': 'site style',
'page:style': 'page style'
}
return knownLabels[this.templateScope] || 'template'
},
labelStyle: function () { return this.name ? 'node_label_italic' : '' },
oneditprepare: function () {
if (RED.editor.__debug === true) {
console.log('ui-template: oneditprepare') // useful for locating this code in the browser debugger
}
if (hasProperty(RED.editor, 'editText') && typeof RED.editor.editText === 'function') {
$('#node-template-expand-editor').show()
} else {
$('#node-template-expand-editor').hide()
}
const that = this
const $templateScope = $('#node-input-templateScope')
// if this groups parent is a subflow template, set the node-config-input-width and node-config-input-height up
// as typedInputs and hide the elementSizer (as it doesn't make sense for subflow templates)
if (RED.nodes.subflow(this.z)) {
// change inputs from hidden to text & display them
$('#node-input-width').attr('type', 'text')
$('#node-input-height').attr('type', 'text')
$('div.form-row.nr-db-ui-element-sizer-row').hide()
$('div.form-row.nr-db-ui-manual-size-row').show()
} else {
// not in a subflow, use the elementSizer
$('div.form-row.nr-db-ui-element-sizer-row').show()
$('div.form-row.nr-db-ui-manual-size-row').hide()
$('#node-input-size').elementSizer({
width: '#node-input-width',
height: '#node-input-height',
group: '#node-input-group'
})
}
if (typeof this.storeOutMessages === 'undefined') {
this.storeOutMessages = true
$('#node-input-storeOutMessages').prop('checked', true)
}
if (typeof this.passthru === 'undefined') {
this.passthru = true
$('#node-input-passthru').prop('checked', true)
}
if (typeof this.templateScope === 'undefined') {
this.templateScope = 'local'
$templateScope.val(this.templateScope)
}
$templateScope.on('change', function () {
$('#template-row-group, #template-row-page, #template-row-ui, #template-row-class').hide()
switch ($templateScope.val()) {
case 'site:style':
$('#template-row-ui').show()
that.editor.getSession().setMode('ace/mode/css')
break
case 'page:style':
$('#template-row-page').show()
that.editor.getSession().setMode('ace/mode/css')
break
case 'site:script':
$('#template-row-ui').show()
that.editor.getSession().setMode('ace/mode/javascript')
break
case 'page:script':
$('#template-row-page').show()
that.editor.getSession().setMode('ace/mode/javascript')
break
case 'widget:ui':
$('#template-row-ui').show()
that.editor.getSession().setMode('ace/mode/html')
break
case 'widget:page':
$('#template-row-page').show()
that.editor.getSession().setMode('ace/mode/html')
break
default:
$('#template-row-group, #template-row-class').show()
that.editor.getSession().setMode('ace/mode/html')
break
}
resize.call(that)
})
this.editor = RED.editor.createEditor({
id: 'node-input-format-editor',
mode: 'ace/mode/html',
value: $('#node-input-format').val()
})
RED.library.create({
url: 'uitemplates', // where to get the data from
type: 'ui-template', // the type of object the library is for
editor: this.editor, // the field name the main text body goes to
mode: 'ace/mode/html',
fields: ['name']
})
this.editor.focus()
RED.popover.tooltip($('#node-template-expand-editor'), c_('label.expand'))
$('#node-template-expand-editor').on('click', function (e) {
e.preventDefault()
const value = that.editor.getValue()
RED.editor.editText({
mode: $templateScope.val() === 'global' ? 'css' : 'html',
value,
width: 'Infinity',
cursor: that.editor.getCursorPosition(),
complete: function (v, cursor) {
that.editor.setValue(v, -1)
that.editor.gotoLine(cursor.row + 1, cursor.column, false)
setTimeout(function () { that.editor.focus() }, 300)
}
})
})
// use jQuery UI tooltip to convert the plain old title attribute to a nice tooltip
$('.ui-node-popover-title').tooltip({
show: {
effect: 'slideDown',
delay: 150
}
})
$templateScope.trigger('change') // trigger the change event to hide/show the group/dashboard row
},
oneditsave: function () {
const annot = this.editor.getSession().getAnnotations()
this.noerr = 0
$('#node-input-noerr').val(0)
for (let k = 0; k < annot.length; k++) {
if (annot[k].type === 'error') {
$('#node-input-noerr').val(annot.length)
this.noerr = annot.length
}
}
$('#node-input-format').val(this.editor.getValue())
this.editor.destroy()
delete this.editor
// ensure we only have one of group/page/dashboard defined
const scope = $('#node-input-templateScope').val()
if (scope === 'local') {
$('#node-input-ui').val('_ADD_')
$('#node-input-page').val('_ADD_')
} else if (scope === 'widget:page') {
$('#node-input-ui').val('_ADD_')
$('#node-input-group').val('_ADD_')
} else if (scope === 'widget:ui') {
$('#node-input-page').val('_ADD_')
$('#node-input-group').val('_ADD_')
} else if (scope === 'site:style') {
$('#node-input-page').val('_ADD_')
$('#node-input-group').val('_ADD_')
} else if (scope === 'page:style') {
$('#node-input-ui').val('_ADD_')
$('#node-input-group').val('_ADD_')
}
},
oneditcancel: function () {
this.editor.destroy()
delete this.editor
},
oneditresize: resize.bind(this)
})
function resize(size) {
const rows = $('#dialog-form>div:not(.node-text-editor-row)')
let fixRowsHeight = 0
for (let i = 0; i < rows.size(); i++) {
fixRowsHeight += $(rows[i]).height()
}
const dialogHeight = $('#dialog-form').height()
const height = dialogHeight - fixRowsHeight
$('.node-text-editor').css('height', height + 'px')
if (RED.editor.__debug === true) {
console.log('dialogHeight', dialogHeight, 'fixRowsHeight', fixRowsHeight, 'height', height, 'editorRow')
}
this.editor && this.editor.resize()
}
})()
</script>
<script type="text/html" data-template-name="ui-template">
<div class="form-row">
<label for="node-input-name"><i class="fa fa-tag"></i> <span data-i18n="node-red:common.label.name"></span></label>
<div style="display:inline-block; width:calc(100% - 105px)">
<input type="text" id="node-input-name" data-i18n="[placeholder]node-red:common.label.name">
</div>
</div>
<div class="form-row">
<label for="node-input-temlplateScope"><i class="fa fa-dot-circle-o"></i> <span data-i18n="ui-template.label.scope"></span></label>
<select style="width:76%" id="node-input-templateScope">
<option value="local" data-i18n="ui-template.label.widget-group"></option>
<option value="widget:page" data-i18n="ui-template.label.widget-page"></option>
<option value="widget:ui" data-i18n="ui-template.label.widget-ui"></option>
<option value="site:style" data-i18n="ui-template.label.site-style"></option>
<option value="page:style" data-i18n="ui-template.label.page-style"></option>
<!-- FUTURE? <option value="site:script" data-i18n="ui-template.label.site-script"></option>
<option value="page:script" data-i18n="ui-template.label.page-script"></option> -->
</select>
</div>
<div id="template-row-ui" class="form-row">
<label for="node-input-ui"><i class="fa fa-bookmark"></i> <span data-i18n="ui-template.label.ui"></label>
<input type="text" id="node-input-ui">
</div>
<div id="template-row-page" class="form-row">
<label for="node-input-page"><i class="fa fa-bookmark"></i> <span data-i18n="ui-template.label.page"></label>
<input type="text" id="node-input-page">
</div>
<div id="template-row-group" class="form-row">
<label for="node-input-group"><i class="fa fa-table"></i> <span data-i18n="ui-template.label.group"></label>
<input type="text" id="node-input-group">
</div>
<div class="form-row nr-db-ui-element-sizer-row">
<label><i class="fa fa-object-group"></i> <span data-i18n="ui-template.label.size">Size</label>
<button class="editor-button" id="node-input-size"></button>
</div>
<div class="form-row nr-db-ui-manual-size-row">
<label><i class="fa fa-arrows-h"></i> <span data-i18n="ui-template.label.width">Width</label>
<input type="hidden" id="node-input-width">
</div>
<div class="form-row nr-db-ui-manual-size-row">
<label><i class="fa fa-arrows-v"></i> <span data-i18n="ui-template.label.height">Height</label>
<input type="hidden" id="node-input-height">
</div>
<!--<div class="form-row" id="template-row-size">
<label><i class="fa fa-object-group"></i> <span data-i18n="ui-template.label.size"></span></label>
<input type="hidden" id="node-input-width">
<input type="hidden" id="node-input-height">
<button class="editor-button" id="node-input-size"></button>
</div>-->
<div class="form-row">
<label for="node-input-className"><i class="fa fa-code"></i> <span data-i18n="ui-template.label.className"></label>
<div style="display: inline;">
<input style="width: 70%;" type="text" id="node-input-className" data-i18n="[placeholder]ui-template.label.classNamePlaceholder" style="flex-grow: 1;">
<a
data-html="true"
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."
class="red-ui-button ui-node-popover-title"
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;">
<i style="font-family: ui-serif;">fx</i>
</a>
</div>
</div>
<div class="form-row" style="margin-bottom:0px;">
<label for="node-input-format" style="width:110px;"><i class="fa fa-copy"></i> <span data-i18n="ui-template.label.template"></span></label>
<input type="hidden" id="node-input-format">
<button id="node-template-expand-editor" class="red-ui-button red-ui-button-small" style="float:right"><i class="fa fa-expand"></i></button>
</div>
<div class="form-row node-text-editor-row" style="display: block;">
<div style="height:250px; min-height:100px" class="node-text-editor" id="node-input-format-editor" ></div>
</div>
<div id="template-pass-store">
<div class="form-row" style="margin-bottom:0px;">
<input type="checkbox" id="node-input-passthru" style="display:inline-block; margin-left:8px; width:auto; vertical-align:top;">
<label for="node-input-passthru" style="width:70%;"> <span data-i18n="ui-template.label.pass-through"></span></label>
</div>
</div>
<!--<div class="form-row" style="margin-bottom:0px;">
<input type="checkbox" id="node-input-storeOutMessages" style="display:inline-block; margin-left:8px; width:auto; vertical-align:top;">
<label for="node-input-storeOutMessages" style="width:70%;"> <span data-i18n="ui-template.label.store-state"></span></label>
</div>
<div class="form-row" style="margin-bottom:0px;">
<input type="checkbox" id="node-input-resendOnRefresh" style="display:inline-block; margin-left:8px; width:auto; vertical-align:top;">
<label for="node-input-resendOnRefresh" style="width:70%;"> <span data-i18n="ui-template.label.resend"></span></label>
</div>
</div> -->
</script>