/* TemplateEngine.js
*
* copyright (c) 2010-2017, Christian Mayer and the CometVisu contributers.
*
* This program is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the Free
* Software Foundation; either version 3 of the License, or (at your option)
* any later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program; if not, write to the Free Software Foundation, Inc.,
* 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA
*/
/**
* Main Template engine
*
* @author Christian Mayer
* @since 2010
* @module lib/TemplateEngine
* @requires dependencies/jquery
* @requires structure/pure
* @requires config/structure_custom
* @requires lib/TrickOMatic
* @requires lib/PageHandler
* @requires lib/PagePartsHandler
* @requires lib/CometVisuClient
* @requires lib/mockup/Client
* @requires lib/EventHandler
*/
///////////////////////////////////////////////////////////////////////
//
// Main:
//
define([
'jquery', '_common', 'structure_custom', 'TrickOMatic', 'PageHandler', 'PagePartsHandler',
'CometVisuClient', 'CometVisuMockup', 'EventHandler', 'MessageBroker', 'ConfigCache',
'Compatibility', 'jquery-ui', 'strftime',
'jquery.ui.touch-punch', 'jquery.svg.min', 'IconHandler',
'widget_break', 'widget_designtoggle',
'widget_group', 'widget_rgb', 'widget_web', 'widget_image',
'widget_imagetrigger', 'widget_include', 'widget_info', 'widget_infoaction', 'widget_infotrigger',
'widget_line', 'widget_multitrigger', 'widget_navbar', 'widget_page',
'widget_pagejump', 'widget_refresh', 'widget_reload', 'widget_slide',
'widget_switch', 'widget_text', 'widget_toggle', 'widget_trigger',
'widget_pushbutton', 'widget_urltrigger', 'widget_unknown', 'widget_audio',
'widget_video', 'widget_wgplugin_info',
'TransformDefault', 'TransformKnx', 'TransformOpenHab'
], function( $, design, VisuDesign_Custom, Trick_O_Matic, PageHandler, PagePartsHandler, CometVisu,
ClientMockup, EventHandler, MessageBroker, ConfigCache ) {
"use strict";
var instance;
function TemplateEngine( undefined ) {
var thisTemplateEngine = this;
this.libraryVersion = 7;
this.libraryCheck = true;
if ($.getUrlVar('libraryCheck')) {
this.libraryCheck = $.getUrlVar('libraryCheck') != 'false'; // true unless set to false
}
var loadReady = { page: false, plugins: false };
function delaySetup( id ) {
loadReady[ id ] = false;
return function() {
delete loadReady[ id ];
thisTemplateEngine.setup_page();
};
};
this.design = new VisuDesign_Custom();
this.pagePartsHandler = new PagePartsHandler();
this.eventHandler = new EventHandler(this);
this.messageBroker = MessageBroker.getInstance();
this.configCache = ConfigCache.getInstance();
var rememberLastPage = false;
this.currentPage = null;
this.currentPageNavbarVisibility = null;
this.configSettings = {
currentPageUnavailableWidth: -1,
currentPageUnavailableHeight: -1,
// if true the whole widget reacts on click events
// if false only the actor in the widget reacts on click events
bindClickToWidget: false,
// threshold where the mobile.css is loaded
maxMobileScreenWidth: 480,
// threshold where different colspans are used
maxScreenWidthColspanS: 599,
maxScreenWidthColspanM: 839,
mappings: {}, // store the mappings
stylings: {} // store the stylings
};
// use to recognize if the screen width has crossed the maxMobileScreenWidth
var lastBodyWidth=0;
this.ga_list = {};
this.widgetData = {}; // hash to store all widget specific data
this.configSuffix;
if ($.getUrlVar("config")) {
this.configSuffix = $.getUrlVar("config");
}
if ($.getUrlVar('enableCache') === "invalid") {
this.configCache.clear(this.configSuffix);
this.enableCache = true;
} else {
this.enableCache = $.getUrlVar('enableCache') ? $.getUrlVar('enableCache') === "true" : true;
}
/**
* Return (reference to) widgetData object by path.
*/
this.widgetDataGet = function( path ) {
return this.widgetData[ path ] || (this.widgetData[ path ] = {});
};
/**
* Return (reference to) widget data by element
*/
this.widgetDataGetByElement = function( element ) {
var
parent = $(element).parent(),
path = parent.attr('id');
if( path === undefined )
path = parent.parent().attr('id');
return this.widgetDataGet( path );
};
/**
* Merge obj in the widgetData.
*/
this.widgetDataInsert = function( path, obj ) {
var thisWidgetData = this.widgetDataGet( path );
for( var attrname in obj )
thisWidgetData[ attrname ] = obj[ attrname ];
return thisWidgetData;
};
/**
* Structure where a design can set a default value that a widget or plugin
* can use.
* This is especially important for design relevant information like colors
* that can not be set though CSS.
*
* Useage: templateEngine.defaults.plugin.foo = {bar: 'baz'};
*/
this.defaults = { widget: {}, plugin: {} };
/**
* Function to test if the path is in a valid form.
* Note: it doesn't check if it exists!
*/
var pathRegEx = /^id(_[0-9]+)+$/;
this.main_scroll;
this.old_scroll = '';
this.visu;
this.scrollSpeed;
this.defaultColumns = 12;
this.minColumnWidth = 120;
this.enableAddressQueue = $.getUrlVar('enableQueue') ? true : false;
this.configSettings.backend = 'default';
if ($.getUrlVar("backend")) {
this.configSettings.backend = $.getUrlVar("backend");
}
this.initBackendClient = function() {
if ($.getUrlVar('testMode')) {
thisTemplateEngine.visu = new ClientMockup();
require(['TransformMockup'], function() {});
}
else if (thisTemplateEngine.configSettings.backend=="oh") {
thisTemplateEngine.visu = new CometVisu('openhab', thisTemplateEngine.configSettings.backendUrl);
}
else if (thisTemplateEngine.configSettings.backend=="oh2") {
thisTemplateEngine.visu = new CometVisu('openhab2', thisTemplateEngine.configSettings.backendUrl);
} else {
thisTemplateEngine.visu = new CometVisu(thisTemplateEngine.configSettings.backend, thisTemplateEngine.configSettings.backendUrl);
}
function update(json) {
for( var key in json ) {
//$.event.trigger('_' + key, json[key]);
if( !(key in thisTemplateEngine.ga_list) )
continue;
var data = json[ key ];
thisTemplateEngine.ga_list[ key ].forEach( function( id ){
if( typeof id === 'string' )
{
var element = document.getElementById( id );
if (element) {
var type = element.dataset.type || 'page'; // only pages have no datatype set
var updateFn = thisTemplateEngine.design.creators[type].update;
if (updateFn) {
var children = element.children;
if (children[0])
updateFn.call(children[0], key, data);
else
updateFn.call(element, key, data);
}
} else {
console.error("no element with id %s found", id);
}
//console.log( element, type, updateFn );
} else if( typeof id === 'function' ) {
id.call( key, data );
}
});
}
};
thisTemplateEngine.visu.update = function(json) { // overload the handler
profileCV( 'first data start (' + thisTemplateEngine.visu.retryCounter + ')' );
update( json );
profileCV( 'first data updated', true );
thisTemplateEngine.visu.update = update; // handle future requests directly
}
thisTemplateEngine.visu.user = 'demo_user'; // example for setting a user
};
this.configSettings.clientDesign = "";
if (typeof this.forceReload == "undefined") {
this.forceReload = false;
}
if ($.getUrlVar('forceReload')) {
this.forceReload = $.getUrlVar('forceReload') != 'false'; // true unless set
// to false
if (this.forceReload === true) {
// do not use cache when use set forceReload
this.enableCache = false;
}
}
if ($.getUrlVar('forceDevice')) {
this.forceMobile = $.getUrlVar('forceDevice') == 'mobile';
this.forceNonMobile = !this.forceMobile;
} else {
this.forceMobile = false;
this.forceNonMobile = false;
}
var uagent = navigator.userAgent.toLowerCase();
this.mobileDevice = (/(android|blackberry|iphone|ipod|series60|symbian|windows ce|palm)/i.test(uagent));
if (/(nexus 7|tablet)/i.test(uagent)) this.mobileDevice = false; // Nexus 7 and Android Tablets have a "big" screen, so prevent Navbar from scrolling
this.mobileDevice |= this.forceMobile; // overwrite detection when set by URL
// "Bug"-Fix for ID: 3204682 "Caching on web server"
// This isn't a real fix for the problem as that's part of the web browser,
// but
// it helps to avoid the problems on the client, e.g. when the config file
// has changed but the browser doesn't even ask the server about it...
this.forceReload = true;
// Disable features that aren't ready yet
// This can be overwritten in the URL with the parameter "maturity"
this.use_maturity;
if ($.getUrlVar('maturity')) {
this.url_maturity = $.getUrlVar('maturity');
if (!isNaN(this.url_maturity - 0)) {
this.use_maturity = this.url_maturity - 0; // given directly as number
} else {
this.use_maturity = Maturity[this.url_maturity]; // or as the ENUM name
}
}
if (isNaN(this.use_maturity)) {
this.use_maturity = design.Maturity.release; // default to release
}
this.transformEncode = function(transformation, value) {
var basetrans = transformation.split('.')[0];
return transformation in Transform ? Transform[transformation]
.encode(value) : (basetrans in Transform ? Transform[basetrans]
.encode(value) : value);
};
this.transformDecode = function(transformation, value) {
var basetrans = transformation.split('.')[0];
return transformation in Transform ? Transform[transformation]
.decode(value) : (basetrans in Transform ? Transform[basetrans]
.decode(value) : value);
};
this.addAddress = function( address, id ) {
if( address in thisTemplateEngine.ga_list )
thisTemplateEngine.ga_list[ address ].push( id );
else
thisTemplateEngine.ga_list[ address ] = [ id ];
};
this.getAddresses = function() {
return Object.keys(thisTemplateEngine.ga_list);
};
/*
* this function implements widget stylings
*/
this.setWidgetStyling = function(e, value, styling) {
var sty = thisTemplateEngine.configSettings.stylings[styling];
if (sty) {
e.removeClass(sty['classnames']); // remove only styling classes
var findValue = function(v, findExact) {
if (undefined === v) {
return false;
}
if (sty[v]) { // fixed value
e.addClass(sty[v]);
return true;
}
else {
var range = sty['range'];
if (findExact && range[v]) {
e.addClass(range[v][1]);
return true;
}
var valueFloat = parseFloat(v);
for (var min in range) {
if (min > valueFloat) continue;
if (range[min][0] < valueFloat) continue; // check max
e.addClass(range[min][1]);
return true;
}
}
return false;
}
if (!findValue(value, false) && sty['defaultValue'] !== undefined) {
findValue(sty['defaultValue'], true);
}
}
return this;
}
this.map = function(value, this_map) {
if (this_map && thisTemplateEngine.configSettings.mappings[this_map]) {
var m = thisTemplateEngine.configSettings.mappings[this_map];
var ret = value;
if (m.formula) {
ret = m.formula(ret);
}
var mapValue = function(v) {
if (m[v]) {
return m[v];
} else if (m['range']) {
var valueFloat = parseFloat(v);
var range = m['range'];
for (var min in range) {
if (min > valueFloat) continue;
if (range[min][0] < valueFloat) continue; // check max
return range[min][1];
}
}
return v; // pass through when nothing was found
}
var ret = mapValue(ret);
if (!ret && m['defaultValue']) {
ret = mapValue(m['defaultValue']);
}
if( ret !== undefined ) {
return ret;
}
}
return value;
};
/**
* Look up the entry for @param value in the mapping @param this_map and
* @return the next value in the list (including wrap around).
*/
this.getNextMappedValue = function(value, this_map) {
if (this_map && thisTemplateEngine.configSettings.mappings[this_map]) {
var keys = Object.keys(thisTemplateEngine.configSettings.mappings[this_map]);
return keys[ (keys.indexOf( "" + value ) + 1) % keys.length ];
}
return value;
}
this.resetPageValues = function() {
thisTemplateEngine.currentPage = null;
thisTemplateEngine.configSettings.currentPageUnavailableWidth=-1;
thisTemplateEngine.currentPageNavbarVisibility=null;
};
this.getCurrentPageNavbarVisibility = function() {
if (thisTemplateEngine.currentPageNavbarVisibility==null) {
thisTemplateEngine.currentPageNavbarVisibility = thisTemplateEngine.pagePartsHandler.getNavbarsVisibility(thisTemplateEngine.currentPage);
}
return thisTemplateEngine.currentPageNavbarVisibility;
};
// return S, M or L depening on the passed width
function getColspanClass( width )
{
if( width <= thisTemplateEngine.configSettings.maxScreenWidthColspanS )
return 'S';
if( width <= thisTemplateEngine.configSettings.maxScreenWidthColspanM )
return 'M';
return 'L';
}
var oldWidth = -1;
this.adjustColumns = function() {
var
width = thisTemplateEngine.getAvailableWidth(),
oldClass = getColspanClass( oldWidth ),
newClass = getColspanClass( width );
oldWidth = width;
return oldClass != newClass;
};
/**
* return the available width for a the currently visible page
* the available width is calculated by subtracting the following elements widths (if they are visible) from the body width
* - Left-Navbar
* - Right-Navbar
*/
this.getAvailableWidth = function() {
// currently this calculation is done once after every page scroll (where thisTemplateEngine.configSettings.currentPageUnavailableWidth is reseted)
// if the screen width falls below the threshold which activates/deactivates the mobile.css
// the calculation has to be done again, even if the page hasn´t changed (e.g. switching between portrait and landscape mode on a mobile can cause that)
var bodyWidth = $('body').width();
var mobileUseChanged = (lastBodyWidth<thisTemplateEngine.configSettings.maxMobileScreenWidth)!=(bodyWidth<thisTemplateEngine.configSettings.maxMobileScreenWidth);
if (thisTemplateEngine.configSettings.currentPageUnavailableWidth<0 || mobileUseChanged || true) {
// console.log("Mobile.css use changed "+mobileUseChanged);
thisTemplateEngine.configSettings.currentPageUnavailableWidth=0;
var navbarVisibility = thisTemplateEngine.getCurrentPageNavbarVisibility(thisTemplateEngine.currentPage);
var widthNavbarLeft = navbarVisibility.left=="true" && $('#navbarLeft').css('display')!="none" ? Math.ceil( $('#navbarLeft').outerWidth() ) : 0;
if (widthNavbarLeft>=bodyWidth) {
// Left-Navbar has the same size as the complete body, this can happen, when the navbar has no content
// maybe there is a better solution to solve this problem
widthNavbarLeft = 0;
}
var widthNavbarRight = navbarVisibility.right=="true" && $('#navbarRight').css('display')!="none" ? Math.ceil( $('#navbarRight').outerWidth() ) : 0;
if (widthNavbarRight>=bodyWidth) {
// Right-Navbar has the same size as the complete body, this can happen, when the navbar has no content
// maybe there is a better solution to solve this problem
widthNavbarRight = 0;
}
thisTemplateEngine.configSettings.currentPageUnavailableWidth = widthNavbarLeft + widthNavbarRight + 1; // remove an additional pixel for Firefox
// console.log("Width: "+bodyWidth+" - "+widthNavbarLeft+" - "+widthNavbarRight);
}
lastBodyWidth = bodyWidth;
return bodyWidth - thisTemplateEngine.configSettings.currentPageUnavailableWidth;
};
/**
* return the available height for a the currently visible page
* the available height is calculated by subtracting the following elements heights (if they are visible) from the window height
* - Top-Navigation
* - Top-Navbar
* - Bottom-Navbar
* - Statusbar
*
* Notice: the former way to use the subtract the $main.position().top value from the total height leads to errors in certain cases
* because the value of $main.position().top is not reliable all the time
*/
this.getAvailableHeight = function() {
var windowHeight = $(window).height();
thisTemplateEngine.configSettings.currentPageUnavailableHeight=0;
var navbarVisibility = thisTemplateEngine.getCurrentPageNavbarVisibility(thisTemplateEngine.currentPage);
var heightStr = "Height: "+windowHeight;
if ($('#top').css('display') != 'none' && $('#top').outerHeight(true)>0) {
thisTemplateEngine.configSettings.currentPageUnavailableHeight+= Math.max( $('#top').outerHeight(true), $('.nav_path').outerHeight(true) );
heightStr+=" - "+Math.max( $('#top').outerHeight(true), $('.nav_path').outerHeight(true) );
}
else {
heightStr+=" - 0";
}
// console.log($('#navbarTop').css('display')+": "+$('#navbarTop').outerHeight(true));
if ($('#navbarTop').css('display') != 'none' && navbarVisibility.top=="true" && $('#navbarTop').outerHeight(true)>0) {
thisTemplateEngine.configSettings.currentPageUnavailableHeight+=$('#navbarTop').outerHeight(true);
heightStr+=" - "+$('#navbarTop').outerHeight(true);
}
else {
heightStr+=" - 0";
}
if ($('#navbarBottom').css('display') != 'none' && navbarVisibility.bottom=="true" && $('#navbarBottom').outerHeight(true)>0) {
thisTemplateEngine.configSettings.currentPageUnavailableHeight+=$('#navbarBottom').outerHeight(true);
heightStr+=" - "+$('#navbarBottom').outerHeight(true);
}
else {
heightStr+=" - 0";
}
if ($('#bottom').css('display') != 'none' && $('#bottom').outerHeight(true)>0) {
thisTemplateEngine.configSettings.currentPageUnavailableHeight+=$('#bottom').outerHeight(true);
heightStr+=" - #bottom:"+$('#bottom').outerHeight(true);
}
else {
heightStr+=" - 0";
}
if (thisTemplateEngine.configSettings.currentPageUnavailableHeight>0) {
thisTemplateEngine.configSettings.currentPageUnavailableHeight+=1;// remove an additional pixel for Firefox
}
//console.log(heightStr);
//console.log(windowHeight+" - "+thisTemplateEngine.configSettings.currentPageUnavailableHeight);
return windowHeight - thisTemplateEngine.configSettings.currentPageUnavailableHeight;
};
/*
* Make sure everything looks right when the window gets resized. This is
* necessary as the scroll effect requires a fixed element size
*/
/**
* Manager for all resizing issues. It ensures that the real resizing
* calculations are only done as often as really necessary.
*/
this.resizeHandling = (function(){
var
invalidBackdrop = true,
invalidNavbar = true,
invalidPagesize = true,
invalidRowspan = true,
invalidScreensize = true,
$pageSize = $('#pageSize'),
$navbarTop = $('#navbarTop'),
$navbarBottom = $('#navbarBottom'),
width = 0,
height = 0;
var request = null;
function makeAllSizesValid()
{
if (!request) {
request = requestAnimationFrame(function () {
invalidPagesize && makePagesizeValid(); // must be first due to depencies
invalidNavbar && makeNavbarValid();
invalidRowspan && makeRowspanValid();
invalidBackdrop && makeBackdropValid();
request = null;
});
}
}
function makeBackdropValid()
{
if( !templateEngine.currentPage )
return;
var widgetData = templateEngine.widgetData[ templateEngine.currentPage.attr('id') ];
if( '2d' === widgetData.type )
{
var
cssPosRegEx = /(\d*)(.*)/,
backdrop = templateEngine.currentPage.children().children().filter(widgetData.backdroptype)[0],
backdropSVG = widgetData.backdroptype === 'embed' ? backdrop.getSVGDocument() : null,
backdropBBox = backdropSVG ? backdropSVG.children[0].getBBox() : {},
backdropNWidth = backdrop.naturalWidth || backdropBBox.width || width,
backdropNHeight = backdrop.naturalHeight || backdropBBox.height || height,
backdropScale = Math.min( width/backdropNWidth, height/backdropNHeight ),
backdropWidth = backdropNWidth * backdropScale,
backdropHeight = backdropNHeight * backdropScale,
backdropPos = widgetData.backdropalign.split(' '),
backdropLeftRaw = backdropPos[0].match( cssPosRegEx ),
backdropTopRaw = backdropPos[1].match( cssPosRegEx ),
backdropLeft = backdropLeftRaw[2] === '%' ? (width >backdropWidth ? ((width -backdropWidth )*(+backdropLeftRaw[1])/100) : 0) : +backdropLeftRaw[1],
backdropTop = backdropTopRaw[2] === '%' ? (height>backdropHeight ? ((height-backdropHeight)*(+backdropTopRaw[1] )/100) : 0) : +backdropTopRaw[1],
uagent = navigator.userAgent.toLowerCase();
if( backdrop.complete === false || (widgetData.backdroptype === 'embed' && backdropSVG === null) )
{
// backdrop not available yet - reload
setTimeout( thisTemplateEngine.resizeHandling.invalidateBackdrop, 100);
return;
}
// Note 1: this here is a work around for older browsers that can't use
// the object-fit property yet.
// Currently (26.05.16) only Safari is known to not support
// object-position although object-fit itself does work
// Note 2: The embed element allways needs it
if(
widgetData.backdroptype === 'embed' ||
( uagent.indexOf('safari') !== -1 && uagent.indexOf('chrome') === -1 )
)
{
$( backdrop ).css({
width: backdropWidth + 'px',
height: backdropHeight + 'px',
left: backdropLeft + 'px',
top: backdropTop + 'px'
});
}
templateEngine.currentPage.find('.widget_container').toArray().forEach( function( widgetContainer ){
var widgetData = templateEngine.widgetDataGet( widgetContainer.id );
if( widgetData.layout )
{
var
layout = widgetData.layout,
// this assumes that a .widget_container has only one child and this
// is the .widget itself
style = widgetContainer.children[0].style;
if( 'x' in layout )
{
var value = layout.x.match( cssPosRegEx );
if( 'px' === value[2] )
{
style.left = (backdropLeft + value[1]*backdropScale) + 'px';
} else {
style.left = layout.x;
}
}
if( 'y' in layout )
{
var value = layout.y.match( cssPosRegEx );
if( 'px' === value[2] )
{
style.top = (backdropTop + value[1]*backdropScale) + 'px';
} else {
style.top = layout.y;
}
}
if( 'width' in layout )
style.width = layout.width;
if( 'height' in layout )
style.height = layout.height;
}
});
}
invalidBackdrop = false;
}
function makeNavbarValid()
{
if (thisTemplateEngine.mobileDevice) {
//do nothing
} else {
if(
($navbarTop.css('display') !== 'none' && $navbarTop.outerHeight(true)<=2) ||
($navbarBottom.css('display') !== 'none' && $navbarBottom.innerHeight() <=2)
) {
// update references
$navbarTop = $('#navbarTop');
$navbarBottom = $('#navbarBottom');
// Top/Bottom-Navbar is not initialized yet, wait some time and recalculate available height
// this is an ugly workaround, if someone can come up with a better solution, feel free to implement it
window.requestAnimationFrame( thisTemplateEngine.resizeHandling.invalidateNavbar );
return;
}
}
if (thisTemplateEngine.adjustColumns()) {
// the amount of columns has changed -> recalculate the widgets widths
thisTemplateEngine.applyColumnWidths();
}
invalidNavbar = false;
}
function makePagesizeValid()
{
width = thisTemplateEngine.getAvailableWidth();
height = thisTemplateEngine.getAvailableHeight();
$pageSize.text('#main,.page{width:' + (width - 0) + 'px;height:' + height + 'px;}');
if (thisTemplateEngine.adjustColumns()) {
// the amount of columns has changed -> recalculate the widgets widths
thisTemplateEngine.applyColumnWidths();
}
invalidPagesize = false;
}
function makeRowspanValid()
{
var
dummyDiv = $(
'<div class="clearfix" id="calcrowspan"><div id="containerDiv" class="widget_container"><div class="widget clearfix text" id="innerDiv" /></div></div>')
.appendTo(document.body).show(),
singleHeight = $('#containerDiv').outerHeight(false),
singleHeightMargin = $('#containerDiv').outerHeight(true),
styles = '';
for( var rowspan in thisTemplateEngine.configSettings.usedRowspans )
{
styles += '.rowspan.rowspan' + rowspan
+ ' { height: '
+ ((rowspan - 1) * singleHeightMargin + singleHeight)
+ "px;}\n";
}
$('#calcrowspan').remove();
// set css style
$('#rowspanStyle').text( styles );
invalidRowspan = false;
}
return {
invalidateBackdrop: function(){
invalidBackdrop = true;
makeAllSizesValid();
},
invalidateNavbar: function(){
invalidNavbar = true;
invalidPagesize = true;
makeAllSizesValid();
},
invalidateRowspan: function(){
invalidRowspan = true;
makeAllSizesValid();
},
invalidateScreensize: function(){
invalidScreensize = true;
invalidPagesize = true;
invalidBackdrop = true;
makeAllSizesValid();
}
};
})();
thisTemplateEngine.configSettings.usedRowspans = {};
this.rowspanClass = function(rowspan) {
thisTemplateEngine.configSettings.usedRowspans[ rowspan ] = true;
return 'rowspan rowspan' + rowspan;
};
thisTemplateEngine.configSettings.pluginsToLoadCount = [];
var xml;
this.parseXML = function(loaded_xml) {
profileCV( 'parseXML' );
xml = loaded_xml;
// erst mal den Cache für AJAX-Requests wieder aktivieren
/*
$.ajaxSetup({
cache : true
});
*/
/*
* First, we try to get a design by url. Secondly, we try to get a predefined
*/
// read predefined design in config
var predefinedDesign = $('pages', xml).attr("design");
if ($('pages', xml).attr("backend")) {
thisTemplateEngine.configSettings.backend = $('pages', xml).attr("backend");
}
if( undefined === $('pages', xml).attr( 'scroll_speed' ) )
thisTemplateEngine.configSettings.scrollSpeed = 400;
else
thisTemplateEngine.configSettings.scrollSpeed = $('pages', xml).attr('scroll_speed') | 0;
if ($('pages', xml).attr('bind_click_to_widget')!=undefined) {
thisTemplateEngine.configSettings.bindClickToWidget = $('pages', xml).attr('bind_click_to_widget')=="true" ? true : false;
}
if ($('pages', xml).attr('default_columns')) {
thisTemplateEngine.configSettings.defaultColumns = $('pages', xml).attr('default_columns');
}
if ($('pages', xml).attr('min_column_width')) {
thisTemplateEngine.configSettings.minColumnWidth = $('pages', xml).attr('min_column_width');
}
thisTemplateEngine.configSettings.screensave_time = $('pages', xml).attr('screensave_time');
thisTemplateEngine.configSettings.screensave_page = $('pages', xml).attr('screensave_page');
// design by url
if ($.getUrlVar("design")) {
thisTemplateEngine.configSettings.clientDesign = $.getUrlVar("design");
}
// design by config file
else if (predefinedDesign) {
thisTemplateEngine.configSettings.clientDesign = predefinedDesign;
}
// selection dialog
else {
thisTemplateEngine.selectDesign();
}
if ($('pages', xml).attr('max_mobile_screen_width'))
thisTemplateEngine.configSettings.maxMobileScreenWidth = $('pages', xml).attr('max_mobile_screen_width');
thisTemplateEngine.configSettings.getCSSlist = [];
if (thisTemplateEngine.configSettings.clientDesign) {
thisTemplateEngine.configSettings.getCSSlist.push( 'css!designs/' + thisTemplateEngine.configSettings.clientDesign + '/basic.css' );
if (!thisTemplateEngine.forceNonMobile) {
thisTemplateEngine.configSettings.getCSSlist.push( 'css!designs/' + thisTemplateEngine.configSettings.clientDesign + '/mobile.css' );
}
thisTemplateEngine.configSettings.getCSSlist.push( 'css!designs/' + thisTemplateEngine.configSettings.clientDesign + '/custom.css' );
thisTemplateEngine.configSettings.getCSSlist.push( 'designs/' + thisTemplateEngine.configSettings.clientDesign + '/design_setup' );
}
// start with the plugins
thisTemplateEngine.configSettings.pluginsToLoad = [];
$('meta > plugins plugin', xml).each(function(i) {
var name = $(this).attr('name');
if (name) {
if (!thisTemplateEngine.configSettings.pluginsToLoad[name]) {
/*
pluginsToLoadCount++;
$.includeScripts(
['plugins/' + name + '/structure_plugin.js'],
delaySetup( 'plugin_' + name)
);
pluginsToLoad[name] = true;
*/
thisTemplateEngine.configSettings.pluginsToLoad.push( 'plugins/' + name + '/structure_plugin' );
}
}
});
/*
if (0 == pluginsToLoadCount) {
delete loadReady.plugins;
}
*/
// then the icons
$('meta > icons icon-definition', xml).each(function(i) {
var $this = $(this);
var name = $this.attr('name');
var uri = $this.attr('uri');
var type = $this.attr('type');
var flavour = $this.attr('flavour');
var color = $this.attr('color');
var styling = $this.attr('styling');
var dynamic = $this.attr('dynamic');
icons.insert(name, uri, type, flavour, color, styling, dynamic);
});
// then the mappings
$('meta > mappings mapping', xml).each(function(i) {
var $this = $(this);
var name = $this.attr('name');
thisTemplateEngine.configSettings.mappings[name] = {};
var formula = $this.find('formula');
if (formula.length > 0) {
var func = eval('var func = function(x){var y;' + formula.text() + '; return y;}; func');
thisTemplateEngine.configSettings.mappings[name]['formula'] = func;
}
$this.find('entry').each(function() {
var $localThis = $(this);
var origin = $localThis.contents();
var value = [];
for (var i = 0; i < origin.length; i++) {
var $v = $(origin[i]);
if ($v.is('icon')) {
value[i] = icons.getIconElement($v.attr('name'), $v.attr('type'), $v.attr('flavour'), $v.attr('color'), $v.attr('styling'), $v.attr('class'));
}
else {
value[i] = $v.text();
}
}
// check for default entry
var isDefaultValue = $localThis.attr('default');
if (isDefaultValue != undefined) {
isDefaultValue = isDefaultValue == "true";
}
else {
isDefaultValue = false;
}
// now set the mapped values
if ($localThis.attr('value')) {
thisTemplateEngine.configSettings.mappings[name][$localThis.attr('value')] = value.length == 1 ? value[0] : value;
if (isDefaultValue) {
thisTemplateEngine.configSettings.mappings[name]['defaultValue'] = $localThis.attr('value');
}
}
else {
if (!thisTemplateEngine.configSettings.mappings[name]['range']) {
thisTemplateEngine.configSettings.mappings[name]['range'] = {};
}
thisTemplateEngine.configSettings.mappings[name]['range'][parseFloat($localThis.attr('range_min'))] = [ parseFloat($localThis.attr('range_max')), value ];
if (isDefaultValue) {
thisTemplateEngine.configSettings.mappings[name]['defaultValue'] = parseFloat($localThis.attr('range_min'));
}
}
});
});
// then the stylings
$('meta > stylings styling', xml).each(function(i) {
var name = $(this).attr('name');
var classnames = '';
thisTemplateEngine.configSettings.stylings[name] = {};
$(this).find('entry').each(function() {
var $localThis = $(this);
classnames += $localThis.text() + ' ';
// check for default entry
var isDefaultValue = $localThis.attr('default');
if (isDefaultValue != undefined) {
isDefaultValue = isDefaultValue == "true";
} else {
isDefaultValue = false;
}
// now set the styling values
if ($localThis.attr('value')) {
thisTemplateEngine.configSettings.stylings[name][$localThis.attr('value')] = $localThis.text();
if (isDefaultValue) {
thisTemplateEngine.configSettings.stylings[name]['defaultValue'] = $localThis.attr('value');
}
} else { // a range
if (!thisTemplateEngine.configSettings.stylings[name]['range'])
thisTemplateEngine.configSettings.stylings[name]['range'] = {};
thisTemplateEngine.configSettings.stylings[name]['range'][parseFloat($localThis.attr('range_min'))] = [parseFloat($localThis.attr('range_max')),$localThis.text()];
if (isDefaultValue) {
thisTemplateEngine.configSettings.stylings[name]['defaultValue'] = parseFloat($localThis.attr('range_min'));
}
}
});
thisTemplateEngine.configSettings.stylings[name]['classnames'] = classnames;
});
var statusContent = "";
// then the status bar
$('meta > statusbar status', xml).each(function(i) {
var type = $(this).attr('type');
var condition = $(this).attr('condition');
var extend = $(this).attr('hrefextend');
var sPath = window.location.pathname;
var sPage = sPath.substring(sPath.lastIndexOf('/') + 1);
// @TODO: make this match once the new editor is finished-ish.
var editMode = 'edit_config.html' == sPage;
// skip this element if it's edit-only and we are non-edit, or the other
// way
// round
if (editMode && '!edit' == condition)
return;
if (!editMode && 'edit' == condition)
return;
var text = $(this).text();
switch (extend) {
case 'all': // append all parameters
var search = window.location.search.replace(/\$/g, '$$$$');
text = text.replace(/(href="[^"]*)(")/g, '$1' + search + '$2');
break;
case 'config': // append config file info
var search = window.location.search.replace(/\$/g, '$$$$');
search = search.replace(/.*(config=[^&]*).*|.*/, '$1');
var middle = text.replace(/.*href="([^"]*)".*/g, '{$1}');
if( 0 < middle.indexOf('?') )
search = '&' + search;
else
search = '?' + search;
text = text.replace(/(href="[^"]*)(")/g, '$1' + search + '$2');
break;
}
statusContent += text;
});
thisTemplateEngine.configSettings.footer = btoa(statusContent);
delete loadReady.page;
thisTemplateEngine.init();
thisTemplateEngine.setup_page();
};
this.init = function() {
thisTemplateEngine.initBackendClient();
require( thisTemplateEngine.configSettings.getCSSlist, delaySetup('design'), function( err ) {
console.log( 'Failed to load design! Falling back to simplified "pure"' );
thisTemplateEngine.configSettings.getCSSlist = [ 'css!designs/pure/basic.css', 'designs/pure/design_setup' ];
require( thisTemplateEngine.configSettings.getCSSlist, delaySetup('design') );
} );
var delaySetupPluginsCallback = delaySetup('plugins');
require( thisTemplateEngine.configSettings.pluginsToLoad, delaySetupPluginsCallback, function( err ) {
console.log( 'Plugin loading error! It happend with: "' + err.requireModules[0] + '". Is the plugin available and written correctly?');
delaySetupPluginsCallback();
});
if (thisTemplateEngine.configSettings.footer) {
$('.footer').append(atob(thisTemplateEngine.configSettings.footer));
}
};
/**
* applies the correct width to the widgets corresponding to the given colspan setting
*/
this.applyColumnWidths = function() {
var
width = thisTemplateEngine.getAvailableWidth();
function dataColspan( data )
{
if( width <= thisTemplateEngine.configSettings.maxScreenWidthColspanS )
return data.colspanS;
if( width <= thisTemplateEngine.configSettings.maxScreenWidthColspanM )
return data.colspanM;
return data.colspan;
}
// all containers
['#navbarTop', '#navbarLeft', '#main', '#navbarRight', '#navbarBottom'].forEach( function( area ){
var
allContainer = $(area + ' .widget_container'),
areaColumns = $( area ).data( 'columns' );
allContainer.each(function(i, e) {
var
$e = $(e),
data = thisTemplateEngine.widgetDataGet( e.id ),
ourColspan = dataColspan( data );
var w = 'auto';
if (ourColspan > 0) {
var areaColspan = areaColumns || thisTemplateEngine.defaultColumns;
w = Math.min(100, ourColspan / areaColspan * 100) + '%';
}
$e.css('width', w);
});
// and elements inside groups
var areaColumns = $('#main').data('columns');
var adjustableElements = $('.group .widget_container');
adjustableElements.each(function(i, e) {
var
$e = $(e),
data = thisTemplateEngine.widgetData[ e.id ],
ourColspan = dataColspan( data );
if (ourColspan == undefined) {
// workaround for nowidget groups
ourColspan = dataColspan( thisTemplateEngine.widgetDataGetByElement($e.children('.group')) );
}
var w = 'auto';
if (ourColspan > 0) {
var areaColspan = areaColumns || thisTemplateEngine.defaultColumns;
var groupColspan = Math.min(areaColspan, dataColspan(thisTemplateEngine.widgetDataGetByElement($e.parentsUntil(
'.widget_container', '.group'))));
w = Math.min(100, ourColspan / groupColspan * 100) + '%'; // in percent
}
$e.css('width', w);
});
});
};
this.setup_page = function() {
// and now setup the pages
profileCV( 'setup_page start' );
// check if the page and the plugins are ready now
for( var key in loadReady ) // test for emptyness
return; // we'll be called again...
// login to backend as it might change some settings needed for further processing
thisTemplateEngine.visu.login(true, function() {
profileCV( 'setup_page running' );
// as we are sure that the default CSS were loaded now:
$('link[href*="mobile.css"]').each(function(){
this.media = 'only screen and (max-width: ' + thisTemplateEngine.configSettings.maxMobileScreenWidth + 'px)';
});
var cache = false;
if (thisTemplateEngine.enableCache && thisTemplateEngine.configCache.isCached()) {
// check if cache is still valid
if (!thisTemplateEngine.configCache.isValid(xml)) {
// cache invalid
cache = false;
thisTemplateEngine.configCache.clear();
} else {
cache = thisTemplateEngine.configCache.getData();
thisTemplateEngine.widgetData = cache.data;
thisTemplateEngine.ga_list = cache.addresses;
var body = $('body');
body.empty();
body.prepend(thisTemplateEngine.configCache.getBody());
thisTemplateEngine.create_objects();
}
}
if (!cache) {
var page = $('pages > page', xml)[0]; // only one page element allowed...
thisTemplateEngine.create_pages(page, 'id');
thisTemplateEngine.design.getCreator('page').createFinal();
}
profileCV( 'setup_page created pages' );
thisTemplateEngine.messageBroker.publish("setup.dom.finished");
if (!cache && thisTemplateEngine.enableCache) {
// cache dom + data
thisTemplateEngine.configCache.dump(xml);
}
profileCV( 'setup_page finished setup.dom.finished' );
var startpage = 'id_';
if ($.getUrlVar('startpage')) {
startpage = $.getUrlVar('startpage');
if( typeof(Storage) !== 'undefined' )
{
if( 'remember' === startpage )
{
startpage = localStorage.getItem( 'lastpage' );
rememberLastPage = true;
if( 'string' !== typeof( startpage ) || 'id_' !== startpage.substr( 0, 3 ) )
startpage = 'id_'; // fix obvious wrong data
} else
if( 'noremember' === startpage )
{
localStorage.removeItem( 'lastpage' );
startpage = 'id_';
rememberLastPage = false;
}
}
// check if startpage contains a page name and find the page id
startpage = thisTemplateEngine.getPageIdByPath(startpage) || startpage;
// check that startpage does exits
if($('#'+startpage+'.page').length === 0)
startpage = 'id_'; // default to top most page
}
thisTemplateEngine.currentPage = $('#'+startpage);
thisTemplateEngine.adjustColumns();
thisTemplateEngine.applyColumnWidths();
thisTemplateEngine.main_scroll = new PageHandler();
if (thisTemplateEngine.configSettings.scrollSpeed != undefined) {
thisTemplateEngine.main_scroll.setSpeed( thisTemplateEngine.configSettings.scrollSpeed );
}
thisTemplateEngine.scrollToPage(startpage,0);
/* CM, 9.4.16:
* TODO: Is this really needed?
* I can't find any source for setting .fast - and when it's set, it's
* most likely not working as scrollToPage should have been used instead
* anyway...
*
$('.fast').bind('click', function() {
thisTemplateEngine.main_scroll.seekTo($(this).text());
});
*/
// reaction on browser back button
window.onpopstate = function(e) {
// where do we come frome?
var lastpage = e.state;
if (lastpage) {
// browser back button takes back to the last page
thisTemplateEngine.scrollToPage(lastpage, 0, true);
}
};
// run the Trick-O-Matic scripts for great SVG backdrops
$('embed').each(function() { this.onload = Trick_O_Matic });
if (thisTemplateEngine.enableAddressQueue) {
// identify addresses on startpage
var startPageAddresses = {};
$('.actor','#'+startpage).each(function() {
var $this = $(this),
data = $this.data();
if( undefined === data.address ) data = $this.parent().data();
for( var addr in data.address )
{
startPageAddresses[addr.substring(1)]=1;
}
});
thisTemplateEngine.visu.setInitialAddresses(Object.keys(startPageAddresses));
}
var addressesToSubscribe = thisTemplateEngine.getAddresses();
if( 0 !== addressesToSubscribe.length )
thisTemplateEngine.visu.subscribe(addressesToSubscribe);
xml = null; // not needed anymore - free the space
$('.icon').each(function(){ fillRecoloredIcon(this);});
$('.loading').removeClass('loading');
this.messageBroker.publish("loading.done");
if( undefined !== thisTemplateEngine.screensave_time )
{
thisTemplateEngine.screensave = setTimeout( function(){thisTemplateEngine.scrollToPage();}, thisTemplateEngine.screensave_time*1000 );
$(document).click( function(){
clearInterval( thisTemplateEngine.screensave );
thisTemplateEngine.screensave = setTimeout( function(){thisTemplateEngine.scrollToPage();}, thisTemplateEngine.screensave_time*1000 );
});
}
profileCV( 'setup_page finish' );
}, this);
};
this.create_pages = function(page, path, flavour, type) {
var creator = thisTemplateEngine.design.getCreator(page.nodeName);
var retval = creator.create(page, path, flavour, type);
if( undefined === retval )
return;
var data = thisTemplateEngine.widgetDataGet( path );
data.type = page.nodeName;
if( 'string' === typeof retval )
{
return '<div class="widget_container '
+ (data.rowspanClass ? data.rowspanClass : '')
+ (data.containerClass ? data.containerClass : '')
+ ('break' === data.type ? 'break_container' : '') // special case for break widget
+ '" id="'+path+'" data-type="'+data.type+'">' + retval + '</div>';
} else {
return jQuery(
'<div class="widget_container '
+ (data.rowspanClass ? data.rowspanClass : '')
+ (data.containerClass ? data.containerClass : '')
+ '" id="'+path+'" data-type="'+data.type+'"/>').append(retval);
}
};
this.create_objects = function() {
for (var path in thisTemplateEngine.widgetData) {
var data = thisTemplateEngine.widgetData[path];
if (data.address && data.updateFn) {
thisTemplateEngine.design.constructDefaultObject(path);
}
var creator = thisTemplateEngine.design.getCreator(data.type);
if (creator.construct) {
creator.construct(path);
}
}
};
/**
* Little helper to find the relevant page path for a given widget.
* @param element The XML element
* @param widgetpath The path / ID of the widget
* @return The path of the parent
*/
this.getPageIdForWidgetId = function( element, widgetpath )
{
var
parent = element.parentNode,
parentpath = widgetpath.replace( /[0-9]*$/, '' );
while( parent && parent.nodeName !== 'page' )
{
parent = parent.parentNode;
parentpath = parentpath.replace( /[0-9]*_$/, '' );
}
return parentpath;
};
this.getPageIdByPath = function(page_name, path) {
if (page_name==null) return null;
if (page_name.match(/^id_[0-9_]*$/) != null) {
// already a page_id
return page_name;
} else {
if (path!=undefined) {
var scope = templateEngine.traversePath(path);
if (scope==null) {
// path is wrong
console.error("path '"+path+"' could not be traversed, no page found");
return null;
}
return templateEngine.getPageIdByName(page_name,scope);
} else {
return templateEngine.getPageIdByName(page_name);
}
}
}
this.traversePath = function(path,root_page_id) {
var path_scope=null;
var index = path.indexOf("/");
if (index>=1) {
// skip escaped slashes like \/
while (path.substr(index-1,1)=="\\") {
var next = path.indexOf("/",index+1);
if (next>=0) {
index=next;
}
}
}
// console.log("traversePath("+path+","+root_page_id+")");
if (index>=0) {
// traverse path one level down
var path_page_name = path.substr(0,index);
path_scope = templateEngine.getPageIdByName(path_page_name,root_page_id);
path = path.substr(path_page_name.length+1);
path_scope = templateEngine.traversePath(path,path_scope);
// console.log(path_page_name+"=>"+path_scope);
return path_scope;
} else {
// bottom path level reached
path_scope = templateEngine.getPageIdByName(path,root_page_id);
return path_scope;
}
return null;
}
this.getPageIdByName = function(page_name,scope) {
if (page_name.match(/^id_[0-9_]*$/) != null) {
// already a page_id
return page_name;
} else {
var page_id=null;
// find Page-ID by name
// decode html code (e.g. like ' => ')
page_name = $("<textarea/>").html(page_name).val();
// remove escaped slashes
page_name = decodeURI(page_name.replace("\\\/","/"));
// console.log("Page: "+page_name+", Scope: "+scope);
var selector = (scope!=undefined && scope!=null) ? '.page[id^="'+scope+'"] h1:contains(' + page_name + ')' : '.page h1:contains(' + page_name + ')';
var pages = $(selector, '#pages');
if (pages.length>1 && thisTemplateEngine.currentPage!=null) {
// More than one Page found -> search in the current pages descendants first
var fallback = true;
pages.each(function(i) {
var p = $(this).closest(".page");
if ($(this).text() == page_name) {
if (p.attr('id').length<thisTemplateEngine.currentPage.attr('id').length) {
// found pages path is shorter the the current pages -> must be an ancestor
if (thisTemplateEngine.currentPage.attr('id').indexOf(p.attr('id'))==0) {
// found page is an ancenstor of the current page -> we take this one
page_id = p.attr("id");
fallback = false;
//break loop
return false;
}
} else {
if (p.attr('id').indexOf(thisTemplateEngine.currentPage.attr('id'))==0) {
// found page is an descendant of the current page -> we take this one
page_id = p.attr("id");
fallback = false;
//break loop
return false;
}
}
}
});
if (fallback) {
// take the first page that fits (old behaviour)
pages.each(function(i) {
if ($(this).text() == page_name) {
page_id = $(this).closest(".page").attr("id");
// break loop
return false;
}
});
}
} else {
pages.each(function(i) {
if ($(this).text() == page_name) {
page_id = $(this).closest(".page").attr("id");
// break loop
return false;
}
});
}
}
if (page_id!=null && page_id.match(/^id_[0-9_]*$/) != null) {
return page_id;
} else {
// not found
return null;
}
}
this.scrollToPage = function(target, speed, skipHistory) {
if( undefined === target )
target = this.screensave_page;
var page_id = thisTemplateEngine.getPageIdByPath(target);
if (page_id==null) {
return;
}
if( undefined === speed )
speed = thisTemplateEngine.configSettings.scrollSpeed;
if( rememberLastPage )
localStorage.lastpage = page_id;
// push new state to history
if (skipHistory === undefined)
window.history.pushState(page_id, page_id, window.location.href);
thisTemplateEngine.main_scroll.seekTo(page_id, speed); // scroll to it
thisTemplateEngine.pagePartsHandler.initializeNavbars(page_id);
$(window).trigger('scrolltopage', page_id);
};
/*
* Show a popup of type "type". The attributes is an type dependend object
* This function returnes a jQuery object that points to the whole popup, so
* it's content can be easily extended
*/
this.showPopup = function(type, attributes) {
return thisTemplateEngine.design.getPopup(type).create(attributes);
};
/*
* Remove the popup. The parameter is the jQuery object returned by the
* showPopup function
*/
this.removePopup = function(jQuery_object) {
jQuery_object.remove();
};
/** ************************************************************************* */
/* FIXME - Question: should this belong to the VisuDesign object so that it */
/* is possible to overload?!? */
/** ************************************************************************* */
this.refreshAction = function(target, src) {
/*
* Special treatment for (external) iframes: we need to clear it and reload
* it in another thread as otherwise stays blank for some targets/sites and
* src = src doesnt work anyway on external This creates though some
* "flickering" so we avoid to use it on images, internal iframes and others
*/
var parenthost = window.location.protocol + "//" + window.location.host;
if (target.nodeName == "IFRAME" && src.indexOf(parenthost) != 0) {
target.src = '';
setTimeout(function() {
target.src = src;
}, 0);
} else {
target.src = src + '&' + new Date().getTime();
}
};
this.setupRefreshAction = function( path, refresh ) {
if (refresh && refresh > 0) {
var
element = $( '#' + path ),
target = $('img', element)[0] || $('iframe', element)[0],
src = target.src;
if (src.indexOf('?') < 0)
src += '?';
thisTemplateEngine.widgetDataGet( path ).internal = setInterval(function() {
thisTemplateEngine.refreshAction(target, src);
}, refresh);
}
};
this.selectDesign = function() {
var $body = $("body");
$("body > *").hide();
$body.css({
backgroundColor : "black"
});
var $div = $("<div id=\"designSelector\" />");
$div.css({
background : "#808080",
width : "400px",
color : "white",
margin : "auto",
padding : "0.5em"
});
$div.html("Loading ...");
$body.append($div);
$.getJSON("./designs/get_designs.php",function(data) {
$div.empty();
$div.append("<h1>Please select design</h1>");
$div.append("<p>The Location/URL will change after you have chosen your design. Please bookmark the new URL if you do not want to select the design every time.</p>");
$.each(data,function(i, element) {
var $myDiv = $("<div />");
$myDiv.css({
cursor : "pointer",
padding : "0.5em 1em",
borderBottom : "1px solid black",
margin : "auto",
width : "262px",
position : "relative"
});
$myDiv
.append("<div style=\"font-weight: bold; margin: 1em 0 .5em;\">Design: "
+ element + "</div>");
$myDiv
.append("<iframe src=\"designs/design_preview.html?design="
+ element
+ "\" width=\"160\" height=\"90\" border=\"0\" scrolling=\"auto\" frameborder=\"0\" style=\"z-index: 1;\"></iframe>");
$myDiv
.append("<img width=\"60\" height=\"30\" src=\"./demo/media/arrow.png\" alt=\"select\" border=\"0\" style=\"margin: 60px 10px 10px 30px;\"/>");
$div.append($myDiv);
var $tDiv = $("<div />");
$tDiv.css({
background : "transparent",
position : "absolute",
height : "90px",
width : "160px",
zIndex : 2
});
var pos = $myDiv.find("iframe").position();
$tDiv.css({
left : pos.left + "px",
top : pos.top + "px"
});
$myDiv.append($tDiv);
$myDiv.hover(function() {
// over
$myDiv.css({
background : "#bbbbbb"
});
}, function() {
// out
$myDiv.css({
background : "transparent"
});
});
$myDiv.click(function() {
if (document.location.search == "") {
document.location.href = document.location.href
+ "?design=" + element;
} else {
document.location.href = document.location.href
+ "&design=" + element;
}
});
});
});
};
// tools for widget handling
/**
* Return a widget (to be precise: the widget_container) for the given path
*/
this.lookupWidget = function( path ) {
var id = path.split( '_' );
var elementNumber = +id.pop();
return $( '.page#' + id.join('_') ).children().children()[ elementNumber+1 ];
};
this.getParentPage = function(page) {
if (0 === page.length) return null;
return getParentPageById(page.attr('id'), true);
};
function getParentPageById(path, isPageId) {
if (0 < path.length) {
var pathParts = path.split('_');
if (isPageId) pathParts.pop();
while (pathParts.length > 1) {
pathParts.pop();
var path = pathParts.join('_') + '_';
if ($('#' + path).hasClass("page")) {
return $('#' + path);
}
}
}
return null;
};
this.getParentPageFromPath = function(path) {
return getParentPageById(path, false);
};
/**
* Load a script and run it before page setup.
* This is needed for plugin that depend on an external library.
*/
this.getPluginDependency = function( url ){
$.getScriptSync( url );
}
/**
* This has to be called by a plugin once it was loaded.
*/
this.pluginLoaded = function(){
pluginsToLoadCount--;
if( 0 >= pluginsToLoadCount )
{
delete loadReady.plugins;
thisTemplateEngine.setup_page();
}
}
/**
* Create a new widget.
*/
this.create = function( path, element ) {
return "created widget '" + path + "': '" + element + "'";
};
/**
* Delete an existing path, i.e. widget, group or even page - including
* child elements.
*/
this.deleteCommand = function( path ) {
console.log( this.lookupWidget( path ), $( '#'+path ) );
//$( this.lookupWidget( path ) ).remove();
return "deleted widget '" + path + "'";
};
/**
* Focus a widget.
*/
this.focus = function( path ) {
$('.focused').removeClass('focused')
$( this.lookupWidget( path ) ).addClass( 'focused' );
};
////////// Reflection API for possible Editor communication: Start //////////
/**
* Return a list of all widgets.
*/
this.list = function() {
var widgetTree = {};
$('.page').each( function(){
var id = this.id.split( '_' );
var thisEntry = widgetTree;
if( 'id' === id.shift() )
{
var thisNumber;
while( thisNumber = id.shift() )
{
if( !(thisNumber in thisEntry) )
thisEntry[ thisNumber ] = {};
thisEntry = thisEntry[ thisNumber ];
}
$( this ).children().children( 'div.widget_container' ).each( function( i ){
if( undefined === thisEntry[ i ] )
{
thisEntry[ i ] = {}
}
var thisWidget = $( this ).children()[0];
thisEntry[ i ].name = ('className' in thisWidget) ? thisWidget.className : 'TODO';
thisEntry[ i ].type = $(this).data('type');
});
}
});
return widgetTree;
};
/**
* Return all attributes of a widget.
*/
this.read = function( path ) {
var widget = this.lookupWidget( path ),
data = $.extend( {}, $( widget ).children().data() ); // copy
delete data.basicvalue;
delete data.value;
return data;
};
/**
* Set the selection state of a widget.
*/
this.select = function( path, state ) {
var container = this.lookupWidget( path )
if( state )
$( container ).addClass( 'selected');
else
$( container ).removeClass( 'selected');
};
/**
* Set all attributes of a widget.
*/
this.write = function( path, attributes ) {
$( this.lookupWidget( path ) ).children().data( attributes );
};
/**
* Reflection API: communication
* Handle messages that might be sent by the editor
*/
this.handleMessage = function( event ) {
// prevend bad or even illegal requests
if( event.origin !== window.location.origin ||
'object' !== typeof event.data ||
!('command' in event.data ) ||
!('parameters' in event.data )
)
return;
var answer = 'bad command',
parameters = event.data.parameters;
// note: as the commands are from external, we have to be a bit more
// carefull for corectness testing
switch( event.data.command )
{
case 'create':
if( 'object' === typeof parameters &&
pathRegEx.test( parameters.path ) &&
'string' === typeof parameters.element
)
answer = thisTemplateEngine.create( parameters.path, parameters.element );
else
answer = 'bad path or element';
break;
case 'delete':
if( pathRegEx.test( parameters ) )
answer = thisTemplateEngine.deleteCommand( parameters );
else
answer = 'bad path';
break;
case 'focus':
if( pathRegEx.test( parameters ) )
answer = thisTemplateEngine.focus( parameters );
else
answer = 'bad path';
break;
case 'list':
answer = thisTemplateEngine.list();
break;
case 'read':
if( pathRegEx.test( parameters ) )
answer = thisTemplateEngine.read( parameters );
else
answer = 'bad path';
break;
case 'select':
if( 'object' === typeof parameters &&
pathRegEx.test( parameters.path ) &&
'boolean' === typeof parameters.state
)
answer = thisTemplateEngine.select( parameters.path, parameters.state );
break;
case 'write':
if( 'object' === typeof parameters &&
pathRegEx.test( parameters.path ) &&
'object' === typeof parameters.attributes
)
answer = thisTemplateEngine.write( parameters.path, parameters.attributes );
break;
}
event.source.postMessage( answer, event.origin );
};
window.addEventListener( 'message', this.handleMessage, false);
////////// Reflection API for possible Editor communication: End //////////
}
return {
// simulate a singleton
getInstance : function() {
if (!instance) {
instance = new TemplateEngine();
}
return instance;
}
};
}); // end require