OSDN Git Service

BugTrack/2447 Show page passage by Web browser/JavaScript process
[pukiwiki/pukiwiki.git] / skin / main.js
1 // PukiWiki - Yet another WikiWikiWeb clone.
2 // main.js
3 // Copyright 2017 PukiWiki Development Team
4 // License: GPL v2 or (at your option) any later version
5 //
6 // PukiWiki JavaScript client script
7 window.addEventListener && window.addEventListener('DOMContentLoaded', function() { // eslint-disable-line no-unused-expressions
8   'use strict';
9   /**
10    * @param {NodeList} nodeList
11    * @param {function(Node, number): void} func
12    */
13   function forEach(nodeList, func) {
14     if (nodeList.forEach) {
15       nodeList.forEach(func);
16     } else {
17       for (var i = 0, n = nodeList.length; i < n; i++) {
18         func(nodeList[i], i);
19       }
20     }
21   }
22   // Name for comment
23   function setYourName() {
24     var NAME_KEY_ID = 'pukiwiki_comment_plugin_name';
25     var actionPathname = null;
26     function getPathname(formAction) {
27       if (actionPathname) return actionPathname;
28       try {
29         var u = new URL(formAction, document.location);
30         var u2 = new URL('./', u);
31         actionPathname = u2.pathname;
32         return u2.pathname;
33       } catch (e) {
34         // Note: Internet Explorer doesn't support URL class
35         var m = formAction.match(/^https?:\/\/([^/]+)(\/([^?&]+\/)?)/);
36         if (m) {
37           actionPathname = m[2]; // pathname
38         } else {
39           actionPathname = '/';
40         }
41         return actionPathname;
42       }
43     }
44     function getNameKey(form) {
45       var pathname = getPathname(form.action);
46       var key = 'path.' + pathname + '.' + NAME_KEY_ID;
47       return key;
48     }
49     function getForm(element) {
50       if (element.form && element.form.tagName === 'FORM') {
51         return element.form;
52       }
53       var e = element.parentElement;
54       for (var i = 0; i < 5; i++) {
55         if (e.tagName === 'FORM') {
56           return e;
57         }
58         e = e.parentElement;
59       }
60       return null;
61     }
62     function handleCommentPlugin(form) {
63       var namePrevious = '';
64       var nameKey = getNameKey(form);
65       if (typeof localStorage !== 'undefined') {
66         namePrevious = localStorage[nameKey];
67       }
68       var onFocusForm = function () {
69         if (form.name && !form.name.value && namePrevious) {
70           form.name.value = namePrevious;
71         }
72       };
73       var addOnForcusForm = function(eNullable) {
74         if (!eNullable) return;
75         if (eNullable.addEventListener) {
76           eNullable.addEventListener('focus', onFocusForm);
77         }
78       };
79       if (namePrevious) {
80         var textList = form.querySelectorAll('input[type=text],textarea');
81         textList.forEach(function (v) {
82           addOnForcusForm(v);
83         });
84       }
85       form.addEventListener('submit', function() {
86         if (typeof localStorage !== 'undefined') {
87           localStorage[nameKey] = form.name.value;
88         }
89       }, false);
90     }
91     function setNameForComment() {
92       if (!document.querySelectorAll) return;
93       var elements = document.querySelectorAll(
94         'input[type=hidden][name=plugin][value=comment],' +
95         'input[type=hidden][name=plugin][value=pcomment],' +
96         'input[type=hidden][name=plugin][value=article],' +
97         'input[type=hidden][name=plugin][value=bugtrack]');
98       for (var i = 0; i < elements.length; i++) {
99         var form = getForm(elements[i]);
100         if (form) {
101           handleCommentPlugin(form);
102         }
103       }
104     }
105     setNameForComment();
106   }
107   // AutoTicketLink
108   function autoTicketLink() {
109     var headReText = '([\\s\\b]|^)';
110     var tailReText = '\\b';
111     var ignoreTags = ['A', 'INPUT', 'TEXTAREA', 'BUTTON',
112       'SCRIPT', 'FRAME', 'IFRAME'];
113     var ticketSiteList = [];
114     function regexEscape(key) {
115       return key.replace(/[-.]/g, function (m) {
116         return '\\' + m;
117       });
118     }
119     function setupSites(siteList) {
120       for (var i = 0, length = siteList.length; i < length; i++) {
121         var site = siteList[i];
122         var reText = '';
123         switch (site.type) {
124           case 'jira':
125             reText = '(' + regexEscape(site.key) + '):([A-Z][A-Z0-9_]+-\\d+)';
126             break;
127           case 'redmine':
128             reText = '(' + regexEscape(site.key) + '):(\\d+)';
129             break;
130           case 'git':
131             reText = '(' + regexEscape(site.key) + '):([0-9a-f]{7,40})';
132             break;
133           default:
134             continue;
135         }
136         site.reText = reText;
137         site.re = new RegExp(headReText + reText + tailReText);
138       }
139     }
140     function getSiteListFromBody() {
141       var defRoot = document.querySelector('#pukiwiki-site-properties .ticketlink-def');
142       if (defRoot && defRoot.value) {
143         var list = JSON.parse(defRoot.value);
144         setupSites(list);
145         return list;
146       }
147       return [];
148     }
149     function getSiteList() {
150       return ticketSiteList;
151     }
152     function ticketToLink(keyText) {
153       var siteList = getSiteList();
154       for (var i = 0; i < siteList.length; i++) {
155         var site = siteList[i];
156         var m = keyText.match(site.re);
157         if (m) {
158           var title = site.title;
159           var ticketKey = m[3];
160           if (title) {
161             title = title.replace(/\$1/g, ticketKey);
162           }
163           return {
164             url: site.base_url + m[3],
165             title: title
166           };
167         }
168       }
169       return null;
170     }
171     function getRegex(list) {
172       var reText = '';
173       for (var i = 0, length = list.length; i < length; i++) {
174         if (reText.length > 0) {
175           reText += '|';
176         }
177         reText += list[i].reText;
178       }
179       return new RegExp(headReText + '(' + reText + ')' + tailReText);
180     }
181     function makeTicketLink(element) {
182       var siteList = getSiteList();
183       if (!siteList || siteList.length === 0) {
184         return;
185       }
186       var re = getRegex(siteList);
187       var f;
188       var m;
189       var text = element.nodeValue;
190       while (m = text.match(re)) { // eslint-disable-line no-cond-assign
191         // m[1]: head, m[2]: keyText
192         if (!f) {
193           f = document.createDocumentFragment();
194         }
195         if (m.index > 0 || m[1].length > 0) {
196           f.appendChild(document.createTextNode(text.substr(0, m.index) + m[1]));
197         }
198         var a = document.createElement('a');
199         a.textContent = m[2];
200         var linkInfo = ticketToLink(a.textContent);
201         a.href = linkInfo.url;
202         a.title = linkInfo.title;
203         f.appendChild(a);
204         text = text.substr(m.index + m[0].length);
205       }
206       if (f) {
207         if (text.length > 0) {
208           f.appendChild(document.createTextNode(text));
209         }
210         element.parentNode.replaceChild(f, element);
211       }
212     }
213     function walkElement(element) {
214       var e = element.firstChild;
215       while (e) {
216         if (e.nodeType === 3 && e.nodeValue &&
217             e.nodeValue.length > 5 && /\S/.test(e.nodeValue)) {
218           var next = e.nextSibling;
219           makeTicketLink(e);
220           e = next;
221         } else {
222           if (e.nodeType === 1 && ignoreTags.indexOf(e.tagName) === -1) {
223             walkElement(e);
224           }
225           e = e.nextSibling;
226         }
227       }
228     }
229     if (!Array.prototype.indexOf || !document.createDocumentFragment) {
230       return;
231     }
232     ticketSiteList = getSiteListFromBody();
233     var target = document.getElementById('body');
234     walkElement(target);
235   }
236   function confirmEditFormLeaving() {
237     function trim(s) {
238       if (typeof s !== 'string') {
239         return s;
240       }
241       return s.replace(/^\s+|\s+$/g, '');
242     }
243     if (!document.querySelector) return;
244     var canceled = false;
245     var pluginNameE = document.querySelector('#pukiwiki-site-properties .plugin-name');
246     if (!pluginNameE) return;
247     var originalText = null;
248     if (pluginNameE.value !== 'edit') return;
249     var editForm = document.querySelector('.edit_form form._plugin_edit_edit_form');
250     if (!editForm) return;
251     var cancelMsgE = editForm.querySelector('#_msg_edit_cancel_confirm');
252     var unloadBeforeMsgE = editForm.querySelector('#_msg_edit_unloadbefore_message');
253     var textArea = editForm.querySelector('textarea[name="msg"]');
254     if (!textArea) return;
255     originalText = textArea.value;
256     var cancelForm = document.querySelector('.edit_form form._plugin_edit_cancel');
257     var submited = false;
258     editForm.addEventListener('submit', function() {
259       canceled = false;
260       submited = true;
261     });
262     cancelForm.addEventListener('submit', function(e) {
263       submited = false;
264       canceled = false;
265       if (trim(textArea.value) === trim(originalText)) {
266         canceled = true;
267         return false;
268       }
269       var message = 'The text you have entered will be discarded. Is it OK?';
270       if (cancelMsgE && cancelMsgE.value) {
271         message = cancelMsgE.value;
272       }
273       if (window.confirm(message)) { // eslint-disable-line no-alert
274         // Execute "Cancel"
275         canceled = true;
276         return true;
277       }
278       e.preventDefault();
279       return false;
280     });
281     window.addEventListener('beforeunload', function(e) {
282       if (canceled) return;
283       if (submited) return;
284       if (trim(textArea.value) === trim(originalText)) return;
285       var message = 'Data you have entered will not be saved.';
286       if (unloadBeforeMsgE && unloadBeforeMsgE.value) {
287         message = unloadBeforeMsgE.value;
288       }
289       e.returnValue = message;
290     }, false);
291   }
292   function showPagePassage() {
293     /**
294      * @param {Date} now
295      * @param {string} dateText
296      */
297     function getPassage(dateText, now) {
298       if (!dateText) {
299         return '';
300       }
301       var units = [{u: 'm', max: 60}, {u: 'h', max: 24}, {u: 'd', max: 1}];
302       var d = new Date();
303       d.setTime(Date.parse(dateText));
304       var t = (now.getTime() - d.getTime()) / (1000 * 60); // minutes
305       var unit = units[0].u; var card = units[0].max;
306       for (var i = 0; i < units.length; i++) {
307         unit = units[i].u; card = units[i].max;
308         if (t < card) break;
309         t = t / card;
310       }
311       return '(' + Math.floor(t) + unit + ')';
312     }
313     var now = new Date();
314     var elements = document.getElementsByClassName('page_passage');
315     forEach(elements, function(e) {
316       var dt = e.getAttribute('data-mtime');
317       if (dt) {
318         var d = new Date(dt);
319         e.textContent = ' ' + getPassage(d, now);
320       }
321     });
322     var links = document.getElementsByClassName('link_page_passage');
323     forEach(links, function(e) {
324       var dt = e.getAttribute('data-mtime');
325       if (dt) {
326         var d = new Date(dt);
327         if (e.title) {
328           e.title = e.title + ' ' + getPassage(d, now);
329         } else {
330           e.title = e.textContent + ' ' + getPassage(d, now);
331         }
332       }
333     });
334   }
335   setYourName();
336   autoTicketLink();
337   confirmEditFormLeaving();
338   showPagePassage();
339 });