:빠른이동/app.js

리버티게임, 모두가 만들어가는 자유로운 게임
< 틀:빠른이동
BANIP (토론 | 기여)님의 2023년 8월 3일 (목) 00:55 판 (preloadLimit 수동설정 가능)
(function(){
    // 프리로딩 가능한 링크 최고 갯수
    var previewLimit = 5;
	
	// 링크로 해당 문서 컨텐츠와 타이틀을 가져오는 프로미스 획득
    var getParsedDocumentPromise = (function(){
        var mwApi = new mw.Api(); 
        return function(option){
            var {href, fullPagename} = option;
            var searchParams = new URL(href).searchParams;
            var searchParamsObject = {};
            searchParams.forEach(function(value, key){
                // title 파라미터는 제외
                if(key === "title") return;
                searchParamsObject[key] = value;
            });

            // href에서 searchParams 추출
            return new Promise(function(resolve, reject){
                var apiParams = {
                    action: 'parse',
                    page: fullPagename,
                    formatversion: 2,
                };
                var requestOption = Object.assign(apiParams,searchParamsObject);
                mwApi.get(requestOption).then(function(data){
                    var title = data.parse.title;
                    var text = data.parse.text;
                    //text내에 #title-meta 요소가 포함되어 있으면 해당 컨텐츠를 title로 변경(제목틀 호환)
                    var $titleMeta = $(text).find("#title-meta");
                    if( $titleMeta.length > 0 ){
                        title = $titleMeta.html();
                    }
                    resolve({ title:title, text:text });
                }, function(e){
                    reject(e);
                }); 
            });
        };
    })();

    var setLoading = (function(){
        // 로딩창 표시 여부 플래그
        var timers = [];
        return function(isSetShow){
            // 이미 로딩 완료된 작업의 경우 로딩창을 표시하지 않기 위해
            // 로딩창 표시 작업시 예약 플래그를 설정하고 약간의 텀을 두기.
            if(isSetShow){
                setLoading.showFlag = true;
                var timer = setTimeout(function(){
                    if(setLoading.showFlag){
                        $("#loading").fadeIn(200);
                        // 타이머에서 제거
                        timers.splice(timers.indexOf(timer), 1);
                    }
                }, 50);
                timers.push(timer);
            } else {
            // 로딩창 삭제 작업시 표시작업이 예약되어있으면 같이 삭제
                setLoading.showFlag = false;
                $("#loading").fadeOut(200);
                timers.forEach(function(timer){
                    clearTimeout(timer);
                });
                timers = [];
            }
        };
    })();


    var toast = function(message){
        // 화면 오른쪽 하단에서 메세지 표시, 2초 후 사라짐
        var $toast = $("<div>").addClass("toast").css({
            position:"fixed",
            bottom: "20px",
            right: "20px",
            padding: "10px 20px",
            background: "#000",
            color: "#fff",
            opacity: 0.8,
            boxShadow: "0 0 10px rgba(0,0,0,0.3)",
            zIndex: 9999,
            display: "none"
        });

        
        $toast.text(message).appendTo("body").fadeIn(200);
        setTimeout(function(){
            $toast.fadeOut(200, function(){
                $toast.remove();
            });
        }, 2000);
    };

    // 선행로딩된 페이지들
    var documentPromiseMap = {};
    var namespace = $("#firstHeading > span.mw-page-title-namespace").val() || "";

	function getPagenameFromUrl(rawUrl){
		var url = new URL(rawUrl, location.origin);
		// url의 searchParams에 title 파라미터가 있을 경우 해당 파라미터로 docname 지정
		var fullPagename = url.searchParams.get("title") || "";

		if(!fullPagename){
			var matchedUrl = decodeURI(url.pathname).match(/[^\/]+\/(.*)/);
			if(matchedUrl){
				// 문서명 파싱 성공시 문서명 지정
				fullPagename = matchedUrl[1];
			} else {
				// 문서명 파싱 실패시 모든 링크명 지정
				fullPagename = decodeURI(url.pathname)
			}
		}
		// fullPagename에 ":"이 없을 경우 스트링 처음위치에 삽입 
		fullPagename = fullPagename.indexOf(":") === -1 ? `:${fullPagename}` : fullPagename;

		// :를 기준으로 네임스페이스와 문서명 분리
		var [namespace, pagename] = fullPagename.split(":");
		return {
			namespace, 
			pagename,
			fullPagename,
		}
	}

    function preloadDocument($doc){
        // 해당 링크의 action, title, oldid 파라미터가 없는 링크만 가져오기
        var targetLinkItems = $doc.find("a").not(".new")
			// 대상이 되는 링크들만 필터링
			.filter(function() {
				var href = this.href;
				if(href === "" || href.match(/^\#/) !== null) return false; // href가 없는 경우 제외
				// 링크에서 가져온 url
				var url = new URL(href);
				var searchParams = url.searchParams;
				// 현재 url
				var nowUrl = new URL(location.href);

				// 동일한 도메인이 아닌 경우 제외
				if(url.origin !== nowUrl.origin) return false;

				// 수정/리비전 확인 링크는 제외
				if( ["action", "oldid"].some(function(key){
					return searchParams.has(key);
				})) return false;

				return true;
			})
			// 필터링된 링크에 문서명 정보를 추가
			.toArray().map(function(el) {
				var { namespace, pagename, fullPagename } = getPagenameFromUrl(el.href);
				return {
					el, href: el.href, namespace, pagename, fullPagename
				}
			})
			// 동일한 네임스페이스만 이동되게 필터링
			.filter(({namespace:thisNamespace}) => thisNamespace === namespace )


        // 필터된게 previewLimit 갯수 이상이면 동작 x, 프로미스맵에 클릭 시 로딩 필요함을 명시
        if(targetLinkItems.length > previewLimit){
            targetLinkItems.forEach(({href}) => {
                documentPromiseMap[href] ||= null
            });
        } else {
            // 필터링된게 프로미스맵에 없는 경우 사전 프리로딩 프로미스 추가
            targetLinkItems.forEach(({href, pagename, namespace}) => {
                documentPromiseMap[href] ||= getParsedDocumentPromise({href, fullPagename: `${namespace}:${pagename}`})
            });
        }
        
        // 가져온 링크로 전체 작업
        targetLinkItems.forEach(function(linkItem){
			// 문서명 파싱 실패시 해당 링크 제외
            if(!linkItem) return;
            $(linkItem.el)
                .click(function(e){
                    e.preventDefault();
					replaceBodyContent(this.href);
                });
        })
    };

	async function replaceBodyContent(url, {urlChange = true} = {}){
		var fullPagename = getPagenameFromUrl(url).fullPagename;
		
		// 프리로드 불필요한 경우(네임스페이스가 다를때, 편집/리비전확인 링크일때) url 영구이동
		if(documentPromiseMap[url] === undefined){
			return location.href = url; 
		}
		// 프리로드되지 않은 이동 가능한 링크면 로딩창 표시 후 해당 페이지 로딩
		if(documentPromiseMap[url] === null){
			documentPromiseMap[url] = getParsedDocumentPromise({href:url, fullPagename});
		}

		// 프로미스가 아직 안끝났으면 로딩창 표시
		setLoading(true);
		// 링크 프로미스에 문제가 있을 시 다시 내용 확인, 성공시 문서 프로미스에 저장, 오류시 에러메세지
		documentPromiseMap[url].then(function({title, text}){
			// 프리로드 프로미스가 정상적으로 불러왔을 경우
			let $thisDoc = $(text)

			// 사전로딩된 내용에서 프리로드 분석 재실행
			preloadDocument( $thisDoc );
			$("#bodyContent").fadeOut(100, function(){
				// 문서내용 교체 후 페이드 인
				$("#firstHeading").html(title);
				// .vector-article-toolbar 내용물 링크 변경 필요
				$(".mw-article-toolbar-container").find("a").each(function(){
					try{
						var $this = $(this);
						var href = $this.attr("href");

						var url = new URL(href);
						url.searchParams.set("title", title);
						$this.attr("href", url.href);
					} catch (e){ } 
				})

				// 현재 url 변경 
				if(urlChange){
					history.pushState(null, null, url);
				}

				$("#bodyContent").html($thisDoc);
				$("#bodyContent").fadeIn(100);
			})
		}).catch(function(e){
			console.error(e)
			// 에러 발생 시 실패한 페이지 다시 프로미스에 저장 시도
			documentPromiseMap[url] = null;

			// 에러메세지 표시
			toast('페이지 로딩에 실패했습니다. 다시 시도해주세요.');
		}).finally(function(){
			// 로딩창 삭제
			setLoading(false);
		});
	}

    (function main(){
        // #enable-preload div 없으면 동작 x
        if(!$(".mw-parser-output .enable-preload").length) return;
        //enable-preload의 data-preload-limit가 있으면 previewLimit에 설정
        let preloadLimit = $(".mw-parser-output .enable-preload").data("preload-limit");
        // preloadLimit가 있고 0-10개일시만 설정
        if(preloadLimit && preloadLimit >= 0 && preloadLimit <= 10){
            previewLimit = preloadLimit;
        }
            

        // #bodyContent에 로딩창 추가
        $("<div>").addClass("content-loading")
            .css({
                position: "absolute",
                top: 0,
                left: 0,
                width: "100%",
                height: "100%",
                background: "#fff",
                opacity: 0.8,
                zIndex: 9999,
                display: "none"
            })
            .attr("id", "loading")
            .append($("<div>").css({
                position: "absolute",
                top: "50%",
                left: "50%",
                transform: "translate(-50%,-50%)"
            })
            .append($("<span>로딩중</span>")))
        .appendTo("#bodyContent");

		// 현재 페이지 프리로드, 함수 내부에서 재귀적으로 프리로드 실행
        preloadDocument($("#mw-content-text > div.mw-parser-output"));

		// url 변경 시(일반적으로 뒤로가기) 프리로드된 문서가 있으면 해당 문서로 이동( 2023년 7월 9일 (일) 14:00 (KST)기준 iOS/파폭에서 동작하지 않음 )
		function handleNavigate(e){
			// 사용자가 url 변경시에만 동작
			if(e.navigationType === "traverse"){
				replaceBodyContent(e.destination.url, {urlChange:false});
			}
		}
		navigation.removeEventListener("navigate", handleNavigate); // 이벤트 중복 등록 방지
		navigation.addEventListener("navigate", handleNavigate); // 이벤트 등록
    })();
})();