<commit_before>// Avoid `chrome` namespace
if (typeof browser === "undefined" &&
    typeof chrome === "object"){
    //console.log("On Chrome");
    var browser = chrome;
}

browser.runtime.onInstalled.addListener(function(e){
    // Open options page to initialize localStorage
    if (e.reason === "install")
        browser.runtime.openOptionsPage();
    else if (e.reason === "update")
        if (localStorage.getItem("replyUser") === null)
            localStorage.setItem("replyUser", 1);

});

//——————————————————————————————————接收来自页面的图片数据上传并返回——————————————————————————————————
const s = localStorage;

browser.runtime.onMessage.addListener(function(request, sender, sendResponse) {
    if ( request.img_base64 ){
        var post_url, patt_id, url_start, url_end, data;
        var img_status;
        //——————————设置微博或 imgur 的信息——————————
        if ( s.getItem("imageHosting") === "weibo" ){
            post_url = "http://picupload.service.weibo.com/interface/pic_upload.php?\
                    ori=1&mime=image%2Fjpeg&data=base64&url=0&markpos=1&logo=&nick=0&marks=1&app=miniblog";
            patt_id = "pid\":\"(.*?)\"";
            url_start = "https://ws2.sinaimg.cn/large/";
            url_end = ".jpg";
            data = {"b64_data": request.img_base64};
        }else{
            post_url = "https://api.imgur.com/3/image";
            patt_id = "id\":\"(.*?)\"";
            url_start = "https://i.imgur.com/";
            url_end = ".png";
            data = {"image": request.img_base64};
        }
        //——————————微博或 imgur 的信息完成——————————

        $.ajax({
            url: post_url,
            method: "POST",
            data: data,
            dataType: "text",
            beforeSend: (xhr) => {
                if ( s.getItem("imageHosting") === "imgur" )
                    xhr.setRequestHeader("Authorization", "Client-ID 9311f6be1c10160");
            },
            success: (data) => {
                try{
                    img_status = url_start + RegExp(patt_id).exec(data)[1] + url_end;
                    //console.log("Succeed: "+ img_status);
                } catch(e){
                    //console.error("Field not found");
                    img_status = "Failed";
                }
            },
            error: () => {
                img_status = "Failed";
                //console.info("Request failed");
            },
            complete: () => {
                sendResponse({img_status: img_status});
            }
        });
        return true;
    }
    //——————————————————————————————————接收来自页面的图片数据上传并返回——————————————————————————————————

    //——————————————————————————————————返回设置选项——————————————————————————————————
    switch (request.action) {
    case "get_preview_status":
        sendResponse({preview_status: Number(s.getItem("preview"))});
        break;
    case "get_dblclickToTop":
        sendResponse({dblclickToTop: Number(s.getItem("dblclickToTop"))});
        break;
    case "get_replySetting":
        sendResponse({
            replyColor: s.getItem("replyColor"),
            replyA: s.getItem("replyA"),
            fold: Number(s.getItem("fold")),
            thankColor: s.getItem("thankColor")
        });
        break;
    case "get_newWindow_status":
        sendResponse({newWindow_status: Number(s.getItem("newWindow"))});
        break;
    case "get_replyUser":
        sendResponse({replyUser: Number(s.getItem("replyUser"))});
        break;
    case "get_blockList":
        $.get("https://www.v2ex.com",function(data,status){
            if(status == "success"){
                var block_list = /blocked = \[(.*?)\];/.exec(data);
                var username = /首页<\/a>   <a href="\/member\/(.+?)"/.exec(data);
                if ( block_list && username ){
                    block_list = block_list[1];
                    username = username[1];
                    browser.tabs.create({url:"/page/block_list.html#"+username+"="+block_list});
                }else{
                    alert("扩展没有获取到任何信息 : (\n或许是您未登录 V2EX 账号");
                }
            }else{
                alert("扩展没有获取到任何信息 : (\n很有可能是网络问题，请稍后再试");
            }
        });
        sendResponse({blockList: "get"});
        break;
    case "get_collectList":
        window.collectNotified = false
        sendResponse({cached: localStorage.collectTopicCachedReplyCountList, latest: localStorage.collectTopicLatestReplyCountList});
        break;
    case "clear_collect":
        localStorage.collectTopicCachedReplyCountList = request.list;
        localStorage.collectTopicLatestReplyCountList = request.list;
        sendResponse(null);
        break;
    case "sync_collect":
        localStorage.collectTopicCachedReplyCountList = request.cached;
        localStorage.collectTopicLatestReplyCountList = request.latest;
        break;
    default:
        throw "invaild action";
    }
});

//——————————————————————————————————返回设置选项——————————————————————————————————

//——————————————————————————————————定时任务初始化——————————————————————————————————

Number(s.getItem("newMsg")) && checkMsg();
Number(s.getItem("followMsg")) && followMsg();
Number(s.getItem("collectMsg")) && collectMsg();
browser.alarms.create("checkMsg", {periodInMinutes: 1});
browser.alarms.create("autoMission", {periodInMinutes: 30});

browser.alarms.onAlarm.addListener(function( a ){
    switch (a.name){
    case "checkMsg":
        Number(s.getItem("newMsg")) && checkMsg();
        Number(s.getItem("followMsg")) && followMsg();
        Number(s.getItem("collectMsg")) && collectMsg();
        break;
    case "autoMission":
        Number(s.getItem("autoMission")) && autoMission();
        Number(s.getItem("autoLoginWeibo")) && autoLoginWeibo();
        break;
    }
});

//——————————————————————————————————定时任务初始化——————————————————————————————————

//——————————————————————————————————检查关注人新主题——————————————————————————————————
function followMsg() {
    $.get('https://www.v2ex.com/my/following', function(data){
        var $html = $("<output>").append($.parseHTML(data))
        window.a = $html
        var topics = $html.find('#Main .box:nth(0) table')
        if (!topics.length) return

        var $firstOne = topics.eq(2)
        var topicId = $firstOne.find('.item_title a').attr('href').substr(3).split('#')[0]
        var topic = $firstOne.find('.item_title').text()
        var author = $firstOne.find('.small.fade > strong:nth-child(3)').text()

        if( s.getItem("followMsgTopicId") == topicId ) return
        s.setItem( "followMsgTopicId", topicId)
        window.newFollowTopicId = topicId
        browser.notifications.create(
            "newFollowTopic" ,
        {
            type       : "basic",
            iconUrl    : "icon/icon38_msg.png",
            title      : "v2ex plus 提醒您",
            message    : `${author} 创作了新主题：${topic}`,
        });
    })
}

//——————————————————————————————————检查关注人新主题——————————————————————————————————

//——————————————————————————————————检查收藏主题新回复——————————————————————————————————
function collectMsg() {
    $.get('https://www.v2ex.com/my/topics', function(data){
        var $html = $("<output>").append($.parseHTML(data))
        var topics = $html.find('div.cell.item')
        if (!topics.length) return

        var cachedReplyCountList = localStorage.collectTopicCachedReplyCountList
        cachedReplyCountList = cachedReplyCountList ? JSON.parse(cachedReplyCountList) : {}
        var latestReplyCountList = localStorage.collectTopicLatestReplyCountList
        latestReplyCountList = latestReplyCountList ? JSON.parse(latestReplyCountList) : {}

        var topicIds = [];
        var newReply = false
        var topicIndex
        for (topicIndex = 0; topicIndex < topics.length; topicIndex++){
            var topic = topics[topicIndex]
            var topicReplyCountEl = $(topic).find('.count_livid, .count_orange')
            var topicReplyCount = topicReplyCountEl.length ? Number(topicReplyCountEl[0].innerText) : 0
            var topicId = Number($(topic).find('.item_title a')[0].href.match(/\/t\/(\d+)/)[1]);
            topicIds.push(topicId)

            if (cachedReplyCountList[topicId] === undefined){
                cachedReplyCountList[topicId] = topicReplyCount
            }

            if (latestReplyCountList[topicId] === undefined){
                latestReplyCountList[topicId] = topicReplyCount
            }else if (latestReplyCountList[topicId] != topicReplyCount){
                latestReplyCountList[topicId] = topicReplyCount
                newReply = true
            }
        }

        for (topicIndex in cachedReplyCountList){
            if(topicIds.indexOf(Number(topicIndex)) === -1){
                delete(cachedReplyCountList[topicIndex])
            }
        }

        for (topicIndex in latestReplyCountList){
            if(topicIds.indexOf(Number(topicIndex)) === -1){
                delete(latestReplyCountList[topicIndex])
            }
        }

        localStorage.collectTopicCachedReplyCountList = JSON.stringify(cachedReplyCountList)
        localStorage.collectTopicLatestReplyCountList = JSON.stringify(latestReplyCountList)

        if (!window.collectNotified && newReply){
            window.collectNotified = true
            browser.notifications.create(
                "newCollectTopicReply" ,
            {
                type       : "basic",
                iconUrl    : "icon/icon38_msg.png",
                title      : "v2ex plus 提醒您",
                message    : "您收藏的主题有了新回复，点击查看",
            });
            //20分钟内最多提示一次
            setTimeout(function(){
                window.collectNotified = false
            }, 1200000)
        }
    })
}

//——————————————————————————————————检查收藏主题新回复——————————————————————————————————

//——————————————————————————————————通知功能——————————————————————————————————

//现在是每5分钟刷新一次状态，除非点击了browserAction
function checkMsg(){
    $.get("https://www.v2ex.com/settings",function(data,status){
        if(status == "success"){
            var sign = RegExp("([0-9]*?) (条未读提醒|unread)").exec(data);
            sign = sign != null && sign[1] || "未登录";
            if ( sign == "未登录" ){
                browser.browserAction.setIcon({path: "icon/icon38_nologin.png"});
            }else if( sign!="0") {
                browser.browserAction.setIcon({path: "icon/icon38_msg.png"});
                browser.notifications.create(
                    "newMsg" ,
                    {
                        type       : "basic",
                        iconUrl    : "icon/icon38_msg.png",
                        title      : "v2ex plus 提醒您",
                        message    : "您有 V2EX 的未读新消息，点击查看。",
                    });
            }else{
                browser.browserAction.setIcon({path: "icon/icon38.png"});
            }
        }else{
            alert("V2EX消息获取失败：" + status);
        }
    });
}

//——————————————————————————————————通知功能——————————————————————————————————

//——————————————————————————————————通知/按钮点击反馈——————————————————————————————————

//清除通知图标，打开通知地址
function clean_msg(){
    browser.browserAction.setIcon({path: "icon/icon38.png"});
    browser.tabs.create({url:"https://www.v2ex.com/notifications"});
}

browser.commands.onCommand.addListener(clean_msg);
browser.browserAction.onClicked.addListener(clean_msg);
browser.notifications.onClicked.addListener(function(notificationId){
    switch (notificationId){
    case "newMsg":
        clean_msg();
        break;
    case "autoMission":
        browser.tabs.create({url:"https://www.v2ex.com/balance"});
        break;
    case "newFollowTopic":
        browser.tabs.create({url:`https://www.v2ex.com/t/${window.newFollowTopicId}?p=1`});
        break;
    case "newCollectTopicReply":
        browser.tabs.create({url:"https://www.v2ex.com/my/topics"});
        break;
    }
});

