diff --git a/openslides/core/static/js/core/pdf.js b/openslides/core/static/js/core/pdf.js new file mode 100644 index 000000000..e58509d59 --- /dev/null +++ b/openslides/core/static/js/core/pdf.js @@ -0,0 +1,527 @@ +(function () { + +'use strict'; + +angular.module('OpenSlidesApp.core.pdf', []) + +.factory('HTMLValidizer', function() { + var HTMLValidizer = {}; + + //checks if str is valid HTML. Returns valid HTML if not, + //return emptystring if empty + HTMLValidizer.validize = function(str) { + if (str) { + var a = document.createElement('div'); + a.innerHTML = str; + angular.forEach(a.childNodes, function (child) { + if (child.nodeType == 1) { + return str; + } + }); + return "
" + str + "
"; + } else { + return ""; //needed for blank "reaons" field + } + }; + return HTMLValidizer; +}) + +.factory('PdfMakeDocumentProvider', [ + 'gettextCatalog', + 'Config', + function(gettextCatalog, Config) { + /** + * Provides the global Document + * @constructor + * @param {object} contentProvider - Object with on method `getContent`, which returns an array for content + * @param {string} defaultFont - Default font for the document + */ + var createInstance = function(contentProvider, defaultFont) { + /** + * Generates header for PDF + * @constructor + */ + var header = function() { + var date = new Date(); + return { + // alignment: 'center', + color: '#555', + fontSize: 10, + margin: [80, 50, 80, 0], //margin: [left, top, right, bottom] + columns: [ + { + text: Config.get('general_event_name').value + ' · ' + Config.get('general_event_description').value , + fontSize:10, + width: '70%' + }, + { + fontSize: 6, + width: '30%', + text: gettextCatalog.getString('As of') + " " + date.toLocaleDateString() + " " + date.toLocaleTimeString(), + alignment: 'right' + }] + }; + }, + /** + * Generates footer line + * @function + * @param {object} currentPage - An object representing the current page + * @param {number} pageCount - number for pages + */ + footer = function(currentPage, pageCount) { + return { + alignment: 'center', + fontSize: 8, + color: '#555', + text: gettextCatalog.getString('Page') + ' ' + currentPage.toString() + ' / ' + pageCount.toString() + }; + }, + /** + * Generates the document(definition) for pdfMake + * @function + */ + getDocument = function() { + var content = contentProvider.getContent(); + return { + pageSize: 'A4', + pageMargins: [80, 90, 80, 60], + defaultStyle: { + font: defaultFont, + fontSize: 10 + }, + header: header, + footer: footer, + content: content, + styles: { + title: { + fontSize: 30, + margin: [0,0,0,20], + bold: true + }, + heading: { + fontSize: 16, + margin: [0,0,0,10], + bold: true + }, + tableofcontent: { + fontSize: 12, + margin: [0,3] + } + } + }; + }; + return { + getDocument: getDocument + }; + }; + return { + createInstance: createInstance + }; + } +]) + +.factory('PdfMakeConverter', [ + 'HTMLValidizer', + function(HTMLValidizer) { + /** + * Converter component for HTML->JSON for pdfMake + * @constructor + * @param {object} images - Key-Value structure representing image.src/BASE64 of images + * @param {object} fonts - Key-Value structure representing fonts (detailed description below) + * @param {object} pdfMake - the converter component enhances pdfMake + */ + var createInstance = function(images, fonts, pdfMake) { + var slice = Function.prototype.call.bind([].slice), + map = Function.prototype.call.bind([].map), + /** + * Adds a custom font to pdfMake.vfs + * @function + * @param {object} fontFiles - object with Files to add to pdfMake.vfs + * { + * normal: $Filename + * bold: $Filename + * italics: $Filename + * bolditalics: $Filename + * } + */ + addFontToVfs = function(fontFiles) { + Object.keys(fontFiles).forEach(function(name) { + var file = fontFiles[name]; + pdfMake.vfs[file.name] = file.content; + }); + }, + /** + * Adds custom fonts to pdfMake + * @function + * @param {object} fontInfo - Font configuration from Backend + * { + * $FontName : { + * normal: $Filename + * bold: $Filename + * italics: $Filename + * bolditalics: $Filename + * } + * } + */ + registerFont = function(fontInfo) { + Object.keys(fontInfo).forEach(function(name) { + var font = fontInfo[name]; + addFontToVfs(font); + pdfMake.fonts = pdfMake.fonts || {}; + pdfMake.fonts[name] = Object.keys(font).reduce(function(fontDefinition, style) { + fontDefinition[style] = font[style].name; + return fontDefinition; + }, {}); + }); + }, + /** + * Convertes HTML for use with pdfMake + * @function + * @param {object} html - html + */ + convertHTML = function(html, scope) { + var elementStyles = { + "b": ["font-weight:bold"], + "strong": ["font-weight:bold"], + "u": ["text-decoration:underline"], + "em": ["font-style:italic"], + "i": ["font-style:italic"], + "h1": ["font-size:30"], + "h2": ["font-size:28"], + "h3": ["font-size:26"], + "h4": ["font-size:24"], + "h5": ["font-size:22"], + "h6": ["font-size:20"], + "a": ["color:blue", "text-decoration:underline"] + }, + /** + * Parses Children of the current paragraph + * @function + * @param {object} converted - + * @param {object} element - + * @param {object} currentParagraph - + * @param {object} styles - + */ + parseChildren = function(converted, element, currentParagraph, styles) { + var elements = []; + var children = element.childNodes; + if (children.length !== 0) { + _.forEach(children, function(child) { + currentParagraph = ParseElement(elements, child, currentParagraph, styles); + }); + } + if (elements.length !== 0) { + _.forEach(elements, function(el) { + converted.push(el); + }); + } + return currentParagraph; + }, + /** + * Extracts the style from an object + * @function + * @param {object} o - the current object + * @param {object} styles - an array with styles + */ + ComputeStyle = function(o, styles) { + styles.forEach(function(singleStyle) { + var styleDefinition = singleStyle.trim().toLowerCase().split(":"); + var style = styleDefinition[0]; + var value = styleDefinition[1]; + if (styleDefinition.length == 2) { + switch (style) { + case "padding-left": + o.margin = [parseInt(value), 0, 0, 0]; + break; + case "font-size": + o.fontSize = parseInt(value); + break; + case "text-align": + switch (value) { + case "right": + case "center": + case "justify": + o.alignment = value; + break; + } + break; + case "font-weight": + switch (value) { + case "bold": + o.bold = true; + break; + } + break; + case "text-decoration": + switch (value) { + case "underline": + o.decoration = "underline"; + break; + case "line-through": + o.decoration = "lineThrough"; + break; + } + break; + case "font-style": + switch (value) { + case "italic": + o.italics = true; + break; + } + break; + case "color": + o.color = value; + break; + case "background-color": + o.background = value; + break; + } + } + }); + }, + /** + * Parses a single HTML element + * @function + * @param {object} alreadyConverted - + * @param {object} element - + * @param {object} currentParagraph - + * @param {object} styles - + */ + ParseElement = function(alreadyConverted, element, currentParagraph, styles) { + styles = styles || []; + if (element.getAttribute) { + var nodeStyle = element.getAttribute("style"); + if (nodeStyle) { + nodeStyle.split(";").forEach(function(nodeStyle) { + var tmp = nodeStyle.replace(/\s/g, ''); + styles.push(tmp); + }); + } + } + var nodeName = element.nodeName.toLowerCase(); + switch (nodeName) { + case "h1": + case "h2": + case "h3": + case "h4": + case "h5": + case "h6": + currentParagraph = create("text"); + /* falls through */ + case "a": + parseChildren(alreadyConverted, element, currentParagraph, styles.concat(elementStyles[nodeName])); + alreadyConverted.push(currentParagraph); + break; + case "b": + case "strong": + case "u": + case "em": + case "i": + parseChildren(alreadyConverted, element, currentParagraph, styles.concat(elementStyles[nodeName])); + break; + case "table": + var t = create("table", { + widths: [], + body: [] + }); + var border = element.getAttribute("border"); + var isBorder = false; + if (border) + if (parseInt(border) == 1) isBorder = true; + if (!isBorder) t.layout = 'noBorders'; + parseChildren(t.table.body, element, currentParagraph, styles); + var widths = element.getAttribute("widths"); + if (!widths) { + if (t.table.body.length !== 0) { + if (t.table.body[0].length !== 0) + for (var k = 0; k < t.table.body[0].length; k++) + t.table.widths.push("*"); + } + } else { + var w = widths.split(","); + for (var ko = 0; ko < w.length; ko++) t.table.widths.push(w[ko]); + } + alreadyConverted.push(t); + break; + case "tbody": + parseChildren(alreadyConverted, element, currentParagraph, styles); + break; + case "tr": + var row = []; + parseChildren(row, element, currentParagraph, styles); + alreadyConverted.push(row); + break; + case "td": + currentParagraph = create("text"); + var st = create("stack"); + st.stack.push(currentParagraph); + var rspan = element.getAttribute("rowspan"); + if (rspan) + st.rowSpan = parseInt(rspan); + var cspan = element.getAttribute("colspan"); + if (cspan) + st.colSpan = parseInt(cspan); + parseChildren(st.stack, element, currentParagraph, styles); + alreadyConverted.push(st); + break; + case "span": + if (scope.lineNumberMode == "inline") { + var lineNumberInline = element.getAttribute("data-line-number"), + lineNumberObjInline = { + text: lineNumberInline, + color: "gray", + fontSize: 5 + }; + currentParagraph.text.push(lineNumberObjInline); + parseChildren(alreadyConverted, element, currentParagraph, styles); + } else if (scope.lineNumberMode == "outside") { + var lineNumberOutline = element.getAttribute("data-line-number"), + lineNumberObject = { + width: 20, + text: lineNumberOutline, + color: "gray", + fontSize: 8, + margin: [0, 2, 0, 0] + }, + col = { + columns: [ + lineNumberObject, + ] + }; + currentParagraph = create("text"); + col.columns.push(currentParagraph); + parseChildren(col.columns[0], element, currentParagraph, styles); + alreadyConverted.push(col); + } else { + parseChildren(alreadyConverted, element, currentParagraph, styles); + } + break; + case "br": + //in case of inline-line-numbers and the os-line-break class ignore the break + if (!(scope.lineNumberMode == "inline" && element.getAttribute("class") == "os-line-break")) { + currentParagraph = create("text"); + alreadyConverted.push(currentParagraph); + } + break; + case "li": + case "div": + currentParagraph = create("text"); + var stackDiv = create("stack"); + stackDiv.stack.push(currentParagraph); + ComputeStyle(stackDiv, styles); + parseChildren(stackDiv.stack, element, currentParagraph); + alreadyConverted.push(stackDiv); + break; + case "p": + currentParagraph = create("text"); + currentParagraph.margin = [0,5]; + var stackP = create("stack"); + stackP.stack.push(currentParagraph); + ComputeStyle(stackP, styles); + parseChildren(stackP.stack, element, currentParagraph); + alreadyConverted.push(stackP); + break; + case "img": + // TODO: need a proper way to calculate the space + // left on the page. + // This requires further information + // A4 in 72dpi: 595px x 842px + var maxResolution = { + width: 435, + height: 830 + }, + width = parseInt(element.getAttribute("width")), + height = parseInt(element.getAttribute("height")); + + if (width > maxResolution.width) { + var scaleByWidth = maxResolution.width/width; + width *= scaleByWidth; + height *= scaleByWidth; + } + if (height > maxResolution.height) { + var scaleByHeight = maxResolution.height/height; + width *= scaleByHeight; + height *= scaleByHeight; + } + + alreadyConverted.push({ + image: BaseMap[element.getAttribute("src")], + width: width, + height: height + }); + break; + case "ul": + var u = create("ul"); + parseChildren(u.ul, element, currentParagraph, styles); + alreadyConverted.push(u); + break; + case "ol": + var o = create("ol"); + parseChildren(o.ol, element, currentParagraph, styles); + alreadyConverted.push(o); + break; + default: + var temporary = create("text", element.textContent.replace(/\n/g, "")); + if (styles) { + ComputeStyle(temporary, styles); + } + // TODO: This if-clause is a hotfix for issue #2442. + // Do this right! Why is currentParagraph undefined? + if (!currentParagraph) { + currentParagraph = {}; + currentParagraph.text = []; + } + currentParagraph.text.push(temporary); + break; + } + return currentParagraph; + }, + /** + * Parses HTML + * @function + * @param {string} converted - + * @param {object} htmlText - + */ + ParseHtml = function(converted, htmlText) { + var html = HTMLValidizer.validize(htmlText); + html = $(html.replace(/\t/g, "").replace(/\n/g, "")); + var emptyParagraph = create("text"); + slice(html).forEach(function(element) { + ParseElement(converted, element); + }); + }, + content = []; + ParseHtml(content, html); + return content; + }, + BaseMap = images, + /** + * Creates containerelements for pdfMake + * e.g create("text":"MyText") result in { text: "MyText" } + * or complex objects create("stack", [{text:"MyText"}, {text:"MyText2"}]) + *for units / paragraphs of text + * + * @function + * @param {string} name - name of the attribute holding content + * @param {object} content - the actual content (maybe empty) + */ + create = function(name, content) { + var o = {}; + content = content || []; + o[name] = content; + return o; + }; + fonts.forEach(function(fontInfo) { + registerFont(fontInfo); + }); + return { + convertHTML: convertHTML, + createElement: create + }; + }; + return { + createInstance: createInstance + }; +}]); + +}()); diff --git a/openslides/core/static/js/core/site.js b/openslides/core/static/js/core/site.js index a5735ee80..c6e94708c 100644 --- a/openslides/core/static/js/core/site.js +++ b/openslides/core/static/js/core/site.js @@ -23,526 +23,6 @@ angular.module('OpenSlidesApp.core.site', [ // Can be used to find out if the projector or the side is used .constant('REALM', 'site') -//for global usage -.factory('HTMLValidizer', function() { - var HTMLValidizer = {}; - - //checks if str is valid HTML. Returns valid HTML if not, - //return emptystring if empty - HTMLValidizer.validize = function(str) { - if (str) { - var a = document.createElement('div'); - a.innerHTML = str; - angular.forEach(a.childNodes, function (child) { - if (child.nodeType == 1) { - return str; - } - }); - return "" + str + "
"; - } else { - return ""; //needed for blank "reaons" field - } - }; - return HTMLValidizer; -}) - -.factory('PdfMakeDocumentProvider', [ - 'gettextCatalog', - 'Config', - function(gettextCatalog, Config) { - /** - * Provides the global Document - * @constructor - * @param {object} contentProvider - Object with on method `getContent`, which returns an array for content - * @param {string} defaultFont - Default font for the document - */ - var createInstance = function(contentProvider, defaultFont) { - /** - * Generates header for PDF - * @constructor - */ - var header = function() { - var date = new Date(); - return { - // alignment: 'center', - color: '#555', - fontSize: 10, - margin: [80, 50, 80, 0], //margin: [left, top, right, bottom] - columns: [ - { - text: Config.get('general_event_name').value + ' · ' + Config.get('general_event_description').value , - fontSize:10, - width: '70%' - }, - { - fontSize: 6, - width: '30%', - text: gettextCatalog.getString('As of') + " " + date.toLocaleDateString() + " " + date.toLocaleTimeString(), - alignment: 'right' - }] - }; - }, - /** - * Generates footer line - * @function - * @param {object} currentPage - An object representing the current page - * @param {number} pageCount - number for pages - */ - footer = function(currentPage, pageCount) { - return { - alignment: 'center', - fontSize: 8, - color: '#555', - text: gettextCatalog.getString('Page') + ' ' + currentPage.toString() + ' / ' + pageCount.toString() - }; - }, - /** - * Generates the document(definition) for pdfMake - * @function - */ - getDocument = function() { - var content = contentProvider.getContent(); - return { - pageSize: 'A4', - pageMargins: [80, 90, 80, 60], - defaultStyle: { - font: defaultFont, - fontSize: 10 - }, - header: header, - footer: footer, - content: content, - styles: { - title: { - fontSize: 30, - margin: [0,0,0,20], - bold: true - }, - heading: { - fontSize: 16, - margin: [0,0,0,10], - bold: true - }, - tableofcontent: { - fontSize: 12, - margin: [0,3] - } - } - }; - }; - return { - getDocument: getDocument - }; - }; - return { - createInstance: createInstance - }; - } -]) -.factory('PdfMakeConverter', [ - 'HTMLValidizer', - function(HTMLValidizer) { - /** - * Converter component for HTML->JSON for pdfMake - * @constructor - * @param {object} images - Key-Value structure representing image.src/BASE64 of images - * @param {object} fonts - Key-Value structure representing fonts (detailed description below) - * @param {object} pdfMake - the converter component enhances pdfMake - */ - var createInstance = function(images, fonts, pdfMake) { - var slice = Function.prototype.call.bind([].slice), - map = Function.prototype.call.bind([].map), - /** - * Adds a custom font to pdfMake.vfs - * @function - * @param {object} fontFiles - object with Files to add to pdfMake.vfs - * { - * normal: $Filename - * bold: $Filename - * italics: $Filename - * bolditalics: $Filename - * } - */ - addFontToVfs = function(fontFiles) { - Object.keys(fontFiles).forEach(function(name) { - var file = fontFiles[name]; - pdfMake.vfs[file.name] = file.content; - }); - }, - /** - * Adds custom fonts to pdfMake - * @function - * @param {object} fontInfo - Font configuration from Backend - * { - * $FontName : { - * normal: $Filename - * bold: $Filename - * italics: $Filename - * bolditalics: $Filename - * } - * } - */ - registerFont = function(fontInfo) { - Object.keys(fontInfo).forEach(function(name) { - var font = fontInfo[name]; - addFontToVfs(font); - pdfMake.fonts = pdfMake.fonts || {}; - pdfMake.fonts[name] = Object.keys(font).reduce(function(fontDefinition, style) { - fontDefinition[style] = font[style].name; - return fontDefinition; - }, {}); - }); - }, - /** - * Convertes HTML for use with pdfMake - * @function - * @param {object} html - html - */ - convertHTML = function(html, scope) { - var elementStyles = { - "b": ["font-weight:bold"], - "strong": ["font-weight:bold"], - "u": ["text-decoration:underline"], - "em": ["font-style:italic"], - "i": ["font-style:italic"], - "h1": ["font-size:30"], - "h2": ["font-size:28"], - "h3": ["font-size:26"], - "h4": ["font-size:24"], - "h5": ["font-size:22"], - "h6": ["font-size:20"], - "a": ["color:blue", "text-decoration:underline"] - }, - /** - * Parses Children of the current paragraph - * @function - * @param {object} converted - - * @param {object} element - - * @param {object} currentParagraph - - * @param {object} styles - - */ - parseChildren = function(converted, element, currentParagraph, styles) { - var elements = []; - var children = element.childNodes; - if (children.length !== 0) { - _.forEach(children, function(child) { - currentParagraph = ParseElement(elements, child, currentParagraph, styles); - }); - } - if (elements.length !== 0) { - _.forEach(elements, function(el) { - converted.push(el); - }); - } - return currentParagraph; - }, - /** - * Extracts the style from an object - * @function - * @param {object} o - the current object - * @param {object} styles - an array with styles - */ - ComputeStyle = function(o, styles) { - styles.forEach(function(singleStyle) { - var styleDefinition = singleStyle.trim().toLowerCase().split(":"); - var style = styleDefinition[0]; - var value = styleDefinition[1]; - if (styleDefinition.length == 2) { - switch (style) { - case "padding-left": - o.margin = [parseInt(value), 0, 0, 0]; - break; - case "font-size": - o.fontSize = parseInt(value); - break; - case "text-align": - switch (value) { - case "right": - case "center": - case "justify": - o.alignment = value; - break; - } - break; - case "font-weight": - switch (value) { - case "bold": - o.bold = true; - break; - } - break; - case "text-decoration": - switch (value) { - case "underline": - o.decoration = "underline"; - break; - case "line-through": - o.decoration = "lineThrough"; - break; - } - break; - case "font-style": - switch (value) { - case "italic": - o.italics = true; - break; - } - break; - case "color": - o.color = value; - break; - case "background-color": - o.background = value; - break; - } - } - }); - }, - /** - * Parses a single HTML element - * @function - * @param {object} alreadyConverted - - * @param {object} element - - * @param {object} currentParagraph - - * @param {object} styles - - */ - ParseElement = function(alreadyConverted, element, currentParagraph, styles) { - styles = styles || []; - if (element.getAttribute) { - var nodeStyle = element.getAttribute("style"); - if (nodeStyle) { - nodeStyle.split(";").forEach(function(nodeStyle) { - var tmp = nodeStyle.replace(/\s/g, ''); - styles.push(tmp); - }); - } - } - var nodeName = element.nodeName.toLowerCase(); - switch (nodeName) { - case "h1": - case "h2": - case "h3": - case "h4": - case "h5": - case "h6": - currentParagraph = create("text"); - /* falls through */ - case "a": - parseChildren(alreadyConverted, element, currentParagraph, styles.concat(elementStyles[nodeName])); - alreadyConverted.push(currentParagraph); - break; - case "b": - case "strong": - case "u": - case "em": - case "i": - parseChildren(alreadyConverted, element, currentParagraph, styles.concat(elementStyles[nodeName])); - break; - case "table": - var t = create("table", { - widths: [], - body: [] - }); - var border = element.getAttribute("border"); - var isBorder = false; - if (border) - if (parseInt(border) == 1) isBorder = true; - if (!isBorder) t.layout = 'noBorders'; - parseChildren(t.table.body, element, currentParagraph, styles); - var widths = element.getAttribute("widths"); - if (!widths) { - if (t.table.body.length !== 0) { - if (t.table.body[0].length !== 0) - for (var k = 0; k < t.table.body[0].length; k++) - t.table.widths.push("*"); - } - } else { - var w = widths.split(","); - for (var ko = 0; ko < w.length; ko++) t.table.widths.push(w[ko]); - } - alreadyConverted.push(t); - break; - case "tbody": - parseChildren(alreadyConverted, element, currentParagraph, styles); - break; - case "tr": - var row = []; - parseChildren(row, element, currentParagraph, styles); - alreadyConverted.push(row); - break; - case "td": - currentParagraph = create("text"); - var st = create("stack"); - st.stack.push(currentParagraph); - var rspan = element.getAttribute("rowspan"); - if (rspan) - st.rowSpan = parseInt(rspan); - var cspan = element.getAttribute("colspan"); - if (cspan) - st.colSpan = parseInt(cspan); - parseChildren(st.stack, element, currentParagraph, styles); - alreadyConverted.push(st); - break; - case "span": - if (scope.lineNumberMode == "inline") { - var lineNumberInline = element.getAttribute("data-line-number"), - lineNumberObjInline = { - text: lineNumberInline, - color: "gray", - fontSize: 5 - }; - currentParagraph.text.push(lineNumberObjInline); - parseChildren(alreadyConverted, element, currentParagraph, styles); - } else if (scope.lineNumberMode == "outside") { - var lineNumberOutline = element.getAttribute("data-line-number"), - lineNumberObject = { - width: 20, - text: lineNumberOutline, - color: "gray", - fontSize: 8, - margin: [0, 2, 0, 0] - }, - col = { - columns: [ - lineNumberObject, - ] - }; - currentParagraph = create("text"); - col.columns.push(currentParagraph); - parseChildren(col.columns[0], element, currentParagraph, styles); - alreadyConverted.push(col); - } else { - parseChildren(alreadyConverted, element, currentParagraph, styles); - } - break; - case "br": - //in case of inline-line-numbers and the os-line-break class ignore the break - if (!(scope.lineNumberMode == "inline" && element.getAttribute("class") == "os-line-break")) { - currentParagraph = create("text"); - alreadyConverted.push(currentParagraph); - } - break; - case "li": - case "div": - currentParagraph = create("text"); - var stackDiv = create("stack"); - stackDiv.stack.push(currentParagraph); - ComputeStyle(stackDiv, styles); - parseChildren(stackDiv.stack, element, currentParagraph); - alreadyConverted.push(stackDiv); - break; - case "p": - currentParagraph = create("text"); - currentParagraph.margin = [0,5]; - var stackP = create("stack"); - stackP.stack.push(currentParagraph); - ComputeStyle(stackP, styles); - parseChildren(stackP.stack, element, currentParagraph); - alreadyConverted.push(stackP); - break; - case "img": - // TODO: need a proper way to calculate the space - // left on the page. - // This requires further information - // A4 in 72dpi: 595px x 842px - var maxResolution = { - width: 435, - height: 830 - }, - width = parseInt(element.getAttribute("width")), - height = parseInt(element.getAttribute("height")); - - if (width > maxResolution.width) { - var scaleByWidth = maxResolution.width/width; - width *= scaleByWidth; - height *= scaleByWidth; - } - if (height > maxResolution.height) { - var scaleByHeight = maxResolution.height/height; - width *= scaleByHeight; - height *= scaleByHeight; - } - - alreadyConverted.push({ - image: BaseMap[element.getAttribute("src")], - width: width, - height: height - }); - break; - case "ul": - var u = create("ul"); - parseChildren(u.ul, element, currentParagraph, styles); - alreadyConverted.push(u); - break; - case "ol": - var o = create("ol"); - parseChildren(o.ol, element, currentParagraph, styles); - alreadyConverted.push(o); - break; - default: - var temporary = create("text", element.textContent.replace(/\n/g, "")); - if (styles) { - ComputeStyle(temporary, styles); - } - // TODO: This if-clause is a hotfix for issue #2442. - // Do this right! Why is currentParagraph undefined? - if (!currentParagraph) { - currentParagraph = {}; - currentParagraph.text = []; - } - currentParagraph.text.push(temporary); - break; - } - return currentParagraph; - }, - /** - * Parses HTML - * @function - * @param {string} converted - - * @param {object} htmlText - - */ - ParseHtml = function(converted, htmlText) { - var html = HTMLValidizer.validize(htmlText); - html = $(html.replace(/\t/g, "").replace(/\n/g, "")); - var emptyParagraph = create("text"); - slice(html).forEach(function(element) { - ParseElement(converted, element); - }); - }, - content = []; - ParseHtml(content, html); - return content; - }, - BaseMap = images, - /** - * Creates containerelements for pdfMake - * e.g create("text":"MyText") result in { text: "MyText" } - * or complex objects create("stack", [{text:"MyText"}, {text:"MyText2"}]) - *for units / paragraphs of text - * - * @function - * @param {string} name - name of the attribute holding content - * @param {object} content - the actual content (maybe empty) - */ - create = function(name, content) { - var o = {}; - content = content || []; - o[name] = content; - return o; - }; - fonts.forEach(function(fontInfo) { - registerFont(fontInfo); - }); - return { - convertHTML: convertHTML, - createElement: create - }; - }; - return { - createInstance: createInstance - }; -}]) - // Provider to register entries for the main menu. .provider('mainMenu', [ function() { diff --git a/openslides/motions/static/js/motions/pdf.js b/openslides/motions/static/js/motions/pdf.js new file mode 100644 index 000000000..877c965ac --- /dev/null +++ b/openslides/motions/static/js/motions/pdf.js @@ -0,0 +1,439 @@ +(function () { + +"use strict"; + +angular.module('OpenSlidesApp.motions.pdf', []) + +.factory('MotionContentProvider', ['gettextCatalog', function(gettextCatalog) { + /** + * Provides the content as JS objects for Motions in pdfMake context + * @constructor + */ + + var createInstance = function(converter, motion, $scope, User) { + + // generates the text of the motion. Also septerates between line-numbers + var textContent = function() { + if ($scope.lineNumberMode == "inline" || $scope.lineNumberMode == "outside") { + /* in order to distinguish between the line-number-types we need to pass the scope + * to the convertHTML function. + * We should avoid this, since this completly breaks compatibilty for every + * other project that might want to use this HTML to PDF parser. + * https://github.com/OpenSlides/OpenSlides/issues/2361 + */ + return converter.convertHTML(motion.getTextWithLineBreaks($scope.version), $scope); + } else { + return converter.convertHTML(motion.getText($scope.version), $scope); + } + }; + + // Generate text of reason + var reasonContent = function() { + return converter.convertHTML(motion.getReason($scope.version), $scope); + }; + + // Generate header text of motion + var motionHeader = function() { + var header = converter.createElement("text", gettextCatalog.getString("Motion") + " " + motion.identifier + ": " + motion.getTitle($scope.version)); + header.bold = true; + header.fontSize = 26; + return header; + }; + + // Generate text of signment + var signment = function() { + var label = converter.createElement("text", gettextCatalog.getString('Submitter') + ':\nStatus:'); + var state = converter.createElement("text", User.get(motion.submitters_id[0]).full_name + '\n'+gettextCatalog.getString(motion.state.name)); + state.width = "70%"; + label.width = "30%"; + label.bold = true; + var signment = converter.createElement("columns", [label, state]); + signment.margin = [10, 20, 0, 10]; + signment.lineHeight = 2.5; + return signment; + }; + + // Generates polls + var polls = function() { + if (!motion.polls.length) return {}; + var pollLabel = converter.createElement("text", gettextCatalog.getString('Voting result') + ":"), + results = function() { + return motion.polls.map(function(poll, index) { + var id = index + 1, + yes = poll.yes ? poll.yes : '-', // if no poll.yes is given set it to '-' + yesRelative = poll.getVote(poll.yes, 'yes').percentStr, + no = poll.no ? poll.no : '-', + noRelative = poll.getVote(poll.no, 'no').percentStr, + abstain = poll.abstain ? poll.abstain : '-', + abstainrelativeGet = poll.getVote(poll.abstain, 'abstain').percentStr, + abstainRelative = abstainrelativeGet ? abstainrelativeGet : '', + valid = poll.votesvalid ? poll.votesvalid : '-', + validRelative = poll.getVote(poll.votesvalid, 'votesvalid').percentStr, + number = { + text: id + ".", + width: "5%" + }, + headerText = { + text: gettextCatalog.getString('Vote'), + width: "15%" + }, + /** + * Generates a part (consisting of different columns) of the polls + * + * Example Ja 100 ( 90% ) + * + * @function + * @param {string} name - E.g. "Ja" + * @param {number} value - E.g.100 + * @param {number} relValue - E.g. 90 + */ + createPart = function(name, value, relValue) { + var indexColumn = converter.createElement("text"); + var nameColumn = converter.createElement("text", "" + name); + var valueColumn = converter.createElement("text", "" + value); + var relColumn = converter.createElement("text", relValue); + valueColumn.width = "40%"; + indexColumn.width = "5%"; + valueColumn.width = "5%"; + valueColumn.alignment = "right"; + relColumn.margin = [5, 0, 0, 0]; + return [indexColumn, nameColumn, valueColumn, relColumn]; + }, + yesPart = converter.createElement("columns", createPart(gettextCatalog.getString("Yes"), yes, yesRelative)), + noPart = converter.createElement("columns", createPart(gettextCatalog.getString("No"), no, noRelative)), + abstainPart = converter.createElement("columns", createPart(gettextCatalog.getString("Abstain"), abstain, abstainRelative)), + totalPart = converter.createElement("columns", createPart(gettextCatalog.getString("Valid votes"), valid, validRelative)), + heading = converter.createElement("columns", [number, headerText]), + pollResult = converter.createElement("stack", [ + heading, yesPart, noPart, abstainPart, totalPart + ]); + + return pollResult; + }, {}); + }; + pollLabel.width = '35%'; + pollLabel.bold = true; + var result = converter.createElement("columns", [pollLabel, results()]); + result.margin = [10, 0, 0, 10]; + result.lineHeight = 1; + return result; + }; + + // Generates title section for motion + var titleSection = function() { + var title = converter.createElement("text", motion.getTitle($scope.version)); + title.bold = true; + title.fontSize = 14; + title.margin = [0, 0, 0, 10]; + return title; + }; + + // Generates reason section for polls + var reason = function() { + var r = converter.createElement("text", gettextCatalog.getString("Reason") + ":"); + r.bold = true; + r.fontSize = 14; + r.margin = [0, 30, 0, 10]; + return r; + }; + + //getters + var getTitle = function() { + return motion.getTitle($scope.verion); + }; + + var getIdentifier = function() { + return motion.identifier; + }; + + var getCategory = function() { + return motion.category; + }; + + // Generates content as a pdfmake consumable + var getContent = function() { + if (reasonContent().length === 0 ) { + return [ + motionHeader(), + signment(), + polls(), + titleSection(), + textContent(), + ]; + } else { + return [ + motionHeader(), + signment(), + polls(), + titleSection(), + textContent(), + reason(), + reasonContent() + ]; + } + }; + return { + getContent: getContent, + getTitle: getTitle, + getIdentifier: getIdentifier, + getCategory: getCategory + }; + }; + + return { + createInstance: createInstance + }; +}]) + +.factory('PollContentProvider', function() { + /** + * Generates a content provider for polls + * @constructor + * @param {string} title - title of poll + * @param {string} id - if of poll + * @param {object} gettextCatalog - for translation + */ + var createInstance = function(title, id, gettextCatalog){ + + //left and top margin for a single sheet + var space = { + left: 30, + top: 30, + bottom: 10 + }, + //size and position of the signing circle + circle = { + yDistance: 6, + size: 8 + }, + //margin for the decision + singleItemMargin = 10, + //space between circle and dicision + columnwidth = 20, + //defines the space under a single sheet + sheetend = 65, + //defines the used fontsize + fontSize = 14; + + /** + * draws a single circle + * @function + * @param {int} y - the relative y coordinate + * @param {int} size - size of the circle in px + */ + var drawCircle = function(y, size) { + return [ + { + type: 'ellipse', + x: 0, + y: y, + lineColor: 'black', + r1: size, + r2: size + } + ]; + }; + + /** + * Returns an entry in the ballot with a circle to draw into + * @function + * @param {string} decision - the name of an entry to decide between, e.g. 'yes' or 'no' + */ + var createBallotEntry = function(decision) { + return { + margin: [space.left+circle.size, singleItemMargin, 0, 0], + columns: [ + { + width: columnwidth, + canvas: drawCircle(circle.yDistance, circle.size) + }, + { + text: decision + } + ], + }; + }; + + /** + * Returns a single section on the ballot paper + * @function + */ + var createSection = function() { + return { + stack: [{ + text: gettextCatalog.getString("Motion") + " " + id, + style: 'header', + margin: [space.left, space.top, 0, 0] + }, { + text: title, + margin: [space.left, 0, 0, space.bottom] + }, + createBallotEntry(gettextCatalog.getString("Yes")), + createBallotEntry(gettextCatalog.getString("No")), + createBallotEntry(gettextCatalog.getString("Abstain")), + ], + margin: [0, 0, 0, sheetend] + }; + }; + + /** + * Returns Content for single motion + * @function + * @param {string} id - if of poll + */ + return { + content: [{ + table: { + headerRows: 1, + widths: ['*', '*'], + body: [ + [createSection(), createSection()], + [createSection(), createSection()], + [createSection(), createSection()], + [createSection(), createSection()] + ], + }, + layout: { + hLineWidth: function() {return 0.5;}, + vLineWidth: function() {return 0.5;}, + hLineColor: function() {return 'gray';}, + vLineColor: function() {return 'gray';}, + } + }], + pageSize: 'A4', + pageMargins: [0, 0, 0, 0], + styles: { + header: { + fontSize: fontSize, + bold: true + } + }, + }; + }; + return { + createInstance: createInstance + }; +}) + +.factory('MotionCatalogContentProvider', ['gettextCatalog', function(gettextCatalog) { + + /** + * Constructor + * @function + * @param {object} allMotions - A sorted array of all motions to parse + * @param {object} $scope - Current $scope + * @param {object} User - Current user + */ + var createInstance = function(allMotions, $scope, User, Category) { + + //function to create the Table of contents + var createTitle = function() { + + return { + text: gettextCatalog.getString("Motions"), + style: "title" + }; + }; + + var createTOContent = function(motionTitles) { + + var heading = { + text: gettextCatalog.getString("Table of contents"), + style: "heading", + }; + + var toc = []; + angular.forEach(motionTitles, function(title) { + toc.push({ + text: gettextCatalog.getString("Motion") + " " + title, + style: "tableofcontent", + }); + }); + + return [ + heading, + toc, + addPageBreak() + ]; + }; + + // function to create the table of catergories (if any) + var createTOCatergories = function() { + if (Category.getAll().length > 0) { + var heading = { + text: gettextCatalog.getString("Categories"), + style: "heading" + }; + + var toc = []; + angular.forEach(Category.getAll(), function(cat) { + toc.push( + { + columns: [ + { + text: cat.prefix, + style: 'tableofcontent', + width: 30 + }, + { + text: cat.name, + style: 'tableofcontent' + } + ] + } + ); + }); + + return [ + heading, + toc, + addPageBreak() + ]; + } else { + // if there are no categories, return "empty string" + // pdfmake takes "null" literally and throws an error + return ""; + } + }; + + // function to apply a pagebreak-keyword + var addPageBreak = function() { + return [ + { + text: '', + pageBreak: 'after' + } + ]; + }; + + // returns the pure content of the motion, parseable by makepdf + var getContent = function() { + var motionContent = []; + var motionTitles = []; + var motionCategories = []; + angular.forEach(allMotions, function(motion, key) { + motionTitles.push(motion.getIdentifier() + ": " + motion.getTitle()); + motionContent.push(motion.getContent()); + if (key < allMotions.length - 1) { + motionContent.push(addPageBreak()); + } + }); + + return [ + createTitle(), + createTOCatergories(), + createTOContent(motionTitles), + motionContent + ]; + }; + return { + getContent: getContent + }; + }; + + return { + createInstance: createInstance + }; +}]); + +}()); diff --git a/openslides/motions/static/js/motions/site.js b/openslides/motions/static/js/motions/site.js index cb7f3491a..a2c187a4c 100644 --- a/openslides/motions/static/js/motions/site.js +++ b/openslides/motions/static/js/motions/site.js @@ -2,439 +2,13 @@ 'use strict'; -angular.module('OpenSlidesApp.motions.site', ['OpenSlidesApp.motions', 'OpenSlidesApp.motions.diff', 'OpenSlidesApp.motions.motionservices']) - -.factory('MotionContentProvider', ['gettextCatalog', function(gettextCatalog) { - /** - * Provides the content as JS objects for Motions in pdfMake context - * @constructor - */ - - var createInstance = function(converter, motion, $scope, User) { - - // generates the text of the motion. Also septerates between line-numbers - var textContent = function() { - if ($scope.lineNumberMode == "inline" || $scope.lineNumberMode == "outside") { - /* in order to distinguish between the line-number-types we need to pass the scope - * to the convertHTML function. - * We should avoid this, since this completly breaks compatibilty for every - * other project that might want to use this HTML to PDF parser. - * https://github.com/OpenSlides/OpenSlides/issues/2361 - */ - return converter.convertHTML(motion.getTextWithLineBreaks($scope.version), $scope); - } else { - return converter.convertHTML(motion.getText($scope.version), $scope); - } - }; - - // Generate text of reason - var reasonContent = function() { - return converter.convertHTML(motion.getReason($scope.version), $scope); - }; - - // Generate header text of motion - var motionHeader = function() { - var header = converter.createElement("text", gettextCatalog.getString("Motion") + " " + motion.identifier + ": " + motion.getTitle($scope.version)); - header.bold = true; - header.fontSize = 26; - return header; - }; - - // Generate text of signment - var signment = function() { - var label = converter.createElement("text", gettextCatalog.getString('Submitter') + ':\nStatus:'); - var state = converter.createElement("text", User.get(motion.submitters_id[0]).full_name + '\n'+gettextCatalog.getString(motion.state.name)); - state.width = "70%"; - label.width = "30%"; - label.bold = true; - var signment = converter.createElement("columns", [label, state]); - signment.margin = [10, 20, 0, 10]; - signment.lineHeight = 2.5; - return signment; - }; - - // Generates polls - var polls = function() { - if (!motion.polls.length) return {}; - var pollLabel = converter.createElement("text", gettextCatalog.getString('Voting result') + ":"), - results = function() { - return motion.polls.map(function(poll, index) { - var id = index + 1, - yes = poll.yes ? poll.yes : '-', // if no poll.yes is given set it to '-' - yesRelative = poll.getVote(poll.yes, 'yes').percentStr, - no = poll.no ? poll.no : '-', - noRelative = poll.getVote(poll.no, 'no').percentStr, - abstain = poll.abstain ? poll.abstain : '-', - abstainrelativeGet = poll.getVote(poll.abstain, 'abstain').percentStr, - abstainRelative = abstainrelativeGet ? abstainrelativeGet : '', - valid = poll.votesvalid ? poll.votesvalid : '-', - validRelative = poll.getVote(poll.votesvalid, 'votesvalid').percentStr, - number = { - text: id + ".", - width: "5%" - }, - headerText = { - text: gettextCatalog.getString('Vote'), - width: "15%" - }, - /** - * Generates a part (consisting of different columns) of the polls - * - * Example Ja 100 ( 90% ) - * - * @function - * @param {string} name - E.g. "Ja" - * @param {number} value - E.g.100 - * @param {number} relValue - E.g. 90 - */ - createPart = function(name, value, relValue) { - var indexColumn = converter.createElement("text"); - var nameColumn = converter.createElement("text", "" + name); - var valueColumn = converter.createElement("text", "" + value); - var relColumn = converter.createElement("text", relValue); - valueColumn.width = "40%"; - indexColumn.width = "5%"; - valueColumn.width = "5%"; - valueColumn.alignment = "right"; - relColumn.margin = [5, 0, 0, 0]; - return [indexColumn, nameColumn, valueColumn, relColumn]; - }, - yesPart = converter.createElement("columns", createPart(gettextCatalog.getString("Yes"), yes, yesRelative)), - noPart = converter.createElement("columns", createPart(gettextCatalog.getString("No"), no, noRelative)), - abstainPart = converter.createElement("columns", createPart(gettextCatalog.getString("Abstain"), abstain, abstainRelative)), - totalPart = converter.createElement("columns", createPart(gettextCatalog.getString("Valid votes"), valid, validRelative)), - heading = converter.createElement("columns", [number, headerText]), - pollResult = converter.createElement("stack", [ - heading, yesPart, noPart, abstainPart, totalPart - ]); - - return pollResult; - }, {}); - }; - pollLabel.width = '35%'; - pollLabel.bold = true; - var result = converter.createElement("columns", [pollLabel, results()]); - result.margin = [10, 0, 0, 10]; - result.lineHeight = 1; - return result; - }; - - // Generates title section for motion - var titleSection = function() { - var title = converter.createElement("text", motion.getTitle($scope.version)); - title.bold = true; - title.fontSize = 14; - title.margin = [0, 0, 0, 10]; - return title; - }; - - // Generates reason section for polls - var reason = function() { - var r = converter.createElement("text", gettextCatalog.getString("Reason") + ":"); - r.bold = true; - r.fontSize = 14; - r.margin = [0, 30, 0, 10]; - return r; - }; - - //getters - var getTitle = function() { - return motion.getTitle($scope.verion); - }; - - var getIdentifier = function() { - return motion.identifier; - }; - - var getCategory = function() { - return motion.category; - }; - - // Generates content as a pdfmake consumable - var getContent = function() { - if (reasonContent().length === 0 ) { - return [ - motionHeader(), - signment(), - polls(), - titleSection(), - textContent(), - ]; - } else { - return [ - motionHeader(), - signment(), - polls(), - titleSection(), - textContent(), - reason(), - reasonContent() - ]; - } - }; - return { - getContent: getContent, - getTitle: getTitle, - getIdentifier: getIdentifier, - getCategory: getCategory - }; - }; - - return { - createInstance: createInstance - }; -}]) - -.factory('PollContentProvider', function() { - /** - * Generates a content provider for polls - * @constructor - * @param {string} title - title of poll - * @param {string} id - if of poll - * @param {object} gettextCatalog - for translation - */ - var createInstance = function(title, id, gettextCatalog){ - - //left and top margin for a single sheet - var space = { - left: 30, - top: 30, - bottom: 10 - }, - //size and position of the signing circle - circle = { - yDistance: 6, - size: 8 - }, - //margin for the decision - singleItemMargin = 10, - //space between circle and dicision - columnwidth = 20, - //defines the space under a single sheet - sheetend = 65, - //defines the used fontsize - fontSize = 14; - - /** - * draws a single circle - * @function - * @param {int} y - the relative y coordinate - * @param {int} size - size of the circle in px - */ - var drawCircle = function(y, size) { - return [ - { - type: 'ellipse', - x: 0, - y: y, - lineColor: 'black', - r1: size, - r2: size - } - ]; - }; - - /** - * Returns an entry in the ballot with a circle to draw into - * @function - * @param {string} decision - the name of an entry to decide between, e.g. 'yes' or 'no' - */ - var createBallotEntry = function(decision) { - return { - margin: [space.left+circle.size, singleItemMargin, 0, 0], - columns: [ - { - width: columnwidth, - canvas: drawCircle(circle.yDistance, circle.size) - }, - { - text: decision - } - ], - }; - }; - - /** - * Returns a single section on the ballot paper - * @function - */ - var createSection = function() { - return { - stack: [{ - text: gettextCatalog.getString("Motion") + " " + id, - style: 'header', - margin: [space.left, space.top, 0, 0] - }, { - text: title, - margin: [space.left, 0, 0, space.bottom] - }, - createBallotEntry(gettextCatalog.getString("Yes")), - createBallotEntry(gettextCatalog.getString("No")), - createBallotEntry(gettextCatalog.getString("Abstain")), - ], - margin: [0, 0, 0, sheetend] - }; - }; - - /** - * Returns Content for single motion - * @function - * @param {string} id - if of poll - */ - return { - content: [{ - table: { - headerRows: 1, - widths: ['*', '*'], - body: [ - [createSection(), createSection()], - [createSection(), createSection()], - [createSection(), createSection()], - [createSection(), createSection()] - ], - }, - layout: { - hLineWidth: function() {return 0.5;}, - vLineWidth: function() {return 0.5;}, - hLineColor: function() {return 'gray';}, - vLineColor: function() {return 'gray';}, - } - }], - pageSize: 'A4', - pageMargins: [0, 0, 0, 0], - styles: { - header: { - fontSize: fontSize, - bold: true - } - }, - }; - }; - return { - createInstance: createInstance - }; -}) - -.factory('MotionCatalogContentProvider', ['gettextCatalog', function(gettextCatalog) { - - /** - * Constructor - * @function - * @param {object} allMotions - A sorted array of all motions to parse - * @param {object} $scope - Current $scope - * @param {object} User - Current user - */ - var createInstance = function(allMotions, $scope, User, Category) { - - //function to create the Table of contents - var createTitle = function() { - - return { - text: gettextCatalog.getString("Motions"), - style: "title" - }; - }; - - var createTOContent = function(motionTitles) { - - var heading = { - text: gettextCatalog.getString("Table of contents"), - style: "heading", - }; - - var toc = []; - angular.forEach(motionTitles, function(title) { - toc.push({ - text: gettextCatalog.getString("Motion") + " " + title, - style: "tableofcontent", - }); - }); - - return [ - heading, - toc, - addPageBreak() - ]; - }; - - // function to create the table of catergories (if any) - var createTOCatergories = function() { - if (Category.getAll().length > 0) { - var heading = { - text: gettextCatalog.getString("Categories"), - style: "heading" - }; - - var toc = []; - angular.forEach(Category.getAll(), function(cat) { - toc.push( - { - columns: [ - { - text: cat.prefix, - style: 'tableofcontent', - width: 30 - }, - { - text: cat.name, - style: 'tableofcontent' - } - ] - } - ); - }); - - return [ - heading, - toc, - addPageBreak() - ]; - } else { - // if there are no categories, return "empty string" - // pdfmake takes "null" literally and throws an error - return ""; - } - }; - - // function to apply a pagebreak-keyword - var addPageBreak = function() { - return [ - { - text: '', - pageBreak: 'after' - } - ]; - }; - - // returns the pure content of the motion, parseable by makepdf - var getContent = function() { - var motionContent = []; - var motionTitles = []; - var motionCategories = []; - angular.forEach(allMotions, function(motion, key) { - motionTitles.push(motion.getIdentifier() + ": " + motion.getTitle()); - motionContent.push(motion.getContent()); - if (key < allMotions.length - 1) { - motionContent.push(addPageBreak()); - } - }); - - return [ - createTitle(), - createTOCatergories(), - createTOContent(motionTitles), - motionContent - ]; - }; - return { - getContent: getContent - }; - }; - - return { - createInstance: createInstance - }; -}]) +angular.module('OpenSlidesApp.motions.site', [ + 'OpenSlidesApp.motions', + 'OpenSlidesApp.motions.diff', + 'OpenSlidesApp.motions.motionservices', + 'OpenSlidesApp.core.pdf', + 'OpenSlidesApp.motions.pdf' +]) .config([ 'mainMenuProvider',