Découvrez les nouveautés de cette version : Fonctionnalités, améliorations et évolutions vous attendent ! 👉 Cliquez ici pour en savoir plus

Modifications pour le document DiagramEditSheet

Modifié par Admin le 19/03/2025 - 19:24

Depuis la version 8.1
modifié par Admin
sur 10/02/2025 - 13:05
Commentaire de modification : Install extension [com.xwiki.diagram:application-diagram/1.22.0]
À la version 6.1
modifié par Admin
sur 25/06/2024 - 07:09
Commentaire de modification : Install extension [com.xwiki.diagram:application-diagram/1.20.5]

Résumé

Détails

XWiki.JavaScriptExtension[1]
Code
... ... @@ -36,34 +36,6 @@
36 36   isCompressed: function() {
37 37   return false;
38 38   },
39 - // TODO: When upgrading the drawio version, ensure that this method is copied from the drawio code and the
40 - // `resolveReferences` parameter of the getFileData is set to `true` to prevent incorrect formatting of links to
41 - // wiki pages in the diagram content. https://github.com/xwikisas/application-diagram/issues/295
42 - createData: function() {
43 - var actualPages = this.ui.pages;
44 -
45 - if (this.isRealtime()) {
46 - // Uses ownPages for getting file data below
47 - this.ui.pages = this.ownPages;
48 -
49 - // Updates view state in own current page
50 - if (this.ui.currentPage != null) {
51 - var ownPage = this.ui.getPageById(
52 - this.ui.currentPage.getId(),
53 - this.ownPages);
54 -
55 - if (ownPage != null) {
56 - ownPage.viewState = this.ui.editor.graph.getViewState();
57 - ownPage.needsUpdate = true;
58 - }
59 - }
60 - }
61 -
62 - var result = this.ui.getFileData(null, null, null, null, null, null, null, null, this,
63 - !this.isCompressed(), true);
64 - this.ui.pages = actualPages;
65 - return result;
66 - },
67 67   open: function() {
68 68   var graphXML = this.getData() || '<mxGraphModel/>';
69 69   this.ui.setFileData(graphXML);
XWiki.JavaScriptExtension[2]
Code
... ... @@ -1,3 +1,516 @@
1 +define('svg-handler', ['jquery'], function($) {
2 + var lineHeight = 15;
3 + var spaceWidth = 5;
4 + var listBorder = 35;
5 + var addTagSpecificStyle = function(element, s, w, h, htmlConverter, str, alt) {
6 + switch (getTagName(element)){
7 + case 'h1':
8 + alt.setAttribute('font-size', Math.round(s.fontSize + 0.6 * s.fontSize) + 'px');
9 + alt.setAttribute('font-weight', 'bold');
10 + break;
11 + case 'h2':
12 + alt.setAttribute('font-size', Math.round(s.fontSize + 0.3 * s.fontSize) + 'px');
13 + alt.setAttribute('font-weight', 'bold');
14 + break;
15 + case 'h3':
16 + alt.setAttribute('font-size', Math.round(s.fontSize + 0.15 * s.fontSize) + 'px');
17 + alt.setAttribute('font-weight', 'bold');
18 + break;
19 + case 'h4':
20 + alt.setAttribute('font-weight', 'bold');
21 + break;
22 + case 'h5':
23 + alt.setAttribute('font-size', Math.round(s.fontSize - 0.15 * s.fontSize) + 'px');
24 + alt.setAttribute('font-weight', 'bold');
25 + break;
26 + case 'h6':
27 + alt.setAttribute('font-size', Math.round(s.fontSize - 0.3 * s.fontSize) + 'px');
28 + alt.setAttribute('font-weight', 'bold');
29 + break;
30 + case 'b':
31 + alt.setAttribute('font-weight', 'bold');
32 + break;
33 + case 'i':
34 + alt.setAttribute('font-style', 'italic');
35 + break;
36 + case 'li':
37 + // Because of the space that is reserved for the bullets delimiters, the list's text is moved more to the
38 + // right. For simulating the same behavior in svg, the text-anchor is removed for all the elements inside this
39 + // container, meaning also for ul and ol elements.
40 + alt.removeAttribute('text-anchor');
41 +
42 + // Adapt elements for XWiki PDF export since links are not displayed when 'a' tag is placed inside a 'text'
43 + // element.
44 + var isLinkTag = getTagName(alt) === 'a';
45 +
46 + // Have a bullet for each row.
47 + var delimiter = document.createElementNS('http://www.w3.org/2000/svg', isLinkTag ? 'text' : 'tspan');
48 + delimiter.textContent = "•";
49 + delimiter.setAttribute('x', w - 2 * spaceWidth);
50 + delimiter.setAttribute('y', h);
51 +
52 + // Move all to a container.
53 + var container = document.createElementNS('http://www.w3.org/2000/svg', isLinkTag ? 'g' : 'text');
54 + container.setAttribute('x', w);
55 + container.setAttribute('y', h);
56 + container.appendChild(delimiter);
57 +
58 + if (isLinkTag) {
59 + // The text inside alt is distributed to 'tspan' elements and since they are showned only if are placed
60 + // under a 'text' parent, we add it between them and the link.
61 + var text = document.createElementNS('http://www.w3.org/2000/svg', 'text');
62 + // Copy only first child since it refers to the actual text content which, dependending on whether the
63 + // text needed wrapping or not, is a 'tspan' with either just text inside or with multiple children
64 + // representing the wrapped text.
65 + text.appendChild($(alt).children()[0]);
66 + $(alt).empty();
67 + alt.appendChild(text);
68 + }
69 +
70 + // If alt comes from a container, we should preserve the original parent.
71 + var parent = alt.parentElement;
72 + container.appendChild(alt);
73 + if (parent != null) {
74 + parent.appendChild(container);
75 + parent.removeAttribute('text-anchor');
76 + }
77 + return container;
78 + case 'ul':
79 + // See comment from case 'li'.
80 + alt.removeAttribute('text-anchor');
81 + break;
82 + case 'ol':
83 + // See comment from care 'li'.
84 + alt.removeAttribute('text-anchor');
85 + break;
86 + case 'u':
87 + alt.setAttribute('text-decoration', 'underline');
88 + break;
89 + case 'a':
90 + var link = document.createElementNS('http://www.w3.org/2000/svg', 'a');
91 + link.setAttribute('x', w);
92 + link.setAttribute('y', h);
93 + link.setAttributeNS('http://www.w3.org/1999/xlink', 'xlink:href', element.href);
94 + // Add link decorations for the text. Some styles added to a 'g' are not distributed to it's children by FOP,
95 + // so we do it manually.
96 + if (getTagName(alt) == 'g') {
97 + var childNodes = $(alt).children();
98 + childNodes.each(function() {
99 + this.setAttribute('fill', '#0000EE');
100 + this.setAttribute('text-decoration', 'underline');
101 + });
102 + } else {
103 + alt.setAttribute('fill', '#0000EE');
104 + alt.setAttribute('text-decoration', 'underline');
105 + }
106 + alt.removeAttribute('text-anchor');
107 + // If alt comes from a container, we should preserve the original parent.
108 + var parent = alt.parentElement;
109 + link.appendChild(alt);
110 + if (parent != null) {
111 + parent.appendChild(link);
112 + }
113 + return link;
114 + default:
115 + return alt;
116 + }
117 + return alt;
118 + };
119 +
120 + var getStrWidth = function(str, s) {
121 + return $('<span></span>')
122 + .css({display: 'none', whiteSpace: 'nowrap'})
123 + .appendTo($('body'))
124 + .text(str)
125 + .width();
126 + };
127 +
128 + var takenCoordinates;
129 + // Creates a row inside a text element that should be wrapped. The coordinates of the first row are already saved.
130 + var createRow = function(w, h, rowNb) {
131 + var row = document.createElementNS('http://www.w3.org/2000/svg', 'tspan');
132 + row.setAttribute('x', w);
133 + row.setAttribute('y', h);
134 + if (rowNb > 0) {
135 + takenCoordinates.push({'w': w, 'h': h});
136 + }
137 + return row;
138 + };
139 +
140 + var wrapText = function(alt, s, w, h, content, contentWidth, maxWidth) {
141 + var words = content.split(/[\s+]/);
142 + var wordWidth, rowWidth = 0, rowNb = 0;
143 + var row = createRow(w, h, rowNb);
144 + alt.appendChild(row);
145 + words.each(function(word) {
146 + word += ' ';
147 + wordWidth = getStrWidth(word, s);
148 + if (rowWidth + wordWidth > maxWidth) {
149 + h += lineHeight;
150 + row = createRow(w, h, ++rowNb);
151 + row.textContent = word;
152 + alt.appendChild(row);
153 + rowWidth = wordWidth;
154 + } else {
155 + row.textContent += word;
156 + rowWidth += wordWidth;
157 + }
158 + });
159 + };
160 +
161 + /*
162 + * Consider containerWidth as the maximal width to do the word wrap when needed. Add the inner html only if the
163 + * element is not a list, since the text should be distributed to children instead of being shown at once. The same
164 + * is applied for when the text is longer then the containerWidth, since it will be distributed to tspan elements.
165 + * Shorten the containerWidth send to text wrapper for 'li' elements since in their case an adittional space is
166 + * added for the bullet delimiter and it shouldn't be included.
167 + */
168 + var addTextContent= function(element, htmlConverter, s, containerWidth, alt, w, h) {
169 + htmlConverter.innerHTML = element.innerText;
170 + var content = htmlConverter.value;
171 + var contentWidth = getStrWidth(content, s);
172 + if (!isListElement(element)) {
173 + if (contentWidth > containerWidth) {
174 + containerWidth = isListItem(element) ? containerWidth - listBorder : containerWidth;
175 + wrapText(alt, s, w, h, content, contentWidth, containerWidth);
176 + } else {
177 + alt.textContent = htmlConverter.value;
178 + }
179 + }
180 + };
181 +
182 + var getTagName = function(element) {
183 + return element && element.tagName && element.tagName.toLowerCase();
184 + };
185 +
186 + var isTextNode = function(node) {
187 + return node && node.nodeType === 3;
188 + };
189 +
190 + var isHeadingElement = function(element) {
191 + return ['h1', 'h2', 'h3', 'h4', 'h5', 'h6'].indexOf(getTagName(element)) >= 0;
192 + };
193 +
194 + var isBlockElement = function(element) {
195 + return ['p', 'div'].indexOf(getTagName(element)) >= 0 || isHeadingElement(element);
196 + };
197 +
198 + var isListElement = function(element) {
199 + return ['ul', 'ol'].indexOf(getTagName(element)) >= 0;
200 + };
201 +
202 + var isInlineElement = function(element) {
203 + return ['b', 'i', 'span'].indexOf(getTagName(element)) >= 0;
204 + };
205 +
206 + var isListItem = function(element) {
207 + return getTagName(element) === 'li';
208 + };
209 +
210 + var isNodeWithPartialStyle = function(node, nbOfSiblings) {
211 + return isTextNode(node) || ((isInlineElement(node) || isBlockElement(node)) && nbOfSiblings > 1);
212 + };
213 +
214 + var isParagraphWithPartialStyle = function(node, nbOfSiblings) {
215 + if (isNodeWithPartialStyle(node, nbOfSiblings)) {
216 + return true;
217 + }
218 +
219 + // Consider also indirect children.
220 + var childNodes = $(node).contents();
221 + if ($(node).children().length > 0) {
222 + for (var i = 0; i < childNodes.length; i++) {
223 + var child = childNodes[i];
224 + if (isParagraphWithPartialStyle(child, childNodes.length) == true) {
225 + return true;
226 + }
227 + }
228 + }
229 + return false;
230 + };
231 +
232 + var isStyleElement = function(element) {
233 + if (element == null) { return false; }
234 + return ['b', 'i', 'u', 'a'].indexOf(getTagName(element)) != -1 || isHeadingElement(element);
235 + };
236 +
237 + /*
238 + * Get multiple styles of an element (i.e. b, i, u, a tags)
239 + */
240 + var getStylesFromElement = function(element) {
241 + var styles = [];
242 + // Partially styled elements are not supported, so consider only the styles of the main element.
243 + var parentTextLength = element.parentElement.textContent.length;
244 + if (element.textContent.length == parentTextLength) {
245 + styles.push(element);
246 + }
247 + // Consider also indirect children.
248 + var childElements = element.getElementsByTagName("*");
249 + for (i = 0; i < childElements.length; i++) {
250 + var child = childElements[i];
251 + if (child.textContent.length == parentTextLength) {
252 + styles.push(child);
253 + }
254 + }
255 + return styles;
256 + };
257 +
258 + // For the current block of text take into consideration if those coordinates are already in use.
259 + // This is used when we are manually computing the container of children elements.
260 + var maybeChangeHeight = function(w, h, element) {
261 + // They didn't took into consideration the size of an heading tag.
262 + h = isHeadingElement(element) ? h - lineHeight : h;
263 + var thisCoordinates = {'w': w, 'h': h};
264 + takenCoordinates.each(function(coordinates) {
265 + if (coordinates['w'] == thisCoordinates['w'] && coordinates['h'] == thisCoordinates['h']) {
266 + h += lineHeight;
267 + thisCoordinates['h'] = h;
268 + }
269 + });
270 + takenCoordinates.push(thisCoordinates);
271 + return h;
272 + };
273 +
274 + var getAdjustedInitialCoordinates = function(w, h, fontSize) {
275 + w = Math.round(w / 2);
276 + // For smaller heights the result seems to corespond to the one in the original view, but for the others the font
277 + // size is relevant.
278 + // TODO: investigate the reasons behind this behavior.
279 + if (h < lineHeight) {
280 + h = Math.round(h / 2);
281 + } else {
282 + h = Math.round(h / 2 - fontSize / 2);
283 + }
284 + return {'w': w, 'h': h};
285 + }
286 +
287 + // Create the basis of a svg element. Calculate new coordinates if is needed.
288 + var createSvgElement = function(s, w, h, tag, element, addAnchor, isPartiallyStyledElement) {
289 + var alt = document.createElementNS('http://www.w3.org/2000/svg', tag);
290 + if (element && (isBlockElement(element) || isListItem(element) || isPartiallyStyledElement)) {
291 + h = maybeChangeHeight(w, h, element);
292 + }
293 + alt.setAttribute('x', w);
294 + alt.setAttribute('y', h);
295 + alt.setAttribute('fill', s.fontColor || 'black');
296 + if (addAnchor) {
297 + alt.setAttribute('text-anchor', 'middle');
298 + }
299 + alt.setAttribute('font-size', Math.round(s.fontSize) + 'px');
300 + alt.setAttribute('font-family', s.fontFamily);
301 +
302 + if ((s.fontStyle & mxConstants.FONT_BOLD) == mxConstants.FONT_BOLD)
303 + {
304 + alt.setAttribute('font-weight', 'bold');
305 + }
306 +
307 + if ((s.fontStyle & mxConstants.FONT_ITALIC) == mxConstants.FONT_ITALIC)
308 + {
309 + alt.setAttribute('font-style', 'italic');
310 + }
311 +
312 + if ((s.fontStyle & mxConstants.FONT_UNDERLINE) == mxConstants.FONT_UNDERLINE)
313 + {
314 + alt.setAttribute('text-decoration', 'underline');
315 + }
316 + return alt;
317 + };
318 +
319 + var convertTextElementToSvg = function(element, s, w, h, htmlConverter, str, containerWidth, isFirstChild) {
320 + if (getTagName(element) == 'br') {
321 + return document.createElementNS('http://www.w3.org/2000/svg', "text");
322 + }
323 + if (isFirstChild) {
324 + takenCoordinates = [];
325 + }
326 + var coordinates = getAdjustedInitialCoordinates(w, h, s.fontSize);
327 + w = coordinates.w;
328 + h = coordinates.h;
329 +
330 + // Use 'tspan' for 'li' elements since they will be move inside a 'text' container along with a row delimiter.
331 + // Update h in case it was manually modified to take into consideration other inserted elements.
332 + var tag = isListItem(element) ? 'tspan' : 'text';
333 + var alt = createSvgElement(s, w, h, tag, element, true);
334 + h = parseInt(alt.getAttribute('y'));
335 +
336 + addTextContent(element, htmlConverter, s, containerWidth, alt, w, h);
337 +
338 + // Convert recursively the children. Have a container that will hold the elements in case there are other children.
339 + // Consider the case when multiple styles are added to an element (e.g. <i><b>txt</b></i>).
340 + // Traverse all children and keep the styles to just one element.
341 + if (element.childElements().length > 0) {
342 + var container = createSvgElement(s, w, h, 'g', null, !isListElement(element));
343 + // TODO: Don't append alt element, unless it has a content.
344 + container.appendChild(alt);
345 + element.childElements().each(function(child) {
346 + if (isStyleElement(child)) {
347 + var styles = getStylesFromElement(child);
348 + styles.each(function(style) {
349 + alt = addTagSpecificStyle(style, s, w, h, htmlConverter, str, alt, true);
350 + });
351 + } else {
352 + var childSvg = convertTextElementToSvg(child, s, w, h, htmlConverter, str, containerWidth, false);
353 + if (childSvg != undefined) {
354 + container.appendChild(childSvg);
355 + }
356 + }
357 + });
358 + }
359 +
360 + // TODO: apply the defined style of the element.
361 + alt = addTagSpecificStyle(element, s, w, h, htmlConverter, str, alt);
362 + if (element.childElements().length > 0) {
363 + return container;
364 + } else {
365 + return alt;
366 + }
367 + };
368 +
369 +
370 + /*
371 + * Converts a partially styled element (e.g. link added to just a word of a phrase) to svg kepping as much as
372 + * possible from the original element.
373 + * When different types of elements are separated vertically (see Example 1), as long as each element is not a
374 + * composite one, the result is mainly as the input. But, for cases when the styled element is inside a phrase (see
375 + * Example 2), the result is a fallback to simple text.
376 + * TODO: In the case of Example 3, when a child of a partially styled paragraph is itself a partially styled
377 + * paragraph with "Text" nodes a fallback must be added. For now, elementsContainer.contents() separates the nodes
378 + * that are connected and there isn't a smooth solution to get which elements were on the same line (maybe consider
379 + * </br> as divider). Even though it looks the same in html, Example 4 works compared to 3 since there are
380 + * no text nodes, but this depends on how the user inserts the text.
381 + *
382 + * Example 1: <div> "Simple text" </br> <b>Bolded text</b> </div>
383 + * Example 2: <div> "Simple text" <b>Bolded text</b> </div>
384 + * Example 3: <div> "Simple text" </br> "Simple text" <b>Bolded text</b> </div>
385 + * Example 4:
386 + * <div>
387 + * "Simple text" </br>
388 + * <span>"Simple text" <b>Bolded text</b></span>
389 + * </div>
390 + */
391 + var convertPartiallyStyledParagraphToSVG = function(childNodes, s, w, h, htmlConverter, str, containerWidth) {
392 + takenCoordinates = [];
393 + var initialWidth = w;
394 + var initialHeight = h;
395 + var coordinates = getAdjustedInitialCoordinates(w, h, s.fontSize);
396 + w = coordinates.w;
397 + h = coordinates.h;
398 +
399 + var container = createSvgElement(s, w, h, /* tag */ 'g', /* element */ null,
400 + /* addAnchor */ !isListElement(childNodes[0]));
401 + // Go to last level of child nodes in case there is a style added to the root element. Keep the styles to add
402 + // them after assembling the element with its children.
403 + var styles = [];
404 + while (childNodes.length == 1 && $(childNodes[0]).children().length > 0) {
405 + var rootNode = childNodes[0];
406 + if (isStyleElement(rootNode)) {
407 + styles.push(rootNode);
408 + }
409 + childNodes = $(rootNode).contents();
410 + }
411 +
412 + // Resolve the basic case where the element is just a phrase with a styled element. Remove this after fixing
413 + // the TODO described in the comment above.
414 + var simpleNodes= childNodes.filter(function() {
415 + return isTextNode(this) || isInlineElement(this);
416 + });
417 +
418 + if(simpleNodes.length == childNodes.length) {
419 + var element = $('<span></span>').html(str)[0];
420 + // We use the unmodified coordinates to prevent adjusting them twice.
421 + var alt = convertTextElementToSvg(element, s, initialWidth, initialHeight, htmlConverter, str, containerWidth,
422 + /* isFirstChild */ true);
423 + return alt;
424 + }
425 +
426 + // Look only on first-level children since for now it is not supported to partially style paragraphs inside a
427 + // partially styled paragraph.
428 + childNodes.each(function(index) {
429 + var element = $(this).clone()[0];
430 + // Convert text content to element for consistency.
431 + if (isTextNode(element)) {
432 + element = $('<span></span>').html(element.textContent)[0];
433 + }
434 +
435 + // Don't do anything for </br> tags or empty elements.
436 + if (getTagName(element) == 'br' || element.innerText == '') {
437 + return;
438 + }
439 +
440 + // Consider that list elements will be moved to a 'text' container and in this case 'tspan' should be used as
441 + // child tag. Specify that it is a partially styled element because in this case it could contain '<br>'
442 + // tags and the children would need to have changed coordinates (heights) when they are created.
443 + var alt = createSvgElement(s, w, h, /* tag */ isListItem(element) ? 'tspan' : 'text', /* element */ element,
444 + /* addAnchor */ true, /* isPartiallyStyledElement */ true);
445 + h = parseInt(alt.getAttribute('y'));
446 +
447 + addTextContent(element, htmlConverter, s, containerWidth, alt, w, h);
448 +
449 + alt = addTagSpecificStyle(element, s, w, h, htmlConverter, str, alt);
450 + container.appendChild(alt);
451 + });
452 + // Consider the case when multiple styles are added to the root element.
453 + styles.each(function(style) {
454 + container = addTagSpecificStyle(style, s, w, h, htmlConverter, str, container, true);
455 + });
456 + return container;
457 +
458 + };
459 +
460 + // Fix the transform attribute of the element that is used when foreignObjects are not supported and the view
461 + // fallbacks to svg. This code is taken from an older draw.io version, since starting with 12.5.0 the transform
462 + // attribute doesn't take into consideration the coordinates, but only the foOffset.
463 + var adjustAlternateTextCoordinates = function(group, x, y , w , h, s, align, valign, overflow, rotation, foOffset) {
464 + x += s.dx;
465 + y += s.dy;
466 + var dx = 0;
467 + var dy = 0;
468 +
469 + if (align == mxConstants.ALIGN_CENTER) {
470 + dx -= w / 2;
471 + } else if (align == mxConstants.ALIGN_RIGHT) {
472 + dx -= w;
473 + }
474 + x += dx;
475 +
476 + if (valign == mxConstants.ALIGN_MIDDLE) {
477 + dy -= h / 2;
478 + } else if (valign == mxConstants.ALIGN_BOTTOM) {
479 + dy -= h;
480 + }
481 +
482 + if (overflow != 'fill' && mxClient.IS_FF && mxClient.IS_WIN) {
483 + dy -= 2;
484 + }
485 + y += dy;
486 +
487 + var tr = (s.scale != 1) ? 'scale(' + s.scale + ')' : '';
488 +
489 + if (s.rotation != 0 && this.rotateHtml) {
490 + tr += 'rotate(' + (s.rotation) + ',' + (w / 2) + ',' + (h / 2) + ')';
491 + var pt = this.rotatePoint((x + w / 2) * s.scale, (y + h / 2) * s.scale, s.rotation, s.rotationCx, s.rotationCy);
492 + x = pt.x - w * s.scale / 2;
493 + y = pt.y - h * s.scale / 2;
494 + } else {
495 + x *= s.scale;
496 + y *= s.scale;
497 + }
498 +
499 + if (rotation != 0) {
500 + tr += 'rotate(' + (rotation) + ',' + (-dx) + ',' + (-dy) + ')';
501 + }
502 +
503 + var transform = 'translate(' + (Math.round(x) + foOffset) + ',' + (Math.round(y) + foOffset) + ')' + tr;
504 + group.setAttribute('transform', transform);
505 + };
506 +
507 + return {
508 + convertTextElementToSvg: convertTextElementToSvg,
509 + isParagraphWithPartialStyle: isParagraphWithPartialStyle,
510 + convertPartiallyStyledParagraphToSVG: convertPartiallyStyledParagraphToSVG,
511 + adjustAlternateTextCoordinates: adjustAlternateTextCoordinates
512 + };
513 +});
1 1  /**
2 2   * Overrides the link dialog in order to support creating links to wiki pages.
3 3   */
... ... @@ -4,9 +4,10 @@
4 4  define('diagram-link-editor', [
5 5   'jquery',
6 6   'diagram-link-handler',
520 + 'svg-handler',
7 7   'draw.io',
8 8   'resourceSelector'
9 -], function($, diagramLinkHandler) {
523 +], function($, diagramLinkHandler, svgHandler) {
10 10   /**
11 11   * Override in order to change the Element type check from a.constructor !== Element, which was failing due to a
12 12   * collision with the PrototypeJS's own implementation of the standard Element.
... ... @@ -42,32 +42,6 @@
42 42   // Do nothing.
43 43   }
44 44  
45 - // Override the Graph.updateSvgLinks method to ensure that the links are preserved in the final SVG.
46 - // We need to directly modify the original method because drawio does not provide a parameter to override the default
47 - // behavior or a smaller method that only performs the replacement.
48 - Graph.prototype.updateSvgLinks = function (node, target, removeCustom) {
49 - var links = node.getElementsByTagName('a');
50 - for (var i = 0; i < links.length; i++) {
51 - if (links[i].getAttribute('target') == null) {
52 - var href = links[i].getAttribute('href');
53 - if (href == null) {
54 - href = links[i].getAttribute('xlink:href');
55 - }
56 -
57 - if (href != null) {
58 - if (target != null && /^https?:\/\//.test(href)) {
59 - // If the href starts with http or https, set the target attribute.
60 - links[i].setAttribute('target', target);
61 - } else if (this.isCustomLink(href)) {
62 - // Handle custom links
63 - let absoluteLink = $('<a/>').attr('href', diagramLinkHandler.getURLFromCustomLink(href)).prop('href');
64 - links[i].setAttribute('href', absoluteLink);
65 - }
66 - }
67 - }
68 - }
69 - };
70 -
71 71   // Overwrite Graph.getSvg in order to replace XWiki custom links with absolute URLs.
72 72   // Also fix the text fallback for viewers with no support for foreignObjects.
73 73   var originalGraphGetSVG = Graph.prototype.getSvg;
... ... @@ -93,8 +93,66 @@
93 93   // Keep only the text content.
94 94   str = $('<div/>').html(str).text() || this.foAltText;
95 95   }
96 - return originalCreateAlternateContent.call(this, fo, x, y, w, h, str, align, valign, wrap, format, overflow,
97 - clip, rotation);
584 + // Update links inside foreignObject to use absolute URLs, since the generated SVG needs to be portable.
585 + $(fo).find('a').each(function() {
586 + var oldLink = $(this).attr('href');
587 + if (diagramLinkHandler.isXWikiCustomLink(oldLink)) {
588 + var newLink = diagramLinkHandler.getURLFromCustomLink(oldLink);
589 + $(this).attr('href', newLink);
590 + }
591 + });
592 +
593 + if (this.foAltText != null) {
594 + var s = this.state;
595 + var htmlConverter = document.createElement('textarea');
596 + var content = document.createElementNS('http://www.w3.org/2000/svg', 'g');
597 + var temporaryContent = document.createElementNS('http://www.w3.org/2000/svg', 'g');
598 + var elementsContainer = $(fo).children().first().children().first();
599 + var childNodes = elementsContainer.contents();
600 + // Consider the container width to be the width of the closest rect parent node. For some shapes, like Actor
601 + // or any non squared shape, there is no rect and we fallback to the initial width of the text node, which is
602 + // less accurate.
603 + var containerWidth = this.state.initialWidth;
604 + var prevNode = $(fo.parentNode).prev()[0];
605 + if (prevNode && prevNode.nodeName == 'rect') {
606 + var containerWidth = prevNode.width.baseVal.valueAsString;
607 + }
608 + try {
609 + if (childNodes.length > 0) {
610 + var isPartiallyStyledElement = false;
611 + var alt;
612 + childNodes.each(function(index) {
613 + // Make a clone of the node, to not alter the original child.
614 + var node = $(this).clone()[0];
615 + // If this paragraph has partially styled nodes (i.e. link added to just a part of the text) create a
616 + // fallback version of it, since not all the cases are supported and the created svg could not be
617 + // compatible with the already created elements. This is done since is too complex to calculate the
618 + // coordinates of the styled node, considering the last element added and that it may be needed to wrap
619 + // the text.
620 + if (svgHandler.isParagraphWithPartialStyle(node, childNodes.length)) {
621 + alt = svgHandler.convertPartiallyStyledParagraphToSVG(childNodes, s, w, h, htmlConverter, str,
622 + containerWidth);
623 + isPartiallyStyledElement = true;
624 + return false;
625 + } else {
626 + alt = svgHandler.convertTextElementToSvg(node, s, w, h, htmlConverter, str, containerWidth, index == 0);
627 + temporaryContent.appendChild(alt);
628 + }
629 + });
630 + content.appendChild(isPartiallyStyledElement ? alt : temporaryContent)
631 + svgHandler.adjustAlternateTextCoordinates(content, x, y, w, h, s, align, valign, overflow, rotation,
632 + this.foOffset);
633 + return content;
634 + } else {
635 + return originalCreateAlternateContent.call(this, fo, x, y, w, h, str, align, valign, wrap, format,
636 + overflow, clip, rotation);
637 + }
638 + } catch (e) {
639 + return originalCreateAlternateContent.apply(this, arguments);
640 + }
641 + } else {
642 + return originalCreateAlternateContent.apply(this, arguments);
643 + }
98 98   };
99 99   return originalDrawState.apply(this, arguments);
100 100   };
... ... @@ -126,7 +126,326 @@
126 126   };
127 127   return converter;
128 128   };
129 -
675 +
676 + // Copied from the drawio version 24.5.4 to include a fix for importing a file in an empty diagram. This method should
677 + // be removed once we upgrade to a drawio version >= 24.4.0. Being a big method, the structure was altered just to
678 + // make it shorter.
679 + var importFilesNew = function(files, x, y, maxSize, fn, resultFn, filterFn, barrierFn,
680 + resizeDialog, maxBytes, resampleThreshold, ignoreEmbeddedXml, evt) {
681 + maxSize = (maxSize != null) ? maxSize : this.maxImageSize;
682 + maxBytes = (maxBytes != null) ? maxBytes : this.maxImageBytes;
683 + var crop = x != null && y != null;
684 + var resizeImages = true;
685 + x = (x != null) ? x : 0;
686 + y = (y != null) ? y : 0;
687 + // Checks if large images are imported
688 + var largeImages = false;
689 + if (!mxClient.IS_CHROMEAPP && files != null) {
690 + var thresh = resampleThreshold || this.resampleThreshold;
691 + for (var i = 0; i < files.length; i++) {
692 + if (files[i].type.substring(0, 9) !== 'image/svg' &&
693 + files[i].type.substring(0, 6) === 'image/' &&
694 + files[i].size > thresh) {
695 + largeImages = true;
696 + break;
697 + }
698 + }
699 + }
700 +
701 + var doImportFiles = mxUtils.bind(this, function() {
702 + var graph = this.editor.graph;
703 + var gs = graph.gridSize;
704 +
705 + fn = (fn != null) ? fn : mxUtils.bind(this, function(data, mimeType, x, y, w, h, filename, done, file) {
706 + try {
707 + if (data != null && data.substring(0, 10) == '<mxlibrary') {
708 + this.spinner.stop();
709 + this.loadLibrary(new LocalLibrary(this, data, filename));
710 + this.showSidebar();
711 +
712 + return null;
713 + }
714 + else {
715 + // Drop on empty file ignores drop location
716 + if (this.isCompatibleString(data) && files.length == 1 && evt != null &&
717 + evt.type == 'drop' && this.isBlankFile() && !this.canUndo()) {
718 + crop = false;
719 + x = 0;
720 + y = 0;
721 + }
722 + return this.importFile(data, mimeType, x, y, w, h, filename,
723 + done, file, crop, ignoreEmbeddedXml, evt);
724 + }
725 + }
726 + catch (e) {
727 + this.handleError(e);
728 + return null;
729 + }
730 + });
731 +
732 + resultFn = (resultFn != null) ? resultFn : mxUtils.bind(this, function(cells) {
733 + graph.setSelectionCells(cells);
734 + });
735 +
736 + if (this.spinner.spin(document.body, mxResources.get('loading'))) {
737 + var count = files.length;
738 + var remain = count;
739 + var queue = [];
740 +
741 + // Barrier waits for all files to be loaded asynchronously
742 + var barrier = mxUtils.bind(this, function(index, fnc) {
743 + queue[index] = fnc;
744 + if (--remain == 0) {
745 + this.spinner.stop();
746 + if (barrierFn != null) {
747 + barrierFn(queue);
748 + }
749 + else {
750 + var cells = [];
751 + graph.getModel().beginUpdate();
752 + try {
753 + for (var j = 0; j < queue.length; j++) {
754 + var tmp = queue[j]();
755 + if (tmp != null) {
756 + cells = cells.concat(tmp);
757 + }
758 + }
759 + }
760 + finally {
761 + graph.getModel().endUpdate();
762 + }
763 + }
764 + resultFn(cells);
765 + }
766 + });
767 +
768 + for (var i = 0; i < count; i++) {
769 + (mxUtils.bind(this, function(index) {
770 + var file = files[index];
771 + if (file != null) {
772 + var reader = new FileReader();
773 + reader.onload = mxUtils.bind(this, function(e) {
774 + if (filterFn == null || filterFn(file)) {
775 + try {
776 + if (file.type.substring(0, 6) == 'image/') {
777 + if (file.type.substring(0, 9) == 'image/svg') {
778 + // Checks if SVG contains content attribute
779 + var data = Graph.clipSvgDataUri(e.target.result);
780 + var comma = data.indexOf(',');
781 + var svgText = decodeURIComponent(escape(atob(data.substring(comma + 1))));
782 + var root = mxUtils.parseXml(svgText);
783 + var svgs = root.getElementsByTagName('svg');
784 + if (svgs.length > 0) {
785 + var svgRoot = svgs[0];
786 + var cont = (ignoreEmbeddedXml) ? null : svgRoot.getAttribute('content');
787 + if (cont != null && cont.charAt(0) != '<' && cont.charAt(0) != '%') {
788 + cont = unescape((window.atob) ? atob(cont) : Base64.decode(cont, true));
789 + }
790 + if (cont != null && cont.charAt(0) == '%') {
791 + cont = decodeURIComponent(cont);
792 + }
793 + if (cont != null && (cont.substring(0, 8) === '<mxfile ' ||
794 + cont.substring(0, 14) === '<mxGraphModel ')) {
795 + barrier(index, mxUtils.bind(this, function() {
796 + return fn(cont, 'text/xml', x + index * gs, y + index * gs, 0, 0, file.name);
797 + }));
798 + }
799 + else {
800 + // SVG needs special handling to add viewbox if missing and
801 + // find initial size from SVG attributes (only for IE11)
802 + barrier(index, mxUtils.bind(this, function() {
803 + try {
804 + // Parses SVG and find width and height
805 + if (root != null) {
806 + var svgs = root.getElementsByTagName('svg');
807 + if (svgs.length > 0) {
808 + var svgRoot = svgs[0];
809 + var w = svgRoot.getAttribute('width');
810 + var h = svgRoot.getAttribute('height');
811 + if (w != null && w.charAt(w.length - 1) != '%') {
812 + w = parseFloat(w);
813 + }
814 + else {
815 + w = NaN;
816 + }
817 + if (h != null && h.charAt(h.length - 1) != '%') {
818 + h = parseFloat(h);
819 + }
820 + else {
821 + h = NaN;
822 + }
823 + // Check if viewBox attribute already exists
824 + var vb = svgRoot.getAttribute('viewBox');
825 + if (vb == null || vb.length == 0) {
826 + svgRoot.setAttribute('viewBox', '0 0 ' + w + ' ' + h);
827 + }
828 + // Uses width and height from viewbox for
829 + // missing width and height attributes
830 + else if (isNaN(w) || isNaN(h)) {
831 + var tokens = vb.split(' ');
832 + if (tokens.length > 3) {
833 + w = parseFloat(tokens[2]);
834 + h = parseFloat(tokens[3]);
835 + }
836 + }
837 + data = Editor.createSvgDataUri(mxUtils.getXml(svgRoot));
838 + var s = Math.min(1, Math.min(maxSize / Math.max(1, w)), maxSize / Math.max(1, h));
839 + var cells = fn(data, file.type, x + index * gs, y + index * gs, Math.max(
840 + 1, Math.round(w * s)), Math.max(1, Math.round(h * s)), file.name);
841 + // Hack to fix width and height asynchronously
842 + if (cells != null && (isNaN(w) || isNaN(h))) {
843 + var img = new Image();
844 + img.onload = mxUtils.bind(this, function() {
845 + w = Math.max(1, img.width);
846 + h = Math.max(1, img.height);
847 + cells[0].geometry.width = w;
848 + cells[0].geometry.height = h;
849 + svgRoot.setAttribute('viewBox', '0 0 ' + w + ' ' + h);
850 + data = Editor.createSvgDataUri(mxUtils.getXml(svgRoot));
851 + var semi = data.indexOf(';');
852 + if (semi > 0) {
853 + data = data.substring(0, semi) + data.substring(data.indexOf(',', semi + 1));
854 + }
855 + graph.setCellStyles('image', data, [cells[0]]);
856 + });
857 + img.src = Editor.createSvgDataUri(mxUtils.getXml(svgRoot));
858 + }
859 + return cells;
860 + }
861 + }
862 + }
863 + catch (e) {
864 + // ignores any SVG parsing errors
865 + }
866 + return null;
867 + }));
868 + }
869 + }
870 + else {
871 + barrier(index, mxUtils.bind(this, function() {
872 + return null;
873 + }));
874 + }
875 + }
876 + else {
877 + // Checks if PNG+XML is available to bypass code below
878 + var containsModel = false;
879 + if (file.type == 'image/png') {
880 + var xml = (ignoreEmbeddedXml) ? null : this.extractGraphModelFromPng(e.target.result);
881 + if (xml != null && xml.length > 0) {
882 + var img = new Image();
883 + img.src = e.target.result;
884 + barrier(index, mxUtils.bind(this, function() {
885 + return fn(xml, 'text/xml', x + index * gs, y + index * gs,
886 + img.width, img.height, file.name);
887 + }));
888 + containsModel = true;
889 + }
890 + }
891 + // Additional asynchronous step for finding image size
892 + if (!containsModel) {
893 + // Cannot load local files in Chrome App
894 + if (mxClient.IS_CHROMEAPP) {
895 + this.spinner.stop();
896 + this.showError(mxResources.get('error'), mxResources.get('dragAndDropNotSupported'),
897 + mxResources.get('cancel'), mxUtils.bind(this, function()
898 + {
899 + // Hides the dialog
900 + }), null, mxResources.get('ok'), mxUtils.bind(this, function()
901 + {
902 + // Redirects to import function
903 + this.actions.get('import').funct();
904 + })
905 + );
906 + }
907 + else {
908 + this.loadImage(e.target.result, mxUtils.bind(this, function(img) {
909 + this.resizeImage(img, e.target.result, mxUtils.bind(this, function(data2, w2, h2) {
910 + barrier(index, mxUtils.bind(this, function() {
911 + // Refuses to insert images above a certain size as they kill the app
912 + if (data2 != null && data2.length < maxBytes) {
913 + var s = (!resizeImages || !this.isResampleImageSize(
914 + file.size, resampleThreshold)) ? 1 :
915 + Math.min(1, Math.min(maxSize / w2, maxSize / h2));
916 + return fn(data2, file.type, x + index * gs, y + index * gs,
917 + Math.round(w2 * s), Math.round(h2 * s), file.name);
918 + }
919 + else {
920 + this.handleError({message: mxResources.get('imageTooBig')});
921 + return null;
922 + }
923 + }));
924 + }), resizeImages, maxSize, resampleThreshold, file.size);
925 + }), mxUtils.bind(this, function() {
926 + this.handleError({message: mxResources.get('invalidOrMissingFile')});
927 + }));
928 + }
929 + }
930 + }
931 + }
932 + else {
933 + var data = e.target.result;
934 + fn(data, file.type, x + index * gs, y + index * gs, 240, 160, file.name, function(cells) {
935 + barrier(index, function() {
936 + return cells;
937 + });
938 + }, file);
939 + }
940 + }
941 + catch (e) {
942 + // Ignores file parsing error
943 + barrier(index, mxUtils.bind(this, function() {
944 + return null;
945 + }));
946 + if (window.console != null) {
947 + console.error(e, file);
948 + }
949 + }
950 + }
951 + else {
952 + // Ignores file and decrements counter
953 + barrier(index, mxUtils.bind(this, function()
954 + {
955 + return null;
956 + }));
957 + }
958 + });
959 + // Handles special cases
960 + if (/(\.v(dx|sdx?))($|\?)/i.test(file.name) || /(\.vs(x|sx?))($|\?)/i.test(file.name)) {
961 + fn(null, file.type, x + index * gs, y + index * gs, 240, 160, file.name, function(cells) {
962 + barrier(index, function() {
963 + return cells;
964 + });
965 + }, file);
966 + }
967 + else if (file.type.substring(0, 5) == 'image' || file.type == 'application/pdf') {
968 + reader.readAsDataURL(file);
969 + }
970 + else {
971 + reader.readAsText(file);
972 + }
973 + }
974 + }))(i);
975 + }
976 + }
977 + });
978 + if (largeImages) {
979 + // Workaround for lost files array in async code
980 + var tmp = [];
981 + for (var i = 0; i < files.length; i++) {
982 + tmp.push(files[i]);
983 + }
984 + files = tmp;
985 + this.confirmImageResize(function(doResize) {
986 + resizeImages = doResize;
987 + doImportFiles();
988 + }, resizeDialog);
989 + }
990 + else {
991 + doImportFiles();
992 + }
993 + };
994 +
130 130   // Override for uploading the image as attachment instead of encode it to Base64.
131 131   var originalImportFiles = EditorUi.prototype.importFiles;
132 132   EditorUi.prototype.importFiles = function(files, x, y, maxSize, fn, resultFn, filterFn, barrierFn, resizeDialog,
... ... @@ -156,7 +156,7 @@
156 156   }
157 157   };
158 158   }
159 - originalImportFiles.apply(this, importFilesArgs);
1024 + importFilesNew.apply(this, importFilesArgs);
160 160   };
161 161  
162 162   // Add support for inserting images by specifying the XWiki attachment reference.
... ... @@ -290,15 +290,6 @@
290 290   };
291 291  });
292 292  
293 -define('diagramMenuTranslations', {
294 - prefix: 'diagram.editor.menu.',
295 - keys: [
296 - // File menu.
297 - 'print.label',
298 - 'print.title'
299 - ]
300 -});
301 -
302 302  /**
303 303   * Integrates draw.io diagram editor in XWiki.
304 304   */
... ... @@ -308,12 +308,11 @@
308 308   'diagram-utils',
309 309   'diagram-url-io',
310 310   'diagram-config',
311 - 'xwiki-l10n!diagramMenuTranslations',
312 312   'diagram-graph-xml-filter',
313 313   'diagram-link-editor',
314 314   'diagram-image-editor',
315 315   'diagram-external-services'
316 -], function($, diagramStore, diagramUtils, diagramUrlIO, diagramConfig, l10n) {
1171 +], function($, diagramStore, diagramUtils, diagramUrlIO, diagramConfig) {
317 317  
318 318   // These variables are used to decide if an image should be uploaded at original resolution or
319 319   // should be declined for being too big.
... ... @@ -345,7 +345,6 @@
345 345  
346 346   var fixEditorUI = function(editorUI) {
347 347   cleanMenu(editorUI);
348 - renameMenu(editorUI);
349 349   fixKeyboardShortcutsAction(editorUI);
350 350   fixEditorButtons($(editorUI.container));
351 351   removeThemeButton();
... ... @@ -385,31 +385,11 @@
385 385   var originalAddSubmenu = Menus.prototype.addSubmenu;
386 386   Menus.prototype.addSubmenu = function(name, menu, parent, label) {
387 387   var subMenu = this.get(name);
388 - if (subMenu && subMenu.isEnabled() !== false) {
389 - return originalAddSubmenu.apply(this, arguments);
1242 + if (subMenu && subMenu.visible !== false) {
1243 + originalAddSubmenu.apply(this, arguments);
390 390   }
391 391   };
392 392  
393 -
394 - /*
395 - * Map with all the menu items that we want to have a title.
396 - */
397 - const titleMap = new Map([
398 - ['print', l10n['print.title']]
399 - ]);
400 -
401 - /*
402 - * Update the title of the menu items.
403 - */
404 - var originalAddMenuItem = Menus.prototype.addMenuItem;
405 - Menus.prototype.addMenuItem = function(menu, key, parent, trigger, sprite, label) {
406 - let item = originalAddMenuItem.apply(this, arguments);
407 - if (item != null && titleMap.has(key)) {
408 - item.title = titleMap.get(key);
409 - }
410 - return item;
411 - };
412 -
413 413   // Remove the language picker because the diagram editor is configured to use the XWiki language.
414 414   var originalCreateMenubar = Menus.prototype.createMenubar;
415 415   Menus.prototype.createMenubar = function(container) {
... ... @@ -429,24 +429,6 @@
429 429   };
430 430  
431 431   //
432 - // Rename menu options to fit our needs.
433 - //
434 - var renameMenu = function(editorUI) {
435 - const menuItems = [
436 - // File menu
437 - ['print', l10n['print.label']]
438 - ];
439 -
440 - // Iterate over the array of tuples
441 - menuItems.forEach(function([menuKey, newLabel]) {
442 - var action = editorUI.actions.actions[menuKey];
443 - if (action) {
444 - action.label = newLabel;
445 - }
446 - });
447 - };
448 -
449 - //
450 450   // Clean the editor menu by removing the features that are not needed.
451 451   //
452 452   var cleanMenu = function(editorUI) {
... ... @@ -459,7 +459,7 @@
459 459   // Help menu
460 460   'downloadDesktop', 'useOffline',
461 461   // ExportAs
462 - 'exportHtml', 'exportPdf'
1278 + 'exportHtml'
463 463   ].forEach(function(actionName) {
464 464   var action = editorUI.actions.actions[actionName];
465 465   if (action) {