//——————————————————————————————————通知/按钮点击反馈——————————————————————————————————

//——————————————————————————————————自动签到——————————————————————————————————
function autoMission(){
    if( s.getItem("autoMission") == new Date().getUTCDate() ){
        //console.log('今天已经成功领取奖励了');
        return;
    }
    console.log('开始签到')
    $.ajax({
        url: "https://www.v2ex.com/",
        success: function(data){
            let sign = data.match("/signout(\\?once=[0-9]+)");
            sign = sign != null && sign[1] || "未登录";
            if ( sign != "未登录" ){
                $.ajax({
                    url: "https://www.v2ex.com/mission/daily/redeem" + sign,
                    success: function(data){
                        if ( data.search("查看我的账户余额") ){
                            let result = data.match(/已连续登录 (\d+?) 天/)
                            browser.notifications.create(
                                "autoMission" ,
                                {
                                    type    : "basic",
                                    iconUrl : "icon/icon38_msg.png",
                                    title   : "v2ex plus 提醒您",
                                    message : `签到成功，${result[0]}。\nTake your passion and make it come true.`,
                                }
                            );
                            s.setItem( "autoMission", new Date().getUTCDate() );
                        }else{
                            alert("罕见错误！基本可以忽略，如果你遇见两次以上请联系开发者，当该提示已打扰到您，请关闭扩展的自动签到功能。");
                        }
                    },
                    error: function(){
                        alert("网络错误！今日奖励领取失败，等待一小时后自动重试或现在手动领取。");
                    }
                });
            }
        },
        error: function(){
            alert("网络错误！今日奖励领取失败，等待一小时后自动重试或现在手动领取。");
        }
    });
}
//——————————————————————————————————自动签到——————————————————————————————————

//——————————————————————————————————自动登陆微博——————————————————————————————————

function autoLoginWeibo(){
    //console.log('0.8 测试版！ 目前准备自动激活微博');
    $.get("http://weibo.com");
}

//——————————————————————————————————自动登陆微博——————————————————————————————————
<commit_msg>revert check msg alarm to 5 min<commit_after>// Avoid `chrome` namespace
if (typeof browser === "undefined" &&
    typeof chrome === "object"){
    //console.log("On Chrome");
    var browser = chrome;
}

browser.runtime.onInstalled.addListener(function(e){
    // Open options page to initialize localStorage
    if (e.reason === "install")
        browser.runtime.openOptionsPage();
    else if (e.reason === "update")
        if (localStorage.getItem("replyUser") === null)
            localStorage.setItem("replyUser", 1);

});

//——————————————————————————————————接收来自页面的图片数据上传并返回——————————————————————————————————
const s = localStorage;

browser.runtime.onMessage.addListener(function(request, sender, sendResponse) {
    if ( request.img_base64 ){
        var post_url, patt_id, url_start, url_end, data;
        var img_status;
        //——————————设置微博或 imgur 的信息——————————
        if ( s.getItem("imageHosting") === "weibo" ){
            post_url = "http://picupload.service.weibo.com/interface/pic_upload.php?\
                    ori=1&mime=image%2Fjpeg&data=base64&url=0&markpos=1&logo=&nick=0&marks=1&app=miniblog";
            patt_id = "pid\":\"(.*?)\"";
            url_start = "https://ws2.sinaimg.cn/large/";
            url_end = ".jpg";
            data = {"b64_data": request.img_base64};
        }else{
            post_url = "https://api.imgur.com/3/image";
            patt_id = "id\":\"(.*?)\"";
            url_start = "https://i.imgur.com/";
            url_end = ".png";
            data = {"image": request.img_base64};
        }
        //——————————微博或 imgur 的信息完成——————————

        $.ajax({
            url: post_url,
            method: "POST",
            data: data,
            dataType: "text",
            beforeSend: (xhr) => {
                if ( s.getItem("imageHosting") === "imgur" )
                    xhr.setRequestHeader("Authorization", "Client-ID 9311f6be1c10160");
            },
            success: (data) => {
                try{
                    img_status = url_start + RegExp(patt_id).exec(data)[1] + url_end;
                    //console.log("Succeed: "+ img_status);
                } catch(e){
                    //console.error("Field not found");
                    img_status = "Failed";
                }
            },
            error: () => {
                img_status = "Failed";
                //console.info("Request failed");
            },
            complete: () => {
                sendResponse({img_status: img_status});
            }
        });
        return true;
    }
    //——————————————————————————————————接收来自页面的图片数据上传并返回——————————————————————————————————

    //——————————————————————————————————返回设置选项——————————————————————————————————
    switch (request.action) {
    case "get_preview_status":
        sendResponse({preview_status: Number(s.getItem("preview"))});
        break;
    case "get_dblclickToTop":
        sendResponse({dblclickToTop: Number(s.getItem("dblclickToTop"))});
        break;
    case "get_replySetting":
        sendResponse({
            replyColor: s.getItem("replyColor"),
            replyA: s.getItem("replyA"),
            fold: Number(s.getItem("fold")),
            thankColor: s.getItem("thankColor")
        });
        break;
    case "get_newWindow_status":
        sendResponse({newWindow_status: Number(s.getItem("newWindow"))});
        break;
    case "get_replyUser":
        sendResponse({replyUser: Number(s.getItem("replyUser"))});
        break;
    case "get_blockList":
        $.get("https://www.v2ex.com",function(data,status){
            if(status == "success"){
                var block_list = /blocked = \[(.*?)\];/.exec(data);
                var username = /首页<\/a>   <a href="\/member\/(.+?)"/.exec(data);
                if ( block_list && username ){
                    block_list = block_list[1];
                    username = username[1];
                    browser.tabs.create({url:"/page/block_list.html#"+username+"="+block_list});
                }else{
                    alert("扩展没有获取到任何信息 : (\n或许是您未登录 V2EX 账号");
                }
            }else{
                alert("扩展没有获取到任何信息 : (\n很有可能是网络问题，请稍后再试");
            }
        });
        sendResponse({blockList: "get"});
        break;
    case "get_collectList":
        window.collectNotified = false
        sendResponse({cached: localStorage.collectTopicCachedReplyCountList, latest: localStorage.collectTopicLatestReplyCountList});
        break;
    case "clear_collect":
        localStorage.collectTopicCachedReplyCountList = request.list;
        localStorage.collectTopicLatestReplyCountList = request.list;
        sendResponse(null);
        break;
    case "sync_collect":
        localStorage.collectTopicCachedReplyCountList = request.cached;
        localStorage.collectTopicLatestReplyCountList = request.latest;
        break;
    default:
        throw "invaild action";
    }
});

//——————————————————————————————————返回设置选项——————————————————————————————————

//——————————————————————————————————定时任务初始化——————————————————————————————————

Number(s.getItem("newMsg")) && checkMsg();
Number(s.getItem("followMsg")) && followMsg();
Number(s.getItem("collectMsg")) && collectMsg();
browser.alarms.create("checkMsg", {periodInMinutes: 5});
browser.alarms.create("autoMission", {periodInMinutes: 30});

browser.alarms.onAlarm.addListener(function( a ){
    switch (a.name){
    case "checkMsg":
        Number(s.getItem("newMsg")) && checkMsg();
        Number(s.getItem("followMsg")) && followMsg();
        Number(s.getItem("collectMsg")) && collectMsg();
        break;
    case "autoMission":
        Number(s.getItem("autoMission")) && autoMission();
        Number(s.getItem("autoLoginWeibo")) && autoLoginWeibo();
        break;
    }
});

//——————————————————————————————————定时任务初始化——————————————————————————————————

//——————————————————————————————————检查关注人新主题——————————————————————————————————
function followMsg() {
    $.get('https://www.v2ex.com/my/following', function(data){
        var $html = $("<output>").append($.parseHTML(data))
        window.a = $html
        var topics = $html.find('#Main .box:nth(0) table')
        if (!topics.length) return

        var $firstOne = topics.eq(2)
        var topicId = $firstOne.find('.item_title a').attr('href').substr(3).split('#')[0]
        var topic = $firstOne.find('.item_title').text()
        var author = $firstOne.find('.small.fade > strong:nth-child(3)').text()

        if( s.getItem("followMsgTopicId") == topicId ) return
        s.setItem( "followMsgTopicId", topicId)
        window.newFollowTopicId = topicId
        browser.notifications.create(
            "newFollowTopic" ,
        {
            type       : "basic",
            iconUrl    : "icon/icon38_msg.png",
            title      : "v2ex plus 提醒您",
            message    : `${author} 创作了新主题：${topic}`,
        });
    })
}

//——————————————————————————————————检查关注人新主题——————————————————————————————————

//——————————————————————————————————检查收藏主题新回复——————————————————————————————————
function collectMsg() {
    $.get('https://www.v2ex.com/my/topics', function(data){
        var $html = $("<output>").append($.parseHTML(data))
        var topics = $html.find('div.cell.item')
        if (!topics.length) return

        var cachedReplyCountList = localStorage.collectTopicCachedReplyCountList
        cachedReplyCountList = cachedReplyCountList ? JSON.parse(cachedReplyCountList) : {}
        var latestReplyCountList = localStorage.collectTopicLatestReplyCountList
        latestReplyCountList = latestReplyCountList ? JSON.parse(latestReplyCountList) : {}

        var topicIds = [];
        var newReply = false
        var topicIndex
        for (topicIndex = 0; topicIndex < topics.length; topicIndex++){
            var topic = topics[topicIndex]
            var topicReplyCountEl = $(topic).find('.count_livid, .count_orange')
            var topicReplyCount = topicReplyCountEl.length ? Number(topicReplyCountEl[0].innerText) : 0
            var topicId = Number($(topic).find('.item_title a')[0].href.match(/\/t\/(\d+)/)[1]);
            topicIds.push(topicId)

            if (cachedReplyCountList[topicId] === undefined){
                cachedReplyCountList[topicId] = topicReplyCount
            }

            if (latestReplyCountList[topicId] === undefined){
                latestReplyCountList[topicId] = topicReplyCount
            }else if (latestReplyCountList[topicId] != topicReplyCount){
                latestReplyCountList[topicId] = topicReplyCount
                newReply = true
            }
        }

        for (topicIndex in cachedReplyCountList){
            if(topicIds.indexOf(Number(topicIndex)) === -1){
                delete(cachedReplyCountList[topicIndex])
            }
        }

        for (topicIndex in latestReplyCountList){
            if(topicIds.indexOf(Number(topicIndex)) === -1){
                delete(latestReplyCountList[topicIndex])
            }
        }

        localStorage.collectTopicCachedReplyCountList = JSON.stringify(cachedReplyCountList)
        localStorage.collectTopicLatestReplyCountList = JSON.stringify(latestReplyCountList)

        if (!window.collectNotified && newReply){
            window.collectNotified = true
            browser.notifications.create(
                "newCollectTopicReply" ,
            {
                type       : "basic",
                iconUrl    : "icon/icon38_msg.png",
                title      : "v2ex plus 提醒您",
                message    : "您收藏的主题有了新回复，点击查看",
            });
            //20分钟内最多提示一次
            setTimeout(function(){
                window.collectNotified = false
            }, 1200000)
        }
    })
}

//——————————————————————————————————检查收藏主题新回复——————————————————————————————————

