UNPKG

node-red-contrib-ui-svg

Version:

A Node-RED widget node to show interactive SVG (vector graphics) in the dashboard

942 lines (866 loc) 148 kB
<!-- Copyright 2019, Bart Butenaers, Stephen McLaughlin Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. --> <style> .ui-svg-ui-autocomplete { max-height: 100px; overflow-y: auto; overflow-x: hidden; /* prevent horizontal scrollbar */ /*padding-right: 20px;/* add padding to account for vertical scrollbar */ } #svg-dialog-div { padding: 0px 0px 0px 0px; } .msgboxOuter { display: table; width: 100px; height:100px; overflow: hidden; } .msgboxOuter .msgboxInner { display: table-cell; vertical-align: middle; width: 100%; margin: 0 auto; text-align: left; } .msgboxInnerLeft{ padding: 20px 0px 10px 20px; float: left; } .msgboxInnerRight{ padding: 20px 10px 10px 20px; width: 300px; } .svgButtonIcon { padding: 4px 0px 0px 0px; width: 40px; text-align: center; height: 40px; } .svgButtonText { /* padding: 4px 4px 4px 4px; */ text-align: center; } .svgButton { box-shadow:inset 0px 0px 15px 3px #23395e; background:linear-gradient(to bottom, #2e466e 5%, #415989 100%); background-color:#2e466e; border-radius:20px; border:1px solid #1f2f47; display:inline-block; cursor:pointer; color:#ffffff; font-family:Arial; font-size:15px; padding:10px 13px; text-decoration:none; text-shadow:0px 1px 0px #263666; text-align: -webkit-center; } .svgButton:hover { background:linear-gradient(to bottom, #415989 5%, #2e466e 100%); background-color:#415989; } .svgButton:active { position:relative; top:1px; } </style> <script src="ui_svg_graphics/lib/jschannel.js"></script> <script src="ui_svg_graphics/lib/beautify-html.js"></script> <script src="ui_svg_graphics/lib/beautify-css.js"></script> <script type="text/javascript"> const messageBox = { show: function(message, title, faicon, options) { options = options || {} var msgbox = $('#ui-svg-message-box'); if (msgbox.length == 0) { var msgboxhtml = '<div id="ui-svg-message-box" title="" class="msgboxOuter" >' + ' <div id="ui-svg-message-box-icon-div" class="msgboxInner">' + ' <div class="msgboxInnerLeft">' + ' <i id="ui-svg-message-box-icon"> </i>' + ' </div>' + ' </div>' + ' <div class="msgboxInner">' + ' <div id="ui-svg-message-box-text" class="msgboxInnerRight" >' + ' </div>' + ' </div>' + '</div>' msgbox = $(msgboxhtml).appendTo('body'); } var icon = msgbox.find("#ui-svg-message-box-icon"); var iconDiv = msgbox.find("#ui-svg-message-box-icon-div"); if(faicon && icon && icon.length){ icon[0].className = "fa fa-4x " + faicon; iconDiv.show(); } else { iconDiv.hide(); } var text = msgbox.find("#ui-svg-message-box-text"); text.html(message); msgbox.attr("title",title); msgbox.dialog({ resizable: options.resizable === false ? options.resizable : true, height: options.height || 100, width: options.width || 400, modal: options.modal === false ? options.modal : true, buttons: options.buttons, close: function() { $(this).dialog("destroy"); $(this).empty();//ensure old content is completely removed $(this).remove(); } }); }, close: function() { $('#ui-svg-message-box').dialog( "close" ); } } // Collect information for the auto-complete fields function updateSvgIdHelpers(node, svgStr) { console.log("updateSvgIdHelpers"); // Reset all information that we have previously collected node.nonAnimationClasses = []; node.animationClasses = []; node.animationListClasses = []; node.animationIds = []; node.animationIdSelectors = []; node.nonAnimationIds = []; node.nonAnimationIdSelectors = []; node.nonAnimationIdsAndActions = []; node.animationIdsAndActions = []; // To be able to parse the string to a DOM, the SVG element needs to be in the svg namespace (i.e. it needs to have an // xmlns attribute with the appropriate value). Otherwise the DOMParser doesn't know how to handle the SVG tags. // Otherwise the querySelectorAll will not work, which means the autocomplete would be empty ... svgStr = svgStr.replace(/^<svg([^>]*[^>]*)>/g, function(match, $1, offset, input_string) { if ($1.indexOf("http://www.w3.org/2000/svg") < 0) { $1 += ' xmlns="http://www.w3.org/2000/svg"'; $1 += ' xmlns:svg="http://www.w3.org/2000/svg"'; } if ($1.indexOf("http://www.w3.org/1999/xlink") < 0) { $1 += ' xmlns:xlink="http://www.w3.org/1999/xlink"'; } return "<svg " + $1 + ">"; }) // Convert the SVG string into a temporary DOM tree, which allows us to search easily through the SVG. // See https://discourse.nodered.org/t/temporary-dom-elements-somewhere-cached/15520 var svgElement = new DOMParser().parseFromString(svgStr, "image/svg+xml"); svgElement.querySelectorAll('[class]').forEach(function (element) { element.classList.forEach(function (className) { if(!className) return; var _class = "." + className; var ele = element.tagName; ele = ele.trim().toLowerCase(); if (ele == "set" || ele == "animate" || ele == "animatemotion" || ele == "animatecolor" || ele == "animatetransform") { if (node.animationClasses.indexOf(_class) < 0) { node.animationClasses.push(_class) } } else { if (node.nonAnimationClasses.indexOf(_class) < 0) { node.nonAnimationClasses.push(_class) } } }) }) svgElement.querySelectorAll('*[id]').forEach(function (element) { if(!element.id) return; var id = element.id; var ele = element.tagName; ele = ele.trim().toLowerCase(); if(ele == "style") return; if (ele == "set" || ele == "animate" || ele == "animatemotion" || ele == "animatecolor" || ele == "animatetransform") { if (node.animationIds.indexOf(id) < 0) { node.animationIds.push(id) node.animationIdSelectors.push("#"+id) node.animationIdsAndActions.push(id) node.animationIdsAndActions.push(id + ".begin"); node.animationIdsAndActions.push(id + ".end"); node.animationIdsAndActions.push(id + ".repeat(1)"); } } else if(ele != "style" ) { if (node.nonAnimationIds.indexOf(id) < 0) { node.nonAnimationIds.push(id); node.nonAnimationIdSelectors.push("#"+id) node.nonAnimationIdsAndActions.push(id + ".click"); node.nonAnimationIdsAndActions.push(id + ".dblclick"); node.nonAnimationIdsAndActions.push(id + ".change"); node.nonAnimationIdsAndActions.push(id + ".contextmenu"); node.nonAnimationIdsAndActions.push(id + ".mouseover"); node.nonAnimationIdsAndActions.push(id + ".mouseout"); node.nonAnimationIdsAndActions.push(id + ".mouseup"); node.nonAnimationIdsAndActions.push(id + ".mousedown"); node.nonAnimationIdsAndActions.push(id + ".focus"); node.nonAnimationIdsAndActions.push(id + ".focusin"); node.nonAnimationIdsAndActions.push(id + ".focusout"); node.nonAnimationIdsAndActions.push(id + ".blur"); node.nonAnimationIdsAndActions.push(id + ".keyup"); node.nonAnimationIdsAndActions.push(id + ".keydown"); node.nonAnimationIdsAndActions.push(id + ".touchstart"); node.nonAnimationIdsAndActions.push(id + ".touchend"); } } }) //Get all animation ids specified by the SMIL animations editable list... var smilAnimationsList = $("#node-input-animations-container").editableList('items'); smilAnimationsList.each(function(i) { var aid = $(this).find(".node-input-animation-id").val(); var cv = $(this).find(".node-input-animation-classValue").val(); if (cv && node.animationListClasses.indexOf(cv) < 0) { node.animationListClasses.push(cv); } if(!aid) return; if (node.animationIds.indexOf(aid) < 0) { node.animationIds.push(aid); node.animationIdsAndActions.push(aid + ".begin"); node.animationIdsAndActions.push(aid + ".end"); node.animationIdsAndActions.push(aid + ".repeat(1)"); } }); } // resize the current tab content divs to their available height function updateEditorHeight(node) { // Calculate the height that is being occupied by the other html elements (i.e. outside of the tab content) var formRows = $("#dialog-form>div:not(#node-svg-tabs-content)"); var height = $("#dialog-form").height() - 5; for (let i=0; i < formRows.length; i++) { height -= $(formRows[i]).outerHeight(true); } // Set the size of the tab content, to fill the area that is left over $("#node-svg-tabs-content").css("height",height+"px"); // Now resize the items (inside the tab content), which have css class "form-row-auto-height" var tabs = $('.node-svg-tab-content'); for (let index = 0; index < tabs.length; index++) { let tab = $(tabs[index]); tab.css("height", height+"px"); let expandRow = $(tab.find('.form-row-auto-height')); if(expandRow && expandRow.length){ let tabHeight = height; let childRows = tab.find('.form-row:not(.form-row-auto-height)'); for (let i=0; i<childRows.size(); i++) { tabHeight -= $(childRows[i]).outerHeight(true); } let ol = $(expandRow.find(".red-ui-editableList-list")); if(ol && ol.length){ ol.editableList("height",tabHeight); } else { expandRow.css("height",tabHeight+"px"); } } } // If there is an ACE editor (for CSS or SVG) visible on the current tab, then that should get a correct height either. // Note that their height depends on whether they are currently being displayed in fullscreen mode or not. if ( node.visibleEditor ) { // Is the visible editor in full-screen? node.fullscreen = document.fullscreenElement ? true : false; // Determine whether the svg or css editor is currently being displayed var editorType; switch(node.currentTabId) { case "node-svg-tab-source": editorType = "source"; // SVG source break; case "node-svg-tab-css": editorType = "css"; break; } var rows = $("#node-svg-tab-"+editorType+">div:not(.node-"+editorType+"-editor-row)"); var height = $("#node-svg-tab-"+editorType).height() - 25; for (var i=0; i<rows.size(); i++) { height = height - $(rows[i]).outerHeight(true); } if ($('#node-input-templateScope').val() === "global") { height += 232; } if (node.fullscreen) { $('#node-expand-svg-'+editorType).html('<i class="fa fa-compress"></i>')//compress icon } else { $('#node-expand-svg-'+editorType).html('<i class="fa fa-expand"></i>')//expand icon } // Set the height of the edit box $('#node-input-svg-'+editorType).css('height',height+'px'); // Make sure the visible ACE editor content will match its new edit box size node.visibleEditor.resize(); } $(".node-input-clickable-payload").typedInput('width');//layout workaround until NR V1 https://discourse.nodered.org/t/toggle-visibility-of-typedinput-field-layout-issue/12756/11 } function getAnimationObjectFromFormRow(ele){ var row = $(ele); var id = row.find(".node-input-animation-id").val(); var targetId = row.find(".node-input-animation-targetId").val(); var classValue = row.find(".node-input-animation-classValue").val(); var attributeName = row.find(".node-input-animation-attributeName").val(); var transformType = row.find(".node-input-animation-transformType").val(); var fromValue = row.find(".node-input-animation-fromValue").val(); var toValue = row.find(".node-input-animation-toValue").val(); var trigger = row.find(".node-input-animation-trigger").val(); var duration = row.find(".node-input-animation-duration").val(); var durationUnit = row.find(".node-input-animation-durationUnit").val(); var repeatCount = row.find(".node-input-animation-repeatCount").val(); var end = row.find(".node-input-animation-end").val(); var delay = row.find(".node-input-animation-delay").val(); var delayUnit = row.find(".node-input-animation-delayUnit").val(); var otherId = row.find(".node-input-animation-otherId").val(); var custom = row.find(".node-input-animation-custom").val(); return {id:id, targetId:targetId, classValue:classValue, attributeName:attributeName, transformType:transformType, fromValue:fromValue, toValue:toValue, trigger:trigger, duration:duration, durationUnit:durationUnit, repeatCount:repeatCount, end:end, delay:delay, otherId:otherId, delayUnit:delayUnit, custom:custom}; } function getBindingFromFormRow(ele){ var row = $(ele); return { selector : row.find(".node-input-binding-selector").val(), bindSource : row.find(".node-input-binding-source").val(), bindType : row.find(".node-input-binding-type").val(), attribute : row.find(".node-input-binding-attribute").val() }; } const DEFAULT_CSS_SVG_NODE = `div.ui-svg svg{ color: var(--nr-dashboard-widgetColor); fill: currentColor !important; } div.ui-svg path { fill: inherit; }`; RED.nodes.registerType('ui_svg_graphics',{ category: 'dashboard', color: 'rgb( 63, 173, 181)', defaults: { group: {type: 'ui_group', required:true}, order: {value: 0}, width: { value: 0, validate: function(v) { var valid = true var width = v||0; var currentGroup = $('#node-input-group').val()|| this.group; var groupNode = RED.nodes.node(currentGroup); valid = !groupNode || +width <= +groupNode.width; $("#node-input-size").toggleClass("input-error",!valid); return valid; }}, height: {value: 0}, svgString: {value: '<svg x="0" y="0" height="100" viewBox="0 0 100 100" width="100" xmlns="http://www.w3.org/2000/svg" xmlns:svg="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">\n' + '<!-- Add here your SVG shapes (circles, rectangles, ...) -->\n' + '<!-- Or remove everything, if you want to paste an entire drawing (<svg...>...</svg>).-->\n' + '</svg>'}, clickableShapes: {value: []}, javascriptHandlers: {value: []}, smilAnimations: {value: []}, bindings: {value: []}, showCoordinates: {value: false}, autoFormatAfterEdit: {value: false}, showBrowserErrors: {value: false}, showBrowserEvents: {value: false}, enableJsDebugging: {value: false}, sendMsgWhenLoaded: {value: false}, noClickWhenDblClick: {value: false}, outputField: {value: "payload"}, editorUrl: {value: "//drawsvg.org/drawsvg.html"}, //showMouseLines: {value: false}, directory: {value: ""}, panning: {value: "disabled"}, zooming: {value: "disabled"}, panOnlyWhenZoomed: {value: false}, doubleClickZoomEnabled: {value: false}, mouseWheelZoomEnabled: {value: false}, dblClickZoomPercentage: {value: 150, validate: function(v) { return !v || (!isNaN(v) && v >= 100)} }, cssString: {value: DEFAULT_CSS_SVG_NODE}, name: {value: ''} }, inputs:1, outputs:1, icon: "svg.png", align: 'left', paletteLabel:"SVG graphics", label: function() { return this.name || "SVG graphics"; }, oneditprepare: function() { var node = this; // Old nodes might not have a dblClickZoomPercentage $('#node-input-dblClickZoomPercentage').val(this.dblClickZoomPercentage || 150); // Make sure the autocomplete fields at least refer to Iterable node fields... // See https://github.com/bartbutenaers/node-red-contrib-ui-svg/issues/33 node.nonAnimationClasses = []; node.animationClasses = []; node.animationListClasses = []; node.animationIds = []; node.animationIdSelectors = []; node.nonAnimationIds = []; node.nonAnimationIdSelectors = []; node.nonAnimationIdsAndActions = []; node.animationIdsAndActions = []; // List of animatable attributes node.attrList = [ 'clipPathUnits', 'color', 'cx', 'cy', 'd', 'display', 'dx', 'dy', 'fill', 'fx', 'fy', 'gradientTransform', 'gradientUnits', 'height', 'lengthAdjust', 'markerHeight', 'markerUnits', 'markerWidth', 'maskContentUnits', 'maskUnits', 'method', 'offset', 'orient', 'pathLength', 'patternContentUnits', 'patternTransform', 'patternUnits', 'points', 'preserveAspectRatio', 'r', 'refX', 'refY', 'rotate', 'rx', 'ry', 'spacing', 'spreadMethod', 'startOffset', 'textLength', 'transform', 'viewBox', 'visibility', 'width', 'x', 'x1', 'x2', 'y', 'y1', 'y2' ] // List of presentation attributes, which are used to style SVG elements and can be used as CSS properties. // SVG presentation attributes are CSS properties that can be used as attributes on SVG elements. // For example there are a couple of ways to give a circle a yellow color: // 1) CSS (inline) style attribute: <circle style="fill: yellow;"/> // 2) CSS (external) style: circle {fill: yellow} // 3) Presentational attribute: <circle fill="yellow"/> // The priority order is: 1 overrules 2, and 2 overrules 1 // So they accomplish the same, but - since the Node-RED dashboard applies its own styles - it is safer to use '1'. // See https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/Presentation node.presentationAttrList = [ 'alignment-baseline', 'baseline-shift', 'clip', 'clip-path', 'clip-rule', 'color', 'color-interpolation', 'color-interpolation-filters', 'color-profile', 'color-rendering', 'cursor', 'direction', 'display', 'dominant-baseline', 'enable-background', 'fill', 'fill-opacity', 'fill-rule', 'filter', 'flood-color', 'flood-opacity', 'font-family', 'font-size', 'font-size-adjust', 'font-stretch', 'font-style', 'font-variant', 'font-weight', 'glyph-orientation-horizontal', 'glyph-orientation-vertical', 'image-rendering', 'kerning', 'letter-spacing', 'lighting-color', 'marker-end', 'marker-mid', 'marker-start', 'mask', 'opacity', 'overflow', 'pointer-events', 'shape-rendering', 'solid-color', 'solid-opacity', 'stop-color', 'stop-opacity', 'stroke', 'stroke-dasharray', 'stroke-dashoffset', 'stroke-linecap', 'stroke-linejoin', 'stroke-miterlimit', 'stroke-opacity', 'stroke-width', 'text-anchor', 'text-decoration', 'text-rendering', 'transform', 'unicode-bidi', 'vector-effect', 'visibility', 'word-spacing', 'writing-mode' ] node.initialised = false; window.node_ace_svg_editor = null; window.node_ace_css_editor = null; $("#node-input-size").elementSizer({ width: "#node-input-width", height: "#node-input-height", group: "#node-input-group" }); if (typeof this.templateScope === 'undefined') { this.templateScope = 'local'; $('#node-input-templateScope').val(this.templateScope); } $('#node-input-templateScope').on('change', function() { if ($('#node-input-templateScope').val() === 'global') { $('#template-row-group, #template-row-size, #template-pass-store').hide(); node._def.defaults.group.required = false; } else { $('#template-row-group, #template-row-size, #template-pass-store').show(); node._def.defaults.group.required = true; } if ( node.visibleEditor ) { // Get the id of the editor container DIV element var editorContainerId = node.visibleEditor.container.parentElement.id; // Determine whether the svg or css editor is currently being displayed var editorType; switch(editorContainerId) { case "node-input-svg-source": editorType = "svg"; break; case "node-input-svg-css": editorType = "css"; break; } var rows = $("#dialog-form>div:not(.node-"+editorType+"-editor-row)"); var height = $("#dialog-form").height(); for (var i=0; i<rows.size(); i++) { height = height - $(rows[i]).outerHeight(true); } if ($('#node-input-templateScope').val() === "global") { height += 240; } var editorRow = $("#dialog-form>div.node-"+editorType+"-editor-row"); height -= (parseInt(editorRow.css("marginTop")) + parseInt(editorRow.css("marginBottom"))); $(".node-"+editorType+"-editor").css("height",height+"px"); node.visibleEditor.resize(); } }) // Show tabsheets node.tabs = RED.tabs.create({ id: "node-svg-tabs", onchange: function(tab) { node.currentTabId = tab.id; //console.log("tabs.onchange",tab); // Show only the content (i.e. the children) of the selected tabsheet, and hide the others $("#node-svg-tabs-content").children().hide(); $("#" + tab.id).show(); node.visibleEditor = null; if(tab.id == "node-svg-tab-animations" || tab.id == "node-svg-tab-clickable" || tab.id == "node-svg-tab-binding" || tab.id == "node-svg-tab-javascript"){ let svgStr = node.svgEditor ? node.svgEditor.getValue() : ""; try { // When tabsheet changes, all autocomplete sources should be updated (since element ids, ... might have changed) updateSvgIdHelpers(node, svgStr); } catch (error) { console.error(error) } } else if (tab.id == "node-svg-tab-source") { node.visibleEditor = node.svgEditor; } else if (tab.id == "node-svg-tab-css") { node.visibleEditor = node.cssEditor; } else if (tab.id == "node-svg-tab-editor") { } // Make sure the content of the tabsheet nicely fills the available area updateEditorHeight(node); } }); node.tabs.addTab({ id: "node-svg-tab-editor", label: "Editor" }); node.tabs.addTab({ id: "node-svg-tab-source", label: "SVG" }); node.tabs.addTab({ id: "node-svg-tab-animations", label: "Animate" }); node.tabs.addTab({ id: "node-svg-tab-clickable", label: "Event" }); node.tabs.addTab({ id: "node-svg-tab-javascript", label: "JS" }); node.tabs.addTab({ id: "node-svg-tab-binding", label: "Binding" }); node.tabs.addTab({ id: "node-svg-tab-settings", label: "Settings" }); node.tabs.addTab({ id: "node-svg-tab-css", label: "CSS" }); function updateSvgEditorButton() { var svgEditorButton = $("#node-display-svg-popup"); var editorUrlMissing = $("#node-display-svg-popup-no-url"); if ($("#node-input-editorUrl").val().trim()) { svgEditorButton.show(); editorUrlMissing.hide(); } else { svgEditorButton.hide(); editorUrlMissing.show(); } } updateSvgEditorButton(); var selectSettingsTab = function () { this.tabs.activateTab("node-svg-tab-settings") }.bind(node) //add click event to "settings" link in the "no url" warning $("#gotoSettingsButton").click(selectSettingsTab); this.fullscreen = false; this.svgEditor = RED.editor.createEditor({ id: 'node-input-svg-source', mode: 'ace/mode/html', value: $("#node-input-svgString").val() }); node.svgEditor.setFontSize(14); window.node_ace_svg_editor = node.svgEditor; this.cssEditor = RED.editor.createEditor({ id: 'node-input-svg-css', mode: 'ace/mode/css', // Apply a default CSS string to older nodes (version 2.2.4 and below) value: $("#node-input-cssString").val() || DEFAULT_CSS_SVG_NODE }); node.cssEditor.setFontSize(14); window.node_ace_css_editor = node.cssEditor; RED.library.create({ url:"uitemplates", // where to get the data from type:"ui_template", // the type of object the library is for editor:this.svgEditor, // the field name the main text body goes to mode:"ace/mode/svg", fields:['name'] }); this.svgEditor.focus(); var fullScrSvgElement = $('#node-svg-tab-source')[0]; var svgFsr = fullScrSvgElement.requestFullscreen || fullScrSvgElement.mozRequestFullScreen || fullScrSvgElement.webkitRequestFullscreen || fullScrSvgElement.msRequestFullScreen; if(svgFsr){ $('#node-expand-svg-source').click(function(e) { e.preventDefault() if (node.fullscreen === false) { if (svgFsr) { svgFsr.call(fullScrSvgElement);//triggers oneditresize() } } else { document.exitFullscreen();//triggers oneditresize() } }) } else { $('#node-expand-svg-source').hide(); } var fullScrCssElement = $('#node-svg-tab-css')[0]; var cssFsr = fullScrCssElement.requestFullscreen || fullScrCssElement.mozRequestFullScreen || fullScrCssElement.webkitRequestFullscreen || fullScrCssElement.msRequestFullScreen; if(cssFsr){ $('#node-expand-svg-css').click(function(e) { e.preventDefault() if (node.fullscreen === false) { if (cssFsr) { cssFsr.call(fullScrCssElement);//triggers oneditresize() } } else { document.exitFullscreen();//triggers oneditresize() } }) } else { $('#node-expand-svg-css').hide(); } function addPropertyRow(container, propertyName) { var propertyRow = $('<div/>',{style:"margin-top:8px;"}).appendTo(container); $('<div/>', {style:"display:inline-block;text-align:right; width:120px; padding-right:10px; box-sizing:border-box;"}) .text(propertyName) .appendTo(propertyRow); return propertyRow; } function getTitle(animation) { if (animation && animation.id != "") { return "Animation ( " + animation.id + " : '" + animation.targetId + "' : '" + animation.attributeName + "' )"; } else { return "Animation ( unknown id )"; } } function updateTitle(titleField, ele){ var par = $(ele).parent().parent(); var anim = getAnimationObjectFromFormRow(par); var newTitle = getTitle(anim); titleField.text(newTitle); } function tagSplit(val) { return val.split(/;\s*/); } function extractLastTag(term) { return tagSplit(term).pop(); } var actionOptions = { //https://www.w3.org/TR/2015/WD-SVG2-20150407/interact.html#SVGEvents 'click':'Click',//A click is defined as a mousedown and mouseup over the same screen location. Works on tablet too. 'dblclick':'Double click', 'contextmenu':'Context menu', 'mousedown':'Mouse down',//Occurs when the pointing device button is pressed over an element. 'mouseup':'Mouse up',//Occurs when the pointing device button is released over an element. 'mouseover':'Mouse over',//Occurs when the pointing device is moved onto an element. 'mouseout':'Mouse out', //Occurs when the pointing device is moved away from an element //'mousemove':'mousemove', //Occurs when the pointing device is moved while it is over an element. 'focus':'Focus',//Occurs when an element receives focus. 'focusin':'Focus in',//Occurs when an element is about to receive focus 'focusout':'Focus out',//Occurs when an element is about to lose focus. 'blur':'Blur',//Occurs when an element loses focus. 'keydown':'Key down',//Occurs when a key is pressed down 'keyup':'Key up', //https://www.w3.org/TR/touch-events/#list-of-touchevent-types 'touchstart':'Touch start', //mobile/tablet only 'touchend':'Touch end', //mobile/tablet only // 'touchmove':'touchmove', // 'touchcancel':'touchcancel', 'change':'Change' } // Create a table of (SMIL) animations // Columns: element id - attributeName - from - to - duration - repeatCount - end var smilAnimationsList = $("#node-input-animations-container").css('min-height','150px').css('min-width','400px').editableList({ addItem: function(container, i, animation) { // By default the list items will be compressed, so show the expand icon var expandIcon = "fa fa-angle-right"; // When animation === {} then we have a new list item (i.e. user pressed the addItem button) if (Object.keys(animation).length === 0) { // Initialize new items in the list. animation = { id : "", targetId : "", attributeName : "", transformType : "rotate", classValue : "", fromValue : "", duration : 1, durationUnit : "s", repeatCount : 0, end : "restore", trigger : "msg", delay : 1, delayUnit : "s", otherId : "", custom : "" }; // New list items should be expanded by default, so show a compress icon. // This way the user becomes aware which properties the new widget offers... expandIcon = "fa fa-angle-down"; } if(animation.expand){ delete animation.expand expandIcon = "fa fa-angle-down"; } container.css({ overflow: 'hidden', whiteSpace: 'nowrap' }); var headerRow = $('<div/>').appendTo(container); // Show a click-able expand/compress icon before each header row. var expandButton = $('<span/>', {style: "margin-left:5px; margin-right:10px;"}).html('<i class="' + expandIcon + '"></i>').appendTo(headerRow); // The header line title depends on the animation id var title = getTitle(animation); var titleField = $('<span/>', {style: "margin-left:10px; margin-right:10px; font-weight:Bold"}).text(title).appendTo(headerRow); titleField.click(function(evt){ expandButton.trigger( "click" ) }) // The COMMON ROWS contain the common animation properties: // - Animation id // - Target id // - Attribute name + (optional) transformType // - From value // - To value // - Duration + DurationUnit // - Repeat count // - End // - Trigger type // Add an 'animation id' property row var animationIdRow = addPropertyRow(container, "Animation id"); var animationIdField = $('<input/>', {class: "node-input-animation-id", style: "width:180px; margin-right:10px;", type: "text"}).appendTo(animationIdRow); animationIdField.val(animation.id); animationIdField.on("change", function(){ updateTitle(titleField, this) }); $('<span/>').text('%').appendTo(animationIdField); // Add a 'target id' property row var targetIdRow = addPropertyRow(container, "Target Element Id"); var targetIdField = $('<input/>', {class: "node-input-animation-targetId", style: "width:180px; margin-right:10px;", type: "text"}).appendTo(targetIdRow); targetIdField.val(animation.targetId); targetIdField.focusin(function() { // Only load autocomplete on selection (performance reasons) if(targetIdField.data('ui-autocomplete') == undefined){ //only load if not already loaded targetIdField.autocomplete({ classes: { "ui-autocomplete": "ui-svg-ui-autocomplete", //add custom class for styling the dropdown }, source: function(req, add){ add($.ui.autocomplete.filter(node.nonAnimationIds || [] , req.term)); } }); } }); targetIdField.on("change", function(){ updateTitle(titleField, this) }); $('<span/>').text('%').appendTo(targetIdField); // Add a 'class' property row var classValueRow = addPropertyRow(container, "Class"); var classValueField = $('<input/>', {class: "node-input-animation-classValue", style: "width:180px; margin-right:10px;", type: "text", placeholder: "class"}).appendTo(classValueRow); classValueField.val(animation.classValue); classValueField.focusin(function() { // Only load autocomplete on selection (performance reasons) if(classValueField.data('ui-autocomplete') == undefined){ //only load if not already loaded classValueField.autocomplete({ classes: { "ui-autocomplete": "ui-svg-ui-autocomplete", //add custom class for styling the dropdown }, source: function(req, add){ add($.ui.autocomplete.filter(node.animationListClasses || [],req.term)); } }); } }); $('<span/>').text('%').appendTo(classValueField); // Add an 'attribute name' property row var attributeNameRow = addPropertyRow(container, "Attribute name"); var attributeNameField = $('<input/>', {class: "node-input-animation-attributeName", style: "width:180px; margin-right:10px;", type: "text"}).appendTo(attributeNameRow); attributeNameField.focusin(function() { // Only load autocomplete on selection (performance reasons) if(attributeNameField.data('ui-autocomplete') == undefined){ //only load if not already loaded attributeNameField.autocomplete({ classes: { "ui-autocomplete": "ui-svg-ui-autocomplete", //add custom class for styling the dropdown }, source: function(req, add){ add($.ui.autocomplete.filter(node.attrList || [], req.term)); } }); } }); attributeNameField.on("change", function(){ updateTitle(titleField, this); // Only show the transform type dropdown when the attribute name is 'transform' if (this.value === "transform") { transformTypeField.show(); } else { transformTypeField.hide(); } }); attributeNameField.val(animation.attributeName); $('<span/>').text('%').appendTo(attributeNameField); // Add a transform 'type' (after the attribute name) var transformTypeField = $('<select/>', {class: "node-input-animation-transformType", style: "width:180px; margin-right:10px;", type: "text"}).appendTo(attributeNameRow); transformTypeField.append($('<option>', {value: "rotate" , text: 'Rotate'})); transformTypeField.append($('<option>', {value: "scale" , text: 'Scale'})); transformTypeField.append($('<option>', {value: "translate" , text: 'Translate'})); transformTypeField.append($('<option>', {value: "skewX" , text: 'Skew X'})); transformTypeField.append($('<option>', {value: "skewY" , text: 'Skew Y'})); transformTypeField.val(animation.transformType || "rotate"); $('<span/>').text('%').appendTo(transformTypeField); // Trigger the attribute name change event handler, to make sure the transform type is shown or hided at startup attributeNameField.change(); // Add a 'from / to value' property row var fromToValueRow = addPropertyRow(container, "From / To"); var fromValueField = $('<input/>', {class: "node-input-animation-fromValue", style: "width:180px; margin-right:10px;", type: "text", placeholder: "from"}).appendTo(fromToValueRow); fromValueField.val(animation.fromValue); $('<span/>').text('%').appendTo(fromValueField); // Add a 'to value' property row var toValueField = $('<input/>', {class: "node-input-animation-toValue", style: "width:180px; margin-right:10px;", type: "text", placeholder: "to"}).appendTo(fromToValueRow); toValueField.val(animation.toValue); $('<span/>').text('%').appendTo(toValueField); // Add a 'duration' property row var durationRow = addPropertyRow(container, "Duration"); var durationField = $('<input/>', {class: "node-input-animation-duration", style: "width:180px; margin-right:10px;", type: "number"}).appendTo(durationRow); durationField.val(animation.duration); $('<span/>').text('%').appendTo(durationField); var delayUnitField = $('<select/>', {class: "node-input-animation-durationUnit", style: "width:120px; margin-right:10px;", type: "text"}).appendTo(durationRow); delayUnitField.append($('<option>', {value: "ms", text: 'milliseconds'})); delayUnitField.append($('<option>', {value: "s", text: 'seconds'})); delayUnitField.append($('<option>', {value: "min", text: 'minutes'})); delayUnitField.val(animation.durationUnit || "s"); // Add a 'repeat count' property row var repeatCountRow = addPropertyRow(container, "Repeat count"); var repeatCountField = $('<input/>', {class: "node-input-animation-repeatCount", style: "width:180px; margin-right:10px;", type: "number"}).appendTo(repeatCountRow); repeatCountField.val(animation.repeatCount); $('<span/>').text('%').appendTo(repeatCountField); // Add a 'animation end' property row var endRow = addPropertyRow(container, "Animation end"); var endField = $('<select/>', {class: "node-input-animation-end", style: "width:180px; margin-right:10px;", type: "text"}).appendTo(endRow); endField.append($('<option>', {value: "freeze", text: 'Freeze new value'})); endField.append($('<option>', {value: "restore", text: 'Restore original value'})); endField.val(animation.end || "restore");//SMIL default is restore // Add a 'trigger' property row var triggerRow = addPropertyRow(container, "Trigger"); var triggerField = $('<select/>',{class:"node-input-animation-trigger", style:"width:180px; margin-right:10px;"}).appendTo(triggerRow); triggerField.append($("<option></option>").val('msg').text("Input message")); triggerField.append($("<option></option>").val('time').text("Time delay")); //triggerField.append($("<option></option>").val('anim').text("Other animation")); triggerField.append($("<option></option>").val('cust').text("Custom")); triggerField.val(animation.trigger); expandButton.click(function(e) { e.preventDefault(); // Switch the icon between expand and compress if (this.firstElementChild.className === "fa fa-angle-right") { this.firstElementChild.className = "fa fa-angle-down"; } else { this.firstElementChild.className = "fa fa-angle-right"; } // Only show the relevant widget type properties triggerField.change(); }); // The (common) row ends here ... // The OTHER rows will only be visible depending on the animation trigger type: // - Delay // - Other id // - Custom // Add a 'delay' property row var delayRow = addPropertyRow(container, "Delay"); var delayField = $('<input/>', {class: "node-input-animation-delay", style: "width:160px; margin-right:10px;", type: "number"}).appendTo(delayRow); delayField.val(animation.delay); $('<span/>').text('%').appendTo(delayField); var delayUnitField = $('<select/>', {class: "node-input-animation-delayUnit", style: "width:120px; margin-right:10px;", type: "text"}).appendTo(delayRow); delayUnitField.append($('<option>', {value: "ms", text: 'milliseconds'})); delayUnitField.append($('<option>', {value: "s", text: 'seconds'})); delayUnitField.append($('<option>', {value: "min", text: 'minutes'})); delayUnitField.val(animation.delayUnit || "s"); // Add a 'custom' property row var customRow = addPropertyRow(container, "Custom"); var customField = $(