aboutsummaryrefslogtreecommitdiff
path: root/src/wmap.js
blob: 6e9606e29904b94517655fd376c14d539ea0d681 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
import { parse } from "wmap-parser";
import { renderToSVG, StageType } from "wmap-renderer-svg";
import DOMPurify from "isomorphic-dompurify";

const internals = {
    kStyleSheet: "/plugins/wmap/wmap.css",
    kDimensions: {
        full: {
            map: {
                mapWidth: 1300,
                mapHeight: 1000,
                padding: 42,
            },
            fonts: {},
        },
        thumbnail: {
            map: {
                mapWidth: 403,
                mapHeight: 310,
                padding: 15,
                vertexWidth: 8,
                vertexHeight: 8,
                arrowHeadSize: 3,
                stageHeight: 50,
            },
            fonts: {
                axisLabel: 8,
                vertexLabel: 9,
                note: 9,
            },
        },
    },

    // Returns HTML that shows a message in the wiki.
    message(text) {
        return `
        <div class="viewer" data-item="viewer" style="width:98%">
          <div style="width:80%; padding:8px; color:gray; background-color:#eee; margin:0 auto; text-align:center">
            <i>${text}</i>
          </div>
        </div>`;
    },

    // Creates a wmap PNG from text.
    async renderImage(source, dimensions) {
        const parsedText = parse(source);

        // Further Improvements. The zoomed version could have a drop down
        // to show the different StageTypes and allow one to see the image
        // with the different types.
        // This  could also potentially be in the toolbar that appears
        // on hover to also show this in the download. It requires some
        // thought on what the right interaction model would be.
        return await renderToSVG(parsedText, StageType.ACTIVITIES, {
            theme: {
                sizes: {
                    ...dimensions.map,
                },
                fonts: {
                    ...dimensions.fonts,
                },
            },
        });
    },

    // Starts a download of the image.
    download(filename, text) {
        const svgWithNamespace = text.replace(
            "<svg",
            '<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"',
        );
        const svgContent = '<?xml version="1.0" encoding="UTF-8"?>\n' + svgWithNamespace;
        const blob = new Blob([svgContent], { type: "image/svg+xml" });
        const url = URL.createObjectURL(blob);
        const element = document.createElement("a");
        element.setAttribute("href", url);
        element.setAttribute("download", filename);
        element.style.display = "none";
        document.body.appendChild(element);
        element.click();
        document.body.removeChild(element);
        URL.revokeObjectURL(url);
    },

    // Replaces the image in a window. Intended for use in a dialog.
    replaceImage(window, imageData) {
        try {
            const $container = window.document.querySelector("main");
            $container.innerHTML = imageData;
        } catch (error) {
            console.error(
                "wmap: Could not replace image. The DOM wasn't ready!",
                error,
            );
        }
    },
};

/**
 * Runs once for each block. Do setup for the block before rendering.
 */
const emit = function emit($item) {
    const hasStyleSheet = [...document.styleSheets].some((styleSheet) =>
        styleSheet.href?.endsWith(internals.kStyleSheet),
    );

    if (!hasStyleSheet) {
        console.log("Adding wmap stylesheet.");
        const link = document.createElement("link");
        link.rel = "stylesheet";
        link.href = internals.kStyleSheet;
        link.type = "text/css";
        document.getElementsByTagName("head")[0].appendChild(link);
    }

    return $item.append(internals.message("Loading Wardley Map"));
};

/**
 * Bind to events on the rendered block. Called once per block.
 * Convention is that double click opens the editor.
 *
 * @param {JQuery} $item the HTML element representing the block.
 * @param {Object} item the configuration for that particular object.
 */