//——————————————————————————————————通知功能——————————————————————————————————

//现在是每5分钟刷新一次状态，除非点击了browserAction
function checkMsg(){
    $.get("https://www.v2ex.com/settings",function(data,status){
        if(status == "success"){
            var sign = RegExp("([0-9]*?) (条未读提醒|unread)").exec(data);
            sign = sign != null && sign[1] || "未登录";
            if ( sign == "未登录" ){
                browser.browserAction.setIcon({path: "icon/icon38_nologin.png"});
            }else if( sign!="0") {
                browser.browserAction.setIcon({path: "icon/icon38_msg.png"});
                browser.notifications.create(
                    "newMsg" ,
                    {
                        type       : "basic",
                        iconUrl    : "icon/icon38_msg.png",
                        title      : "v2ex plus 提醒您",
                        message    : "您有 V2EX 的未读新消息，点击查看。",
                    });
            }else{
                browser.browserAction.setIcon({path: "icon/icon38.png"});
            }
        }else{
            alert("V2EX消息获取失败：" + status);
        }
    });
}

//——————————————————————————————————通知功能——————————————————————————————————

//——————————————————————————————————通知/按钮点击反馈——————————————————————————————————

//清除通知图标，打开通知地址
function clean_msg(){
    browser.browserAction.setIcon({path: "icon/icon38.png"});
    browser.tabs.create({url:"https://www.v2ex.com/notifications"});
}

browser.commands.onCommand.addListener(clean_msg);
browser.browserAction.onClicked.addListener(clean_msg);
browser.notifications.onClicked.addListener(function(notificationId){
    switch (notificationId){
    case "newMsg":
        clean_msg();
        break;
    case "autoMission":
        browser.tabs.create({url:"https://www.v2ex.com/balance"});
        break;
    case "newFollowTopic":
        browser.tabs.create({url:`https://www.v2ex.com/t/${window.newFollowTopicId}?p=1`});
        break;
    case "newCollectTopicReply":
        browser.tabs.create({url:"https://www.v2ex.com/my/topics"});
        break;
    }
});

//——————————————————————————————————通知/按钮点击反馈——————————————————————————————————

//——————————————————————————————————自动签到——————————————————————————————————
function autoMission(){
    if( s.getItem("autoMission") == new Date().getUTCDate() ){
        //console.log('今天已经成功领取奖励了');
        return;
    }
    console.log('开始签到')
    $.ajax({
        url: "https://www.v2ex.com/",
        success: function(data){
            let sign = data.match("/signout(\\?once=[0-9]+)");
            sign = sign != null && sign[1] || "未登录";
            if ( sign != "未登录" ){
                $.ajax({
                    url: "https://www.v2ex.com/mission/daily/redeem" + sign,
                    success: function(data){
                        if ( data.search("查看我的账户余额") ){
                            let result = data.match(/已连续登录 (\d+?) 天/)
                            browser.notifications.create(
                                "autoMission" ,
                                {
                                    type    : "basic",
                                    iconUrl : "icon/icon38_msg.png",
                                    title   : "v2ex plus 提醒您",
                                    message : `签到成功，${result[0]}。\nTake your passion and make it come true.`,
                                }
                            );
                            s.setItem( "autoMission", new Date().getUTCDate() );
                        }else{
                            alert("罕见错误！基本可以忽略，如果你遇见两次以上请联系开发者，当该提示已打扰到您，请关闭扩展的自动签到功能。");
                        }
                    },
                    error: function(){
                        alert("网络错误！今日奖励领取失败，等待一小时后自动重试或现在手动领取。");
                    }
                });
            }
        },
        error: function(){
            alert("网络错误！今日奖励领取失败，等待一小时后自动重试或现在手动领取。");
        }
    });
}
//——————————————————————————————————自动签到——————————————————————————————————

//——————————————————————————————————自动登陆微博——————————————————————————————————

function autoLoginWeibo(){
    //console.log('0.8 测试版！ 目前准备自动激活微博');
    $.get("http://weibo.com");
}

//——————————————————————————————————自动登陆微博——————————————————————————————————
<|endoftext|>


<commit_before>import React, { useContext, useEffect, useMemo, useRef, useState } from "react";
import PropTypes from "prop-types";
import styledSystemPropTypes from "@styled-system/prop-types";
import {
  additionalYears,
  findMatchedFormatAndValue,
  formattedValue,
  formatToISO,
  isDateValid,
  parseDate,
  parseISODate,
  checkISOFormatAndLength,
  getSeparator,
} from "./__internal__/utils";
import useLocale from "../../hooks/__internal__/useLocale";
import Events from "../../__internal__/utils/helpers/events";
import {
  filterStyledSystemMarginProps,
  filterOutStyledSystemSpacingProps,
} from "../../style/utils";
import getFormatData from "./__internal__/date-formats";
import StyledDateInput from "./date.style";
import Textbox from "../textbox";
import DatePicker from "./__internal__/date-picker";
import DateRangeContext from "../date-range/date-range.context";

const marginPropTypes = filterStyledSystemMarginProps(
  styledSystemPropTypes.space
);

const DateInput = ({
  adaptiveLabelBreakpoint,
  allowEmptyValue,
  autoFocus,
  "data-component": dataComponent,
  "data-element": dataElement,
  "data-role": dataRole,
  disabled,
  disablePortal = false,
  helpAriaLabel,
  labelInline,
  minDate,
  maxDate,
  onBlur,
  onChange,
  onClick,
  onFocus,
  onKeyDown,
  pickerProps = {},
  readOnly,
  size = "medium",
  tooltipPosition,
  value,
  ...rest
}) => {
  const wrapperRef = useRef();
  const parentRef = useRef();
  const inputRef = useRef();
  const pickerRef = useRef();
  const alreadyFocused = useRef(false);
  const isBlurBlocked = useRef(false);
  const focusedViaPicker = useRef(false);
  const l = useLocale();
  const { dateFnsLocale } = l.date;
  const { format, formats } = useMemo(() => getFormatData(dateFnsLocale()), [
    dateFnsLocale,
  ]);
  const { inputRefMap, setInputRefMap } = useContext(DateRangeContext);
  const inputName = dataElement?.split("-")[0];
  const [open, setOpen] = useState(false);
  const [selectedDays, setSelectedDays] = useState(
    checkISOFormatAndLength(value)
      ? parseISODate(value)
      : parseDate(format, value)
  );
  const isInitialValue = useRef(true);

  const computeInvalidRawValue = (inputValue) =>
    allowEmptyValue && !inputValue.length ? inputValue : null;

  const buildCustomEvent = (ev) => {
    const { id, name } = ev.target;

    const [matchedFormat, matchedValue] = findMatchedFormatAndValue(
      ev.target.value,
      formats
    );

    const formattedValueString =
      ev.type === "blur"
        ? formattedValue(format, selectedDays)
        : ev.target.value;
    const rawValue = isDateValid(parseDate(matchedFormat, matchedValue))
      ? formatToISO(...additionalYears(matchedFormat, matchedValue))
      : computeInvalidRawValue(ev.target.value);

    ev.target = {
      ...(name && { name }),
      ...(id && { id }),
      value: {
        formattedValue: formattedValueString,
        rawValue,
      },
    };

    return ev;
  };

  const handleChange = (ev) => {
    isInitialValue.current = false;
    onChange(buildCustomEvent(ev));
  };

  const focusInput = () => {
    focusedViaPicker.current = true;
    inputRef.current?.focus();
  };

  const handleDayClick = (day, ev) => {
    setSelectedDays(day);
    onChange(
      buildCustomEvent({
        ...ev,
        target: {
          ...ev.target,
          value: formattedValue(format, day),
        },
      })
    );
    focusInput();
    setOpen(false);
  };

  const handleBlur = (ev) => {
    if (disabled || readOnly) {
      return;
    }

    let event;

    if (isDateValid(selectedDays)) {
      event = buildCustomEvent(ev);

      const currentValue = checkISOFormatAndLength(value)
        ? formattedValue(format, parseISODate(value))
        : value;
      const [, matchedValue] = findMatchedFormatAndValue(currentValue, formats);

      if (formattedValue(format, selectedDays) !== matchedValue) {
        onChange(event);
      }
    } else {
      const { id, name } = ev.target;

      ev.target = {
        ...(name && { name }),
        ...(id && { id }),
        value: {
          formattedValue: ev.target.value,
          rawValue: computeInvalidRawValue(ev.target.value),
        },
      };

      event = ev;
    }

    if (isBlurBlocked.current) {
      return;
    }

    if (onBlur) {
      onBlur(event);
    }
  };

  const handleFocus = (ev) => {
    if (disabled || readOnly) {
      return;
    }

    isBlurBlocked.current = false;

    if (!open && !alreadyFocused.current) {
      setOpen(true);
    } else {
      alreadyFocused.current = false;
    }

    if (onFocus) {
      onFocus(ev);
    }
  };

  const handleKeyDown = (ev) => {
    if (onKeyDown) {
      onKeyDown(ev);
    }

    if (Events.isTabKey(ev)) {
      setOpen(false);
      alreadyFocused.current = false;
    }
  };

  const handleClick = (ev) => {
    if (disabled || readOnly) {
      return;
    }

    if (onClick) {
      onClick(ev);
    }
  };

  const handleMouseDown = (ev) => {
    if (disabled || readOnly) {
      return;
    }

    if (setInputRefMap) {
      isBlurBlocked.current = true;
    }

    if (ev.target.type === "text" && !open) {
      setOpen(true);
    } else if (ev.target.type !== "text") {
      alreadyFocused.current = true;
      setOpen((prev) => !prev);
    }
  };

  const handleIconMouseDown = (e) => {
    isBlurBlocked.current = true;
    handleMouseDown(e);
  };

  const handlePickerMouseDown = () => {
    isBlurBlocked.current = true;
  };

  const assignInput = (input) => {
    inputRef.current = input.current;
    parentRef.current = input.current.parentElement;

    if (inputRefMap && inputRefMap[inputName]?.setOpen !== setOpen) {
      setInputRefMap({
        [inputName]: { isBlurBlocked, setOpen },
      });
    }
  };

  useEffect(() => {
    const fnClosePicker = (ev) => {
      if (
        open &&
        !Events.composedPath(ev).includes(parentRef.current) &&
        !Events.composedPath(ev).includes(pickerRef.current)
      ) {
        alreadyFocused.current = true;
        inputRef.current.focus();
        isBlurBlocked.current = false;
        inputRef.current.blur();
        setOpen(false);
        alreadyFocused.current = false;
      }
    };

    document.addEventListener("mousedown", fnClosePicker);

    return function cleanup() {
      document.removeEventListener("mousedown", fnClosePicker);
    };
  }, [open]);

  useEffect(() => {
    const [matchedFormat, matchedValue] = findMatchedFormatAndValue(
      value,
      formats
    );

    if (
      matchedFormat &&
      matchedValue &&
      isDateValid(parseDate(matchedFormat, matchedValue))
    ) {
      setSelectedDays(
        parseDate(...additionalYears(matchedFormat, matchedValue))
      );
    } else if (checkISOFormatAndLength(value) && isInitialValue.current) {
      setSelectedDays(parseISODate(value));
    } else {
      setSelectedDays(undefined);
    }
  }, [value, formats]);

  const computedValue = () => {
    if (checkISOFormatAndLength(value) && isInitialValue.current) {
      return formattedValue(format, parseISODate(value));
    }

    const valueSeparator = getSeparator(value);
    const formatSeparator = getSeparator(format);
    const replaceSeparators = () =>
      value
        .split("")
        .map((char) => (char === valueSeparator ? formatSeparator : char))
        .join("");

    if (
      isInitialValue.current &&
      valueSeparator !== formatSeparator &&
      isDateValid(parseDate(format, replaceSeparators()))
    ) {
      isInitialValue.current = false;

      const [matchedFormat, matchedValue] = findMatchedFormatAndValue(
        replaceSeparators(),
        formats
      );
      return formattedValue(
        format,
        parseDate(...additionalYears(matchedFormat, matchedValue))
      );
    }

    return value;
  };

  return (
    <StyledDateInput
      ref={wrapperRef}
      role="presentation"
      size={size}
      labelInline={labelInline}
      data-component={dataComponent || "date"}
      data-element={dataElement}
      data-role={dataRole}
      {...filterStyledSystemMarginProps(rest)}
    >
      <Textbox
        {...filterOutStyledSystemSpacingProps(rest)}
        value={computedValue()}
        onBlur={handleBlur}
        onChange={handleChange}
        onClick={handleClick}
        onFocus={handleFocus}
        onKeyDown={handleKeyDown}
        iconOnClick={handleClick}
        onMouseDown={handleMouseDown}
        iconOnMouseDown={handleIconMouseDown}
        inputIcon="calendar"
        labelInline={labelInline}
        inputRef={assignInput}
        adaptiveLabelBreakpoint={adaptiveLabelBreakpoint}
        tooltipPosition={tooltipPosition}
        helpAriaLabel={helpAriaLabel}
        autoFocus={autoFocus}
        size={size}
        disabled={disabled}
        readOnly={readOnly}
      />
      <DatePicker
        disablePortal={disablePortal}
        inputElement={parentRef}
        pickerProps={pickerProps}
        selectedDays={selectedDays}
        setSelectedDays={setSelectedDays}
        onDayClick={handleDayClick}
        minDate={minDate}
        maxDate={maxDate}
        ref={pickerRef}
        pickerMouseDown={handlePickerMouseDown}
        open={open}
      />
    </StyledDateInput>
  );
};

