1 // PukiWiki - Yet another WikiWikiWeb clone.
3 // Copyright 2017-2019 PukiWiki Development Team
4 // License: GPL v2 or (at your option) any later version
6 // PukiWiki JavaScript client script
7 window.addEventListener && window.addEventListener('DOMContentLoaded', function() { // eslint-disable-line no-unused-expressions
10 * @param {NodeList} nodeList
11 * @param {function(Node, number): void} func
13 function forEach(nodeList, func) {
14 if (nodeList.forEach) {
15 nodeList.forEach(func);
17 for (var i = 0, n = nodeList.length; i < n; i++) {
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;
29 var u = new URL(formAction, document.location);
30 var u2 = new URL('./', u);
31 actionPathname = u2.pathname;
34 // Note: Internet Explorer doesn't support URL class
35 var m = formAction.match(/^https?:\/\/([^/]+)(\/([^?&]+\/)?)/);
37 actionPathname = m[2]; // pathname
41 return actionPathname;
44 function getNameKey(form) {
45 var pathname = getPathname(form.action);
46 var key = 'path.' + pathname + '.' + NAME_KEY_ID;
49 function getForm(element) {
50 if (element.form && element.form.tagName === 'FORM') {
53 var e = element.parentElement;
54 for (var i = 0; i < 5; i++) {
55 if (e.tagName === 'FORM') {
62 function handleCommentPlugin(form) {
63 var namePrevious = '';
64 var nameKey = getNameKey(form);
65 if (typeof localStorage !== 'undefined') {
66 namePrevious = localStorage[nameKey];
68 var onFocusForm = function () {
69 if (form.name && !form.name.value && namePrevious) {
70 form.name.value = namePrevious;
73 var addOnForcusForm = function(eNullable) {
74 if (!eNullable) return;
75 if (eNullable.addEventListener) {
76 eNullable.addEventListener('focus', onFocusForm);
80 var textList = form.querySelectorAll('input[type=text],textarea');
81 textList.forEach(function (v) {
85 form.addEventListener('submit', function() {
86 if (typeof localStorage !== 'undefined') {
87 localStorage[nameKey] = form.name.value;
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]);
101 handleCommentPlugin(form);
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 var jiraProjects = null;
115 var jiraDefaultInfo = null;
116 function regexEscape(key) {
117 return key.replace(/[-.]/g, function (m) {
121 function setupSites(siteList) {
122 for (var i = 0, length = siteList.length; i < length; i++) {
123 var site = siteList[i];
127 reText = '(' + regexEscape(site.key) + '):([A-Z][A-Z0-9]{1,20}-\\d{1,10})';
130 reText = '(' + regexEscape(site.key) + '):(\\d{1,10})';
133 reText = '(' + regexEscape(site.key) + '):([0-9a-f]{7,40})';
138 site.reText = reText;
139 site.re = new RegExp(headReText + reText + tailReText);
142 function getJiraSite() {
143 var reText = '()([A-Z][A-Z0-9]{1,20}-\\d{1,10})';
145 title: 'Builtin JIRA',
149 re: new RegExp(headReText + reText + tailReText)
153 function getSiteListFromBody() {
154 var defRoot = document.querySelector('#pukiwiki-site-properties .ticketlink-def');
155 if (defRoot && defRoot.value) {
156 var list = JSON.parse(defRoot.value);
162 function getJiraProjectsFromBody() {
163 var defRoot = document.querySelector('#pukiwiki-site-properties .ticketlink-jira-def');
164 if (defRoot && defRoot.value) {
166 return JSON.parse(defRoot.value); // List
173 function getJiraDefaultInfoFromBody() {
174 var defRoot = document.querySelector('#pukiwiki-site-properties .ticketlink-jira-default-def');
175 if (defRoot && defRoot.value) {
177 return JSON.parse(defRoot.value); // object
184 function getSiteList() {
185 return ticketSiteList;
187 function getJiraProjectList() {
190 function getDefaultJira() {
191 return jiraDefaultInfo;
193 function ticketToLink(keyText) {
194 var siteList = getSiteList();
195 for (var i = 0; i < siteList.length; i++) {
196 var site = siteList[i];
197 var m = keyText.match(site.re);
199 var ticketKey = m[3];
200 var title = ticketKey;
202 if (site.type === '_jira_') {
204 var projects = getJiraProjectList();
205 var hyphen = keyText.indexOf('-');
207 var projectKey = keyText.substr(0, hyphen);
209 for (var j = 0; j < projects.length; j++) {
211 if (p.key === projectKey) {
213 title = p.title.replace(/\$1/g, ticketKey);
215 ticketUrl = p.base_url + ticketKey;
221 var defaultJira = getDefaultJira();
223 if (defaultJira.title) {
224 title = defaultJira.title.replace(/\$1/g, ticketKey);
226 ticketUrl = defaultJira.base_url + ticketKey;
234 // Explicit TicketLink
236 title = site.title.replace(/\$1/g, ticketKey);
238 ticketUrl = site.base_url + ticketKey;
248 function getRegex(list) {
250 for (var i = 0, length = list.length; i < length; i++) {
251 if (reText.length > 0) {
254 reText += list[i].reText;
256 return new RegExp(headReText + '(' + reText + ')' + tailReText);
258 function makeTicketLink(element) {
259 var siteList = getSiteList();
260 if (!siteList || siteList.length === 0) {
263 var re = getRegex(siteList);
266 var text = element.nodeValue;
267 while (m = text.match(re)) { // eslint-disable-line no-cond-assign
268 // m[1]: head, m[2]: keyText
270 f = document.createDocumentFragment();
272 if (m.index > 0 || m[1].length > 0) {
273 f.appendChild(document.createTextNode(text.substr(0, m.index) + m[1]));
276 var linkInfo = ticketToLink(linkKey);
278 var a = document.createElement('a');
279 a.textContent = linkKey;
280 a.href = linkInfo.url;
281 a.title = linkInfo.title;
284 f.appendChild(document.createTextNode(m[2]));
286 text = text.substr(m.index + m[0].length);
289 if (text.length > 0) {
290 f.appendChild(document.createTextNode(text));
292 element.parentNode.replaceChild(f, element);
295 function walkElement(element) {
296 var e = element.firstChild;
298 if (e.nodeType === 3 && e.nodeValue &&
299 e.nodeValue.length > 5 && /\S/.test(e.nodeValue)) {
300 var next = e.nextSibling;
304 if (e.nodeType === 1 && ignoreTags.indexOf(e.tagName) === -1) {
311 if (!Array.prototype.indexOf || !document.createDocumentFragment) {
314 ticketSiteList = getSiteListFromBody();
315 jiraProjects = getJiraProjectsFromBody();
316 jiraDefaultInfo = getJiraDefaultInfoFromBody();
317 if (jiraDefaultInfo || (jiraProjects && jiraProjects.length > 0)) {
318 ticketSiteList.push(getJiraSite());
320 var target = document.getElementById('body');
323 function confirmEditFormLeaving() {
325 if (typeof s !== 'string') {
328 return s.replace(/^\s+|\s+$/g, '');
330 if (!document.querySelector) return;
331 var canceled = false;
332 var pluginNameE = document.querySelector('#pukiwiki-site-properties .plugin-name');
333 if (!pluginNameE) return;
334 var originalText = null;
335 if (pluginNameE.value !== 'edit') return;
336 var editForm = document.querySelector('.edit_form form._plugin_edit_edit_form');
337 if (!editForm) return;
338 var cancelMsgE = editForm.querySelector('#_msg_edit_cancel_confirm');
339 var unloadBeforeMsgE = editForm.querySelector('#_msg_edit_unloadbefore_message');
340 var textArea = editForm.querySelector('textarea[name="msg"]');
341 if (!textArea) return;
342 originalText = textArea.value;
343 var isPreview = false;
344 var inEditE = document.querySelector('#pukiwiki-site-properties .page-in-edit');
345 if (inEditE && inEditE.value) {
346 isPreview = (inEditE.value === 'true');
348 var cancelForm = document.querySelector('.edit_form form._plugin_edit_cancel');
349 var submited = false;
350 editForm.addEventListener('submit', function() {
354 cancelForm.addEventListener('submit', function(e) {
357 if (trim(textArea.value) === trim(originalText)) {
361 var message = 'The text you have entered will be discarded. Is it OK?';
362 if (cancelMsgE && cancelMsgE.value) {
363 message = cancelMsgE.value;
365 if (window.confirm(message)) { // eslint-disable-line no-alert
373 window.addEventListener('beforeunload', function(e) {
374 if (canceled) return;
375 if (submited) return;
377 if (trim(textArea.value) === trim(originalText)) return;
379 var message = 'Data you have entered will not be saved.';
380 if (unloadBeforeMsgE && unloadBeforeMsgE.value) {
381 message = unloadBeforeMsgE.value;
383 e.returnValue = message;
386 function showPagePassage() {
389 * @param {string} dateText
391 function getSimplePassage(dateText, now) {
395 var units = [{u: 'm', max: 60}, {u: 'h', max: 24}, {u: 'd', max: 1}];
397 d.setTime(Date.parse(dateText));
398 var t = (now.getTime() - d.getTime()) / (1000 * 60); // minutes
399 var unit = units[0].u; var card = units[0].max;
400 for (var i = 0; i < units.length; i++) {
401 unit = units[i].u; card = units[i].max;
405 return '' + Math.floor(t) + unit;
409 * @param {string} dateText
411 function getPassage(dateText, now) {
412 return '(' + getSimplePassage(dateText, now) + ')';
414 var now = new Date();
415 var elements = document.getElementsByClassName('page_passage');
416 forEach(elements, function(e) {
417 var dt = e.getAttribute('data-mtime');
419 var d = new Date(dt);
420 e.textContent = ' ' + getPassage(d, now);
423 var links = document.getElementsByClassName('link_page_passage');
424 forEach(links, function(e) {
425 var dt = e.getAttribute('data-mtime');
427 var d = new Date(dt);
429 e.title = e.title + ' ' + getPassage(d, now);
431 e.title = e.textContent + ' ' + getPassage(d, now);
435 var simplePassages = document.getElementsByClassName('simple_passage');
436 forEach(simplePassages, function(e) {
437 var dt = e.getAttribute('data-mtime');
439 var d = new Date(dt);
440 e.textContent = getSimplePassage(d, now);
444 var newItems = document.getElementsByClassName('__plugin_new');
445 forEach(newItems, function(e) {
446 var dt = e.getAttribute('data-mtime');
448 var d = new Date(dt);
449 var diff = now.getTime() - d.getTime();
450 var daySpan = diff / 1000 / 60 / 60 / 24;
452 e.textContent = ' New!';
453 e.title = getPassage(d, now);
454 if (e.classList && e.classList.add) {
455 e.classList.add('new1');
457 } else if (daySpan < 5) {
458 e.textContent = ' New';
459 e.title = getPassage(d, now);
460 if (e.classList && e.classList.add) {
461 e.classList.add('new5');
467 function convertExternalLinkToCushionPageLink() {
468 function domainQuote(domain) {
469 return domain.replace(/\./g, '\\.');
471 function domainsToRegex(domains) {
473 domains.forEach(function(domain) {
474 if (domain.substr(0, 2) === '*.') {
476 var apex = domain.substr(2);
477 var r = new RegExp('((^.*\\.)|^)' + domainQuote(apex) + '$', 'i');
481 regexList.push(new RegExp('^' + domainQuote(domain) + '$', 'i'));
486 function domainMatch(domain, regexList) {
487 for (var i = 0, n = regexList.length; i < n; i++) {
488 if (regexList[i].test(domain)) {
494 function removeCushionPageLinks() {
495 var links = document.querySelectorAll('a.external-link');
496 forEach(links, function(link) {
497 var originalUrl = link.getAttribute('data-original-url');
499 link.setAttribute('href', originalUrl);
503 if (!document.querySelector || !JSON) return;
504 if (!Array || !Array.prototype || !Array.prototype.indexOf) return;
505 var extLinkDef = document.querySelector('#pukiwiki-site-properties .external-link-cushion');
506 if (!extLinkDef || !extLinkDef.value) return;
507 var extLinkInfo = JSON.parse(extLinkDef.value);
508 if (!extLinkInfo) return;
509 var refInternalDomains = extLinkInfo.internal_domains;
510 var silentExternalDomains = extLinkInfo.silent_external_domains;
511 if (!Array.isArray(refInternalDomains)) {
512 refInternalDomains = [];
514 var internalDomains = refInternalDomains.slice();
515 var location = document.location;
516 if (location.protocol === 'file:') {
517 removeCushionPageLinks();
520 if (location.protocol !== 'http:' && location.protocol !== 'https:') return;
521 if (internalDomains.indexOf(location.hostname) < 0) {
522 internalDomains.push(location.hostname);
524 if (!Array.isArray(silentExternalDomains)) {
525 silentExternalDomains = [];
527 var propsE = document.querySelector('#pukiwiki-site-properties .site-props');
528 if (!propsE || !propsE.value) return;
529 var siteProps = JSON.parse(propsE.value);
530 var sitePathname = siteProps && siteProps.base_uri_pathname;
531 if (!sitePathname) return;
532 var internalDomainsR = domainsToRegex(internalDomains);
533 var silentExternalDomainsR = domainsToRegex(silentExternalDomains);
534 var links = document.querySelectorAll('a:not(.external-link):not(.internal-link)');
535 var classListEnabled = null;
536 forEach(links, function(link) {
537 if (classListEnabled === null) {
538 classListEnabled = link.classList && link.classList.add && true;
540 if (!classListEnabled) return;
541 var href = link.getAttribute('href');
542 if (!href) return; // anchor without href attribute (a name)
543 var m = href.match(/^https?:\/\/([0-9a-zA-Z.-]+)(:\d+)?/);
546 if (domainMatch(host, internalDomainsR)) {
547 link.classList.add('internal-link');
549 if (domainMatch(host, silentExternalDomainsR) ||
550 link.textContent.replace(/\s+/g, '') === '') {
551 // Don't show extenal link icons on these domains
552 link.classList.add('external-link-silent');
554 link.classList.add('external-link');
555 link.setAttribute('title', href);
556 link.setAttribute('data-original-url', href);
557 link.setAttribute('href', sitePathname + '?cmd=external_link&url=' + encodeURIComponent(href));
560 link.classList.add('internal-link');
564 function makeTopicpathTitle() {
565 if (!document.createDocumentFragment || !window.JSON) return;
566 var sitePropE = document.querySelector('#pukiwiki-site-properties');
567 if (!sitePropE) return;
568 var pageNameE = sitePropE.querySelector('.page-name');
569 if (!pageNameE || !pageNameE.value) return;
570 var pageName = pageNameE.value;
571 var topicpathE = sitePropE.querySelector('.topicpath-links');
572 if (!topicpathE || !topicpathE.value) return;
573 var topicpathLinks = JSON.parse(topicpathE.value);
574 if (!topicpathLinks) return;
575 var titleH1 = document.querySelector('h1.title');
576 if (!titleH1) return;
577 var aList = titleH1.querySelectorAll('a');
578 if (!aList || aList.length > 1) return;
579 var a = titleH1.querySelector('a');
581 if (a.textContent !== pageName) return;
582 var fragment = document.createDocumentFragment();
583 for (var i = 0, n = topicpathLinks.length; i < n; i++) {
584 var path = topicpathLinks[i];
585 var a1 = document.createElement('a');
586 a1.setAttribute('href', path.uri);
587 a1.setAttribute('title', path.page);
588 a1.textContent = path.leaf;
589 fragment.appendChild(a1);
590 var span = document.createElement('span');
591 span.className = 'topicpath-slash';
592 span.textContent = '/';
593 fragment.appendChild(span);
595 var a2 = document.createElement('a');
596 a2.setAttribute('href', a.getAttribute('href'));
597 a2.setAttribute('title', 'Backlinks');
598 a2.textContent = a.textContent.replace(/^.+\//, '');
599 fragment.appendChild(a2);
600 a.parentNode.replaceChild(fragment, a);
604 confirmEditFormLeaving();
606 convertExternalLinkToCushionPageLink();
607 makeTopicpathTitle();