wiki-plugin-hitchhiker 0.4.0 → 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/client/wiki-ux.js +23 -1784
- package/client/wiki-ux.js.map +7 -0
- package/package.json +74 -4
- package/pages/about-hitchhiker-plugin +16 -0
package/client/wiki-ux.js
CHANGED
|
@@ -1,1800 +1,39 @@
|
|
|
1
|
-
/*
|
|
2
|
-
* wiki-ux.
|
|
3
|
-
*
|
|
4
|
-
* Loaded globally on every page by the wiki-security-hitchhiker bootstrap
|
|
5
|
-
* (its client/security.js injects a <script> tag for this file). It runs in the
|
|
6
|
-
* browser AFTER the wiki client bundle, and reaches the client only through the
|
|
7
|
-
* public `window.wiki` / `window.plugins` surface — it never patches the bundle.
|
|
8
|
-
*
|
|
9
|
-
* It installs three overrides:
|
|
10
|
-
* 1. Factory chooser — a friendlier, curated, searchable plugin picker
|
|
11
|
-
* (keeps the built-in factory `bind`, so every existing
|
|
12
|
-
* drag-and-drop behaviour via drop.js is preserved).
|
|
13
|
-
* 2. Extra drops — a capture-phase drop handler that recognises a few
|
|
14
|
-
* media URLs the built-in factory would otherwise punt.
|
|
15
|
-
* 3. Page-flag click — opens an in-lineup "Tools" page for the clicked page
|
|
16
|
-
* instead of reopening the origin site directly (the
|
|
17
|
-
* original behaviour is preserved as the first tool).
|
|
18
|
-
*
|
|
19
|
-
* First pass / pilot: private farm only. Interactive, JS-backed tools on the
|
|
20
|
-
* Tools page are intentionally left for a follow-up (a small toolkit item type
|
|
21
|
-
* on wiki-plugin-hitchhiker); for now the Tools page offers links + wikilinks.
|
|
22
|
-
*/
|
|
23
|
-
;(function () {
|
|
24
|
-
'use strict'
|
|
1
|
+
/* wiki-plugin-hitchhiker 0.5.1 */
|
|
2
|
+
(()=>{(function(){"use strict";var P=!1,S=0,j=setInterval(function(){S++;var e=window.wiki&&window.plugins&&window.plugins.factory&&window.plugins.factory.bind&&window.$&&typeof window.wiki.pageHandler<"u";if(e&&!P){P=!0,clearInterval(j);try{_()}catch(t){console.error("[wiki-ux] install failed",t)}}else S>100&&clearInterval(j)},200),p="font-size:1.05em;min-height:44px;padding:10px 18px;cursor:pointer;",z='<svg viewBox="0 0 16 16" width="1em" height="1em" fill="currentColor" style="vertical-align:-0.15em;margin-right:.4em" aria-hidden="true"><path d="M5 5.372v.878c0 .414.336.75.75.75h4.5a.75.75 0 0 0 .75-.75v-.878a2.25 2.25 0 1 1 1.5 0v.878a2.25 2.25 0 0 1-2.25 2.25h-1.5v2.128a2.251 2.251 0 1 1-1.5 0V8.5h-1.5A2.25 2.25 0 0 1 3.5 6.25v-.878a2.25 2.25 0 1 1 1.5 0ZM5 3.25a.75.75 0 1 0-1.5 0 .75.75 0 0 0 1.5 0Zm6.75.75a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5Zm-3 8.75a.75.75 0 1 0-1.5 0 .75.75 0 0 0 1.5 0Z"></path></svg>',k=!1,dt={"*":{recommend:["paragraph","markdown","image","html","reference"],hide:[]}};function _(){st(),B(),X(),M(),U(),G(),console.log("[wiki-ux] installed")}function F(e){if(e.hasClass("hearth-page")||e.find(".item.hearth").length)return!0;try{var t=window.wiki.lineup.atKey(e.data("key")),a=t&&t.getRawPage&&t.getRawPage().story;return!!(a&&a.some(function(i){return i.type==="hearth"}))}catch{return!1}}function H(e,t){var a=/^https?:/.test(e)?e:location.protocol+"//"+e;return fetch(a+"/"+t+".json").then(function(i){return i.ok?i.json():null}).then(function(i){return!!(i&&i.story&&i.story.some(function(r){return r.type==="hearth"}))}).catch(function(){return!1})}function A(e){window.wiki&&window.wiki.showResult;var t=document.createElement("div");t.textContent="\u{1F3E0} "+e,t.style.cssText="position:fixed;left:50%;bottom:28px;transform:translateX(-50%);z-index:99999;background:rgba(200,120,40,0.95);color:#fff;padding:10px 16px;border-radius:8px;font:14px/1.4 -apple-system,sans-serif;box-shadow:0 4px 16px rgba(0,0,0,.3);max-width:80vw;",document.body.appendChild(t),setTimeout(function(){t.remove()},4200)}function U(){document.addEventListener("dragstart",function(e){var t=e.target&&e.target.closest?e.target.closest(".page h1 a"):null;t&&F(window.$(t).closest(".page"))&&(e.preventDefault(),e.stopImmediatePropagation(),A("This page keeps to its hearth \u2014 it will not be dragged out."))},!0)}function B(){document.addEventListener("click",function(e){var t=e.target,a=t&&t.closest?t.closest(".page h1 a"):null;if(a&&(e.preventDefault(),e.stopImmediatePropagation(),!a.closest(".page.wiki-ux-tools"))){if(e.shiftKey){try{W(a)}catch{R(a)}return}try{ot(a)}catch(i){console.error("[wiki-ux] flag page failed, opening origin site instead",i),R(a)}}},!0)}function C(e,t,a){var i=window.wiki,r=t==="welcome-visitors"?"view/welcome-visitors":"view/welcome-visitors/view/"+t;if(e.hasClass("remote")){var n=a;if(!n)try{var o=i.lineup.atKey(e.data("key"));n=o&&o.getRemoteSite&&o.getRemoteSite()}catch{}if(n)return i.site(n).getDirectURL(r)}return"/"+r}function W(e){var t=window.$(e).closest(".page"),a=t.attr("id");window.location.assign(C(t,a,h(t)))}function h(e){if(!e.hasClass("remote"))return"";try{var t=window.wiki.lineup.atKey(e.data("key"));return t&&t.getRemoteSite&&t.getRemoteSite()||e.data("site")||""}catch{return e.data("site")||""}}function m(e,t,a){a=a||{};var i=window.$,r=window.wiki,n=e.prevAll(".page").length;r.showResult(t);var o=i(".main > .page").last();o.insertAfter(e);try{r.lineup.changePageIndex(o.data("key"),n+1)}catch{}o.addClass("wiki-ux-tools"),a.ghost||o.removeClass("ghost");try{o[0].scrollIntoView({behavior:"smooth",inline:"center",block:"nearest"})}catch{}return o}function M(){document.addEventListener("click",function(e){var t=e.target;if(!(!t||!t.closest)){var a=t.closest(".wiki-ux-tool-fork")?"fork":t.closest(".wiki-ux-tool-sim")?"sim":t.closest(".wiki-ux-tool-trash")?"trash":t.closest(".wiki-ux-tool-clean")?"clean":t.closest(".wiki-ux-tool-flag")?"flag":t.closest(".wiki-ux-tool-toolpage")?"toolpage":null;if(a){e.preventDefault();var i=window.$,r=t.closest(".wiki-ux-tool-"+a),n=i(r).closest(".page"),o=n.prev(".page");try{a==="fork"?N(o,n):a==="sim"?q(o,n):a==="trash"?V(o,n):a==="clean"?Z(o,n):a==="flag"?window.plugins.fishPicker&&window.plugins.fishPicker.open(o,n):a==="toolpage"&&K(o,n,r.getAttribute("data-slug"))}catch(s){console.error('[wiki-ux] tool "'+a+'" failed',s)}}}},!0)}var D="demo.localhost";function K(e,t,a){if(a){var i=function(r){return fetch("//"+r+"/"+a+".json").then(function(n){if(!n.ok)throw new Error(""+n.status);return n.json()})};i(location.host).catch(function(){return i(D)}).then(function(r){m(e,window.wiki.newPage(r),{ghost:!0}),t.remove()}).catch(function(){window.alert('Tool page "'+a+'" not found on '+location.host+" or "+D+".")})}}function N(e,t){var a=e.find(".fork-page");if(a.length){a.trigger("click"),t.remove();return}if(window.isAuthenticated!==!0||/_rev\d+$/.test(e.attr("id")||"")){console.warn("[wiki-ux] fork: no .fork-page control on the source page");return}var i={type:"fork"};if(e.hasClass("local"))e.removeClass("local");else{var r=h(e);r&&(i.site=r)}e.removeClass("ghost"),window.wiki.pageHandler.put(e,i),t.remove()}function V(e,t){var a=window.wiki,i=e.data("data")||{},r=i.title||e.attr("id")||"this page";if(window.confirm('Trash "'+r+`"?
|
|
25
3
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
// from a matching site (e.g. bot.pi5) shows a single explicit "run · from
|
|
30
|
-
// <site>" button — never auto-run — that runs on the viewer's own loopback
|
|
31
|
-
// shell. A per-browser list, editable without redeploy (localStorage). Vouch
|
|
32
|
-
// for / revoke a site from the console: window.trustAuthor('bot.pi5').
|
|
33
|
-
try {
|
|
34
|
-
var TRUST_KEY = 'wiki-trusted-authors'
|
|
35
|
-
var loadTrust = function () {
|
|
36
|
-
try { return JSON.parse(localStorage.getItem(TRUST_KEY)) || [] } catch (e) { return [] }
|
|
37
|
-
}
|
|
38
|
-
window.trustedAuthors = loadTrust()
|
|
39
|
-
window.trustAuthor = function (site) {
|
|
40
|
-
var l = loadTrust()
|
|
41
|
-
if (site && l.indexOf(site) === -1) l.push(site)
|
|
42
|
-
localStorage.setItem(TRUST_KEY, JSON.stringify(l))
|
|
43
|
-
return (window.trustedAuthors = l)
|
|
44
|
-
}
|
|
45
|
-
window.untrustAuthor = function (site) {
|
|
46
|
-
var l = loadTrust().filter(function (s) { return s !== site })
|
|
47
|
-
localStorage.setItem(TRUST_KEY, JSON.stringify(l))
|
|
48
|
-
return (window.trustedAuthors = l)
|
|
49
|
-
}
|
|
50
|
-
} catch (e) { window.trustedAuthors = window.trustedAuthors || [] }
|
|
4
|
+
It moves to the recycler and can be recovered from Recycled Pages.`)){var n;try{n=a.lineup.atKey(e.data("key"))}catch{}if(!n){window.alert("Could not identify the page to trash.");return}a.pageHandler.delete(n,e,function(o){if(o){window.alert("Trash failed: "+o);return}t.remove(),e.remove()})}}function Z(e,t){var a=window.wiki,i=e.data("data")||{},r=i.title||e.attr("id")||"this page",n=i.story||[],o={title:r,story:n,journal:[{type:"create",item:{title:r,story:n},date:Date.now(),certificate:"from marvin"}]};a.showResult(a.newPage(o),{$page:t})}function G(){fetch("/system/factories.json").then(function(e){return e.ok?e.json():[]}).then(function(e){window.catalog||(window.catalog=e),k=(e||[]).some(function(t){return t&&t.name==="Similarity"})}).catch(function(){})}function q(e,t){var a=e.data("data")||{},i=a.title||e.attr("id")||"this page",r=(e.attr("id")||"").split("_rev")[0],n=h(e)||location.hostname,o=t.find(".wiki-ux-tool-sim");o.prop("disabled",!0).text("\u{1F50D} Searching\u2026");var s=[i].concat((a.story||[]).filter(function(u){return u.type==="markdown"||u.type==="paragraph"}).map(function(u){return(u.text||"").trim()}).filter(Boolean)).join(`
|
|
5
|
+
`).slice(0,2e3);fetch(location.origin+"/system/search-report.json",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({query:i,seed:{site:n,slug:r},excludePage:{site:n,slug:r},text:s,domains:["*"],limit:12})}).then(function(u){if(!u.ok)throw new Error("search-report failed: "+u.status);return u.json()}).then(function(u){m(t,window.wiki.newPage(u),{ghost:!0}),o.prop("disabled",!1).text("\u{1F50D} Similarity search")}).catch(function(u){console.error("[wiki-ux] similarity failed",u),o.prop("disabled",!1).text("\u{1F50D} Similarity search"),window.alert("Similarity search failed: "+u.message)})}function Y(e){var t=window.wiki,a=t.lineup.crumbs(e.data("key"),location.host),i=a[0],r=t.site(i).getDirectURL("").split("/")[0];return r===""&&(r=window.location.protocol),{url:r+"//"+a.join("/"),target:i}}function R(e){var t=window.$,a=t(e).closest(".page"),i=Y(a);window.open(i.url,i.target).focus()}var g="wiki-ux-drop-target",J=["text/uri-list","text/x-moz-url","text/plain","text/html","Files"];function X(){var e=function(i){return i&&i.closest?i.closest(".page h1 a"):null},t=function(i){if(!i||!i.types)return!1;for(var r=0;r<i.types.length;r++)if(J.indexOf(i.types[r])!==-1)return!0;return!1},a=function(){for(var i=document.querySelectorAll("."+g),r=0;r<i.length;r++)i[r].classList.remove(g)};document.addEventListener("dragover",function(i){var r=e(i.target);if(!(!r||!t(i.dataTransfer))){i.preventDefault();try{i.dataTransfer.dropEffect="copy"}catch{}r.classList.contains(g)||(a(),r.classList.add(g))}},!0),document.addEventListener("dragleave",function(i){var r=e(i.target);r&&(!i.relatedTarget||!r.contains(i.relatedTarget))&&r.classList.remove(g)},!0),document.addEventListener("drop",function(i){var r=e(i.target);if(a(),!!r){i.preventDefault(),i.stopPropagation();try{var n=window.$(r).closest(".page"),o=$(i.dataTransfer);if(o.kind==="page"&&o.site&&o.slug){H(o.site,o.slug).then(function(s){s?A("That page keeps to its hearth \u2014 it declines to be copied here."):L(n,o)});return}L(n,o)}catch(s){console.error("[wiki-ux] drop failed",s)}}},!0),document.addEventListener("dragend",a,!0)}function Q(e){var t=e.match(/^https?:\/\/([a-zA-Z0-9:.-]+)(\/([a-zA-Z0-9:.-]+)\/([a-z0-9-]+(_rev\d+)?))+$/);if(!t)return null;var a=t[1],i=t[3],r=t[4];return["view","local","origin"].indexOf(i)!==-1&&(i=a),{site:i,slug:r}}function $(e){var t={kind:"unknown",url:"",site:"",slug:"",text:"",html:"",files:[],types:[]};if(!e)return t;if(t.types=Array.prototype.slice.call(e.types||[]),e.files&&e.files.length){for(var a=0;a<e.files.length;a++){var i=e.files[a];t.files.push({name:i.name,type:i.type,size:i.size})}t.kind="file"}try{t.text=e.getData("text/plain")||""}catch{}try{t.html=e.getData("text/html")||""}catch{}var r="";try{r=e.getData("text/uri-list")||e.getData("URL")||e.getData("text/x-moz-url")||""}catch{}if(r&&(r=r.split(`
|
|
6
|
+
`)[0].trim()),!r&&/^https?:\/\//i.test(t.text)&&(r=t.text.trim()),r){t.url=r;var n=Q(r);n?(t.kind="page",t.site=n.site,t.slug=n.slug):t.kind="link"}else if(t.kind==="unknown"){var o=(t.text||"").trim();o&&/^[a-z0-9.-]+\.[a-z]{2,}$/i.test(o)?(t.kind="site",t.site=o):o&&(t.kind="text")}return t}var v=null;function tt(){return v||(v=fetch(location.origin+"/system/outposts.json").then(function(e){return e.ok?e.json():{}}).then(function(e){return(e&&Array.isArray(e.unfurl)?e.unfurl:[]).filter(Boolean)}).catch(function(){return[]}),v)}function et(e){for(var t=e.slice(),a=t.length-1;a>0;a--){var i=Math.floor(Math.random()*(a+1)),r=t[a];t[a]=t[i],t[i]=r}return t}function O(e){return tt().then(function(t){var a=et(t),i=function(r){if(r>=a.length)return null;var n=typeof AbortController=="function"?new AbortController:null,o=n&&setTimeout(function(){n.abort()},2e3);return fetch(a[r]+"/system/unfurl.json?url="+encodeURIComponent(e),n?{signal:n.signal}:{}).then(function(s){return s.ok?s.json():null}).catch(function(){return null}).then(function(s){return o&&clearTimeout(o),s&&typeof s=="object"&&"title"in s?(s.outpost=a[r],s):i(r+1)})};return i(0)})}function it(e){var t=e.image?'<img src="'+e.image+'" style="width:100%;max-height:240px;object-fit:cover;display:block">':"",a=e.provider?'<div style="font-size:12px;opacity:.7">'+c(e.provider)+"</div>":"",i=e.description?'<div style="font-size:13px;opacity:.85;margin-top:4px">'+c(e.description)+"</div>":"";return'<div style="border:1px solid rgba(128,128,128,.35);border-radius:8px;overflow:hidden;max-width:480px">'+t+'<div style="padding:8px 10px">'+a+'<div style="font-weight:600;font-size:15px"><a href="'+c(e.url)+'" target="_blank" rel="noopener">'+c(e.title||e.url)+"</a></div>"+i+'<div style="font-size:11px;opacity:.55;margin-top:6px;word-break:break-all">'+c(e.url)+"</div></div></div>"}var w=null;function at(){return w?Promise.resolve(w):fetch(location.origin+"/system/farm-domains.json").then(function(e){return e.ok?e.json():{local:[],public:[]}}).then(function(e){return w=e,e}).catch(function(){return w={local:[],public:[]},w})}function rt(e,t){return t&&t.local&&t.local.indexOf(e)!==-1?"local":t&&t.public&&t.public.indexOf(e)!==-1?"public":"federation"}function E(e){return Array.isArray(e)?e:e&&Array.isArray(e.data)?e.data:null}function nt(e,t){return new Promise(function(a){var i=function(n){if(!n)return a({isWiki:!1,exists:!1,title:""});for(var o=null,s=0;s<n.length;s++)if(n[s]&&n[s].slug===t){o=n[s];break}a({isWiki:!0,exists:!!o,title:o?o.title||t:""})};try{var r=window.wiki&&window.wiki.neighborhood&&window.wiki.neighborhood[e];if(r&&r.sitemap)return i(E(r.sitemap));window.wiki.site(e).get("system/sitemap.json",function(n,o){i(n?null:E(o))})}catch{a({isWiki:!1,exists:!1,title:""})}})}function L(e,t){t.kind==="page"?Promise.all([nt(t.site,t.slug),at()]).then(function(a){var i=a[0];i.isWiki?(t.wikiTitle=i.title||t.slug,t.pageExists=i.exists,t.farm=rt(t.site,a[1]),t.kind=t.farm==="federation"?"federation-page":"farm-page",y(e,t,null)):(t.kind="link",O(t.url).then(function(r){y(e,t,r)}))}):t.url?O(t.url).then(function(a){y(e,t,a)}):y(e,t,null)}function y(e,t,a){var i=window.wiki,r=e.data("data")||{},n=r.title||e.attr("id")||"this page",o={"farm-page":"a page on our farm","federation-page":"a page in the wider federation",site:"a wiki site",link:"a link / URL",text:"plain text",file:"a file"}[t.kind]||"an unrecognised drop",s=function(d){return(d||"").slice(0,2e3)},u=[{type:"markdown",id:l(),text:"# \u{1F4E5} Dropped onto "+n+`
|
|
51
7
|
|
|
52
|
-
|
|
53
|
-
var RECENT_MAX = 6
|
|
8
|
+
You dropped **`+o+"** onto its flag."}];if(t.kind==="farm-page"||t.kind==="federation-page"){var x=t.farm==="local"?"our **local** farm":t.farm==="public"?"our **public** farm":"the **wider federation**";u.push({type:"markdown",id:l(),text:"# "+(t.wikiTitle||t.slug)+`
|
|
54
9
|
|
|
55
|
-
|
|
56
|
-
// the hitchhiker item's big fork button in hitchhiker.js).
|
|
57
|
-
var BTN = 'font-size:1.05em;min-height:44px;padding:10px 18px;cursor:pointer;'
|
|
58
|
-
// GitHub "repo-forked" glyph — the standard fork icon (not the ⚑ flag).
|
|
59
|
-
var FORK_SVG =
|
|
60
|
-
'<svg viewBox="0 0 16 16" width="1em" height="1em" fill="currentColor" ' +
|
|
61
|
-
'style="vertical-align:-0.15em;margin-right:.4em" aria-hidden="true">' +
|
|
62
|
-
'<path d="M5 5.372v.878c0 .414.336.75.75.75h4.5a.75.75 0 0 0 .75-.75v-.878a2.25 2.25 0 1 1 1.5 0v.878a2.25 ' +
|
|
63
|
-
'2.25 0 0 1-2.25 2.25h-1.5v2.128a2.251 2.251 0 1 1-1.5 0V8.5h-1.5A2.25 2.25 0 0 1 3.5 6.25v-.878a2.25 2.25 0 1 1 ' +
|
|
64
|
-
'1.5 0ZM5 3.25a.75.75 0 1 0-1.5 0 .75.75 0 0 0 1.5 0Zm6.75.75a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5Zm-3 ' +
|
|
65
|
-
'8.75a.75.75 0 1 0-1.5 0 .75.75 0 0 0 1.5 0Z"></path></svg>'
|
|
10
|
+
A wiki page on `+x+" \u2014 `"+c(t.site)+"`."+(t.pageExists?"":`
|
|
66
11
|
|
|
67
|
-
|
|
68
|
-
var simInstalled = false
|
|
12
|
+
_(not listed in the site sitemap \u2014 may be new or removed)_`)}),u.push({type:"reference",id:l(),site:t.site,slug:t.slug,title:t.wikiTitle||t.slug,text:t.farm==="federation"?"federated page":"our page"})}a&&(a.title||a.image)&&u.push({type:"html",id:l(),text:it(a)}),u.push({type:"markdown",id:l(),text:"# Details"}),t.kind==="farm-page"||t.kind==="federation-page"?u.push({type:"markdown",id:l(),text:`# Wiki page
|
|
69
13
|
|
|
70
|
-
|
|
71
|
-
// chooser for a host; `recommend` floats them to the top. Host '*' applies to all.
|
|
72
|
-
var CURATION = {
|
|
73
|
-
'*': { recommend: ['paragraph', 'markdown', 'image', 'html', 'reference'], hide: [] },
|
|
74
|
-
}
|
|
14
|
+
- site: \``+c(t.site)+"`\n- slug: `"+c(t.slug)+"`\n- farm: `"+c(t.farm)+"`\n- in sitemap: `"+(t.pageExists?"yes":"no")+"`"}):t.kind==="site"&&u.push({type:"markdown",id:l(),text:`# Wiki site
|
|
75
15
|
|
|
76
|
-
|
|
77
|
-
var installed = false
|
|
78
|
-
var tries = 0
|
|
79
|
-
var timer = setInterval(function () {
|
|
80
|
-
tries++
|
|
81
|
-
var ready =
|
|
82
|
-
window.wiki &&
|
|
83
|
-
window.plugins &&
|
|
84
|
-
window.plugins.factory &&
|
|
85
|
-
window.plugins.factory.bind &&
|
|
86
|
-
window.$ &&
|
|
87
|
-
typeof window.wiki.pageHandler !== 'undefined'
|
|
88
|
-
if (ready && !installed) {
|
|
89
|
-
installed = true
|
|
90
|
-
clearInterval(timer)
|
|
91
|
-
try {
|
|
92
|
-
install()
|
|
93
|
-
} catch (e) {
|
|
94
|
-
console.error('[wiki-ux] install failed', e)
|
|
95
|
-
}
|
|
96
|
-
} else if (tries > 100) {
|
|
97
|
-
clearInterval(timer) // ~20s; give up quietly
|
|
98
|
-
}
|
|
99
|
-
}, 200)
|
|
16
|
+
\``+c(t.site)+"` \u2014 [welcome](//"+c(t.site)+"/view/welcome-visitors)"}),t.url&&u.push({type:"markdown",id:l(),text:`# URL
|
|
100
17
|
|
|
101
|
-
|
|
102
|
-
installFactoryChooser()
|
|
103
|
-
installExtraDrops()
|
|
104
|
-
installStyles()
|
|
105
|
-
installFlagToolPage()
|
|
106
|
-
installDropTarget()
|
|
107
|
-
installToolButtons()
|
|
108
|
-
installFishFlag()
|
|
109
|
-
installHearthGuard()
|
|
110
|
-
detectSimilarity()
|
|
111
|
-
reEmitOpenFactories()
|
|
112
|
-
console.log('[wiki-ux] installed: chooser + extra-drops + flag tool page + fish picker + hearth guard')
|
|
113
|
-
}
|
|
18
|
+
\``+t.url+"`"}),t.text&&t.text!==t.url&&u.push({type:"markdown",id:l(),text:"# Text\n\n```\n"+s(t.text)+"\n```"}),t.html&&u.push({type:"markdown",id:l(),text:"# HTML\n\n```\n"+s(c(t.html))+"\n```"}),t.files.length&&u.push({type:"markdown",id:l(),text:`# Files
|
|
114
19
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
// The mark (halo + notice) is rendered by wiki-plugin-hearth; the teeth are
|
|
118
|
-
// here, farm-wide: block dragging its flag out, and refuse to import a
|
|
119
|
-
// hearth-marked page dropped in. Enforcement is total only where our client
|
|
120
|
-
// runs — outside, the item's own text is the ask. See hearth-pages spec.
|
|
121
|
-
// ===========================================================================
|
|
122
|
-
function pageIsHearth($page) {
|
|
123
|
-
// rendered marker (hearth item emit adds the class) or raw story fallback
|
|
124
|
-
if ($page.hasClass('hearth-page') || $page.find('.item.hearth').length) return true
|
|
125
|
-
try {
|
|
126
|
-
var po = window.wiki.lineup.atKey($page.data('key'))
|
|
127
|
-
var story = po && po.getRawPage && po.getRawPage().story
|
|
128
|
-
return !!(story && story.some(function (i) { return i.type === 'hearth' }))
|
|
129
|
-
} catch (e) {
|
|
130
|
-
return false
|
|
131
|
-
}
|
|
132
|
-
}
|
|
20
|
+
`+t.files.map(function(d){return"- "+c(d.name)+" ("+c(d.type||"?")+", "+d.size+" bytes)"}).join(`
|
|
21
|
+
`)}),u.push({type:"markdown",id:l(),text:`# Raw dataTransfer types
|
|
133
22
|
|
|
134
|
-
|
|
135
|
-
function fetchIsHearth(site, slug) {
|
|
136
|
-
var url = /^https?:/.test(site) ? site : location.protocol + '//' + site
|
|
137
|
-
return fetch(url + '/' + slug + '.json')
|
|
138
|
-
.then(function (r) { return r.ok ? r.json() : null })
|
|
139
|
-
.then(function (p) {
|
|
140
|
-
return !!(p && p.story && p.story.some(function (i) { return i.type === 'hearth' }))
|
|
141
|
-
})
|
|
142
|
-
.catch(function () { return false })
|
|
143
|
-
}
|
|
23
|
+
\``+c((t.types||[]).join(", ")||"(none)")+"`"}),m(e,i.newPage({title:n+" \u{1F4E5}",story:u,journal:[]}),{ghost:!0})}function ot(e){var t=window.$,a=window.wiki,i=t(e).closest(".page"),r=i.data("data")||{},n=r.title||i.attr("id")||"this page",o=i.attr("id"),s=location.host,u=window.isAuthenticated===!0,x=C(i,o,h(i)),d=[{type:"markdown",id:l(),text:`# Simplify
|
|
144
24
|
|
|
145
|
-
|
|
146
|
-
if (window.wiki && window.wiki.showResult) {
|
|
147
|
-
// reuse the client's own flash if present; else a minimal toast
|
|
148
|
-
}
|
|
149
|
-
var t = document.createElement('div')
|
|
150
|
-
t.textContent = '\u{1F3E0} ' + msg
|
|
151
|
-
t.style.cssText =
|
|
152
|
-
'position:fixed;left:50%;bottom:28px;transform:translateX(-50%);z-index:99999;' +
|
|
153
|
-
'background:rgba(200,120,40,0.95);color:#fff;padding:10px 16px;border-radius:8px;' +
|
|
154
|
-
'font:14px/1.4 -apple-system,sans-serif;box-shadow:0 4px 16px rgba(0,0,0,.3);max-width:80vw;'
|
|
155
|
-
document.body.appendChild(t)
|
|
156
|
-
setTimeout(function () { t.remove() }, 4200)
|
|
157
|
-
}
|
|
25
|
+
Collapse the lineup down to just **Welcome Visitors** and **`+n+"**, clearing away the trail of pages you clicked through to get here."},{type:"html",id:l(),text:'<p><a class="wiki-ux-tool-simplify" href="'+c(x)+'"><button style="'+p+'">\u2302 Simplify to Welcome Visitors + '+c(n)+"</button></a></p>"}],I=k?[{type:"markdown",id:l(),text:`# \u{1F527} Tool pages
|
|
158
26
|
|
|
159
|
-
|
|
160
|
-
// outbound: block the drag of a hearth page's flag before it can leave
|
|
161
|
-
document.addEventListener(
|
|
162
|
-
'dragstart',
|
|
163
|
-
function (e) {
|
|
164
|
-
var a = e.target && e.target.closest ? e.target.closest('.page h1 a') : null
|
|
165
|
-
if (!a) return
|
|
166
|
-
if (pageIsHearth(window.$(a).closest('.page'))) {
|
|
167
|
-
e.preventDefault()
|
|
168
|
-
e.stopImmediatePropagation()
|
|
169
|
-
hearthToast('This page keeps to its hearth — it will not be dragged out.')
|
|
170
|
-
}
|
|
171
|
-
},
|
|
172
|
-
true,
|
|
173
|
-
)
|
|
174
|
-
}
|
|
27
|
+
Open a tool page beside **`+n+"** \u2014 its searches take this page as their subject."},{type:"html",id:l(),text:'<p><button class="wiki-ux-tool-toolpage" data-slug="similar-page-tool" style="'+p+'">\u{1F50D} Similar Page Tool</button> <button class="wiki-ux-tool-toolpage" data-slug="search-related-page" style="'+p+'">\u{1F5C2}\uFE0F Search Related Page</button></p>'}]:[],f,b;if(u){b=n+" \u{1F6E0}\uFE0F";var T=i.hasClass("remote"),lt=i.hasClass("local"),ut=T||lt,ct=h(i);if(f=[{type:"paragraph",id:l(),text:"Tools for **"+n+"**."}],ut){var ft=T?"This page lives on **"+(ct||"another wiki")+"** \u2014 the blue halo. **Forking** copies it onto **"+s+"**, so you get your own editable version here and it joins your wiki. The original stays where it is; forking never changes it.":"This page has **unsaved local changes** \u2014 the yellow halo of death (it only exists in this browser). **Forking** writes it to the server here on **"+s+"**, making it a real, saved, shareable page.";f.push({type:"markdown",id:l(),text:`# Fork this page
|
|
175
28
|
|
|
176
|
-
|
|
177
|
-
// 1. Factory chooser — replace emit only, keep the original bind
|
|
178
|
-
// ===========================================================================
|
|
179
|
-
var origFactoryEmit = window.plugins.factory.emit
|
|
180
|
-
var origFactoryBind = window.plugins.factory.bind
|
|
29
|
+
`+ft}),f.push({type:"html",id:l(),text:'<p><button class="wiki-ux-tool-fork" style="'+p+'">'+z+"Fork this page to "+c(s)+"</button></p>"})}k&&(f.push({type:"markdown",id:l(),text:`# \u{1F50D} Similarity search
|
|
181
30
|
|
|
182
|
-
|
|
183
|
-
// localStorage record, set from a `new` item's FACTORY buttons/DSL).
|
|
184
|
-
// 'classic' — the default, and the fallback on any error — is the untouched
|
|
185
|
-
// built-in factory menu. 'list' (né 'chooser') is the searchable list with
|
|
186
|
-
// per-plugin summaries; 'rollover' is tabs + pills with a hover description.
|
|
187
|
-
function factoryStyle() {
|
|
188
|
-
try {
|
|
189
|
-
var d = JSON.parse(localStorage.getItem('wiki-new') || '{}')
|
|
190
|
-
if (d.factory === 'list' || d.factory === 'chooser') return 'list'
|
|
191
|
-
if (d.factory === 'rollover') return 'rollover'
|
|
192
|
-
return 'classic'
|
|
193
|
-
} catch (e) {
|
|
194
|
-
return 'classic'
|
|
195
|
-
}
|
|
196
|
-
}
|
|
31
|
+
Find pages across the whole farm that are semantically similar to **`+n+"** \u2014 seeded by this page itself; opens a ranked ghost report you can read and fork."}),f.push({type:"html",id:l(),text:'<p><button class="wiki-ux-tool-sim" style="'+p+'">\u{1F50D} Similarity search</button></p>'})),f=f.concat(I),f.push({type:"markdown",id:l(),text:`# Trash this page
|
|
197
32
|
|
|
198
|
-
|
|
199
|
-
window.plugins.factory.emit = function ($item, item) {
|
|
200
|
-
// mirror the wrapper's marker class, then render the selected style
|
|
201
|
-
$item.addClass('emit')
|
|
202
|
-
var style = factoryStyle()
|
|
203
|
-
if (style === 'classic') {
|
|
204
|
-
return origFactoryEmit.call(this, $item, item)
|
|
205
|
-
}
|
|
206
|
-
try {
|
|
207
|
-
if (style === 'rollover') rolloverEmit($item, item)
|
|
208
|
-
else chooserEmit($item, item)
|
|
209
|
-
} catch (e) {
|
|
210
|
-
console.error('[wiki-ux] factory skin "' + style + '" failed, falling back', e)
|
|
211
|
-
return origFactoryEmit.call(this, $item, item)
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
}
|
|
33
|
+
Move **`+n+"** to the recycler. It disappears from the site but is not destroyed \u2014 recover it any time from [[Recycled Pages]]."}),f.push({type:"html",id:l(),text:'<p><button class="wiki-ux-tool-trash" style="'+p+'">\u{1F5D1}️ Trash this page</button></p>'}),f.push({type:"markdown",id:l(),text:`# Clean the journal
|
|
215
34
|
|
|
216
|
-
|
|
217
|
-
// The 2-3 line "what does this plugin do" text shown by the list and
|
|
218
|
-
// rollover skins. Source of truth is the plugin's own about page
|
|
219
|
-
// (about-<type>-plugin, served same-origin from the plugin package);
|
|
220
|
-
// factory.json's one-line title is the instant fallback while it loads.
|
|
221
|
-
// Cached in localStorage for a day, truncated at a word boundary.
|
|
35
|
+
Open a fresh **ghost** copy of this page whose journal is stripped back to a single "create" entry \u2014 as if written today, with no fork trail or remote attribution. Fork that ghost to keep the cleaned version.`}),f.push({type:"html",id:l(),text:'<p><button class="wiki-ux-tool-clean" style="'+p+'">\u{1F9F9} Clean the journal</button></p>'}),window.isOwner===!0&&!T&&(f.push({type:"markdown",id:l(),text:`# \u{1F41F} Site flag
|
|
222
36
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
function readSummaries() {
|
|
228
|
-
try {
|
|
229
|
-
return JSON.parse(localStorage.getItem(SUMMARY_KEY) || '{}')
|
|
230
|
-
} catch (e) {
|
|
231
|
-
return {}
|
|
232
|
-
}
|
|
233
|
-
}
|
|
234
|
-
function writeSummaries(map) {
|
|
235
|
-
try {
|
|
236
|
-
localStorage.setItem(SUMMARY_KEY, JSON.stringify(map))
|
|
237
|
-
} catch (e) {}
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
// Flatten fedwiki markdown to plain prose and cap it at ~3 small-print lines.
|
|
241
|
-
function summarize(text) {
|
|
242
|
-
var t = (text || '')
|
|
243
|
-
.replace(/\[\[([^\]]+)\]\]/g, '$1') // [[Page]] → Page
|
|
244
|
-
.replace(/\[(?:https?:\/\/\S+)\s+([^\]]+)\]/g, '$1') // [url label] → label
|
|
245
|
-
.replace(/[*_`#>]/g, '') // emphasis / heading marks
|
|
246
|
-
.replace(/\s+/g, ' ')
|
|
247
|
-
.trim()
|
|
248
|
-
if (t.length <= SUMMARY_MAX) return t
|
|
249
|
-
var cut = t.slice(0, SUMMARY_MAX)
|
|
250
|
-
return cut.slice(0, cut.lastIndexOf(' ')) + ' …'
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
// cb(text) fires at once with the best text to hand; fires again with the
|
|
254
|
-
// about-page paragraph when a fresh fetch lands.
|
|
255
|
-
function pluginSummary(entry, cb) {
|
|
256
|
-
var key = entry.name.toLowerCase()
|
|
257
|
-
var cache = readSummaries()
|
|
258
|
-
var hit = cache[key]
|
|
259
|
-
if (hit && Date.now() - hit.at < SUMMARY_TTL) {
|
|
260
|
-
cb(hit.text)
|
|
261
|
-
return
|
|
262
|
-
}
|
|
263
|
-
cb(summarize(entry.title || ''))
|
|
264
|
-
var slug = 'about-' + key.replace(/\s+/g, '-') + '-plugin'
|
|
265
|
-
fetch('/' + slug + '.json')
|
|
266
|
-
.then(function (r) {
|
|
267
|
-
return r.ok ? r.json() : null
|
|
268
|
-
})
|
|
269
|
-
.then(function (page) {
|
|
270
|
-
var first =
|
|
271
|
-
page &&
|
|
272
|
-
(page.story || []).find(function (i) {
|
|
273
|
-
return (i.type === 'markdown' || i.type === 'paragraph') && (i.text || '').trim()
|
|
274
|
-
})
|
|
275
|
-
var text = first ? summarize(first.text) : summarize(entry.title || '')
|
|
276
|
-
if (!text) return
|
|
277
|
-
var c = readSummaries()
|
|
278
|
-
c[key] = { text: text, at: Date.now() }
|
|
279
|
-
writeSummaries(c)
|
|
280
|
-
cb(text)
|
|
281
|
-
})
|
|
282
|
-
.catch(function () {})
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
function curationFor(host) {
|
|
286
|
-
var base = CURATION['*'] || { recommend: [], hide: [] }
|
|
287
|
-
var site = CURATION[host] || {}
|
|
288
|
-
return {
|
|
289
|
-
recommend: (site.recommend || base.recommend || []).slice(),
|
|
290
|
-
hide: (base.hide || []).concat(site.hide || []),
|
|
291
|
-
}
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
function getRecent() {
|
|
295
|
-
try {
|
|
296
|
-
return JSON.parse(localStorage.getItem(RECENT_KEY) || '[]')
|
|
297
|
-
} catch (e) {
|
|
298
|
-
return []
|
|
299
|
-
}
|
|
300
|
-
}
|
|
301
|
-
function pushRecent(type) {
|
|
302
|
-
try {
|
|
303
|
-
var r = getRecent().filter(function (t) {
|
|
304
|
-
return t !== type
|
|
305
|
-
})
|
|
306
|
-
r.unshift(type)
|
|
307
|
-
localStorage.setItem(RECENT_KEY, JSON.stringify(r.slice(0, RECENT_MAX)))
|
|
308
|
-
} catch (e) {}
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
// Shared stylesheet for the list and rollover factory skins.
|
|
312
|
-
var FACTORY_CSS =
|
|
313
|
-
'<style id="wiki-ux-factory-styles">' +
|
|
314
|
-
'.wiki-ux-chooser, .wiki-ux-rollover { font-family: sans-serif; }' +
|
|
315
|
-
'.wiki-ux-hint { font-size: 11.5px; color: #999; margin: 0 0 4px; }' +
|
|
316
|
-
'.wiki-ux-group h4 { margin: 10px 0 3px; font-size: .78em; letter-spacing: .06em;' +
|
|
317
|
-
' text-transform: uppercase; color: #999; font-weight: normal; }' +
|
|
318
|
-
'.wiki-ux-group ul { list-style: none; margin: 0; padding: 0; }' +
|
|
319
|
-
'.wiki-ux-row { margin: 0 0 7px; }' +
|
|
320
|
-
'.wiki-ux-pick { font-size: 14px; color: #3465a4; text-decoration: none; font-weight: normal; }' +
|
|
321
|
-
'.wiki-ux-pick:hover { text-decoration: underline; }' +
|
|
322
|
-
'.wiki-ux-sum { font-size: 11.5px; line-height: 1.35; color: #888; margin-top: 1px;' +
|
|
323
|
-
' display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; overflow: hidden; }' +
|
|
324
|
-
'.wiki-ux-rollover { padding: .5em .8em; background: #f4f4f8; border-left: 3px solid #888; }' +
|
|
325
|
-
'.wiki-ux-tabs { display: flex; gap: 4px; margin-bottom: .5em; }' +
|
|
326
|
-
'.wiki-ux-tab { padding: .2em .7em; border: 1px solid #bbb; border-radius: 3px;' +
|
|
327
|
-
' background: #fff; cursor: pointer; font-size: .85em; color: #444; }' +
|
|
328
|
-
'.wiki-ux-tab:hover { background: #e8e8f0; }' +
|
|
329
|
-
'.wiki-ux-tab.wiki-ux-active { background: #5b6fd9; color: #fff; border-color: #5b6fd9; }' +
|
|
330
|
-
'.wiki-ux-pills { min-height: 2em; }' +
|
|
331
|
-
'.wiki-ux-pillbtn { display: inline-block; padding: .15em .5em; margin: 2px 1px;' +
|
|
332
|
-
' background: #dde; border: 1px solid transparent; border-radius: 3px;' +
|
|
333
|
-
' font-size: .8em; color: #334; cursor: pointer; }' +
|
|
334
|
-
'.wiki-ux-pillbtn:hover { background: #5b6fd9; color: #fff; }' +
|
|
335
|
-
'.wiki-ux-rollover-info { margin-top: .6em; padding-top: .5em; border-top: 1px solid #ddd;' +
|
|
336
|
-
' font-size: 11.5px; line-height: 1.4; color: #777; min-height: 3.2em; }' +
|
|
337
|
-
'.wiki-ux-rollover-info .wiki-ux-info-name { color: #444; }' +
|
|
338
|
-
'</style>'
|
|
339
|
-
|
|
340
|
-
function injectFactoryCss() {
|
|
341
|
-
if (!document.getElementById('wiki-ux-factory-styles'))
|
|
342
|
-
document.head.insertAdjacentHTML('beforeend', FACTORY_CSS)
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
function chooserEmit($item, item) {
|
|
346
|
-
var $ = window.$
|
|
347
|
-
var wiki = window.wiki
|
|
348
|
-
injectFactoryCss()
|
|
349
|
-
|
|
350
|
-
// A labelled factory (item.prompt) keeps the built-in prompt behaviour.
|
|
351
|
-
if (item.prompt) {
|
|
352
|
-
return origFactoryEmit.call(window.plugins.factory, $item, item)
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
var render = function (catalog) {
|
|
356
|
-
var cur = curationFor(location.host)
|
|
357
|
-
var recent = getRecent()
|
|
358
|
-
var entries = (catalog || []).filter(function (info) {
|
|
359
|
-
return info && info.name && info.category && cur.hide.indexOf(info.name.toLowerCase()) === -1
|
|
360
|
-
})
|
|
361
|
-
|
|
362
|
-
$item.html(
|
|
363
|
-
'' +
|
|
364
|
-
'<div class="wiki-ux-chooser">' +
|
|
365
|
-
'<p class="wiki-ux-hint">Double-click to edit · drop text, an image or a link · or choose a plugin</p>' +
|
|
366
|
-
'<input class="wiki-ux-search" type="text" placeholder="Search plugins…" ' +
|
|
367
|
-
'style="width:100%;box-sizing:border-box;margin:4px 0;padding:4px 6px;">' +
|
|
368
|
-
'<div class="wiki-ux-groups"></div>' +
|
|
369
|
-
'</div>',
|
|
370
|
-
)
|
|
371
|
-
|
|
372
|
-
var $groups = $item.find('.wiki-ux-groups')
|
|
373
|
-
|
|
374
|
-
var byName = {}
|
|
375
|
-
entries.forEach(function (e) {
|
|
376
|
-
byName[e.name.toLowerCase()] = e
|
|
377
|
-
})
|
|
378
|
-
var recommended = cur.recommend
|
|
379
|
-
.concat(recent)
|
|
380
|
-
.map(function (t) {
|
|
381
|
-
return byName[t]
|
|
382
|
-
})
|
|
383
|
-
.filter(Boolean)
|
|
384
|
-
// de-dupe, preserve order
|
|
385
|
-
var seen = {}
|
|
386
|
-
recommended = recommended.filter(function (e) {
|
|
387
|
-
var k = e.name.toLowerCase()
|
|
388
|
-
if (seen[k]) return false
|
|
389
|
-
seen[k] = 1
|
|
390
|
-
return true
|
|
391
|
-
})
|
|
392
|
-
|
|
393
|
-
var drawList = function (filter) {
|
|
394
|
-
var f = (filter || '').trim().toLowerCase()
|
|
395
|
-
var match = function (e) {
|
|
396
|
-
if (!f) return true
|
|
397
|
-
return (e.name + ' ' + (e.title || '')).toLowerCase().indexOf(f) !== -1
|
|
398
|
-
}
|
|
399
|
-
var sections = []
|
|
400
|
-
if (!f && recommended.length) {
|
|
401
|
-
sections.push({ label: 'Suggested', list: recommended.filter(match) })
|
|
402
|
-
}
|
|
403
|
-
var cats = { format: [], data: [], other: [] }
|
|
404
|
-
entries.forEach(function (e) {
|
|
405
|
-
if (!match(e)) return
|
|
406
|
-
var c = e.category === 'format' || e.category === 'data' ? e.category : 'other'
|
|
407
|
-
cats[c].push(e)
|
|
408
|
-
})
|
|
409
|
-
sections.push({ label: 'Format', list: cats.format })
|
|
410
|
-
sections.push({ label: 'Data', list: cats.data })
|
|
411
|
-
sections.push({ label: 'Other', list: cats.other })
|
|
412
|
-
|
|
413
|
-
var html = sections
|
|
414
|
-
.filter(function (s) {
|
|
415
|
-
return s.list.length
|
|
416
|
-
})
|
|
417
|
-
.map(function (s) {
|
|
418
|
-
var rows = s.list
|
|
419
|
-
.map(function (e) {
|
|
420
|
-
// Plain link, no bold; the 2-3 line summary sits under it in
|
|
421
|
-
// small print (about-page first paragraph, async-filled).
|
|
422
|
-
return (
|
|
423
|
-
'<li class="wiki-ux-row"><a class="wiki-ux-pick" href="#" ' +
|
|
424
|
-
'data-type="' + e.name.toLowerCase() + '" ' +
|
|
425
|
-
'data-name="' + esc(e.name) + '">' + esc(e.name) + '</a>' +
|
|
426
|
-
'<div class="wiki-ux-sum" data-sum-for="' + esc(e.name) + '">' +
|
|
427
|
-
esc(summarize(e.title || '')) + '</div></li>'
|
|
428
|
-
)
|
|
429
|
-
})
|
|
430
|
-
.join('')
|
|
431
|
-
return (
|
|
432
|
-
'<div class="wiki-ux-group"><h4>' + s.label + '</h4><ul>' + rows + '</ul></div>'
|
|
433
|
-
)
|
|
434
|
-
})
|
|
435
|
-
.join('')
|
|
436
|
-
$groups.html(html || '<p style="opacity:.6;">No matching plugins.</p>')
|
|
437
|
-
|
|
438
|
-
// Upgrade each row's summary to the about-page first paragraph.
|
|
439
|
-
var listed = {}
|
|
440
|
-
sections.forEach(function (s) {
|
|
441
|
-
s.list.forEach(function (e) {
|
|
442
|
-
listed[e.name] = e
|
|
443
|
-
})
|
|
444
|
-
})
|
|
445
|
-
Object.keys(listed).forEach(function (name) {
|
|
446
|
-
pluginSummary(listed[name], function (text) {
|
|
447
|
-
$groups.find('.wiki-ux-sum[data-sum-for="' + name + '"]').text(text)
|
|
448
|
-
})
|
|
449
|
-
})
|
|
450
|
-
}
|
|
451
|
-
|
|
452
|
-
drawList('')
|
|
453
|
-
|
|
454
|
-
$item.find('.wiki-ux-search').on('input', function () {
|
|
455
|
-
drawList(this.value)
|
|
456
|
-
})
|
|
457
|
-
|
|
458
|
-
$item.on('click', 'a.wiki-ux-pick', function (evt) {
|
|
459
|
-
evt.preventDefault()
|
|
460
|
-
var type = this.getAttribute('data-type')
|
|
461
|
-
var name = this.getAttribute('data-name')
|
|
462
|
-
choose($item, item, type, name, catalog)
|
|
463
|
-
})
|
|
464
|
-
}
|
|
465
|
-
|
|
466
|
-
if (window.catalog) {
|
|
467
|
-
render(window.catalog)
|
|
468
|
-
} else {
|
|
469
|
-
wiki.origin.get('system/factories.json', function (err, data) {
|
|
470
|
-
window.catalog = data || []
|
|
471
|
-
render(window.catalog)
|
|
472
|
-
})
|
|
473
|
-
}
|
|
474
|
-
}
|
|
475
|
-
|
|
476
|
-
// Rollover skin — the wiki-plugin-new look: tabbed category sections, pill
|
|
477
|
-
// buttons, and a small-print display at the foot that fills with a plugin's
|
|
478
|
-
// 2-3 line summary (its about page's first paragraph) as you roll over the
|
|
479
|
-
// pills. Click a pill to choose.
|
|
480
|
-
function rolloverEmit($item, item) {
|
|
481
|
-
var wiki = window.wiki
|
|
482
|
-
injectFactoryCss()
|
|
483
|
-
|
|
484
|
-
// A labelled factory (item.prompt) keeps the built-in prompt behaviour.
|
|
485
|
-
if (item.prompt) {
|
|
486
|
-
return origFactoryEmit.call(window.plugins.factory, $item, item)
|
|
487
|
-
}
|
|
488
|
-
|
|
489
|
-
var render = function (catalog) {
|
|
490
|
-
var cur = curationFor(location.host)
|
|
491
|
-
var recent = getRecent()
|
|
492
|
-
var entries = (catalog || []).filter(function (info) {
|
|
493
|
-
return info && info.name && info.category && cur.hide.indexOf(info.name.toLowerCase()) === -1
|
|
494
|
-
})
|
|
495
|
-
|
|
496
|
-
var byName = {}
|
|
497
|
-
entries.forEach(function (e) {
|
|
498
|
-
byName[e.name.toLowerCase()] = e
|
|
499
|
-
})
|
|
500
|
-
var suggested = cur.recommend
|
|
501
|
-
.concat(recent)
|
|
502
|
-
.map(function (t) {
|
|
503
|
-
return byName[t]
|
|
504
|
-
})
|
|
505
|
-
.filter(Boolean)
|
|
506
|
-
var seen = {}
|
|
507
|
-
suggested = suggested.filter(function (e) {
|
|
508
|
-
var k = e.name.toLowerCase()
|
|
509
|
-
if (seen[k]) return false
|
|
510
|
-
seen[k] = 1
|
|
511
|
-
return true
|
|
512
|
-
})
|
|
513
|
-
|
|
514
|
-
var cats = { format: [], data: [], other: [] }
|
|
515
|
-
entries.forEach(function (e) {
|
|
516
|
-
var c = e.category === 'format' || e.category === 'data' ? e.category : 'other'
|
|
517
|
-
cats[c].push(e)
|
|
518
|
-
})
|
|
519
|
-
var tabs = []
|
|
520
|
-
if (suggested.length) tabs.push({ key: 'suggested', label: 'Suggested', list: suggested })
|
|
521
|
-
tabs.push({ key: 'format', label: 'Format', list: cats.format })
|
|
522
|
-
tabs.push({ key: 'data', label: 'Data', list: cats.data })
|
|
523
|
-
tabs.push({ key: 'other', label: 'Other', list: cats.other })
|
|
524
|
-
tabs = tabs.filter(function (t) {
|
|
525
|
-
return t.list.length
|
|
526
|
-
})
|
|
527
|
-
|
|
528
|
-
var DEFAULT_INFO = 'Roll over a plugin to see what it does — click to choose. ' +
|
|
529
|
-
'Double-click the box to write plain text; drop text, an image or a link.'
|
|
530
|
-
|
|
531
|
-
$item.html(
|
|
532
|
-
'<div class="wiki-ux-rollover">' +
|
|
533
|
-
'<div class="wiki-ux-tabs">' +
|
|
534
|
-
tabs
|
|
535
|
-
.map(function (t, i) {
|
|
536
|
-
return (
|
|
537
|
-
'<button class="wiki-ux-tab' + (i === 0 ? ' wiki-ux-active' : '') +
|
|
538
|
-
'" data-tab="' + t.key + '">' + t.label + '</button>'
|
|
539
|
-
)
|
|
540
|
-
})
|
|
541
|
-
.join('') +
|
|
542
|
-
'</div>' +
|
|
543
|
-
'<div class="wiki-ux-pills"></div>' +
|
|
544
|
-
'<div class="wiki-ux-rollover-info">' + DEFAULT_INFO + '</div>' +
|
|
545
|
-
'</div>',
|
|
546
|
-
)
|
|
547
|
-
|
|
548
|
-
var $pills = $item.find('.wiki-ux-pills')
|
|
549
|
-
var $info = $item.find('.wiki-ux-rollover-info')
|
|
550
|
-
|
|
551
|
-
var drawPills = function (key) {
|
|
552
|
-
var tab = tabs.find(function (t) {
|
|
553
|
-
return t.key === key
|
|
554
|
-
})
|
|
555
|
-
$pills.html(
|
|
556
|
-
(tab ? tab.list : [])
|
|
557
|
-
.map(function (e) {
|
|
558
|
-
return (
|
|
559
|
-
'<button class="wiki-ux-pillbtn" data-type="' + e.name.toLowerCase() +
|
|
560
|
-
'" data-name="' + esc(e.name) + '">' + esc(e.name) + '</button>'
|
|
561
|
-
)
|
|
562
|
-
})
|
|
563
|
-
.join(' '),
|
|
564
|
-
)
|
|
565
|
-
}
|
|
566
|
-
drawPills(tabs.length ? tabs[0].key : null)
|
|
567
|
-
|
|
568
|
-
$item.on('click', '.wiki-ux-tab', function () {
|
|
569
|
-
$item.find('.wiki-ux-tab').removeClass('wiki-ux-active')
|
|
570
|
-
this.classList.add('wiki-ux-active')
|
|
571
|
-
drawPills(this.getAttribute('data-tab'))
|
|
572
|
-
$info.html(DEFAULT_INFO)
|
|
573
|
-
})
|
|
574
|
-
|
|
575
|
-
$item.on('mouseenter', '.wiki-ux-pillbtn', function () {
|
|
576
|
-
var name = this.getAttribute('data-name')
|
|
577
|
-
var entry = byName[name.toLowerCase()]
|
|
578
|
-
if (!entry) return
|
|
579
|
-
pluginSummary(entry, function (text) {
|
|
580
|
-
$info.html('<span class="wiki-ux-info-name">' + esc(name) + '</span> — ' + esc(text))
|
|
581
|
-
})
|
|
582
|
-
})
|
|
583
|
-
|
|
584
|
-
$item.on('click', '.wiki-ux-pillbtn', function (evt) {
|
|
585
|
-
evt.preventDefault()
|
|
586
|
-
choose($item, item, this.getAttribute('data-type'), this.getAttribute('data-name'), catalog)
|
|
587
|
-
})
|
|
588
|
-
}
|
|
589
|
-
|
|
590
|
-
if (window.catalog) {
|
|
591
|
-
render(window.catalog)
|
|
592
|
-
} else {
|
|
593
|
-
wiki.origin.get('system/factories.json', function (err, data) {
|
|
594
|
-
window.catalog = data || []
|
|
595
|
-
render(window.catalog)
|
|
596
|
-
})
|
|
597
|
-
}
|
|
598
|
-
}
|
|
599
|
-
|
|
600
|
-
function choose($item, item, type, name, catalog) {
|
|
601
|
-
var wiki = window.wiki
|
|
602
|
-
pushRecent(type)
|
|
603
|
-
$item.removeClass('factory').addClass((item.type = type))
|
|
604
|
-
$item.off()
|
|
605
|
-
var entry = (catalog || []).find(function (e) {
|
|
606
|
-
return e.name === name
|
|
607
|
-
})
|
|
608
|
-
// load the plugin, then hand off to its editor (or the plain text editor)
|
|
609
|
-
wiki.getPlugin(type, function () {
|
|
610
|
-
try {
|
|
611
|
-
if (entry && entry.editor && window.plugins[type] && window.plugins[type].editor) {
|
|
612
|
-
window.plugins[type].editor($item, item)
|
|
613
|
-
} else {
|
|
614
|
-
wiki.textEditor($item, item)
|
|
615
|
-
}
|
|
616
|
-
} catch (e) {
|
|
617
|
-
console.log('[wiki-ux] editor failed, falling back to textEditor:', e)
|
|
618
|
-
wiki.textEditor($item, item)
|
|
619
|
-
}
|
|
620
|
-
})
|
|
621
|
-
}
|
|
622
|
-
|
|
623
|
-
// ===========================================================================
|
|
624
|
-
// 2. Extra drops — wrap bind, add a capture-phase handler for media URLs
|
|
625
|
-
// ===========================================================================
|
|
626
|
-
function installExtraDrops() {
|
|
627
|
-
window.plugins.factory.bind = function ($item, item) {
|
|
628
|
-
var ret = origFactoryBind.call(this, $item, item)
|
|
629
|
-
try {
|
|
630
|
-
attachExtraDrop($item, item)
|
|
631
|
-
} catch (e) {
|
|
632
|
-
console.error('[wiki-ux] extra-drop attach failed', e)
|
|
633
|
-
}
|
|
634
|
-
return ret
|
|
635
|
-
}
|
|
636
|
-
}
|
|
637
|
-
|
|
638
|
-
function attachExtraDrop($item, item) {
|
|
639
|
-
var el = $item[0]
|
|
640
|
-
if (!el) return
|
|
641
|
-
el.addEventListener(
|
|
642
|
-
'drop',
|
|
643
|
-
function (e) {
|
|
644
|
-
// never touch file/image drops or internal page-flag drags — let the
|
|
645
|
-
// original factory bind (drop.js) handle those.
|
|
646
|
-
if (e.dataTransfer && e.dataTransfer.files && e.dataTransfer.files.length) return
|
|
647
|
-
var uri =
|
|
648
|
-
(e.dataTransfer && (e.dataTransfer.getData('text/uri-list') || e.dataTransfer.getData('text'))) || ''
|
|
649
|
-
uri = uri.trim()
|
|
650
|
-
if (!/^https?:\/\//i.test(uri)) return
|
|
651
|
-
var recognised = classifyUrl(uri)
|
|
652
|
-
if (!recognised) return // fall through to the original handler
|
|
653
|
-
e.preventDefault()
|
|
654
|
-
e.stopImmediatePropagation()
|
|
655
|
-
becomeType($item, item, recognised.type, recognised.fields)
|
|
656
|
-
},
|
|
657
|
-
true, // capture — runs before the jQuery bubble-phase handler
|
|
658
|
-
)
|
|
659
|
-
}
|
|
660
|
-
|
|
661
|
-
function classifyUrl(url) {
|
|
662
|
-
if (/(?:youtube\.com\/watch|youtu\.be\/|vimeo\.com\/\d)/i.test(url)) {
|
|
663
|
-
return { type: 'video', fields: { text: url + '\n(double-click to edit caption)\n' } }
|
|
664
|
-
}
|
|
665
|
-
if (/\.(mp3|ogg|wav|m4a|flac)(\?|#|$)/i.test(url)) {
|
|
666
|
-
return { type: 'audio', fields: { text: url } }
|
|
667
|
-
}
|
|
668
|
-
if (/\.pdf(\?|#|$)/i.test(url)) {
|
|
669
|
-
return { type: 'reference', fields: { site: location.host, text: 'PDF: ' + url } }
|
|
670
|
-
}
|
|
671
|
-
return null
|
|
672
|
-
}
|
|
673
|
-
|
|
674
|
-
// Replicates factory.js syncEditAction using only the public wiki surface.
|
|
675
|
-
function becomeType($item, item, type, fields) {
|
|
676
|
-
var wiki = window.wiki
|
|
677
|
-
var $ = window.$
|
|
678
|
-
Object.keys(fields || {}).forEach(function (k) {
|
|
679
|
-
item[k] = fields[k]
|
|
680
|
-
})
|
|
681
|
-
item.type = type
|
|
682
|
-
var $page = $item.parents('.page:first')
|
|
683
|
-
$item.empty().off().removeClass('factory').addClass(type)
|
|
684
|
-
$item.data('pageElement', $page)
|
|
685
|
-
$item.data('item', item)
|
|
686
|
-
wiki.getPlugin(type, function (plugin) {
|
|
687
|
-
if (plugin && plugin.emit) plugin.emit($item, item)
|
|
688
|
-
if (plugin && plugin.bind) plugin.bind($item, item)
|
|
689
|
-
})
|
|
690
|
-
wiki.pageHandler.put($page, { type: 'edit', id: item.id, item: item })
|
|
691
|
-
}
|
|
692
|
-
|
|
693
|
-
// ===========================================================================
|
|
694
|
-
// 3. Page-flag click → in-lineup Tools page
|
|
695
|
-
// ===========================================================================
|
|
696
|
-
function installFlagToolPage() {
|
|
697
|
-
document.addEventListener(
|
|
698
|
-
'click',
|
|
699
|
-
function (e) {
|
|
700
|
-
var t = e.target
|
|
701
|
-
var a = t && t.closest ? t.closest('.page h1 a') : null
|
|
702
|
-
if (!a) return
|
|
703
|
-
e.preventDefault()
|
|
704
|
-
e.stopImmediatePropagation() // pre-empt the built-in handleHeaderClick
|
|
705
|
-
// A Tools page's own flag is a no-op (don't recurse into Tools: Tools:…).
|
|
706
|
-
if (a.closest('.page.wiki-ux-tools')) return
|
|
707
|
-
// Shift-click is a shortcut straight to Simplify (skip the Tools page).
|
|
708
|
-
if (e.shiftKey) {
|
|
709
|
-
try {
|
|
710
|
-
doSimplify(a)
|
|
711
|
-
} catch (err) {
|
|
712
|
-
openOriginSite(a)
|
|
713
|
-
}
|
|
714
|
-
return
|
|
715
|
-
}
|
|
716
|
-
try {
|
|
717
|
-
openFlagPage(a)
|
|
718
|
-
} catch (err) {
|
|
719
|
-
console.error('[wiki-ux] flag page failed, opening origin site instead', err)
|
|
720
|
-
openOriginSite(a)
|
|
721
|
-
}
|
|
722
|
-
},
|
|
723
|
-
true,
|
|
724
|
-
)
|
|
725
|
-
}
|
|
726
|
-
|
|
727
|
-
// "Simplify" target: the lineup collapsed to welcome-visitors + this page.
|
|
728
|
-
// Local pages resolve on this host; remote pages resolve on their origin site.
|
|
729
|
-
function simplifyUrl($page, slug, remoteSite) {
|
|
730
|
-
var wiki = window.wiki
|
|
731
|
-
var path = slug === 'welcome-visitors' ? 'view/welcome-visitors' : 'view/welcome-visitors/view/' + slug
|
|
732
|
-
if ($page.hasClass('remote')) {
|
|
733
|
-
var site = remoteSite
|
|
734
|
-
if (!site) {
|
|
735
|
-
try {
|
|
736
|
-
var po = wiki.lineup.atKey($page.data('key'))
|
|
737
|
-
site = po && po.getRemoteSite && po.getRemoteSite()
|
|
738
|
-
} catch (e) {}
|
|
739
|
-
}
|
|
740
|
-
if (site) return wiki.site(site).getDirectURL(path)
|
|
741
|
-
}
|
|
742
|
-
return '/' + path
|
|
743
|
-
}
|
|
744
|
-
|
|
745
|
-
// Shift-click shortcut on a page flag: navigate straight to the Simplify
|
|
746
|
-
// lineup (Welcome Visitors + the clicked page), skipping the Tools page.
|
|
747
|
-
function doSimplify(a) {
|
|
748
|
-
var $src = window.$(a).closest('.page')
|
|
749
|
-
var slug = $src.attr('id')
|
|
750
|
-
window.location.assign(simplifyUrl($src, slug, remoteSiteOf($src)))
|
|
751
|
-
}
|
|
752
|
-
|
|
753
|
-
function remoteSiteOf($page) {
|
|
754
|
-
if (!$page.hasClass('remote')) return ''
|
|
755
|
-
try {
|
|
756
|
-
var po = window.wiki.lineup.atKey($page.data('key'))
|
|
757
|
-
return (po && po.getRemoteSite && po.getRemoteSite()) || $page.data('site') || ''
|
|
758
|
-
} catch (e) {
|
|
759
|
-
return $page.data('site') || ''
|
|
760
|
-
}
|
|
761
|
-
}
|
|
762
|
-
|
|
763
|
-
// Insert a generated page into the lineup immediately to the right of $src,
|
|
764
|
-
// preserving any pages already to its right (unlike showResult's wipe). The
|
|
765
|
-
// lineup key order mirrors DOM order, so we position by index. Marks the page
|
|
766
|
-
// green (plugin) and tags it so its own flag click is a no-op.
|
|
767
|
-
function insertPageAfter($src, pageObject, opts) {
|
|
768
|
-
opts = opts || {}
|
|
769
|
-
var $ = window.$
|
|
770
|
-
var wiki = window.wiki
|
|
771
|
-
var srcIndex = $src.prevAll('.page').length // lineup index == DOM index
|
|
772
|
-
wiki.showResult(pageObject) // no {$page} → append at end, wipe nothing
|
|
773
|
-
var $new = $('.main > .page').last()
|
|
774
|
-
$new.insertAfter($src) // move into position in the DOM
|
|
775
|
-
try {
|
|
776
|
-
wiki.lineup.changePageIndex($new.data('key'), srcIndex + 1) // and in the lineup
|
|
777
|
-
} catch (e) {}
|
|
778
|
-
$new.addClass('wiki-ux-tools') // its own flag click is a no-op
|
|
779
|
-
if (!opts.ghost) $new.removeClass('ghost') // Tools/Navigate: crisp; report: stay ghost
|
|
780
|
-
try {
|
|
781
|
-
$new[0].scrollIntoView({ behavior: 'smooth', inline: 'center', block: 'nearest' })
|
|
782
|
-
} catch (e) {}
|
|
783
|
-
return $new
|
|
784
|
-
}
|
|
785
|
-
|
|
786
|
-
// One delegated handler for every Tools-page action button. Each button lives
|
|
787
|
-
// in the Tools page; the page it acts on is the Tools page's previous sibling
|
|
788
|
-
// in the lineup (showResult appends the Tools page right after its source).
|
|
789
|
-
function installToolButtons() {
|
|
790
|
-
document.addEventListener(
|
|
791
|
-
'click',
|
|
792
|
-
function (e) {
|
|
793
|
-
var el = e.target
|
|
794
|
-
if (!el || !el.closest) return
|
|
795
|
-
var kind = el.closest('.wiki-ux-tool-fork')
|
|
796
|
-
? 'fork'
|
|
797
|
-
: el.closest('.wiki-ux-tool-sim')
|
|
798
|
-
? 'sim'
|
|
799
|
-
: el.closest('.wiki-ux-tool-trash')
|
|
800
|
-
? 'trash'
|
|
801
|
-
: el.closest('.wiki-ux-tool-clean')
|
|
802
|
-
? 'clean'
|
|
803
|
-
: el.closest('.wiki-ux-tool-flag')
|
|
804
|
-
? 'flag'
|
|
805
|
-
: el.closest('.wiki-ux-tool-toolpage')
|
|
806
|
-
? 'toolpage'
|
|
807
|
-
: null
|
|
808
|
-
if (!kind) return
|
|
809
|
-
e.preventDefault()
|
|
810
|
-
var $ = window.$
|
|
811
|
-
var btn = el.closest('.wiki-ux-tool-' + kind)
|
|
812
|
-
var $tools = $(btn).closest('.page')
|
|
813
|
-
var $src = $tools.prev('.page')
|
|
814
|
-
try {
|
|
815
|
-
if (kind === 'fork') doFork($src, $tools)
|
|
816
|
-
else if (kind === 'sim') doSimilarity($src, $tools)
|
|
817
|
-
else if (kind === 'trash') doTrash($src, $tools)
|
|
818
|
-
else if (kind === 'clean') doClean($src, $tools)
|
|
819
|
-
else if (kind === 'flag') openFishPicker($src, $tools)
|
|
820
|
-
else if (kind === 'toolpage') openToolPage($src, $tools, btn.getAttribute('data-slug'))
|
|
821
|
-
} catch (err) {
|
|
822
|
-
console.error('[wiki-ux] tool "' + kind + '" failed', err)
|
|
823
|
-
}
|
|
824
|
-
},
|
|
825
|
-
true,
|
|
826
|
-
)
|
|
827
|
-
}
|
|
828
|
-
|
|
829
|
-
// Tool pages: real, forkable wiki pages (Similar Page Tool, Search Related
|
|
830
|
-
// Page) whose SUBJECT-mode similarity items act on the page BEFORE them in
|
|
831
|
-
// the lineup. Fetched from this site first, then the canonical home, and
|
|
832
|
-
// inserted directly after the HOST page — so the tool's previous sibling is
|
|
833
|
-
// its subject. The Tools page closes; the tool has the context it needs.
|
|
834
|
-
var TOOL_PAGE_HOME = 'demo.localhost'
|
|
835
|
-
function openToolPage($src, $tools, slug) {
|
|
836
|
-
if (!slug) return
|
|
837
|
-
var fetchPage = function (site) {
|
|
838
|
-
return fetch('//' + site + '/' + slug + '.json').then(function (r) {
|
|
839
|
-
if (!r.ok) throw new Error('' + r.status)
|
|
840
|
-
return r.json()
|
|
841
|
-
})
|
|
842
|
-
}
|
|
843
|
-
fetchPage(location.host)
|
|
844
|
-
.catch(function () { return fetchPage(TOOL_PAGE_HOME) })
|
|
845
|
-
.then(function (page) {
|
|
846
|
-
insertPageAfter($src, window.wiki.newPage(page), { ghost: true })
|
|
847
|
-
$tools.remove()
|
|
848
|
-
})
|
|
849
|
-
.catch(function () {
|
|
850
|
-
window.alert('Tool page "' + slug + '" not found on ' + location.host + ' or ' + TOOL_PAGE_HOME + '.')
|
|
851
|
-
})
|
|
852
|
-
}
|
|
853
|
-
|
|
854
|
-
// Fork: reuse the source page's own native .fork-page control (still in the
|
|
855
|
-
// lineup) — it already handles the local / remote / recycler cases. The
|
|
856
|
-
// hitchhiker FORK item (client/hitchhiker.js) forks its own page the same
|
|
857
|
-
// way; keep the two in step.
|
|
858
|
-
function doFork($src, $tools) {
|
|
859
|
-
var $forkBtn = $src.find('.fork-page')
|
|
860
|
-
if ($forkBtn.length) {
|
|
861
|
-
$forkBtn.trigger('click')
|
|
862
|
-
$tools.remove() // close the Tools page; the forked source page shows
|
|
863
|
-
return
|
|
864
|
-
}
|
|
865
|
-
// Logged in but the "wiki" edit toggle is off — the flag isn't rendered.
|
|
866
|
-
// Fork directly with the same pageHandler call the flag makes; the
|
|
867
|
-
// server authorizes by session, not by the toggle. (Same fallback as the
|
|
868
|
-
// hitchhiker FORK item in hitchhiker.js — keep the two in step.)
|
|
869
|
-
if (window.isAuthenticated !== true || /_rev\d+$/.test($src.attr('id') || '')) {
|
|
870
|
-
console.warn('[wiki-ux] fork: no .fork-page control on the source page')
|
|
871
|
-
return
|
|
872
|
-
}
|
|
873
|
-
var action = { type: 'fork' }
|
|
874
|
-
if ($src.hasClass('local')) {
|
|
875
|
-
$src.removeClass('local')
|
|
876
|
-
} else {
|
|
877
|
-
var site = remoteSiteOf($src)
|
|
878
|
-
if (site) action.site = site
|
|
879
|
-
}
|
|
880
|
-
$src.removeClass('ghost')
|
|
881
|
-
window.wiki.pageHandler.put($src, action)
|
|
882
|
-
$tools.remove()
|
|
883
|
-
}
|
|
884
|
-
|
|
885
|
-
// Trash: move the page to the recycler via the exposed pageHandler.delete.
|
|
886
|
-
// Recoverable from Recycled Pages — not a permanent delete. Confirm first.
|
|
887
|
-
function doTrash($src, $tools) {
|
|
888
|
-
var wiki = window.wiki
|
|
889
|
-
var raw = $src.data('data') || {}
|
|
890
|
-
var title = raw.title || $src.attr('id') || 'this page'
|
|
891
|
-
if (!window.confirm('Trash "' + title + '"?\n\nIt moves to the recycler and can be recovered from Recycled Pages.')) return
|
|
892
|
-
var pageObject
|
|
893
|
-
try {
|
|
894
|
-
pageObject = wiki.lineup.atKey($src.data('key'))
|
|
895
|
-
} catch (e) {}
|
|
896
|
-
if (!pageObject) {
|
|
897
|
-
window.alert('Could not identify the page to trash.')
|
|
898
|
-
return
|
|
899
|
-
}
|
|
900
|
-
wiki.pageHandler.delete(pageObject, $src, function (err) {
|
|
901
|
-
if (err) {
|
|
902
|
-
window.alert('Trash failed: ' + err)
|
|
903
|
-
return
|
|
904
|
-
}
|
|
905
|
-
$tools.remove()
|
|
906
|
-
$src.remove()
|
|
907
|
-
})
|
|
908
|
-
}
|
|
909
|
-
|
|
910
|
-
// Clean the journal: open a GHOST copy whose journal is one clean create entry
|
|
911
|
-
// (the fedwiki-journal convention). Same slug — fork the ghost to keep it.
|
|
912
|
-
function doClean($src, $tools) {
|
|
913
|
-
var wiki = window.wiki
|
|
914
|
-
var raw = $src.data('data') || {}
|
|
915
|
-
var title = raw.title || $src.attr('id') || 'this page'
|
|
916
|
-
var story = raw.story || []
|
|
917
|
-
var cleaned = {
|
|
918
|
-
title: title,
|
|
919
|
-
story: story,
|
|
920
|
-
journal: [{ type: 'create', item: { title: title, story: story }, date: Date.now(), certificate: 'from marvin' }],
|
|
921
|
-
}
|
|
922
|
-
wiki.showResult(wiki.newPage(cleaned), { $page: $tools }) // ghost, opens after Tools
|
|
923
|
-
}
|
|
924
|
-
|
|
925
|
-
// Detect wiki-plugin-similarity via the aggregated factory catalog (this also
|
|
926
|
-
// warms window.catalog for the chooser). Sets simInstalled for the Tools page.
|
|
927
|
-
function detectSimilarity() {
|
|
928
|
-
fetch('/system/factories.json')
|
|
929
|
-
.then(function (r) {
|
|
930
|
-
return r.ok ? r.json() : []
|
|
931
|
-
})
|
|
932
|
-
.then(function (cat) {
|
|
933
|
-
if (!window.catalog) window.catalog = cat
|
|
934
|
-
simInstalled = (cat || []).some(function (e) {
|
|
935
|
-
return e && e.name === 'Similarity'
|
|
936
|
-
})
|
|
937
|
-
})
|
|
938
|
-
.catch(function () {})
|
|
939
|
-
}
|
|
940
|
-
|
|
941
|
-
// Similarity power tool: pages like the source page, FARM-WIDE, opened as a
|
|
942
|
-
// GHOST report. One POST to the similarity plugin's report route, seeded by
|
|
943
|
-
// the page itself: the server prefers the page's stored vector (seed), falls
|
|
944
|
-
// back to embedding the supplied text (title + opening prose) for remote or
|
|
945
|
-
// not-yet-indexed pages, and excludes the page and its fork family from the
|
|
946
|
-
// results. Replaces the old current-domain-only client-side scan, whose GET
|
|
947
|
-
// embed fallback died with 431 on long pages.
|
|
948
|
-
function doSimilarity($src, $tools) {
|
|
949
|
-
var raw = $src.data('data') || {}
|
|
950
|
-
var title = raw.title || $src.attr('id') || 'this page'
|
|
951
|
-
var slug = ($src.attr('id') || '').split('_rev')[0]
|
|
952
|
-
var site = remoteSiteOf($src) || location.hostname
|
|
953
|
-
var $btn = $tools.find('.wiki-ux-tool-sim')
|
|
954
|
-
$btn.prop('disabled', true).text('🔍 Searching…')
|
|
955
|
-
|
|
956
|
-
// Embed fallback text: title + opening prose. Capped — the BGE-small
|
|
957
|
-
// embedder truncates around 512 tokens anyway.
|
|
958
|
-
var text = [title]
|
|
959
|
-
.concat(
|
|
960
|
-
(raw.story || [])
|
|
961
|
-
.filter(function (i) { return i.type === 'markdown' || i.type === 'paragraph' })
|
|
962
|
-
.map(function (i) { return (i.text || '').trim() })
|
|
963
|
-
.filter(Boolean),
|
|
964
|
-
)
|
|
965
|
-
.join('\n')
|
|
966
|
-
.slice(0, 2000)
|
|
967
|
-
|
|
968
|
-
fetch(location.origin + '/system/search-report.json', {
|
|
969
|
-
method: 'POST',
|
|
970
|
-
headers: { 'Content-Type': 'application/json' },
|
|
971
|
-
body: JSON.stringify({
|
|
972
|
-
query: title,
|
|
973
|
-
seed: { site: site, slug: slug },
|
|
974
|
-
excludePage: { site: site, slug: slug },
|
|
975
|
-
text: text,
|
|
976
|
-
domains: ['*'],
|
|
977
|
-
limit: 12,
|
|
978
|
-
}),
|
|
979
|
-
})
|
|
980
|
-
.then(function (r) {
|
|
981
|
-
if (!r.ok) throw new Error('search-report failed: ' + r.status)
|
|
982
|
-
return r.json()
|
|
983
|
-
})
|
|
984
|
-
.then(function (page) {
|
|
985
|
-
// Server-built GHOST report (stub-filtered, fork-bundled), after Tools.
|
|
986
|
-
insertPageAfter($tools, window.wiki.newPage(page), { ghost: true })
|
|
987
|
-
$btn.prop('disabled', false).text('🔍 Similarity search')
|
|
988
|
-
})
|
|
989
|
-
.catch(function (e) {
|
|
990
|
-
console.error('[wiki-ux] similarity failed', e)
|
|
991
|
-
$btn.prop('disabled', false).text('🔍 Similarity search')
|
|
992
|
-
window.alert('Similarity search failed: ' + e.message)
|
|
993
|
-
})
|
|
994
|
-
}
|
|
995
|
-
|
|
996
|
-
function originUrl($page) {
|
|
997
|
-
var wiki = window.wiki
|
|
998
|
-
var crumbs = wiki.lineup.crumbs($page.data('key'), location.host)
|
|
999
|
-
var target = crumbs[0]
|
|
1000
|
-
var prefix = wiki.site(target).getDirectURL('').split('/')[0]
|
|
1001
|
-
if (prefix === '') prefix = window.location.protocol
|
|
1002
|
-
return { url: prefix + '//' + crumbs.join('/'), target: target }
|
|
1003
|
-
}
|
|
1004
|
-
|
|
1005
|
-
function openOriginSite(a) {
|
|
1006
|
-
var $ = window.$
|
|
1007
|
-
var $page = $(a).closest('.page')
|
|
1008
|
-
var o = originUrl($page)
|
|
1009
|
-
window.open(o.url, o.target).focus()
|
|
1010
|
-
}
|
|
1011
|
-
|
|
1012
|
-
// ===========================================================================
|
|
1013
|
-
// Drop TARGET: drop a URL / link / page-flag ONTO a page flag → ghost report.
|
|
1014
|
-
// ===========================================================================
|
|
1015
|
-
var DROP_CLASS = 'wiki-ux-drop-target'
|
|
1016
|
-
var DROPPABLE_TYPES = ['text/uri-list', 'text/x-moz-url', 'text/plain', 'text/html', 'Files']
|
|
1017
|
-
|
|
1018
|
-
function installDropTarget() {
|
|
1019
|
-
var flagOf = function (t) {
|
|
1020
|
-
return t && t.closest ? t.closest('.page h1 a') : null
|
|
1021
|
-
}
|
|
1022
|
-
var hasDroppable = function (dt) {
|
|
1023
|
-
if (!dt || !dt.types) return false
|
|
1024
|
-
for (var i = 0; i < dt.types.length; i++) if (DROPPABLE_TYPES.indexOf(dt.types[i]) !== -1) return true
|
|
1025
|
-
return false
|
|
1026
|
-
}
|
|
1027
|
-
var clearHighlights = function () {
|
|
1028
|
-
var els = document.querySelectorAll('.' + DROP_CLASS)
|
|
1029
|
-
for (var i = 0; i < els.length; i++) els[i].classList.remove(DROP_CLASS)
|
|
1030
|
-
}
|
|
1031
|
-
|
|
1032
|
-
// dragover must preventDefault to allow a drop; getData is unreadable here,
|
|
1033
|
-
// so we decide purely from dataTransfer.types.
|
|
1034
|
-
document.addEventListener(
|
|
1035
|
-
'dragover',
|
|
1036
|
-
function (e) {
|
|
1037
|
-
var flag = flagOf(e.target)
|
|
1038
|
-
if (!flag || !hasDroppable(e.dataTransfer)) return
|
|
1039
|
-
e.preventDefault()
|
|
1040
|
-
try {
|
|
1041
|
-
e.dataTransfer.dropEffect = 'copy'
|
|
1042
|
-
} catch (_) {}
|
|
1043
|
-
if (!flag.classList.contains(DROP_CLASS)) {
|
|
1044
|
-
clearHighlights()
|
|
1045
|
-
flag.classList.add(DROP_CLASS)
|
|
1046
|
-
}
|
|
1047
|
-
},
|
|
1048
|
-
true,
|
|
1049
|
-
)
|
|
1050
|
-
document.addEventListener(
|
|
1051
|
-
'dragleave',
|
|
1052
|
-
function (e) {
|
|
1053
|
-
var flag = flagOf(e.target)
|
|
1054
|
-
if (flag && (!e.relatedTarget || !flag.contains(e.relatedTarget))) flag.classList.remove(DROP_CLASS)
|
|
1055
|
-
},
|
|
1056
|
-
true,
|
|
1057
|
-
)
|
|
1058
|
-
document.addEventListener(
|
|
1059
|
-
'drop',
|
|
1060
|
-
function (e) {
|
|
1061
|
-
var flag = flagOf(e.target)
|
|
1062
|
-
clearHighlights()
|
|
1063
|
-
if (!flag) return
|
|
1064
|
-
e.preventDefault()
|
|
1065
|
-
e.stopPropagation()
|
|
1066
|
-
try {
|
|
1067
|
-
var $target = window.$(flag).closest('.page')
|
|
1068
|
-
var drop = decodeDrop(e.dataTransfer)
|
|
1069
|
-
// inbound hearth guard: refuse to import a page that keeps to its hearth
|
|
1070
|
-
if (drop.kind === 'page' && drop.site && drop.slug) {
|
|
1071
|
-
fetchIsHearth(drop.site, drop.slug).then(function (isHearth) {
|
|
1072
|
-
if (isHearth) {
|
|
1073
|
-
hearthToast('That page keeps to its hearth — it declines to be copied here.')
|
|
1074
|
-
} else {
|
|
1075
|
-
buildDropReport($target, drop)
|
|
1076
|
-
}
|
|
1077
|
-
})
|
|
1078
|
-
return
|
|
1079
|
-
}
|
|
1080
|
-
buildDropReport($target, drop)
|
|
1081
|
-
} catch (err) {
|
|
1082
|
-
console.error('[wiki-ux] drop failed', err)
|
|
1083
|
-
}
|
|
1084
|
-
},
|
|
1085
|
-
true,
|
|
1086
|
-
)
|
|
1087
|
-
document.addEventListener('dragend', clearHighlights, true)
|
|
1088
|
-
}
|
|
1089
|
-
|
|
1090
|
-
// Mirrors wiki-client/lib/drop.js isUrl/isPage (that module is bundled/private).
|
|
1091
|
-
function isPageUrl(url) {
|
|
1092
|
-
var m = url.match(/^https?:\/\/([a-zA-Z0-9:.-]+)(\/([a-zA-Z0-9:.-]+)\/([a-z0-9-]+(_rev\d+)?))+$/)
|
|
1093
|
-
if (!m) return null
|
|
1094
|
-
var origin = m[1],
|
|
1095
|
-
site = m[3],
|
|
1096
|
-
slug = m[4]
|
|
1097
|
-
if (['view', 'local', 'origin'].indexOf(site) !== -1) site = origin
|
|
1098
|
-
return { site: site, slug: slug }
|
|
1099
|
-
}
|
|
1100
|
-
|
|
1101
|
-
function decodeDrop(dt) {
|
|
1102
|
-
var out = { kind: 'unknown', url: '', site: '', slug: '', text: '', html: '', files: [], types: [] }
|
|
1103
|
-
if (!dt) return out
|
|
1104
|
-
out.types = Array.prototype.slice.call(dt.types || [])
|
|
1105
|
-
if (dt.files && dt.files.length) {
|
|
1106
|
-
for (var i = 0; i < dt.files.length; i++) {
|
|
1107
|
-
var f = dt.files[i]
|
|
1108
|
-
out.files.push({ name: f.name, type: f.type, size: f.size })
|
|
1109
|
-
}
|
|
1110
|
-
out.kind = 'file'
|
|
1111
|
-
}
|
|
1112
|
-
try {
|
|
1113
|
-
out.text = dt.getData('text/plain') || ''
|
|
1114
|
-
} catch (_) {}
|
|
1115
|
-
try {
|
|
1116
|
-
out.html = dt.getData('text/html') || ''
|
|
1117
|
-
} catch (_) {}
|
|
1118
|
-
var url = ''
|
|
1119
|
-
try {
|
|
1120
|
-
url = dt.getData('text/uri-list') || dt.getData('URL') || dt.getData('text/x-moz-url') || ''
|
|
1121
|
-
} catch (_) {}
|
|
1122
|
-
if (url) url = url.split('\n')[0].trim() // x-moz-url is "url\ntitle"
|
|
1123
|
-
if (!url && /^https?:\/\//i.test(out.text)) url = out.text.trim()
|
|
1124
|
-
if (url) {
|
|
1125
|
-
out.url = url
|
|
1126
|
-
var page = isPageUrl(url)
|
|
1127
|
-
if (page) {
|
|
1128
|
-
out.kind = 'page'
|
|
1129
|
-
out.site = page.site
|
|
1130
|
-
out.slug = page.slug
|
|
1131
|
-
} else {
|
|
1132
|
-
out.kind = 'link'
|
|
1133
|
-
}
|
|
1134
|
-
} else if (out.kind === 'unknown') {
|
|
1135
|
-
var t = (out.text || '').trim()
|
|
1136
|
-
if (t && /^[a-z0-9.-]+\.[a-z]{2,}$/i.test(t)) {
|
|
1137
|
-
out.kind = 'site' // bare hostname = a neighborhood/site flag
|
|
1138
|
-
out.site = t
|
|
1139
|
-
} else if (t) {
|
|
1140
|
-
out.kind = 'text'
|
|
1141
|
-
}
|
|
1142
|
-
}
|
|
1143
|
-
return out
|
|
1144
|
-
}
|
|
1145
|
-
|
|
1146
|
-
// Ask our own server to unfurl a URL into a static preview card (no third-party
|
|
1147
|
-
// calls from the browser; the thumbnail comes back inlined as a data URI).
|
|
1148
|
-
// Link previews come from an Outpost — a host running wiki-plugin-unfurl —
|
|
1149
|
-
// called straight from the browser. This farm only tells us which ones exist
|
|
1150
|
-
// (GET /system/outposts.json, a list). We shuffle and try each in turn with a
|
|
1151
|
-
// short timeout; the first card wins; every failure is swallowed. No Outpost,
|
|
1152
|
-
// or none answering, means null — and renderDropReport already draws a plain
|
|
1153
|
-
// report for a null card. Absence is normal, not an error.
|
|
1154
|
-
var _outposts = null
|
|
1155
|
-
function outposts() {
|
|
1156
|
-
if (_outposts) return _outposts
|
|
1157
|
-
_outposts = fetch(location.origin + '/system/outposts.json')
|
|
1158
|
-
.then(function (r) { return r.ok ? r.json() : {} })
|
|
1159
|
-
.then(function (j) { return (j && Array.isArray(j.unfurl) ? j.unfurl : []).filter(Boolean) })
|
|
1160
|
-
.catch(function () { return [] })
|
|
1161
|
-
return _outposts
|
|
1162
|
-
}
|
|
1163
|
-
function shuffle(a) {
|
|
1164
|
-
var b = a.slice()
|
|
1165
|
-
for (var i = b.length - 1; i > 0; i--) { var j = Math.floor(Math.random() * (i + 1)); var t = b[i]; b[i] = b[j]; b[j] = t }
|
|
1166
|
-
return b
|
|
1167
|
-
}
|
|
1168
|
-
function unfurl(url) {
|
|
1169
|
-
return outposts().then(function (list) {
|
|
1170
|
-
var order = shuffle(list)
|
|
1171
|
-
var tryNext = function (i) {
|
|
1172
|
-
if (i >= order.length) return null
|
|
1173
|
-
var ctl = typeof AbortController === 'function' ? new AbortController() : null
|
|
1174
|
-
var timer = ctl && setTimeout(function () { ctl.abort() }, 2000)
|
|
1175
|
-
return fetch(order[i] + '/system/unfurl.json?url=' + encodeURIComponent(url), ctl ? { signal: ctl.signal } : {})
|
|
1176
|
-
.then(function (r) { return r.ok ? r.json() : null })
|
|
1177
|
-
.catch(function () { return null })
|
|
1178
|
-
.then(function (card) {
|
|
1179
|
-
if (timer) clearTimeout(timer)
|
|
1180
|
-
if (card && typeof card === 'object' && 'title' in card) { card.outpost = order[i]; return card }
|
|
1181
|
-
return tryNext(i + 1)
|
|
1182
|
-
})
|
|
1183
|
-
}
|
|
1184
|
-
return tryNext(0)
|
|
1185
|
-
})
|
|
1186
|
-
}
|
|
1187
|
-
|
|
1188
|
-
function cardHtml(card) {
|
|
1189
|
-
var img = card.image
|
|
1190
|
-
? '<img src="' + card.image + '" style="width:100%;max-height:240px;object-fit:cover;display:block">'
|
|
1191
|
-
: ''
|
|
1192
|
-
var provider = card.provider ? '<div style="font-size:12px;opacity:.7">' + esc(card.provider) + '</div>' : ''
|
|
1193
|
-
var desc = card.description ? '<div style="font-size:13px;opacity:.85;margin-top:4px">' + esc(card.description) + '</div>' : ''
|
|
1194
|
-
return (
|
|
1195
|
-
'<div style="border:1px solid rgba(128,128,128,.35);border-radius:8px;overflow:hidden;max-width:480px">' +
|
|
1196
|
-
img +
|
|
1197
|
-
'<div style="padding:8px 10px">' +
|
|
1198
|
-
provider +
|
|
1199
|
-
'<div style="font-weight:600;font-size:15px"><a href="' + esc(card.url) + '" target="_blank" rel="noopener">' +
|
|
1200
|
-
esc(card.title || card.url) +
|
|
1201
|
-
'</a></div>' +
|
|
1202
|
-
desc +
|
|
1203
|
-
'<div style="font-size:11px;opacity:.55;margin-top:6px;word-break:break-all">' + esc(card.url) + '</div>' +
|
|
1204
|
-
'</div></div>'
|
|
1205
|
-
)
|
|
1206
|
-
}
|
|
1207
|
-
|
|
1208
|
-
// "is this host ours?" — cached list from our own server.
|
|
1209
|
-
var _farmDomains = null
|
|
1210
|
-
function farmDomains() {
|
|
1211
|
-
if (_farmDomains) return Promise.resolve(_farmDomains)
|
|
1212
|
-
return fetch(location.origin + '/system/farm-domains.json')
|
|
1213
|
-
.then(function (r) {
|
|
1214
|
-
return r.ok ? r.json() : { local: [], public: [] }
|
|
1215
|
-
})
|
|
1216
|
-
.then(function (f) {
|
|
1217
|
-
_farmDomains = f
|
|
1218
|
-
return f
|
|
1219
|
-
})
|
|
1220
|
-
.catch(function () {
|
|
1221
|
-
_farmDomains = { local: [], public: [] }
|
|
1222
|
-
return _farmDomains
|
|
1223
|
-
})
|
|
1224
|
-
}
|
|
1225
|
-
function farmClass(site, farm) {
|
|
1226
|
-
if (farm && farm.local && farm.local.indexOf(site) !== -1) return 'local'
|
|
1227
|
-
if (farm && farm.public && farm.public.indexOf(site) !== -1) return 'public'
|
|
1228
|
-
return 'federation'
|
|
1229
|
-
}
|
|
1230
|
-
|
|
1231
|
-
// wiki.site().get returns the sitemap as {data,lastModified} (or, cached in the
|
|
1232
|
-
// neighborhood, as the raw array) — normalize both.
|
|
1233
|
-
function sitemapArray(g) {
|
|
1234
|
-
if (Array.isArray(g)) return g
|
|
1235
|
-
if (g && Array.isArray(g.data)) return g.data
|
|
1236
|
-
return null
|
|
1237
|
-
}
|
|
1238
|
-
// Confirm a candidate {site,slug} is really a wiki page → {isWiki, exists, title}.
|
|
1239
|
-
function probeWiki(site, slug) {
|
|
1240
|
-
return new Promise(function (resolve) {
|
|
1241
|
-
var done = function (arr) {
|
|
1242
|
-
if (!arr) return resolve({ isWiki: false, exists: false, title: '' })
|
|
1243
|
-
var entry = null
|
|
1244
|
-
for (var i = 0; i < arr.length; i++)
|
|
1245
|
-
if (arr[i] && arr[i].slug === slug) {
|
|
1246
|
-
entry = arr[i]
|
|
1247
|
-
break
|
|
1248
|
-
}
|
|
1249
|
-
resolve({ isWiki: true, exists: !!entry, title: entry ? entry.title || slug : '' })
|
|
1250
|
-
}
|
|
1251
|
-
try {
|
|
1252
|
-
var n = window.wiki && window.wiki.neighborhood && window.wiki.neighborhood[site]
|
|
1253
|
-
if (n && n.sitemap) return done(sitemapArray(n.sitemap)) // instant if cached
|
|
1254
|
-
window.wiki.site(site).get('system/sitemap.json', function (err, got) {
|
|
1255
|
-
done(err ? null : sitemapArray(got))
|
|
1256
|
-
})
|
|
1257
|
-
} catch (e) {
|
|
1258
|
-
resolve({ isWiki: false, exists: false, title: '' })
|
|
1259
|
-
}
|
|
1260
|
-
})
|
|
1261
|
-
}
|
|
1262
|
-
|
|
1263
|
-
// Orchestrate: confirm wiki pages, else fetch an unfurl card, then render.
|
|
1264
|
-
function buildDropReport($target, d) {
|
|
1265
|
-
if (d.kind === 'page') {
|
|
1266
|
-
Promise.all([probeWiki(d.site, d.slug), farmDomains()]).then(function (res) {
|
|
1267
|
-
var p = res[0]
|
|
1268
|
-
if (p.isWiki) {
|
|
1269
|
-
d.wikiTitle = p.title || d.slug
|
|
1270
|
-
d.pageExists = p.exists
|
|
1271
|
-
d.farm = farmClass(d.site, res[1]) // local | public | federation
|
|
1272
|
-
d.kind = d.farm === 'federation' ? 'federation-page' : 'farm-page'
|
|
1273
|
-
renderDropReport($target, d, null)
|
|
1274
|
-
} else {
|
|
1275
|
-
// URL shaped like a lineup, but the host isn't a wiki → treat as a link.
|
|
1276
|
-
d.kind = 'link'
|
|
1277
|
-
unfurl(d.url).then(function (card) {
|
|
1278
|
-
renderDropReport($target, d, card)
|
|
1279
|
-
})
|
|
1280
|
-
}
|
|
1281
|
-
})
|
|
1282
|
-
} else if (d.url) {
|
|
1283
|
-
unfurl(d.url).then(function (card) {
|
|
1284
|
-
renderDropReport($target, d, card)
|
|
1285
|
-
})
|
|
1286
|
-
} else {
|
|
1287
|
-
renderDropReport($target, d, null)
|
|
1288
|
-
}
|
|
1289
|
-
}
|
|
1290
|
-
|
|
1291
|
-
function renderDropReport($target, d, card) {
|
|
1292
|
-
var wiki = window.wiki
|
|
1293
|
-
var raw = $target.data('data') || {}
|
|
1294
|
-
var targetTitle = raw.title || $target.attr('id') || 'this page'
|
|
1295
|
-
var kindLabel =
|
|
1296
|
-
{
|
|
1297
|
-
'farm-page': 'a page on our farm',
|
|
1298
|
-
'federation-page': 'a page in the wider federation',
|
|
1299
|
-
site: 'a wiki site',
|
|
1300
|
-
link: 'a link / URL',
|
|
1301
|
-
text: 'plain text',
|
|
1302
|
-
file: 'a file',
|
|
1303
|
-
}[d.kind] || 'an unrecognised drop'
|
|
1304
|
-
var cap = function (s) {
|
|
1305
|
-
return (s || '').slice(0, 2000)
|
|
1306
|
-
}
|
|
1307
|
-
|
|
1308
|
-
var story = [
|
|
1309
|
-
{
|
|
1310
|
-
type: 'markdown',
|
|
1311
|
-
id: rand(),
|
|
1312
|
-
text: '# 📥 Dropped onto ' + targetTitle + '\n\nYou dropped **' + kindLabel + '** onto its flag.',
|
|
1313
|
-
},
|
|
1314
|
-
]
|
|
1315
|
-
// Lead: confirmed wiki page → title + provenance + a live reference.
|
|
1316
|
-
if (d.kind === 'farm-page' || d.kind === 'federation-page') {
|
|
1317
|
-
var where =
|
|
1318
|
-
d.farm === 'local' ? 'our **local** farm' : d.farm === 'public' ? 'our **public** farm' : 'the **wider federation**'
|
|
1319
|
-
story.push({
|
|
1320
|
-
type: 'markdown',
|
|
1321
|
-
id: rand(),
|
|
1322
|
-
text:
|
|
1323
|
-
'# ' +
|
|
1324
|
-
(d.wikiTitle || d.slug) +
|
|
1325
|
-
'\n\nA wiki page on ' +
|
|
1326
|
-
where +
|
|
1327
|
-
' — `' +
|
|
1328
|
-
esc(d.site) +
|
|
1329
|
-
'`.' +
|
|
1330
|
-
(d.pageExists ? '' : '\n\n_(not listed in the site sitemap — may be new or removed)_'),
|
|
1331
|
-
})
|
|
1332
|
-
story.push({
|
|
1333
|
-
type: 'reference',
|
|
1334
|
-
id: rand(),
|
|
1335
|
-
site: d.site,
|
|
1336
|
-
slug: d.slug,
|
|
1337
|
-
title: d.wikiTitle || d.slug,
|
|
1338
|
-
text: d.farm === 'federation' ? 'federated page' : 'our page',
|
|
1339
|
-
})
|
|
1340
|
-
}
|
|
1341
|
-
// Lead with the rich preview card when we have one.
|
|
1342
|
-
if (card && (card.title || card.image)) story.push({ type: 'html', id: rand(), text: cardHtml(card) })
|
|
1343
|
-
|
|
1344
|
-
story.push({ type: 'markdown', id: rand(), text: '# Details' })
|
|
1345
|
-
if (d.kind === 'farm-page' || d.kind === 'federation-page') {
|
|
1346
|
-
story.push({
|
|
1347
|
-
type: 'markdown',
|
|
1348
|
-
id: rand(),
|
|
1349
|
-
text:
|
|
1350
|
-
'# Wiki page\n\n- site: `' + esc(d.site) + '`\n- slug: `' + esc(d.slug) + '`\n- farm: `' + esc(d.farm) +
|
|
1351
|
-
'`\n- in sitemap: `' + (d.pageExists ? 'yes' : 'no') + '`',
|
|
1352
|
-
})
|
|
1353
|
-
} else if (d.kind === 'site') {
|
|
1354
|
-
story.push({ type: 'markdown', id: rand(), text: '# Wiki site\n\n`' + esc(d.site) + '` — [welcome](//' + esc(d.site) + '/view/welcome-visitors)' })
|
|
1355
|
-
}
|
|
1356
|
-
if (d.url) story.push({ type: 'markdown', id: rand(), text: '# URL\n\n`' + d.url + '`' })
|
|
1357
|
-
if (d.text && d.text !== d.url) story.push({ type: 'markdown', id: rand(), text: '# Text\n\n```\n' + cap(d.text) + '\n```' })
|
|
1358
|
-
if (d.html) story.push({ type: 'markdown', id: rand(), text: '# HTML\n\n```\n' + cap(esc(d.html)) + '\n```' })
|
|
1359
|
-
if (d.files.length)
|
|
1360
|
-
story.push({
|
|
1361
|
-
type: 'markdown',
|
|
1362
|
-
id: rand(),
|
|
1363
|
-
text: '# Files\n\n' + d.files.map(function (f) {
|
|
1364
|
-
return '- ' + esc(f.name) + ' (' + esc(f.type || '?') + ', ' + f.size + ' bytes)'
|
|
1365
|
-
}).join('\n'),
|
|
1366
|
-
})
|
|
1367
|
-
story.push({ type: 'markdown', id: rand(), text: '# Raw dataTransfer types\n\n`' + esc((d.types || []).join(', ') || '(none)') + '`' })
|
|
1368
|
-
|
|
1369
|
-
insertPageAfter($target, wiki.newPage({ title: targetTitle + ' 📥', story: story, journal: [] }), { ghost: true })
|
|
1370
|
-
}
|
|
1371
|
-
|
|
1372
|
-
// Clicking a page flag opens, inserted to the right of that page:
|
|
1373
|
-
// • logged in → a Tools page (Fork / Trash / Clean journal / Simplify)
|
|
1374
|
-
// • logged out → a Navigate page (Simplify + this site's recent Changes)
|
|
1375
|
-
function openFlagPage(a) {
|
|
1376
|
-
var $ = window.$
|
|
1377
|
-
var wiki = window.wiki
|
|
1378
|
-
var $src = $(a).closest('.page')
|
|
1379
|
-
var raw = $src.data('data') || {}
|
|
1380
|
-
var pageTitle = raw.title || $src.attr('id') || 'this page'
|
|
1381
|
-
var slug = $src.attr('id')
|
|
1382
|
-
var host = location.host
|
|
1383
|
-
var loggedIn = window.isAuthenticated === true
|
|
1384
|
-
|
|
1385
|
-
// Simplify — shared by both pages (a plain nav link; needs no login).
|
|
1386
|
-
var surl = simplifyUrl($src, slug, remoteSiteOf($src))
|
|
1387
|
-
var simplifySection = [
|
|
1388
|
-
{
|
|
1389
|
-
type: 'markdown',
|
|
1390
|
-
id: rand(),
|
|
1391
|
-
text:
|
|
1392
|
-
'# Simplify\n\nCollapse the lineup down to just **Welcome Visitors** and **' +
|
|
1393
|
-
pageTitle +
|
|
1394
|
-
'**, clearing away the trail of pages you clicked through to get here.',
|
|
1395
|
-
},
|
|
1396
|
-
{
|
|
1397
|
-
type: 'html',
|
|
1398
|
-
id: rand(),
|
|
1399
|
-
text:
|
|
1400
|
-
'<p><a class="wiki-ux-tool-simplify" href="' +
|
|
1401
|
-
esc(surl) +
|
|
1402
|
-
'"><button style="' + BTN + '">⌂ Simplify to Welcome Visitors + ' +
|
|
1403
|
-
esc(pageTitle) +
|
|
1404
|
-
'</button></a></p>',
|
|
1405
|
-
},
|
|
1406
|
-
]
|
|
1407
|
-
|
|
1408
|
-
// Tool pages — real, forkable wiki pages that act on this page via the
|
|
1409
|
-
// similarity DSL's SUBJECT keyword. Shown to visitors and owners alike:
|
|
1410
|
-
// the searches are read-only. Gated on the similarity plugin being here.
|
|
1411
|
-
var toolPagesSection = simInstalled
|
|
1412
|
-
? [
|
|
1413
|
-
{
|
|
1414
|
-
type: 'markdown',
|
|
1415
|
-
id: rand(),
|
|
1416
|
-
text:
|
|
1417
|
-
'# 🔧 Tool pages\n\nOpen a tool page beside **' +
|
|
1418
|
-
pageTitle +
|
|
1419
|
-
'** — its searches take this page as their subject.',
|
|
1420
|
-
},
|
|
1421
|
-
{
|
|
1422
|
-
type: 'html',
|
|
1423
|
-
id: rand(),
|
|
1424
|
-
text:
|
|
1425
|
-
'<p><button class="wiki-ux-tool-toolpage" data-slug="similar-page-tool" style="' + BTN + '">🔍 Similar Page Tool</button> ' +
|
|
1426
|
-
'<button class="wiki-ux-tool-toolpage" data-slug="search-related-page" style="' + BTN + '">🗂️ Search Related Page</button></p>',
|
|
1427
|
-
},
|
|
1428
|
-
]
|
|
1429
|
-
: []
|
|
1430
|
-
|
|
1431
|
-
var story, title
|
|
1432
|
-
|
|
1433
|
-
if (loggedIn) {
|
|
1434
|
-
// ===== Tools page: editing actions (owner only) =====
|
|
1435
|
-
title = pageTitle + ' 🛠️'
|
|
1436
|
-
var isRemote = $src.hasClass('remote') // blue halo — from another server
|
|
1437
|
-
var isLocal = $src.hasClass('local') // yellow halo of death — unsaved
|
|
1438
|
-
var showFork = isRemote || isLocal
|
|
1439
|
-
var remoteSite = remoteSiteOf($src)
|
|
1440
|
-
|
|
1441
|
-
story = [{ type: 'paragraph', id: rand(), text: 'Tools for **' + pageTitle + '**.' }]
|
|
1442
|
-
|
|
1443
|
-
if (showFork) {
|
|
1444
|
-
var why = isRemote
|
|
1445
|
-
? 'This page lives on **' +
|
|
1446
|
-
(remoteSite || 'another wiki') +
|
|
1447
|
-
'** — the blue halo. **Forking** copies it onto **' +
|
|
1448
|
-
host +
|
|
1449
|
-
'**, so you get your own editable version here and it joins your wiki. The original stays where it is; forking never changes it.'
|
|
1450
|
-
: 'This page has **unsaved local changes** — the yellow halo of death (it only exists in this browser). **Forking** writes it to the server here on **' +
|
|
1451
|
-
host +
|
|
1452
|
-
'**, making it a real, saved, shareable page.'
|
|
1453
|
-
story.push({ type: 'markdown', id: rand(), text: '# Fork this page\n\n' + why })
|
|
1454
|
-
story.push({
|
|
1455
|
-
type: 'html',
|
|
1456
|
-
id: rand(),
|
|
1457
|
-
text: '<p><button class="wiki-ux-tool-fork" style="' + BTN + '">' + FORK_SVG + 'Fork this page to ' + esc(host) + '</button></p>',
|
|
1458
|
-
})
|
|
1459
|
-
}
|
|
1460
|
-
|
|
1461
|
-
// Similarity power tool — only when wiki-plugin-similarity is installed.
|
|
1462
|
-
// Sits directly below Fork, or first when there is no Fork.
|
|
1463
|
-
if (simInstalled) {
|
|
1464
|
-
story.push({
|
|
1465
|
-
type: 'markdown',
|
|
1466
|
-
id: rand(),
|
|
1467
|
-
text:
|
|
1468
|
-
'# 🔍 Similarity search\n\nFind pages across the whole farm that are semantically similar to **' +
|
|
1469
|
-
pageTitle +
|
|
1470
|
-
'** — seeded by this page itself; opens a ranked ghost report you can read and fork.',
|
|
1471
|
-
})
|
|
1472
|
-
story.push({
|
|
1473
|
-
type: 'html',
|
|
1474
|
-
id: rand(),
|
|
1475
|
-
text: '<p><button class="wiki-ux-tool-sim" style="' + BTN + '">🔍 Similarity search</button></p>',
|
|
1476
|
-
})
|
|
1477
|
-
}
|
|
1478
|
-
story = story.concat(toolPagesSection)
|
|
1479
|
-
|
|
1480
|
-
story.push({
|
|
1481
|
-
type: 'markdown',
|
|
1482
|
-
id: rand(),
|
|
1483
|
-
text:
|
|
1484
|
-
'# Trash this page\n\nMove **' +
|
|
1485
|
-
pageTitle +
|
|
1486
|
-
'** to the recycler. It disappears from the site but is not destroyed — recover it any time from [[Recycled Pages]].',
|
|
1487
|
-
})
|
|
1488
|
-
story.push({
|
|
1489
|
-
type: 'html',
|
|
1490
|
-
id: rand(),
|
|
1491
|
-
text: '<p><button class="wiki-ux-tool-trash" style="' + BTN + '">🗑️ Trash this page</button></p>',
|
|
1492
|
-
})
|
|
1493
|
-
|
|
1494
|
-
story.push({
|
|
1495
|
-
type: 'markdown',
|
|
1496
|
-
id: rand(),
|
|
1497
|
-
text:
|
|
1498
|
-
'# Clean the journal\n\nOpen a fresh **ghost** copy of this page whose journal is stripped back to a single "create" entry — as if written today, with no fork trail or remote attribution. Fork that ghost to keep the cleaned version.',
|
|
1499
|
-
})
|
|
1500
|
-
story.push({
|
|
1501
|
-
type: 'html',
|
|
1502
|
-
id: rand(),
|
|
1503
|
-
text: '<p><button class="wiki-ux-tool-clean" style="' + BTN + '">🧹 Clean the journal</button></p>',
|
|
1504
|
-
})
|
|
1505
|
-
|
|
1506
|
-
// Fish Picker — only for the OWNER, on their OWN site (not a remote page).
|
|
1507
|
-
if (window.isOwner === true && !isRemote) {
|
|
1508
|
-
story.push({
|
|
1509
|
-
type: 'markdown',
|
|
1510
|
-
id: rand(),
|
|
1511
|
-
text:
|
|
1512
|
-
'# 🐟 Site flag\n\nDesign this site’s identity flag (favicon) with the **Fish Picker** — species = you, colour = this site’s topic school, a Mouse (or robo-mouse) for human/AI.',
|
|
1513
|
-
})
|
|
1514
|
-
story.push({
|
|
1515
|
-
type: 'html',
|
|
1516
|
-
id: rand(),
|
|
1517
|
-
text: '<p><button class="wiki-ux-tool-flag" style="' + BTN + '">🐟 Fish Picker</button></p>',
|
|
1518
|
-
})
|
|
1519
|
-
}
|
|
1520
|
-
|
|
1521
|
-
story = story.concat(simplifySection)
|
|
1522
|
-
} else {
|
|
1523
|
-
// ===== Navigate page: for a logged-out visitor =====
|
|
1524
|
-
title = pageTitle + ' 🧭'
|
|
1525
|
-
story = [{ type: 'paragraph', id: rand(), text: 'Ways to get around from **' + pageTitle + '**.' }]
|
|
1526
|
-
.concat(toolPagesSection)
|
|
1527
|
-
.concat(simplifySection)
|
|
1528
|
-
.concat([
|
|
1529
|
-
{ type: 'markdown', id: rand(), text: '# Changes to this Site' },
|
|
1530
|
-
{ type: 'paragraph', id: rand(), text: 'Below we list recent changes to pages on [[this site]].' },
|
|
1531
|
-
{ type: 'activity', id: rand(), text: 'NEIGHBORHOOD no\nNARRATIVE ' },
|
|
1532
|
-
])
|
|
1533
|
-
}
|
|
1534
|
-
|
|
1535
|
-
// Title = original + a tools/compass emoji (reads as the same page, tagged).
|
|
1536
|
-
// asSlug strips the emoji but the trailing space leaves a hyphen, so the slug
|
|
1537
|
-
// stays distinct from the source. `plugin` → the green plugin-page halo.
|
|
1538
|
-
var pageObject = wiki.newPage({ title: title, story: story, journal: [], plugin: 'hitchhiker' })
|
|
1539
|
-
insertPageAfter($src, pageObject)
|
|
1540
|
-
}
|
|
1541
|
-
|
|
1542
|
-
// ===========================================================================
|
|
1543
|
-
// 5. Fish Picker — the site identity flag editor (owner only)
|
|
1544
|
-
// ===========================================================================
|
|
1545
|
-
function installFishFlag() {
|
|
1546
|
-
if (document.getElementById('fish-picker-gen')) return
|
|
1547
|
-
var s = document.createElement('script')
|
|
1548
|
-
s.id = 'fish-picker-gen'
|
|
1549
|
-
s.src = '/plugins/hitchhiker/fish.js' // sets window.FishPicker
|
|
1550
|
-
document.head.appendChild(s)
|
|
1551
|
-
// Bridge: an embedded (sandboxed, session-less) Fish Picker frame asks us —
|
|
1552
|
-
// the authenticated page — to persist its favicon. The POST is owner-gated
|
|
1553
|
-
// server-side, so relaying is safe; we just carry the session cookie.
|
|
1554
|
-
window.addEventListener('message', function (e) {
|
|
1555
|
-
var d = e.data
|
|
1556
|
-
if (!d || d.type !== 'fishpicker:save' || typeof d.image !== 'string' || d.image.slice(0, 10) !== 'data:image') return
|
|
1557
|
-
var reply = function (ok, status) {
|
|
1558
|
-
try { e.source && e.source.postMessage({ type: 'fishpicker:saved', ok: ok, status: status }, '*') } catch (_) {}
|
|
1559
|
-
}
|
|
1560
|
-
fetch('/favicon.png', {
|
|
1561
|
-
method: 'POST',
|
|
1562
|
-
credentials: 'include',
|
|
1563
|
-
headers: { 'Content-Type': 'application/json' },
|
|
1564
|
-
body: JSON.stringify({ image: d.image }),
|
|
1565
|
-
})
|
|
1566
|
-
.then(function (r) { if (r.ok) bustFavicon(); reply(r.ok, r.status) })
|
|
1567
|
-
.catch(function () { reply(false, 0) })
|
|
1568
|
-
})
|
|
1569
|
-
}
|
|
1570
|
-
|
|
1571
|
-
// Open the Fish Picker for THIS site's flag. Seeds from /status/identity.json
|
|
1572
|
-
// if present, else from the {person}.{topic} domain.
|
|
1573
|
-
function openFishPicker($src, $tools) {
|
|
1574
|
-
var FP = window.FishPicker
|
|
1575
|
-
if (!FP) return window.alert('Fish Picker still loading — try again in a moment.')
|
|
1576
|
-
var host = location.hostname
|
|
1577
|
-
var labels = host.split('.').filter(Boolean)
|
|
1578
|
-
var st = { id: labels[0] || 'owner', name: labels[0] || '', type: 'human', topic: labels[1] || 'fish', species: FP.speciesFor(labels[0] || ''), variant: 3 }
|
|
1579
|
-
var SPECIES = ['koi', 'pufferfish', 'pike']
|
|
1580
|
-
var opt = function (arr, sel) {
|
|
1581
|
-
return arr.map(function (x) { return '<option' + (x === sel ? ' selected' : '') + '>' + x + '</option>' }).join('')
|
|
1582
|
-
}
|
|
1583
|
-
var identity = function () {
|
|
1584
|
-
return { name: st.name, id: st.type === 'agent' ? st.id + '-agent' : st.id, type: st.type, topicGroup: st.topic, species: st.species, variant: st.variant, publicKey: null }
|
|
1585
|
-
}
|
|
1586
|
-
var dlg = document.createElement('dialog')
|
|
1587
|
-
dlg.id = 'fish-picker-dialog'
|
|
1588
|
-
dlg.style.cssText = 'max-width:520px;border:1px solid #ccc;border-radius:10px;padding:0'
|
|
1589
|
-
dlg.innerHTML =
|
|
1590
|
-
'<form method="dialog" style="margin:0;padding:16px 18px;font:14px system-ui,sans-serif">' +
|
|
1591
|
-
'<h2 style="margin:0 0 8px;font-size:16px">🐟 Site flag — ' + esc(host) + '</h2>' +
|
|
1592
|
-
'<div id="fp-preview" style="display:flex;gap:10px;align-items:flex-end;min-height:132px;margin:6px 0"></div>' +
|
|
1593
|
-
'<div style="display:flex;gap:12px;flex-wrap:wrap;align-items:center">' +
|
|
1594
|
-
'<label>Species <select id="fp-species">' + opt(SPECIES, st.species) + '</select></label>' +
|
|
1595
|
-
'<label>School <select id="fp-topic">' + opt(Object.keys(FP.SCHOOLS), st.topic) + '</select></label>' +
|
|
1596
|
-
'<label><input type="checkbox" id="fp-agent"> Agent (AI)</label>' +
|
|
1597
|
-
'<label>Variant <button type="button" id="fp-vd">−</button> <span id="fp-v">' + st.variant + '</span> <button type="button" id="fp-vu">+</button></label>' +
|
|
1598
|
-
'</div>' +
|
|
1599
|
-
'<pre id="fp-json" style="background:#f6f5f2;border-radius:6px;padding:8px;font-size:11px;max-height:120px;overflow:auto;margin:8px 0"></pre>' +
|
|
1600
|
-
'<menu style="display:flex;gap:8px;justify-content:flex-end;padding:0;margin:6px 0 0">' +
|
|
1601
|
-
'<button value="cancel">Cancel</button><button value="save" style="' + BTN + '">Save flag</button></menu>' +
|
|
1602
|
-
'</form>'
|
|
1603
|
-
document.body.appendChild(dlg)
|
|
1604
|
-
var render = function () {
|
|
1605
|
-
dlg.querySelector('#fp-preview').innerHTML = FP.fishSVG(st, 32) + FP.fishSVG(st, 64) + FP.fishSVG(st, 128)
|
|
1606
|
-
dlg.querySelector('#fp-json').textContent = JSON.stringify(identity(), null, 1)
|
|
1607
|
-
}
|
|
1608
|
-
dlg.querySelector('#fp-species').onchange = function () { st.species = this.value; render() }
|
|
1609
|
-
dlg.querySelector('#fp-topic').onchange = function () { st.topic = this.value; render() }
|
|
1610
|
-
dlg.querySelector('#fp-agent').onchange = function () { st.type = this.checked ? 'agent' : 'human'; render() }
|
|
1611
|
-
dlg.querySelector('#fp-vu').onclick = function () { st.variant++; dlg.querySelector('#fp-v').textContent = st.variant; render() }
|
|
1612
|
-
dlg.querySelector('#fp-vd').onclick = function () { st.variant = Math.max(0, st.variant - 1); dlg.querySelector('#fp-v').textContent = st.variant; render() }
|
|
1613
|
-
dlg.addEventListener('close', function () {
|
|
1614
|
-
if (dlg.returnValue === 'save') saveFishFlag(st)
|
|
1615
|
-
dlg.remove()
|
|
1616
|
-
if ($tools) $tools.remove()
|
|
1617
|
-
})
|
|
1618
|
-
var seed = function (id) {
|
|
1619
|
-
if (id) {
|
|
1620
|
-
st.id = (id.id || st.id).replace(/-agent$/, '') // stored agent id is base+"-agent"
|
|
1621
|
-
st.name = id.name || st.name
|
|
1622
|
-
st.type = id.type || st.type
|
|
1623
|
-
st.topic = id.topicGroup || st.topic
|
|
1624
|
-
st.species = id.species || st.species
|
|
1625
|
-
if (id.variant != null) st.variant = id.variant
|
|
1626
|
-
dlg.querySelector('#fp-species').value = st.species
|
|
1627
|
-
dlg.querySelector('#fp-topic').value = st.topic
|
|
1628
|
-
dlg.querySelector('#fp-agent').checked = st.type === 'agent'
|
|
1629
|
-
dlg.querySelector('#fp-v').textContent = st.variant
|
|
1630
|
-
}
|
|
1631
|
-
render()
|
|
1632
|
-
}
|
|
1633
|
-
// seed from the current favicon's embedded identity, else /status/identity.json, else domain
|
|
1634
|
-
fetch('/favicon.png?t=' + Date.now())
|
|
1635
|
-
.then(function (r) { return r.ok ? r.arrayBuffer() : null })
|
|
1636
|
-
.then(function (ab) {
|
|
1637
|
-
if (ab) {
|
|
1638
|
-
var txt = pngReadText(new Uint8Array(ab), 'fishpicker')
|
|
1639
|
-
if (txt) {
|
|
1640
|
-
try {
|
|
1641
|
-
return JSON.parse(decodeURIComponent(txt))
|
|
1642
|
-
} catch (e) {}
|
|
1643
|
-
}
|
|
1644
|
-
}
|
|
1645
|
-
return fetch('/status/identity.json')
|
|
1646
|
-
.then(function (r) { return r.ok ? r.json() : null })
|
|
1647
|
-
.catch(function () { return null })
|
|
1648
|
-
})
|
|
1649
|
-
.then(seed)
|
|
1650
|
-
.catch(function () { seed(null) })
|
|
1651
|
-
render()
|
|
1652
|
-
dlg.showModal()
|
|
1653
|
-
}
|
|
1654
|
-
|
|
1655
|
-
// v1.5b: persist the flag by POSTing the favicon to fedwiki's built-in,
|
|
1656
|
-
// owner-gated POST /favicon.png — reusing the farm's own security
|
|
1657
|
-
// (securityhandler.isAuthorized), so NO new route and NO new auth. The identity
|
|
1658
|
-
// record rides along in a PNG tEXt chunk ("fishpicker"), so the favicon is
|
|
1659
|
-
// self-describing and the picker re-seeds from it — no identity.json file needed.
|
|
1660
|
-
function saveFishFlag(st) {
|
|
1661
|
-
var FP = window.FishPicker
|
|
1662
|
-
var identity = { name: st.name, id: st.type === 'agent' ? st.id + '-agent' : st.id, type: st.type, topicGroup: st.topic, species: st.species, variant: st.variant, publicKey: null }
|
|
1663
|
-
var svg = FP.fishSVG(st, 128)
|
|
1664
|
-
var img = new Image()
|
|
1665
|
-
img.onload = function () {
|
|
1666
|
-
var cv = document.createElement('canvas')
|
|
1667
|
-
cv.width = cv.height = 64
|
|
1668
|
-
cv.getContext('2d').drawImage(img, 0, 0, 64, 64)
|
|
1669
|
-
cv.toBlob(function (blob) {
|
|
1670
|
-
blob.arrayBuffer().then(function (ab) {
|
|
1671
|
-
var png = pngInsertText(new Uint8Array(ab), 'fishpicker', encodeURIComponent(JSON.stringify(identity)))
|
|
1672
|
-
fetch('/favicon.png', {
|
|
1673
|
-
method: 'POST',
|
|
1674
|
-
credentials: 'include',
|
|
1675
|
-
headers: { 'Content-Type': 'application/json' },
|
|
1676
|
-
body: JSON.stringify({ image: 'data:image/png;base64,' + b64(png) }),
|
|
1677
|
-
})
|
|
1678
|
-
.then(function (r) {
|
|
1679
|
-
if (r.ok) {
|
|
1680
|
-
bustFavicon()
|
|
1681
|
-
window.alert('🐟 Flag saved to ' + location.hostname + '.')
|
|
1682
|
-
} else if (r.status === 403) {
|
|
1683
|
-
window.alert('Not saved — you must be signed in as the owner of this site.')
|
|
1684
|
-
} else {
|
|
1685
|
-
window.alert('Save failed (HTTP ' + r.status + ').')
|
|
1686
|
-
}
|
|
1687
|
-
})
|
|
1688
|
-
.catch(function (e) { window.alert('Save failed: ' + (e && e.message)) })
|
|
1689
|
-
})
|
|
1690
|
-
}, 'image/png')
|
|
1691
|
-
}
|
|
1692
|
-
img.src = 'data:image/svg+xml;base64,' + btoa(unescape(encodeURIComponent(svg)))
|
|
1693
|
-
}
|
|
1694
|
-
|
|
1695
|
-
// Refresh the favicon in every page header (cache-bust) after a save.
|
|
1696
|
-
function bustFavicon() {
|
|
1697
|
-
try {
|
|
1698
|
-
var imgs = document.querySelectorAll('img.favicon')
|
|
1699
|
-
for (var i = 0; i < imgs.length; i++) imgs[i].src = '/favicon.png?t=' + Date.now()
|
|
1700
|
-
} catch (e) {}
|
|
1701
|
-
}
|
|
1702
|
-
|
|
1703
|
-
// ---- PNG tEXt chunk helpers — insert/read a keyword=text metadata chunk ----
|
|
1704
|
-
var CRC_TABLE = (function () {
|
|
1705
|
-
var t = [], c, n, k
|
|
1706
|
-
for (n = 0; n < 256; n++) { c = n; for (k = 0; k < 8; k++) c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1; t[n] = c >>> 0 }
|
|
1707
|
-
return t
|
|
1708
|
-
})()
|
|
1709
|
-
function crc32(bytes) {
|
|
1710
|
-
var c = 0xffffffff
|
|
1711
|
-
for (var i = 0; i < bytes.length; i++) c = CRC_TABLE[(c ^ bytes[i]) & 0xff] ^ (c >>> 8)
|
|
1712
|
-
return (c ^ 0xffffffff) >>> 0
|
|
1713
|
-
}
|
|
1714
|
-
function u32(n) { return [(n >>> 24) & 255, (n >>> 16) & 255, (n >>> 8) & 255, n & 255] }
|
|
1715
|
-
function pngInsertText(png, keyword, text) {
|
|
1716
|
-
var data = []
|
|
1717
|
-
for (var i = 0; i < keyword.length; i++) data.push(keyword.charCodeAt(i) & 255)
|
|
1718
|
-
data.push(0)
|
|
1719
|
-
for (var j = 0; j < text.length; j++) data.push(text.charCodeAt(j) & 255)
|
|
1720
|
-
var body = [0x74, 0x45, 0x58, 0x74].concat(data) // 'tEXt' + data
|
|
1721
|
-
var chunk = u32(data.length).concat(body).concat(u32(crc32(new Uint8Array(body))))
|
|
1722
|
-
var iend = png.length - 12 // IEND is the final 12 bytes; insert before it
|
|
1723
|
-
var out = new Uint8Array(png.length + chunk.length)
|
|
1724
|
-
out.set(png.subarray(0, iend), 0)
|
|
1725
|
-
out.set(new Uint8Array(chunk), iend)
|
|
1726
|
-
out.set(png.subarray(iend), iend + chunk.length)
|
|
1727
|
-
return out
|
|
1728
|
-
}
|
|
1729
|
-
function pngReadText(png, keyword) {
|
|
1730
|
-
var p = 8
|
|
1731
|
-
while (p + 8 <= png.length) {
|
|
1732
|
-
var len = (png[p] << 24) | (png[p + 1] << 16) | (png[p + 2] << 8) | png[p + 3]
|
|
1733
|
-
var type = String.fromCharCode(png[p + 4], png[p + 5], png[p + 6], png[p + 7])
|
|
1734
|
-
if (type === 'tEXt') {
|
|
1735
|
-
var s = ''
|
|
1736
|
-
for (var i = 0; i < len; i++) s += String.fromCharCode(png[p + 8 + i])
|
|
1737
|
-
var z = s.indexOf('\0')
|
|
1738
|
-
if (z !== -1 && s.slice(0, z) === keyword) return s.slice(z + 1)
|
|
1739
|
-
}
|
|
1740
|
-
if (type === 'IEND') break
|
|
1741
|
-
p += 12 + len
|
|
1742
|
-
}
|
|
1743
|
-
return null
|
|
1744
|
-
}
|
|
1745
|
-
function b64(bytes) {
|
|
1746
|
-
var s = ''
|
|
1747
|
-
for (var i = 0; i < bytes.length; i++) s += String.fromCharCode(bytes[i])
|
|
1748
|
-
return btoa(s)
|
|
1749
|
-
}
|
|
1750
|
-
|
|
1751
|
-
// ===========================================================================
|
|
1752
|
-
// helpers
|
|
1753
|
-
// ===========================================================================
|
|
1754
|
-
function reEmitOpenFactories() {
|
|
1755
|
-
var $ = window.$
|
|
1756
|
-
$('.item.factory').each(function (_i, el) {
|
|
1757
|
-
var $i = $(el)
|
|
1758
|
-
var it = $i.data('item')
|
|
1759
|
-
if (it && it.type === 'factory') {
|
|
1760
|
-
$i.empty()
|
|
1761
|
-
try {
|
|
1762
|
-
window.plugins.factory.emit($i, it)
|
|
1763
|
-
} catch (e) {}
|
|
1764
|
-
}
|
|
1765
|
-
})
|
|
1766
|
-
}
|
|
1767
|
-
|
|
1768
|
-
// Scoped stylesheet for wiki-ux-generated pages.
|
|
1769
|
-
function installStyles() {
|
|
1770
|
-
if (document.getElementById('wiki-ux-style')) return
|
|
1771
|
-
var s = document.createElement('style')
|
|
1772
|
-
s.id = 'wiki-ux-style'
|
|
1773
|
-
// Generated pages have no sister sites, so their twins band's <p> is empty
|
|
1774
|
-
// and self-collapses, pulling the title ~1em higher than normal pages. Add
|
|
1775
|
-
// the missing 1em back so the title aligns with about/normal page titles.
|
|
1776
|
-
s.textContent = [
|
|
1777
|
-
'.page.wiki-ux-tools .twins{min-height:2em}',
|
|
1778
|
-
// drop-target highlight — deliberately loud: enlarged, pulsing favicon + a label.
|
|
1779
|
-
'@keyframes wikiUxPulse{0%{box-shadow:0 0 0 0 rgba(0,190,190,.7)}70%{box-shadow:0 0 0 14px rgba(0,190,190,0)}100%{box-shadow:0 0 0 0 rgba(0,190,190,0)}}',
|
|
1780
|
-
'.page h1 a.wiki-ux-drop-target{position:relative}',
|
|
1781
|
-
'.page h1 a.wiki-ux-drop-target img.favicon{transform:scale(1.5);border-radius:4px;outline:3px solid #0bb;animation:wikiUxPulse 1s infinite}',
|
|
1782
|
-
".page h1 a.wiki-ux-drop-target::after{content:'\\2935 drop to preview';position:absolute;left:0;top:100%;margin-top:5px;white-space:nowrap;font-size:11px;font-weight:600;color:#0bb;background:rgba(0,190,190,.14);padding:1px 6px;border-radius:3px;z-index:5}",
|
|
1783
|
-
'.page:has(h1 a.wiki-ux-drop-target) .header{background:rgba(0,190,190,.08);border-radius:4px}',
|
|
1784
|
-
].join('')
|
|
1785
|
-
document.head.appendChild(s)
|
|
1786
|
-
}
|
|
1787
|
-
|
|
1788
|
-
function esc(s) {
|
|
1789
|
-
return ('' + (s == null ? '' : s))
|
|
1790
|
-
.replace(/&/g, '&')
|
|
1791
|
-
.replace(/</g, '<')
|
|
1792
|
-
.replace(/>/g, '>')
|
|
1793
|
-
.replace(/"/g, '"')
|
|
1794
|
-
}
|
|
1795
|
-
function rand() {
|
|
1796
|
-
var h = ''
|
|
1797
|
-
for (var i = 0; i < 16; i++) h += ((Math.random() * 16) | 0).toString(16)
|
|
1798
|
-
return h
|
|
1799
|
-
}
|
|
1800
|
-
})()
|
|
37
|
+
Design this site\u2019s identity flag (favicon) with the **Fish Picker** \u2014 species = you, colour = this site\u2019s topic school, a Mouse (or robo-mouse) for human/AI.`}),f.push({type:"html",id:l(),text:'<p><button class="wiki-ux-tool-flag" style="'+p+'">\u{1F41F} Fish Picker</button></p>'})),f=f.concat(d)}else b=n+" \u{1F9ED}",f=[{type:"paragraph",id:l(),text:"Ways to get around from **"+n+"**."}].concat(I).concat(d).concat([{type:"markdown",id:l(),text:"# Changes to this Site"},{type:"paragraph",id:l(),text:"Below we list recent changes to pages on [[this site]]."},{type:"activity",id:l(),text:`NEIGHBORHOOD no
|
|
38
|
+
NARRATIVE `}]);var pt=a.newPage({title:b,story:f,journal:[],plugin:"hitchhiker"});m(i,pt)}function c(e){return(""+(e??"")).replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""")}function l(){for(var e="",t=0;t<16;t++)e+=(Math.random()*16|0).toString(16);return e}function st(){if(!document.getElementById("wiki-ux-style")){var e=document.createElement("style");e.id="wiki-ux-style",e.textContent=[".page.wiki-ux-tools .twins{min-height:2em}","@keyframes wikiUxPulse{0%{box-shadow:0 0 0 0 rgba(0,190,190,.7)}70%{box-shadow:0 0 0 14px rgba(0,190,190,0)}100%{box-shadow:0 0 0 0 rgba(0,190,190,0)}}",".page h1 a.wiki-ux-drop-target{position:relative}",".page h1 a.wiki-ux-drop-target img.favicon{transform:scale(1.5);border-radius:4px;outline:3px solid #0bb;animation:wikiUxPulse 1s infinite}",".page h1 a.wiki-ux-drop-target::after{content:'\\2935 drop to preview';position:absolute;left:0;top:100%;margin-top:5px;white-space:nowrap;font-size:11px;font-weight:600;color:#0bb;background:rgba(0,190,190,.14);padding:1px 6px;border-radius:3px;z-index:5}",".page:has(h1 a.wiki-ux-drop-target) .header{background:rgba(0,190,190,.08);border-radius:4px}"].join(""),document.head.appendChild(e)}}})();})();
|
|
39
|
+
//# sourceMappingURL=wiki-ux.js.map
|