DateInput.propTypes = {
  ...Textbox.propTypes,
  ...marginPropTypes,
  /** Pass any props that match the [DayPickerProps](https://react-day-picker.js.org/api/DayPicker)
   * interface to override default behaviors
   * */
  pickerProps: PropTypes.object,
  /** Boolean to toggle where DatePicker is rendered in relation to the Date Input */
  disablePortal: PropTypes.bool,
  /** Minimum possible date YYYY-MM-DD */
  minDate: PropTypes.string,
  /** Maximum possible date YYYY-MM-DD */
  maxDate: PropTypes.string,
  /** Specify a callback triggered on change */
  onChange: PropTypes.func.isRequired,
  /** The current date string */
  value: PropTypes.string.isRequired,
  /** Boolean to allow the input to have an empty value */
  allowEmptyValue: PropTypes.bool,
};

export default DateInput;
<commit_msg>refactor(date): add ClickAwayWrapper to close picker when click outside detected<commit_after>import React, { useContext, useEffect, useMemo, useRef, useState } from "react";
import PropTypes from "prop-types";
import styledSystemPropTypes from "@styled-system/prop-types";
import {
  additionalYears,
  findMatchedFormatAndValue,
  formattedValue,
  formatToISO,
  isDateValid,
  parseDate,
  parseISODate,
  checkISOFormatAndLength,
  getSeparator,
} from "./__internal__/utils";
import useLocale from "../../hooks/__internal__/useLocale";
import Events from "../../__internal__/utils/helpers/events";
import {
  filterStyledSystemMarginProps,
  filterOutStyledSystemSpacingProps,
} from "../../style/utils";
import getFormatData from "./__internal__/date-formats";
import StyledDateInput from "./date.style";
import Textbox from "../textbox";
import DatePicker from "./__internal__/date-picker";
import DateRangeContext from "../date-range/date-range.context";
import ClickAwayWrapper from "../../__internal__/click-away-wrapper";

const marginPropTypes = filterStyledSystemMarginProps(
  styledSystemPropTypes.space
);

const DateInput = ({
  adaptiveLabelBreakpoint,
  allowEmptyValue,
  autoFocus,
  "data-component": dataComponent,
  "data-element": dataElement,
  "data-role": dataRole,
  disabled,
  disablePortal = false,
  helpAriaLabel,
  labelInline,
  minDate,
  maxDate,
  onBlur,
  onChange,
  onClick,
  onFocus,
  onKeyDown,
  pickerProps = {},
  readOnly,
  size = "medium",
  tooltipPosition,
  value,
  ...rest
}) => {
  const wrapperRef = useRef();
  const parentRef = useRef();
  const inputRef = useRef();
  const pickerRef = useRef();
  const alreadyFocused = useRef(false);
  const isBlurBlocked = useRef(false);
  const focusedViaPicker = useRef(false);
  const l = useLocale();
  const { dateFnsLocale } = l.date;
  const { format, formats } = useMemo(() => getFormatData(dateFnsLocale()), [
    dateFnsLocale,
  ]);
  const { inputRefMap, setInputRefMap } = useContext(DateRangeContext);
  const inputName = dataElement?.split("-")[0];
  const [open, setOpen] = useState(false);
  const [selectedDays, setSelectedDays] = useState(
    checkISOFormatAndLength(value)
      ? parseISODate(value)
      : parseDate(format, value)
  );
  const isInitialValue = useRef(true);

  const computeInvalidRawValue = (inputValue) =>
    allowEmptyValue && !inputValue.length ? inputValue : null;

  const buildCustomEvent = (ev) => {
    const { id, name } = ev.target;

    const [matchedFormat, matchedValue] = findMatchedFormatAndValue(
      ev.target.value,
      formats
    );

    const formattedValueString =
      ev.type === "blur"
        ? formattedValue(format, selectedDays)
        : ev.target.value;
    const rawValue = isDateValid(parseDate(matchedFormat, matchedValue))
      ? formatToISO(...additionalYears(matchedFormat, matchedValue))
      : computeInvalidRawValue(ev.target.value);

    ev.target = {
      ...(name && { name }),
      ...(id && { id }),
      value: {
        formattedValue: formattedValueString,
        rawValue,
      },
    };

    return ev;
  };

  const handleChange = (ev) => {
    isInitialValue.current = false;
    onChange(buildCustomEvent(ev));
  };

  const focusInput = () => {
    focusedViaPicker.current = true;
    inputRef.current?.focus();
  };

  const handleDayClick = (day, ev) => {
    setSelectedDays(day);
    onChange(
      buildCustomEvent({
        ...ev,
        target: {
          ...ev.target,
          value: formattedValue(format, day),
        },
      })
    );
    focusInput();
    setOpen(false);
  };

  const handleBlur = (ev) => {
    if (disabled || readOnly) {
      return;
    }

    let event;

    if (isDateValid(selectedDays)) {
      event = buildCustomEvent(ev);

      const currentValue = checkISOFormatAndLength(value)
        ? formattedValue(format, parseISODate(value))
        : value;
      const [, matchedValue] = findMatchedFormatAndValue(currentValue, formats);

      if (formattedValue(format, selectedDays) !== matchedValue) {
        onChange(event);
      }
    } else {
      const { id, name } = ev.target;

      ev.target = {
        ...(name && { name }),
        ...(id && { id }),
        value: {
          formattedValue: ev.target.value,
          rawValue: computeInvalidRawValue(ev.target.value),
        },
      };

      event = ev;
    }

    if (isBlurBlocked.current) {
      return;
    }

    if (onBlur) {
      onBlur(event);
    }
  };

  const handleFocus = (ev) => {
    if (disabled || readOnly) {
      return;
    }

    isBlurBlocked.current = false;

    if (!open && !alreadyFocused.current) {
      setOpen(true);
    } else {
      alreadyFocused.current = false;
    }

    if (onFocus) {
      onFocus(ev);
    }
  };

  const handleKeyDown = (ev) => {
    if (onKeyDown) {
      onKeyDown(ev);
    }

    if (Events.isTabKey(ev)) {
      setOpen(false);
      alreadyFocused.current = false;
    }
  };

  const handleClick = (ev) => {
    if (disabled || readOnly) {
      return;
    }

    if (onClick) {
      onClick(ev);
    }
  };

  const handleMouseDown = (ev) => {
    if (disabled || readOnly) {
      return;
    }

    if (setInputRefMap) {
      isBlurBlocked.current = true;
    }

    if (ev.target.type === "text" && !open) {
      setOpen(true);
    } else if (ev.target.type !== "text") {
      alreadyFocused.current = true;
      setOpen((prev) => !prev);
    }
  };

  const handleIconMouseDown = (e) => {
    isBlurBlocked.current = true;
    handleMouseDown(e);
  };

  const handlePickerMouseDown = () => {
    isBlurBlocked.current = true;
  };

  const assignInput = (input) => {
    inputRef.current = input.current;
    parentRef.current = input.current.parentElement;

    if (inputRefMap && inputRefMap[inputName]?.setOpen !== setOpen) {
      setInputRefMap({
        [inputName]: { isBlurBlocked, setOpen },
      });
    }
  };

  useEffect(() => {
    const [matchedFormat, matchedValue] = findMatchedFormatAndValue(
      value,
      formats
    );

    if (
      matchedFormat &&
      matchedValue &&
      isDateValid(parseDate(matchedFormat, matchedValue))
    ) {
      setSelectedDays(
        parseDate(...additionalYears(matchedFormat, matchedValue))
      );
    } else if (checkISOFormatAndLength(value) && isInitialValue.current) {
      setSelectedDays(parseISODate(value));
    } else {
      setSelectedDays(undefined);
    }
  }, [value, formats]);

  const computedValue = () => {
    if (checkISOFormatAndLength(value) && isInitialValue.current) {
      return formattedValue(format, parseISODate(value));
    }

    const valueSeparator = getSeparator(value);
    const formatSeparator = getSeparator(format);
    const replaceSeparators = () =>
      value
        .split("")
        .map((char) => (char === valueSeparator ? formatSeparator : char))
        .join("");

    if (
      isInitialValue.current &&
      valueSeparator !== formatSeparator &&
      isDateValid(parseDate(format, replaceSeparators()))
    ) {
      isInitialValue.current = false;

      const [matchedFormat, matchedValue] = findMatchedFormatAndValue(
        replaceSeparators(),
        formats
      );
      return formattedValue(
        format,
        parseDate(...additionalYears(matchedFormat, matchedValue))
      );
    }

    return value;
  };

  const handleClickAway = () => {
    if (open) {
      alreadyFocused.current = true;
      inputRef.current.focus();
      isBlurBlocked.current = false;
      inputRef.current.blur();
      setOpen(false);
      alreadyFocused.current = false;
    }
  };

  return (
    <ClickAwayWrapper
      handleClickAway={handleClickAway}
      eventTypeId="mousedown"
      targets={[parentRef, pickerRef]}
    >
      <StyledDateInput
        ref={wrapperRef}
        role="presentation"
        size={size}
        labelInline={labelInline}
        data-component={dataComponent || "date"}
        data-element={dataElement}
        data-role={dataRole}
        {...filterStyledSystemMarginProps(rest)}
      >
        <Textbox
          {...filterOutStyledSystemSpacingProps(rest)}
          value={computedValue()}
          onBlur={handleBlur}
          onChange={handleChange}
          onClick={handleClick}
          onFocus={handleFocus}
          onKeyDown={handleKeyDown}
          iconOnClick={handleClick}
          onMouseDown={handleMouseDown}
          iconOnMouseDown={handleIconMouseDown}
          inputIcon="calendar"
          labelInline={labelInline}
          inputRef={assignInput}
          adaptiveLabelBreakpoint={adaptiveLabelBreakpoint}
          tooltipPosition={tooltipPosition}
          helpAriaLabel={helpAriaLabel}
          autoFocus={autoFocus}
          size={size}
          disabled={disabled}
          readOnly={readOnly}
        />
        <DatePicker
          disablePortal={disablePortal}
          inputElement={parentRef}
          pickerProps={pickerProps}
          selectedDays={selectedDays}
          setSelectedDays={setSelectedDays}
          onDayClick={handleDayClick}
          minDate={minDate}
          maxDate={maxDate}
          ref={pickerRef}
          pickerMouseDown={handlePickerMouseDown}
          open={open}
        />
      </StyledDateInput>
    </ClickAwayWrapper>
  );
};