const bind = async function bind($item, item) {
    $item.on("dblclick", () => {
        return wiki.textEditor($item, item);
    });

    $item.on("click", "a", async (event) => {
        const { currentTarget } = event;
        const action = currentTarget.dataset?.action;

        if (!action) {
            return;
        }
        event.stopPropagation();
        event.preventDefault();

        switch (action) {
            case "download":
                {
                    let imageData = await internals.renderImage(
                        item.text,
                        internals.kDimensions.full,
                    );
                    const slug = $item.parents(".page").attr("id");
                    internals.download(`${slug}.svg`, imageData);
                }
                break;
            case "zoom":
                {
                    const shouldOpenStandalone = !!event.shiftKey;
                    const target = shouldOpenStandalone ? "_blank" : "wmap";
                    const dialog = window.open(
                        "/plugins/wmap/dialog/#",
                        target,
                        "popup,height=600,width=800",
                    );

                    if (!dialog || dialog.closed) {
                        console.error("wmap: Failed to open dialog.");
                        return;
                    }

                    let imageData = await internals.renderImage(
                        item.text,
                        internals.kDimensions.full,
                    );

                    // We *MUST* check both readyState and href, because
                    // readyState will be complete when it first loads,
                    // and this will be an empty window.
                    if (
                        dialog.document.readyState === "complete" &&
                        dialog.document.location.href.includes(
                            "/plugins/wmap/dialog/",
                        )
                    ) {
                        internals.replaceImage(dialog, imageData);
                    } else {
                        console.info("We not loaded");
                        dialog.addEventListener(
                            "load",
                            () => {
                                internals.replaceImage(dialog, imageData);
                            },
                            { once: true },
                        );
                    }
                }
                break;
        }
    });

    try {
        let thumbnailData = await internals.renderImage(
            item.text,
            internals.kDimensions.thumbnail,
        );
        $item.find(".viewer").html(`
            <article class="wardley-map thumbnail">${thumbnailData}</article>
            <nav class="actions">
                <menu>
                    <li><a href="#" data-action="download" title="Download"><img width="18" height="18" alt="download" src='data:image/svg+xml;charset=utf-8,<svg xmlns="http://www.w3.org/2000/svg" enable-background="new 0 0 24 24" viewBox="0 0 24 24" fill="grey"><g><rect fill="none" height="24" width="24"/></g><g><path d="M5,20h14v-2H5V20z M19,9h-4V3H9v6H5l7,7L19,9z"/></g></svg>'></a></li>
                    <li><a href="#" data-action="zoom" title="Zoom"><img width="18" height="18" alt="toggle zoom" src='data:image/svg+xml;charset=utf-8,<svg xmlns="http://www.w3.org/2000/svg" enable-background="new 0 0 24 24" viewBox="0 0 24 24"><g><rect fill="none" height="24" width="24"/></g><g><g><g><path fill="grey" d="M15,3l2.3,2.3l-2.89,2.87l1.42,1.42L18.7,6.7L21,9V3H15z M3,9l2.3-2.3l2.87,2.89l1.42-1.42L6.7,5.3L9,3H3V9z M9,21 l-2.3-2.3l2.89-2.87l-1.42-1.42L5.3,17.3L3,15v6H9z M21,15l-2.3,2.3l-2.87-2.89l-1.42,1.42l2.89,2.87L15,21h6V15z"/></g></g></g></svg>'></a></li>
                </menu>
            </nav>
            `);

        $item.find('svg text:gt(7)').on('click', function(event) {
            const title = this.innerHTML;
            let $page = $item.parents('.page')
            wiki.pageHandler.context = wiki.lineup.atKey($page.data('key')).getContext()
            wiki.doInternalLink(title, event.shiftKey ? null : $page)
        });
    } catch (err) {
        console.log("Failed to parse wardley map: ", err);
        $item.html(internals.message(err.message));
    }
};

/**
 * On load, attempt to add the emit and bind functions to window, as these
 * are called by the wiki plugin manager.
 */
if (typeof window !== "undefined" && window !== null) {
    if (!window.plugins.wmap) {
        window.plugins.wmap = { emit, bind };
    }
}

const expand = function (text) {
    return DOMPurify.sanitize(text);
};

export const wmap = typeof window === "undefined" ? { expand } : undefined;