DateInput.propTypes = {
  ...Textbox.propTypes,
  ...marginPropTypes,
  /** Pass any props that match the [DayPickerProps](https://react-day-picker-v7.netlify.app/docs/getting-started/)
   * interface to override default behaviors
   * */
  pickerProps: PropTypes.object,
  /** Boolean to toggle where DatePicker is rendered in relation to the Date Input */
  disablePortal: PropTypes.bool,
  /** Minimum possible date YYYY-MM-DD */
  minDate: PropTypes.string,
  /** Maximum possible date YYYY-MM-DD */
  maxDate: PropTypes.string,
  /** Specify a callback triggered on change */
  onChange: PropTypes.func.isRequired,
  /** The current date string */
  value: PropTypes.string.isRequired,
  /** Boolean to allow the input to have an empty value */
  allowEmptyValue: PropTypes.bool,
};

export default DateInput;
<|endoftext|>


<commit_before>const USER_AGENT = 'Explode/0.5';
const API_ROOT = 'http://api.longurl.org/v2/';
const FETCH_DELAY = 800;
const SERVICES_CACHE_TIME = 86400 * 1000;
const EXTRA_SERVICES = ['j.mp', 'flic.kr', 'w33.us', 'guao.cc', 'jan.io',
                        'disq.us'];

var outstandingReqs = [];
var curReq = null;

/* Functions for dealing with the LongURL API */

function xhrGet(url, callback) {
    var xhr = new XMLHttpRequest();
    xhr.open('GET', url, true);
    xhr.onreadystatechange = function(resp) {
        if (xhr.readyState == 4)
            callback(xhr);
    };
    xhr.send();
}

function apiUrl(method, params) {
    var url = API_ROOT + method + '?format=json&user-agent=' +
        encodeURIComponent(USER_AGENT);
    for (k in params)
        url += '&' + k + '=' + encodeURIComponent(params[k]);
    return url;
}

/* Setup the service list; fetch and cache again if needed */

function loadCachedServices() {
    var services = JSON.parse(localStorage['services']);
    EXTRA_SERVICES.forEach(function(s) {
        services[s] = {host: s, regex: null};
    });
    return services;
}

chrome.extension.onRequest.addListener(function(req, sender, callback) {
    if (req.servicesPlease) {
        if (localStorage['servicesExpire'] > Date.now()) {
            callback({services: loadCachedServices()});
        } else {
            xhrGet(apiUrl('services'), function(xhr) {
                var date = Date.parse(xhr.getResponseHeader('Date'));
                localStorage['services'] = xhr.responseText;
                localStorage['servicesExpire'] = date + SERVICES_CACHE_TIME;
                callback({services: loadCachedServices()});
            });
        }
    }
});

/* Handle a bunch of requests from the content script. We stuff the
 * callback into req so we can pull it out later. All the requests
 * should finish coming in almost immediately, at which point the first
 * req's XHR will be running and the rest will be queued up. */

chrome.extension.onConnect.addListener(function (port) {
    switch (port.name) {
    case 'explodeUrlRequest':
        port.onMessage.addListener(function (req) {
            handleReq(req, port);
        });
    }
});

function handleReq(req, port) {
    if (localStorage[req.url]) {
        console.log('cached: ' + req.url);
        updateLink({url: req.url, port: port});
    } else {
        console.log('new: ' + req.url);
        port.postMessage({url: req.url, loading: true});
        outstandingReqs.push({url: req.url, port: port});
        fetchReqs();
    }
}

/* The main loop, so to speak. */

function fetchReqs() {
    if (curReq || outstandingReqs.length == 0)
        return;
    curReq = outstandingReqs.shift();
    if (localStorage[curReq.url]) {
        updateLink(curReq);
        fetchNextReq();
    } else {
        xhrGet(apiUrl('expand', {title: 1, url: curReq.url}), function(xhr) {
            var res = loadResponse(xhr.responseText);
            if (res) {
                localStorage[curReq.url] = JSON.stringify(res);
                updateLink(curReq);
            } else {
                curReq.port.postMessage({url: curReq.url, failed: true});
            }
            fetchNextReq();
        });
    }
}

function fetchNextReq() {
    curReq = null;
    setTimeout(fetchReqs, FETCH_DELAY);
}

function normalize(s) {
    /* XXX: interpret entities. */
    return s ? s.replace(/\s+/g, ' ') : null;
}

function loadResponse(t) {
    try {
        var res = JSON.parse(t);
        if (res['long-url']) {
            return { longUrl: res['long-url'], title: normalize(res.title) };
        } else {
            console.log('error: ' + t);
        }
    } catch (e) {
        console.log('no reply: ' + curReq.url);
    }
    return null;
}

/* And here's where we actually invoke the callback once the URL and
 * title come back from LongURL. */

function updateLink(req) {
    var info = JSON.parse(localStorage[req.url]);
    info.url = req.url;
    info.munge = localStorage['mungeLinks'] == 'true';
    req.port.postMessage(info);
}
<commit_msg>Categorize objects<commit_after>const USER_AGENT = 'Explode/0.5';
const API_ROOT = 'http://api.longurl.org/v2/';
const FETCH_DELAY = 800;
const SERVICES_CACHE_TIME = 86400 * 1000;
const EXTRA_SERVICES = ['j.mp', 'flic.kr', 'w33.us', 'guao.cc', 'jan.io',
                        'disq.us'];

var outstandingReqs = [];
var curReq = null;

/* Functions for dealing with the LongURL API */

function xhrGet(url, callback) {
    var xhr = new XMLHttpRequest();
    xhr.open('GET', url, true);
    xhr.onreadystatechange = function(resp) {
        if (xhr.readyState == 4)
            callback(xhr);
    };
    xhr.send();
}

function apiUrl(method, params) {
    var url = API_ROOT + method + '?format=json&user-agent=' +
        encodeURIComponent(USER_AGENT);
    for (k in params)
        url += '&' + k + '=' + encodeURIComponent(params[k]);
    return url;
}

/* Setup the service list; fetch and cache again if needed */

function loadCachedServices() {
    var services = JSON.parse(localStorage['services']);
    EXTRA_SERVICES.forEach(function(s) {
        services[s] = {host: s, regex: null};
    });
    return services;
}

chrome.extension.onRequest.addListener(function(req, sender, callback) {
    if (req.servicesPlease) {
        if (localStorage['servicesExpire'] > Date.now()) {
            callback({services: loadCachedServices()});
        } else {
            xhrGet(apiUrl('services'), function(xhr) {
                var date = Date.parse(xhr.getResponseHeader('Date'));
                localStorage['services'] = xhr.responseText;
                localStorage['servicesExpire'] = date + SERVICES_CACHE_TIME;
                callback({services: loadCachedServices()});
            });
        }
    }
});

/* Handle a bunch of requests from the content script. We stuff the
 * callback into req so we can pull it out later. All the requests
 * should finish coming in almost immediately, at which point the first
 * req's XHR will be running and the rest will be queued up. */

chrome.extension.onConnect.addListener(function (port) {
    switch (port.name) {
    case 'explodeUrlRequest':
        port.onMessage.addListener(function (req) {
            handleReq(req, port);
        });
    }
});

function handleReq(req, port) {
    if (localStorage[req.url]) {
        console.log('cached: ' + req.url);
        updateLink({url: req.url, port: port});
    } else {
        console.log('new: ' + req.url);
        port.postMessage({url: req.url, loading: true});
        outstandingReqs.push({url: req.url, port: port});
        fetchReqs();
    }
}

/* The main loop, so to speak. */

function fetchReqs() {
    if (curReq || outstandingReqs.length == 0)
        return;
    curReq = outstandingReqs.shift();
    if (localStorage[curReq.url]) {
        updateLink(curReq);
        fetchNextReq();
    } else {
        xhrGet(apiUrl('expand', {title: 1, url: curReq.url}), function(xhr) {
            var res = loadResponse(xhr.responseText);
            if (res) {
                localStorage[curReq.url] = JSON.stringify(res);
                updateLink(curReq);
            } else {
                curReq.port.postMessage({url: curReq.url, failed: true});
            }
            fetchNextReq();
        });
    }
}

function fetchNextReq() {
    curReq = null;
    setTimeout(fetchReqs, FETCH_DELAY);
}

function normalize(s) {
    var textarea = document.createElement('textarea');
    if (s) textarea.innerHTML = s.replace(/\s+/g, ' ');
    return textarea.value;
}

function loadResponse(t) {
    try {
        var res = JSON.parse(t);
        if (res['long-url']) {
            return { longUrl: res['long-url'], title: normalize(res.title) };
        } else {
            console.log('error: ' + t);
        }
    } catch (e) {
        console.log('no reply: ' + curReq.url);
    }
    return null;
}

/* And here's where we actually invoke the callback once the URL and
 * title come back from LongURL. */

function updateLink(req) {
    var info = JSON.parse(localStorage[req.url]);
    info.url = req.url;
    info.munge = localStorage['mungeLinks'] == 'true';
    req.port.postMessage(info);
}
<|endoftext|>


<commit_before>'use strict';

/**
 * @ngdoc directive
 * @name vleApp.directive:fieldInfo
 * @description
 * # fieldInfo
 */
angular.module('vleApp')
  .directive('fieldInfo', function (Dataset, Drop) {
    return {
      templateUrl: 'components/fieldinfo/fieldinfo.html',
      restrict: 'E',
      replace: true,
      scope: {
        field: '=',
        showType: '=',
        showInfo: '=',
        showCaret: '=',
        popupContent: '=',
        showRemove: '=',
        removeAction: '&',
        action: '&'
      },
      link: function(scope, element) {
        var funcsPopup;

        scope.typeNames = Dataset.typeNames;
        scope.stats = Dataset.stats[scope.field.name];

        scope.clicked = function($event){
          if(scope.action && $event.target !== element.find('.fa-caret-down')[0]) {
            scope.action($event);
          }
        };

        scope.$watch('popupContent', function(popupContent) {
          if (!popupContent) { return; }

          funcsPopup = new Drop({
            content: popupContent,
            target: element.find('.caret')[0],
            position: 'bottom left',
            openOn: 'click'
          });
        });
      },
      controller: function($scope, Dataset) {
        var statsField = $scope.field.aggr === 'count' ? 'count' : $scope.field.name;
        $scope.stats = Dataset.stats[statsField];
      }
    };
  });<commit_msg>revert bad remove<commit_after>'use strict';

/**
 * @ngdoc directive
 * @name vleApp.directive:fieldInfo
 * @description
 * # fieldInfo
 */
angular.module('vleApp')
  .directive('fieldInfo', function (Dataset, Drop) {
    return {
      templateUrl: 'components/fieldinfo/fieldinfo.html',
      restrict: 'E',
      replace: true,
      scope: {
        field: '=',
        showType: '=',
        showInfo: '=',
        showCaret: '=',
        popupContent: '=',
        showRemove: '=',
        removeAction: '&',
        action: '&'
      },
      link: function(scope, element) {
        var funcsPopup;

        scope.typeNames = Dataset.typeNames;
        scope.stats = Dataset.stats[scope.field.name];

        scope.clicked = function($event){
          if(scope.action && $event.target !== element.find('.fa-caret-down')[0]) {
            scope.action($event);
          }
        };

        scope.func = function(field) {
          return field.aggr || field.fn ||
            (field.bin && 'bin') ||
            field._aggr || field._fn ||
            (field._bin && 'bin') || (field._any && '*');
        };

        scope.$watch('popupContent', function(popupContent) {
          if (!popupContent) { return; }

          funcsPopup = new Drop({
            content: popupContent,
            target: element.find('.caret')[0],
            position: 'bottom left',
            openOn: 'click'
          });
        });
      },
      controller: function($scope, Dataset) {
        var statsField = $scope.field.aggr === 'count' ? 'count' : $scope.field.name;
        $scope.stats = Dataset.stats[statsField];
      }
    };
  });<|endoftext|>


<commit_before>'use strict';

var passport = require('passport'),
    OpenAm = require('openam').OpenAm,
    util = require('util'),
    url = require('url'),
    InternalOpenAmError = require('./errors/internalopenamerror');

var OpenAmStrategy = (function () {

    /**
     * Reconstructs the original URL of the request.
     *
     * This function builds a URL that corresponds the original URL requested by the
     * client, including the protocol (http or https) and host.
     * If the request passed through any proxies that terminate SSL, the
     * `X-Forwarded-Proto` header is used to detect if the request was encrypted to
     * the proxy.
     *
     * @param {Object} req
     * @return {String}
     * @api private
     * @static
     */
    function _originalURL(req) {
      var headers = req.headers;

      return util.format('%s://%s%s',
          (req.connection.encrypted || headers['x-forwarded-proto'] === 'https') ? 'https' : 'http',
          headers.host,
          req.url || ''
      );
    };

    /**
     * Sanitize options to sane values.
     *
     * @param {Object} options
     * @api private
     * @static
     */
    function _sanitizeOptions(options) {
        return {
          openAmBaseUrl: options.openAmBaseUrl,
          openAmRealm: (options.openAmRealm === undefined) ? '/' : options.openAmRealm,
          openAmCookieName: (options.openAmCookieName === undefined) ? 'iPlanetDirectoryPro' : options.openAmCookieName,
          enableLoginRedirect: (options.enableLoginRedirect === undefined) ? false: options.enableLoginRedirect,
          enableUserProfile: (options.enableUserProfile === undefined) ? false : options.enableUserProfile,
          callbackUrl: options.callbackUrl
        };
    }
    /**
     * `OpenAmStrategy` constructor.
     *
     * @param {Object} options
     * @param {Function} verify
     * @api public
     */
    var openAmStrategy = function (options, verify) {

        /**
         * Get OpenAM token from incoming request.
         *
         * @param {Object} req
         * @api private
         */
        function _getOpenAmToken(req) {

          var cookies = {};
          if (req.headers.cookie) {
            req.headers.cookie.split(';').forEach(function (cookie) {
              var parts = cookie.split('=');
              cookies[parts[0].trim()] = (parts[1] || '').trim();
            });
          }

          return cookies[options.openAmCookieName];
        };

        /**
         * Handles incoming invalid tokens be either redirecting the request to the OpenAM login page or failing.
         *
         * @param {Object} req
         * @param {Function} fail
         * @api private
         */
        function _handleInvalidToken(req, redirect, fail) {
          if (options.enableLoginRedirect) {
            var callbackUrl = options.callbackUrl || req.path;

            if (!url.parse(callbackUrl).protocol) {
              // The callback URL is relative, resolve a fully qualified URL from the URL of the originating request.
              callbackUrl = url.resolve(_originalURL(req), callbackUrl);
            }

            var location = openam.getLoginUiUrl({
              goto: callbackUrl
            });
            redirect(location);
          } else {
            return fail();
          }
        };

        /**
         * Load user profile in accordinace to the normalized Portable Contacts
         * contact schema prefered by Passport.
         *
         * @see http://portablecontacts.net/draft-spec.html#schema
         * @param {String} token
         * @param {Function} done
         * @api private
         */
        function _loadUserProfile(token, done) {
          openam.getAttributes(token, function (err, data) {
            if (err) {
              return done(new InternalOpenAmError('Failed to retrieve user attributes.', err));
            }

            var profile = {
              id: data.tokenid,
              username: data.uid,
              displayName: data.cn,
              name: {
                familyName: data.sn,
                givenName: data.givenname
              },
              email: data.mail,
              _raw: data
            };

            done(null, profile);
          });
        };

        /**
         * Authenticate request by delegating to a service provider using OpenAM.
         *
         * @param {Object} req
         * @param {Object} options
         * @api public
         */
        this.authenticate = function (req) {

          var self = this;

          if (req.query && req.query.error) {
            // TODO: Error information pertaining to OpenAM flows is encoded in the query parameters, and should be propagated to the application.
            return this.fail();
          }

          var token = _getOpenAmToken(req);

          if (!token) {
            return _handleInvalidToken(req, self.redirect, self.fail);
          }

          openam.isTokenValid(token, function (valid) {
            if (!valid) {
              return _handleInvalidToken(req, self.redirect, self.fail);
            }

            if (!options.enableUserProfile) {
              return self.success();
            }

            _loadUserProfile(token, function (err, profile) {
              if (err) { return self.error(err); }

              verify(token, profile, function (err, user, info) {
                if (err) { return self.error(err); }

                self.success(user, info);
              });
            });
          });
        };

        // Passport setup
        this.name = openAmStrategy.strategyName;
        passport.Strategy.call(this);

        // Object setup
        options = _sanitizeOptions(options || {});

        if (!options.openAmBaseUrl) {
          throw new Error('OpenAmStrategy requires a openAmBaseUrl option.');
        }

        var openam = new OpenAm(options.openAmBaseUrl, options.openAmRealm, options.openAmCookieName);
    }

    openAmStrategy.strategyName = 'openam';

    util.inherits(constructor, passport.Strategy);

    return openAmStrategy;
})();

/**
 * Static route middleware to authenticate incoming connections.
 *
 * @param {Object} req
 * @param {Object} res
 * @param {Function} next
 *
 * @api public
 * @static
 */
OpenAmStrategy.ensureAuthenticated = function (req, res, next) {
  if (req.isAuthenticated()) {
    return next();
  }
  else if (req._passport) {
    req._passport.instance.authenticate(OpenAmStrategy.strategyName)(req, res, next);
  }
  else {
    next(new Error('Incorrect Passport configuration.'));
  }
};

module.exports = OpenAmStrategy;
<commit_msg>Simple doc fix.<commit_after>'use strict';

var passport = require('passport'),
    OpenAm = require('openam').OpenAm,
    util = require('util'),
    url = require('url'),
    InternalOpenAmError = require('./errors/internalopenamerror');

var OpenAmStrategy = (function () {

    /**
     * Reconstructs the original URL of the request.
     *
     * This function builds a URL that corresponds the original URL requested by the
     * client, including the protocol (http or https) and host.
     * If the request passed through any proxies that terminate SSL, the
     * `X-Forwarded-Proto` header is used to detect if the request was encrypted to
     * the proxy.
     *
     * @param {Object} req
     * @return {String}
     * @api private
     * @static
     */
    function _originalURL(req) {
      var headers = req.headers;

      return util.format('%s://%s%s',
          (req.connection.encrypted || headers['x-forwarded-proto'] === 'https') ? 'https' : 'http',
          headers.host,
          req.url || ''
      );
    };

    /**
     * Sanitize options to sane values.
     *
     * @param {Object} options
     * @api private
     * @static
     */
    function _sanitizeOptions(options) {
        return {
          openAmBaseUrl: options.openAmBaseUrl,
          openAmRealm: (options.openAmRealm === undefined) ? '/' : options.openAmRealm,
          openAmCookieName: (options.openAmCookieName === undefined) ? 'iPlanetDirectoryPro' : options.openAmCookieName,
          enableLoginRedirect: (options.enableLoginRedirect === undefined) ? false: options.enableLoginRedirect,
          enableUserProfile: (options.enableUserProfile === undefined) ? false : options.enableUserProfile,
          callbackUrl: options.callbackUrl
        };
    }
    /**
     * `OpenAmStrategy` constructor.
     *
     * @param {Object} options
     * @param {Function} verify
     * @api public
     */
    var openAmStrategy = function (options, verify) {

        /**
         * Get OpenAM token from incoming request.
         *
         * @param {Object} req
         * @api private
         */
        function _getOpenAmToken(req) {

          var cookies = {};
          if (req.headers.cookie) {
            req.headers.cookie.split(';').forEach(function (cookie) {
              var parts = cookie.split('=');
              cookies[parts[0].trim()] = (parts[1] || '').trim();
            });
          }

          return cookies[options.openAmCookieName];
        };

        /**
         * Handles incoming invalid tokens be either redirecting the request to the OpenAM login page or failing.
         *
         * @param {Object} req
         * @param {Function} fail
         * @api private
         */
        function _handleInvalidToken(req, redirect, fail) {
          if (options.enableLoginRedirect) {
            var callbackUrl = options.callbackUrl || req.path;

            if (!url.parse(callbackUrl).protocol) {
              // The callback URL is relative, resolve a fully qualified URL from the URL of the originating request.
              callbackUrl = url.resolve(_originalURL(req), callbackUrl);
            }

            var location = openam.getLoginUiUrl({
              goto: callbackUrl
            });
            redirect(location);
          } else {
            return fail();
          }
        };

        /**
         * Load user profile in accordinace to the normalized Portable Contacts
         * contact schema prefered by Passport.
         *
         * @see http://portablecontacts.net/draft-spec.html#schema
         * @param {String} token
         * @param {Function} done
         * @api private
         */
        function _loadUserProfile(token, done) {
          openam.getAttributes(token, function (err, data) {
            if (err) {
              return done(new InternalOpenAmError('Failed to retrieve user attributes.', err));
            }

            var profile = {
              id: data.tokenid,
              username: data.uid,
              displayName: data.cn,
              name: {
                familyName: data.sn,
                givenName: data.givenname
              },
              email: data.mail,
              _raw: data
            };

            done(null, profile);
          });
        };

        /**
         * Authenticate request by delegating to a service provider using OpenAM.
         *
         * @param {Object} req
         * @param {Object} options
         * @api public
         */
        this.authenticate = function (req) {

          var self = this;

          if (req.query && req.query.error) {
            // TODO: Error information pertaining to OpenAM flows is encoded in the query parameters, and should be propagated to the application.
            return this.fail();
          }

          var token = _getOpenAmToken(req);

          if (!token) {
            return _handleInvalidToken(req, self.redirect, self.fail);
          }

          openam.isTokenValid(token, function (valid) {
            if (!valid) {
              return _handleInvalidToken(req, self.redirect, self.fail);
            }

            if (!options.enableUserProfile) {
              return self.success();
            }

            _loadUserProfile(token, function (err, profile) {
              if (err) { return self.error(err); }

              verify(token, profile, function (err, user, info) {
                if (err) { return self.error(err); }

                self.success(user, info);
              });
            });
          });
        };

        // Passport setup
        this.name = openAmStrategy.strategyName;
        passport.Strategy.call(this);

        // OpenAmStrategy setup
        options = _sanitizeOptions(options || {});

        if (!options.openAmBaseUrl) {
          throw new Error('OpenAmStrategy requires a openAmBaseUrl option.');
        }

        var openam = new OpenAm(options.openAmBaseUrl, options.openAmRealm, options.openAmCookieName);
    }

    openAmStrategy.strategyName = 'openam';

    util.inherits(constructor, passport.Strategy);

    return openAmStrategy;
})();

/**
 * Static route middleware to authenticate incoming connections.
 *
 * @param {Object} req
 * @param {Object} res
 * @param {Function} next
 *
 * @api public
 * @static
 */
OpenAmStrategy.ensureAuthenticated = function (req, res, next) {
  if (req.isAuthenticated()) {
    return next();
  }
  else if (req._passport) {
    req._passport.instance.authenticate(OpenAmStrategy.strategyName)(req, res, next);
  }
  else {
    next(new Error('Incorrect Passport configuration.'));
  }
};

module.exports = OpenAmStrategy;
<|endoftext|>


<commit_before>// Copyright 2012 Mark Cavage, Inc.  All rights reserved.

var assert = require('assert-plus');
var formidable = require('formidable');

var errors = require('../errors');

///--- Globals

var BadRequestError = errors.BadRequestError;

///--- API

/**
 * Returns a plugin that will parse the HTTP request body IFF the
 * contentType is multipart/form-data
 *
 * If req.params already contains a given key, that key is skipped and an
 * error is logged.
 *
 * @return {Function} restify handler.
 * @throws {TypeError} on bad input
 */
function multipartBodyParser(options) {
        if (!options)
                options = {};
        assert.object(options, 'options');

        var override = options.overrideParams;
        function parseMultipartBody(req, res, next) {
                if (req.getContentType() !== 'multipart/form-data' ||
                    (req.getContentLength() === 0 && !req.isChunked()))
                        return (next());

                var form = new formidable.IncomingForm();
                form.keepExtensions = options.keepExtensions ? true : false;
                if (options.uploadDir)
                        form.uploadDir = options.uploadDir;

                form.parse(req, function (err, fields, files) {
                        if (err)
                                return (next(new BadRequestError(err.message)));

                           req.body = fields;
                           req.files = files;

                           if (options.mapParams !== false) {
                                   Object.keys(fields).forEach(function (k) {
                                           if (req.params[k] && !override)
                                                   return (false);

                                           req.params[k] = fields[k];
                                           return (true);
                                   });
                           }

                           return (next());
                });

                return (false);
        }

        return (parseMultipartBody);
}

module.exports = multipartBodyParser;
<commit_msg>Files mapped but not read and some refactor<commit_after>// Copyright 2012 Mark Cavage, Inc.  All rights reserved.

var assert = require('assert-plus');
var formidable = require('formidable');

var errors = require('../errors');

///--- Globals

var BadRequestError = errors.BadRequestError;

///--- API

/**
 * Returns a plugin that will parse the HTTP request body IFF the
 * contentType is multipart/form-data
 *
 * If req.params already contains a given key, that key is skipped and an
 * error is logged.
 *
 * @return {Function} restify handler.
 * @throws {TypeError} on bad input
 */

function multipartBodyParser(options) {
    options = options || {};
    assert.object(options, 'options');
    var override = options.overrideParams;

    function parseMultipartBody(req, res, next) {
        if (req.getContentType() !== 'multipart/form-data' || (req.getContentLength() === 0 && !req.isChunked()))
            return (next());

        var form = new formidable.IncomingForm();
        form.keepExtensions = options.keepExtensions ? true : false;
        form.uploadDir = options.uploadDir || form.uploadDir;

        form.parse(req, function (err, fields, files) {
            if (err) return (next(new BadRequestError(err.message)));
            req.body = fields;
            req.files = files;
            function mapParams (obj) {
                Object.keys(obj).forEach(function (k) {
                    if (req.params[k] && !override) return (false);
                    req.params[k] = obj[k];
                    return (true);
                });
            }
            if (options.mapParams !== false) {
                mapParams(fields);
                mapParams(files);
            }
            return (next());
        });

        return (false);
    }

    return (parseMultipartBody);
}

module.exports = multipartBodyParser;
<|endoftext|>


<commit_before>import React from 'react';
import PropTypes from 'prop-types';
import { Link as RouterLink } from 'react-router';
import Icon from '../icon';
import Event from '../../utils/helpers/events';
import { LinkStyle, LinkStyleAnchor } from './link.style';
import OptionsHelper from '../../utils/helpers/options-helper';

class Link extends React.Component {
  static safeProps = ['onClick'];

  onKeyDown = (ev) => {
    if (this.props.onKeyDown) {
      this.props.onKeyDown(ev);
    }

    // return early if there is no onClick or there is a href prop
    // or the event is not an enter key
    if (this.props.href || !Event.isEnterKey(ev)) {
      return;
    }

    if (this.props.onClick) {
      this.props.onClick(ev);
    }
  }

  renderLinkIcon = (currentAlignment = 'left') => {
    const hasProperAlignment = this.props.icon && (this.props.iconAlign === currentAlignment);

    return hasProperAlignment ? this.icon : null;
  }

  get icon() {
    return (
      <Icon
        type={ this.props.icon }
        tooltipMessage={ this.props.tooltipMessage }
        tooltipAlign={ this.props.tooltipAlign }
        tooltipPosition={ this.props.tooltipPosition }
        bgTheme='none'
        iconColor='business-color'
        disabled={ this.props.disabled }
      />
    );
  }

  get tabIndex() {
    return this.props.tabbable && !this.props.disabled ? '0' : '-1';
  }

  get componentProps() {
    const {
      disabled,
      iconAlign
    } = this.props;

    return {
      disabled,
      iconAlign,
      onKeyDown: this.onKeyDown,
      tabIndex: this.tabIndex
    };
  }

  /**
   * className `@carbon-link__content` is related to `ShowEditPod` component
   * */
  renderLinkContent() {
    return (

        {this.renderLinkIcon()}

        {this.props.children}

        {this.renderLinkIcon('right')}

    );
  }

  renderLink() {
    const {
      href,
      to
    } = this.props;

    if (this.props.to) {
      return (
        <LinkStyleAnchor
          as={ RouterLink }
          to={ to }
          { ...this.componentProps }
        >
          {this.renderLinkContent()}
        </LinkStyleAnchor>
      );
    }

    return (
      <LinkStyleAnchor
        href={ href }
        { ...this.componentProps }
      >
        {this.renderLinkContent()}
      </LinkStyleAnchor>
    );
  }

  render() {
    const { disabled, className, onClick } = this.props;

    return (
      <LinkStyle
        data-component='link'
        disabled={ disabled }
        className={ className }
        onClick={ onClick }
      >
        {this.renderLink()}
      </LinkStyle>
    );
  }
}

Link.propTypes = {
  /** Child content to render in the link. */
  children: PropTypes.node,
  /** Classes to apply to the component. */
  className: PropTypes.string,
  /** The disabled state of the link. */
  disabled: PropTypes.bool,
  /** An href for an anchor tag. */
  href: PropTypes.string,
  /** An icon to display next to the link. */
  icon: PropTypes.string,
  /** Which side of the link to the render the link. */
  iconAlign: PropTypes.string,
  /** Function called when the mouse is clicked. */
  onClick: PropTypes.func,
  /** Function called when a key is pressed. */
  onKeyDown: PropTypes.func,
  /** Whether to include the link in the tab order of the page */
  tabbable: PropTypes.bool,
  /** Using `to` instead of `href` will create a React Router link rather than a web href. */
  to: PropTypes.string,
  /** A message to display as a tooltip to the link. */
  tooltipMessage: PropTypes.string,
  /** Positions the tooltip with the link. */
  tooltipPosition: PropTypes.oneOf(OptionsHelper.positions),
  /** Aligns the tooltip. */
  tooltipAlign: PropTypes.oneOf(OptionsHelper.alignAroundEdges)
};

Link.defaultProps = {
  iconAlign: 'left',
  tabbable: true
};

export default Link;
<commit_msg>fixed data-role attribute not passed to the main container<commit_after>import React from 'react';
import PropTypes from 'prop-types';
import { Link as RouterLink } from 'react-router';
import Icon from '../icon';
import Event from '../../utils/helpers/events';
import { LinkStyle, LinkStyleAnchor } from './link.style';
import OptionsHelper from '../../utils/helpers/options-helper';
import tagComponent from '../../utils/helpers/tags/tags';

class Link extends React.Component {
  static safeProps = ['onClick'];

  onKeyDown = (ev) => {
    if (this.props.onKeyDown) {
      this.props.onKeyDown(ev);
    }

    // return early if there is no onClick or there is a href prop
    // or the event is not an enter key
    if (this.props.href || !Event.isEnterKey(ev)) {
      return;
    }

    if (this.props.onClick) {
      this.props.onClick(ev);
    }
  }

  renderLinkIcon = (currentAlignment = 'left') => {
    const hasProperAlignment = this.props.icon && (this.props.iconAlign === currentAlignment);

    return hasProperAlignment ? this.icon : null;
  }

  get icon() {
    return (
      <Icon
        type={ this.props.icon }
        tooltipMessage={ this.props.tooltipMessage }
        tooltipAlign={ this.props.tooltipAlign }
        tooltipPosition={ this.props.tooltipPosition }
        bgTheme='none'
        iconColor='business-color'
        disabled={ this.props.disabled }
      />
    );
  }

  get tabIndex() {
    return this.props.tabbable && !this.props.disabled ? '0' : '-1';
  }

  get componentProps() {
    const {
      disabled,
      iconAlign
    } = this.props;

    return {
      disabled,
      iconAlign,
      onKeyDown: this.onKeyDown,
      tabIndex: this.tabIndex
    };
  }

  /**
   * className `@carbon-link__content` is related to `ShowEditPod` component
   * */
  renderLinkContent() {
    return (

        {this.renderLinkIcon()}

        {this.props.children}

        {this.renderLinkIcon('right')}

    );
  }

  renderLink() {
    const {
      href,
      to
    } = this.props;

    if (this.props.to) {
      return (
        <LinkStyleAnchor
          as={ RouterLink }
          to={ to }
          { ...this.componentProps }
        >
          {this.renderLinkContent()}
        </LinkStyleAnchor>
      );
    }

    return (
      <LinkStyleAnchor
        href={ href }
        { ...this.componentProps }
      >
        {this.renderLinkContent()}
      </LinkStyleAnchor>
    );
  }

  render() {
    const { disabled, className, onClick } = this.props;

    return (
      <LinkStyle
        disabled={ disabled }
        className={ className }
        onClick={ onClick }
        { ...tagComponent('link', this.props) }
      >
        {this.renderLink()}
      </LinkStyle>
    );
  }
}

Link.propTypes = {
  /** Child content to render in the link. */
  children: PropTypes.node,
  /** Classes to apply to the component. */
  className: PropTypes.string,
  /** The disabled state of the link. */
  disabled: PropTypes.bool,
  /** An href for an anchor tag. */
  href: PropTypes.string,
  /** An icon to display next to the link. */
  icon: PropTypes.string,
  /** Which side of the link to the render the link. */
  iconAlign: PropTypes.string,
  /** Function called when the mouse is clicked. */
  onClick: PropTypes.func,
  /** Function called when a key is pressed. */
  onKeyDown: PropTypes.func,
  /** Whether to include the link in the tab order of the page */
  tabbable: PropTypes.bool,
  /** Using `to` instead of `href` will create a React Router link rather than a web href. */
  to: PropTypes.string,
  /** A message to display as a tooltip to the link. */
  tooltipMessage: PropTypes.string,
  /** Positions the tooltip with the link. */
  tooltipPosition: PropTypes.oneOf(OptionsHelper.positions),
  /** Aligns the tooltip. */
  tooltipAlign: PropTypes.oneOf(OptionsHelper.alignAroundEdges)
};

Link.defaultProps = {
  iconAlign: 'left',
  tabbable: true
};

export default Link;
<|endoftext|>


<commit_before>import React from 'react';
import PropTypes from 'prop-types';
import { storiesOf } from '@storybook/react';
import { select } from '@storybook/addon-knobs';
import { action } from '@storybook/addon-actions';
import { State, Store } from '@sambego/storybook-state';
import { dlsThemeSelector, classicThemeSelector } from '../../../.storybook/theme-selectors';
import DefaultPages from './pages.component';
import Page from './page/page.component';
import DialogFullScreen from '../dialog-full-screen';
import Heading from '../heading/heading';
import { OriginalButton } from '../button/button.component';
import getDocGenInfo from '../../utils/helpers/docgen-info';
import docgenInfo from './docgenInfo.json';

Page.__docgenInfo = getDocGenInfo(
  docgenInfo,
  /page\.js(?!spec)/
);

DefaultPages.__docgenInfo = getDocGenInfo(
  docgenInfo,
  /pages(?!spec)/
);

const store = new Store({
  open: false,
  pageIndex: 0,
  pageHistory: [],
  previouspageHistoryPointer: 0,
  isDisabled: false,
  transitionTime: 600
});

const handleSlide = (_, pageIndex) => {
  action('slide')(`Page index: ${pageIndex}`);

  if (store.get('isDisabled')) return store.get('previouspageHistoryPointer');

  const newpageHistory = [...store.get('pageHistory'), pageIndex];

  store.set({
    isDisabled: true,
    pageHistory: newpageHistory,
    pageIndex: (pageIndex || 0),
    previouspageHistoryPointer: newpageHistory.length - 1
  });

  setTimeout(() => {
    store.set({ isDisabled: false });
  }, store.get('transitionTime'));

  return pageIndex;
};

const handlePreviousSlide = (ev) => {
  ev.preventDefault();
  if (store.get('isDisabled')) return;
  const previouHistoryPointer = store.get('previouspageHistoryPointer');
  const pointer = (previouHistoryPointer - 1) > 0 ? (previouHistoryPointer - 1) : 0;

  store.set({
    isDisabled: true,
    pageHistory: store.get('pageHistory').slice(0, -1),
    pageIndex: (store.get('pageHistory')[pointer] || 0),
    previouspageHistoryPointer: pointer
  });

  setTimeout(() => {
    store.set({ isDisabled: false });
  }, store.get('transitionTime'));
};

const handleOpen = () => {
  action('open')();
  store.set({ open: true });
};

const handleCancel = () => {
  action('cancel')();
  store.set({
    open: false,
    pageHistory: [0]
  });
};

const CustomState = (props) => {
  return (
    <State store={ store }>
      { props.children }
    </State>
  );
};

CustomState.propTypes = {
  children: PropTypes.node
};

const DialogState = props => new CustomState(props);
const PageState = props => new CustomState(props);
const indexConfig = [0, 1, 2];
const pageIndex = () => select('pageIndex', indexConfig, indexConfig[0]);

function makeStory(name, themeSelector) {
  const component = () => {
    return (

        <OriginalButton onClick={ handleOpen }>Open Preview</OriginalButton>
        <DialogState>
          <DialogFullScreen
            open={ store.get('open') }
            onCancel={ handleCancel }
          >
            <PageState>
              <DefaultPages
                pageIndex={ handleSlide(null, pageIndex()) }
              >
                <Page title={ <Heading title='My First Page' /> }>
                  <OriginalButton onClick={ ev => handleSlide(ev, 1) }>
                    Go to second page
                  </OriginalButton>
                </Page>

                <Page title={ <Heading title='My Second Page' backLink={ handlePreviousSlide } /> }>
                  <OriginalButton onClick={ ev => handleSlide(ev, 0) }>
                    Go to first page
                  </OriginalButton>
                  <OriginalButton onClick={ ev => handleSlide(ev, 2) }>
                    Go to third page
                  </OriginalButton>
                </Page>

                <Page title={ <Heading title='My Third Page' backLink={ handlePreviousSlide } /> }>
                  <OriginalButton onClick={ (ev) => { handleSlide(ev, 1); } }>
                    Go to second page
                  </OriginalButton>
                </Page>
              </DefaultPages>
            </PageState>
          </DialogFullScreen>
        </DialogState>

    );
  };

  const metadata = {
    themeSelector,
    info: {
      text:
Allows to slide to different pages in a full screen dialog.
,
      propTablesExclude: [OriginalButton, DialogFullScreen, DialogState, PageState, DefaultPages, Page, State]
    }
  };

  return [name, component, metadata];
}

storiesOf('Pages', module)
  .add(...makeStory('default', dlsThemeSelector))
  .add(...makeStory('classic', classicThemeSelector));
<commit_msg>FE-1587 removed button to allow for continuation only forward with actions<commit_after>import React from 'react';
import PropTypes from 'prop-types';
import { storiesOf } from '@storybook/react';
import { select } from '@storybook/addon-knobs';
import { action } from '@storybook/addon-actions';
import { State, Store } from '@sambego/storybook-state';
import { dlsThemeSelector, classicThemeSelector } from '../../../.storybook/theme-selectors';
import DefaultPages from './pages.component';
import Page from './page/page.component';
import DialogFullScreen from '../dialog-full-screen';
import Heading from '../heading/heading';
import { OriginalButton } from '../button/button.component';
import getDocGenInfo from '../../utils/helpers/docgen-info';
import docgenInfo from './docgenInfo.json';

Page.__docgenInfo = getDocGenInfo(
  docgenInfo,
  /page\.js(?!spec)/
);

DefaultPages.__docgenInfo = getDocGenInfo(
  docgenInfo,
  /pages(?!spec)/
);

const store = new Store({
  open: false,
  pageIndex: 0,
  pageHistory: [],
  previouspageHistoryPointer: 0,
  isDisabled: false,
  transitionTime: 600
});

const handleSlide = (_, pageIndex) => {
  action('slide')(`Page index: ${pageIndex}`);

  if (store.get('isDisabled')) return store.get('previouspageHistoryPointer');

  const newpageHistory = [...store.get('pageHistory'), pageIndex];

  store.set({
    isDisabled: true,
    pageHistory: newpageHistory,
    pageIndex: (pageIndex || 0),
    previouspageHistoryPointer: newpageHistory.length - 1
  });

  setTimeout(() => {
    store.set({ isDisabled: false });
  }, store.get('transitionTime'));

  return pageIndex;
};

const handlePreviousSlide = (ev) => {
  ev.preventDefault();
  if (store.get('isDisabled')) return;
  const previouHistoryPointer = store.get('previouspageHistoryPointer');
  const pointer = (previouHistoryPointer - 1) > 0 ? (previouHistoryPointer - 1) : 0;

  store.set({
    isDisabled: true,
    pageHistory: store.get('pageHistory').slice(0, -1),
    pageIndex: (store.get('pageHistory')[pointer] || 0),
    previouspageHistoryPointer: pointer
  });

  setTimeout(() => {
    store.set({ isDisabled: false });
  }, store.get('transitionTime'));
};

const handleOpen = () => {
  action('open')();
  store.set({ open: true });
};

const handleCancel = () => {
  action('cancel')();
  store.set({
    open: false,
    pageHistory: [0]
  });
};

const CustomState = (props) => {
  return (
    <State store={ store }>
      { props.children }
    </State>
  );
};

CustomState.propTypes = {
  children: PropTypes.node
};

const DialogState = props => new CustomState(props);
const PageState = props => new CustomState(props);
const indexConfig = [0, 1, 2];
const pageIndex = () => select('pageIndex', indexConfig, indexConfig[0]);

function makeStory(name, themeSelector) {
  const component = () => {
    return (

        <OriginalButton onClick={ handleOpen }>Open Preview</OriginalButton>
        <DialogState>
          <DialogFullScreen
            open={ store.get('open') }
            onCancel={ handleCancel }
          >
            <PageState>
              <DefaultPages
                pageIndex={ handleSlide(null, pageIndex()) }
              >
                <Page title={ <Heading title='My First Page' /> }>
                  <OriginalButton onClick={ ev => handleSlide(ev, 1) }>
                    Go to second page
                  </OriginalButton>
                </Page>

                <Page title={ <Heading title='My Second Page' backLink={ handlePreviousSlide } /> }>
                  <OriginalButton onClick={ (ev) => { handleSlide(ev, 2); } }>
                    Go to third page
                  </OriginalButton>
                </Page>

                <Page title={ <Heading title='My Third Page' backLink={ handlePreviousSlide } /> }>
                  <OriginalButton onClick={ (ev) => { handleSlide(ev, 1); } }>
                    Go to second page
                  </OriginalButton>
                </Page>
              </DefaultPages>
            </PageState>
          </DialogFullScreen>
        </DialogState>

    );
  };

  const metadata = {
    themeSelector,
    info: {
      text:
Allows to slide to different pages in a full screen dialog.
,
      propTablesExclude: [OriginalButton, DialogFullScreen, DialogState, PageState, DefaultPages, Page, State]
    }
  };

  return [name, component, metadata];
}

storiesOf('Pages', module)
  .add(...makeStory('default', dlsThemeSelector))
  .add(...makeStory('classic', classicThemeSelector));
<|endoftext|>