<commit_before>// Rest and Recovery
// A Roll20 API script to handle recovery on the resource attributes on the D&D 5th Edition by Roll20 sheet.
on('ready', () => {
    const version = '0.0.2';
    const sheetVersion = 'D&D 5th Edition by Roll20';
    log('Rest and Recovery v' + version + ' is ready!  Designed for use with the ' + sheetVersion + ' character sheet!');
                const shortRestList = ['Spell Slots', 'Warlock Spell Slots','Spell Points','Channel Divinity', 'Wild Shapes', 'Second Wind','Action Surge','Superiority Dice', 'Ki', 'Ki Points','Visions of the Past','Enthralling Performance','Words of Terror','Unbreakable Majesty','Spirit Totem','Arcane Shot','Detect Portal','Ethereal Step','Magic-Users Nemesis','Favored by the Gods','Wind Soul','Bladesong','Blade Song','Arcane Abeyance','Illusory Self','Shapechanger'];
                const longRestList = ['Rage', 'Rages', 'Lay on Hands', 'Sorcery Points','Flash of Genius','Divine Intervention','Eyes of the Grave','Warding Flare','Wrath of the Storm','Cleansing Touch','War Priest Attack','Sentinel at Deaths Door','Embodiment of the Law','Universal Speech','Mantle of Majesty','Indomitable','Infectious Inspiration','Shadow Lore','Balm of the Summer Court','Natural Recovery','Fungal Infestation','Hidden Paths','Walker in Dreams','Faithful Summons','Fighting Spirit','Warding Maneuver','Strength Before Death','Unwavering Mark','Wholeness of Body','Glorious Defense','Undying Sentinel','Invincible Conqueror','Holy Nimbus','Living Legend','Emissary of Redemption','Elder Champion','Avenging Angel','Dread Lord','Hunters Sense','Unerring Eye','Spell Thief','Strength of the Grave','Tides of Chaos','Unearthly Recovery','Mystic Arcanum-6th','Mystic Arcanum-7th','Mystic Arcanum-8th','Mystic Arcanum-9th','Eldritch Master','Chronal Shift','Momentary Stasis','Benign Transposition','Instinctive Charm','Power Surge','Violent Attraction','Event Horizon'];
                const ammoList = ['Crossbow bolts', 'Arrows', 'Bullets', 'Sling Bullets', 'Needles', 'Darts'];

    on('chat:message', (msg) => {
        if ('api' === msg.type && /!r-(short|long|charges|ammo|help|list)\b/i.test(msg.content) && msg.selected) {
            let idList = [];
            let nameList = [];
            var rReport = '';
            const header = "
";
            const footer = '
';

            function sendMessage(messageText) {
                sendChat("Rest and Recovery", "/w " + msg.who + header + messageText + footer);

            }

            function checkRestList(rName) {

                rRecoverName = rName;

                if (ammoList.includes(rName)) {
                    rRecoverName = rName + '+ammo';
                } else if (longRestList.includes(rName)) {
                    rRecoverName = rName + '+LR';
                } else if (shortRestList.includes(rName)) {
                    rRecoverName = rName + '+SR';
                } else {
                    rRecoverName = rName + '+x';
                }

                return rRecoverName;
            }

            const attrLookup = (characterid, name, caseSensitive) => {
                let match = name.match(/^(repeating_.*)_\$(\d+)_.*$/);
                if (match) {
                    let index = match[2],
                        attrMatcher = new RegExp(`^${name.replace(/_\$\d+_/,'_([-\\da-zA-Z]+)_')}$`, (caseSensitive ? '' : 'i')),
                        createOrderKeys = [],
                        attrs = _.chain(findObjs({
                            type: 'attribute',
                            characterid
                        }))
                        .map((a) => {
                            return {
                                attr: a,
                                match: a.get('name').match(attrMatcher)
                            };
                        })
                        .filter((o) => o.match)
                        .each((o) => createOrderKeys.push(o.match[1]))
                        .reduce((m, o) => {
                            m[o.match[1]] = o.attr;
                            return m;
                        }, {})
                        .value(),
                        sortOrderKeys = _.chain(((findObjs({
                            type: 'attribute',
                            characterid,
                            name: `_reporder_${match[1]}`
                        })[0] || {
                            get: _.noop
                        }).get('current') || '').split(/\s*,\s*/))
                        .intersection(createOrderKeys)
                        .union(createOrderKeys)
                        .value();
                    if (index < sortOrderKeys.length && _.has(attrs, sortOrderKeys[index])) {
                        return attrs[sortOrderKeys[index]];
                    }
                    return;
                }
                return findObjs({
                    type: 'attribute',
                    characterid,
                    name: name
                }, {
                    caseInsensitive: !caseSensitive
                })[0];
            };

            //Dice rolling function
            const parse = (txt) => {
                const tokenize = /(\d+d\d+|\d+|\+|-)/ig;
                const dieparts = /^(\d+)?d(\d+)$/i;
                const ops = {
                    '+': (m, n) => m + n,
                    '-': (m, n) => m - n
                };
                let op = '+';

                return (txt.replace(/\s+/g, '').match(tokenize) || []).reduce((m, t) => {
                    let matches = t.match(dieparts);
                    if (matches) {
                        return ops[op](m, [...Array(parseInt(matches[1]) || 1)].reduce(m => m + randomInteger(parseInt(matches[2])), 0));
                    } else if (/^\d+$/.test(t)) {
                        return ops[op](m, parseInt(t));
                    } else {
                        op = t;
                        return m;
                    }
                }, 0);
            };

            //Get character list. Future versions may handle multiple characters
            let TCData = msg.selected
                .map(o => getObj('graphic', o._id))
                .filter(o => undefined !== o)
                .filter(t => t.get('represents').length)
                .map(t => ({
                    token: t,
                    character: getObj('character', t.get('represents'))
                }))
                .filter(o => undefined !== o.character);
            var allIDs = TCData.map(n => n.character.get('_id'));

            var characterID = (allIDs[0]);
            var myCharacter = getObj('character', characterID);

            var resourceName_l = [],
                resourceRecovery_l = [],
                resourceCurrent_l = [],
                resourceMax_l = [],
                resourceName_r = [],
                resourceRecovery_r = [],
                resourceCurrent_r = [],
                resourceMax_r = [];

            resourceName_c = getAttrByName(characterID, "class_resource_name");
            resourceName_c = checkRestList(resourceName_c);
            resourceRecovery_c = resourceName_c.split(/\+(.+)/)[1]; //Regex splits on first occurence of '+', so the dice strings are preserved
            resourceCurrent_c = getAttrByName(characterID, "class_resource");
            resourceMax_c = getAttrByName(characterID, "class_resource", "max");

            resourceName_o = getAttrByName(characterID, "other_resource_name");
            resourceName_o = checkRestList(resourceName_o);
            resourceRecovery_o = resourceName_o.split(/\+(.+)/)[1];
            resourceCurrent_o = getAttrByName(characterID, "other_resource");
            resourceMax_o = getAttrByName(characterID, "other_resource", "max");

            //Get Repeating Resources
            var i = 0
            do {
                if (attrLookup(characterID, 'repeating_resource_$' + i + '_resource_left_name')) {
                    resourceName_l[i] = getAttrByName(characterID, 'repeating_resource_$' + i + '_resource_left_name');
                    resourceName_l[i] = checkRestList(resourceName_l[i]);
                    resourceRecovery_l[i] = resourceName_l[i].split(/\+(.+)/)[1];
                    resourceCurrent_l[i] = getAttrByName(characterID, 'repeating_resource_$' + i + '_resource_left');
                    resourceMax_l[i] = getAttrByName(characterID, 'repeating_resource_$' + i + '_resource_left', 'max');
                }

                if (attrLookup(characterID, 'repeating_resource_$' + i + '_resource_right_name')) {
                    resourceName_r[i] = getAttrByName(characterID, 'repeating_resource_$' + i + '_resource_right_name');
                    resourceName_r[i] = checkRestList(resourceName_r[i]);
                    resourceRecovery_r[i] = resourceName_r[i].split(/\+(.+)/)[1];
                    resourceCurrent_r[i] = getAttrByName(characterID, 'repeating_resource_$' + i + '_resource_right');
                    resourceMax_r[i] = getAttrByName(characterID, 'repeating_resource_$' + i + '_resource_right', 'max');
                }
                i++;
            }
            while (i < 10);

            //recover resources function
            function recoverResource(rFullName, rName, rRecover, rCurrent, rMax) {
                let r = attrLookup(characterID, rCurrent);

                if ((rRecover.includes("SR") && msg.content.includes("short")) || ((rRecover.includes("LR") || rRecover.includes("SR")) && msg.content.includes("long"))) {

                    if (rRecover.includes('d')) {

                        rRecover = rRecover.split(/SR|LR/)[1];
                        let recoverAmount = parse(rRecover);
                        r.set('current', Math.min(Number(r.get('current')) + recoverAmount, rMax));
                        rReport = rReport + getAttrByName(characterID, rName) + ' added ' + recoverAmount + 'charges.
';
                    } else {
                        r.set('current', rMax);
                        rReport = rReport + getAttrByName(characterID, rName) + ' has been fully restored.
';

                    }
                } else if (rRecover.includes("d") && !rRecover.includes('R') && msg.content.includes("charges")) {
                    let recoverAmount = parse(rRecover);
                    r.set('current', Math.min(Number(r.get('current')) + recoverAmount, rMax));
                    rReport = rReport + getAttrByName(characterID, rName) + ' added ' + recoverAmount + 'charges.
';
                } else if (rRecover.includes('ammo') && msg.content.includes("ammo")) {
                    if (rMax - Number(r.get('current')) > 0) {
                        let recoverAmount = (rMax - Number(r.get('current'))) + 'd2-' + (rMax - Number(r.get('current')));
                        recoverAmount = parse(recoverAmount);
                        r.set('current', Math.min(Number(r.get('current')) + recoverAmount, rMax));
                        r.set('max', r.get('current'));
                        rReport = rReport + recoverAmount + ' ' + getAttrByName(characterID, rName) + ' have been recovered.
';
                    }
                }
            }

            //Run Recoveries Here
            recoverResource(resourceName_c, "class_resource_name", resourceRecovery_c, "class_resource", resourceMax_c);
            recoverResource(resourceName_o, "other_resource_name", resourceRecovery_o, "other_resource", resourceMax_o);

            i = 0;
            do {
                if (resourceName_l[i]) {
                    recoverResource(resourceName_l[i], 'repeating_resource_$' + i + '_resource_left_name', resourceRecovery_l[i], 'repeating_resource_$' + i + '_resource_left', resourceMax_l[i]);

                }
                i++;
            }
            while (resourceName_l[i]);

            i = 0;
            do {
                if (resourceName_r[i]) {
                    recoverResource(resourceName_r[i], 'repeating_resource_$' + i + '_resource_right_name', resourceRecovery_r[i], 'repeating_resource_$' + i + '_resource_right', resourceMax_r[i]);
                }
                i++;
            }
            while (resourceName_r[i]);

            if (msg.content.includes("help")) {
                sendMessage('
Rest and Recovery

A Roll20 API script to handle recovery on the resource attributes on the D&D 5th Edition by Roll20 sheet.To use this script, resources must include a code in their name, separated from the name by a plus sign. You can include standard dice expressions as well. "1d6" is used in all examples, but you can do 2d6+3, 3d20, etc. Here are examples of the commands given and the codes that are affected.
!r-short
Used for Short Rest

+SR This resource will return to its maximumm value

+SR1d6 This resource will add 1d6 to the resource up to its maximum value
!r-long
Used for Long Rest

+LR This resource will return to its maximumm value

+LR1d6 This resource will add 1d6 to the resource up to its maximum value
!r-Charges
*used for restoring charges that are user-controlled, such as "at dawn" or "under a full moon".

+1d6
!r-Ammo
no code is used here. The script looks for common ammo types: Crossbow bolts, Arrows, Bullets, etc. It rolls 1d2 for each piece of ammo expended. If the result is a "2", the ammo is recovered. The max and current values are adjusted to reflect the new total.
Special Cases
Finally, the following special cases exist. Class Resources that have any of the following names are recognized and handled appropriately:
These are recovered on a Short or Long Rest:

Spell Slots, Warlock Spell Slots

Channel Divinity

Wild Shape

Superiority Dice

Ki Points, Ki
These are recovered on a Long Rest.

Rages

Lay on Hands

Sorcery Points

Bardic Inspiration needs a +SR or +LR code, since the recovery rate changes at fifth level
');
            } else {
             if (msg.content.includes("list")) {
                sendMessage('
Rest and Recovery

Short Rest Class Resources
'+shortRestList.join('

')+'

Long Rest Class Resources
'+longRestList.join('

')+'

Ammo Types
'+ammoList.join('

')+'
');
            } else {
                if (rReport) {
                    sendMessage(rReport);
                } else {
                    sendMessage('No resources were changed.');
}
                }
                log(rReport);
            }

        }
    });
});
<commit_msg>Update Rest-And-Recovery.js<commit_after>// Rest and Recovery
// A Roll20 API script to handle recovery on the resource attributes on the D&D 5th Edition by Roll20 sheet.
on('ready', () => {
    const version = '0.0.3';
    const sheetVersion = 'D&D 5th Edition by Roll20';
    log('Rest and Recovery v' + version + ' is ready!  Designed for use with the ' + sheetVersion + ' character sheet!');
                const shortRestList = ['Spell Slots', 'Warlock Spell Slots','Spell Points','Channel Divinity', 'Wild Shapes', 'Second Wind','Action Surge','Superiority Dice', 'Ki', 'Ki Points','Visions of the Past','Enthralling Performance','Words of Terror','Unbreakable Majesty','Spirit Totem','Arcane Shot','Detect Portal','Ethereal Step','Magic-Users Nemesis','Favored by the Gods','Wind Soul','Bladesong','Blade Song','Arcane Abeyance','Illusory Self','Shapechanger','Fey Step', 'Hidden Step', 'Firbolg Magic', 'Stones Endurance', 'Saving Face', 'Grovel Cower and Beg', 'Hungry Jaws', 'Fury of the Small', 'Breath Weapon'];
                const longRestList = ['Rage', 'Rages', 'Lay on Hands', 'Sorcery Points','Flash of Genius','Divine Intervention','Eyes of the Grave','Warding Flare','Wrath of the Storm','Cleansing Touch','War Priest Attack','Sentinel at Deaths Door','Embodiment of the Law','Universal Speech','Mantle of Majesty','Indomitable','Infectious Inspiration','Shadow Lore','Balm of the Summer Court','Natural Recovery','Fungal Infestation','Hidden Paths','Walker in Dreams','Faithful Summons','Fighting Spirit','Warding Maneuver','Strength Before Death','Unwavering Mark','Wholeness of Body','Glorious Defense','Undying Sentinel','Invincible Conqueror','Holy Nimbus','Living Legend','Emissary of Redemption','Elder Champion','Avenging Angel','Dread Lord','Hunters Sense','Unerring Eye','Spell Thief','Strength of the Grave','Tides of Chaos','Unearthly Recovery','Mystic Arcanum-6th','Mystic Arcanum-7th','Mystic Arcanum-8th','Mystic Arcanum-9th','Eldritch Master','Chronal Shift','Momentary Stasis','Benign Transposition','Instinctive Charm','Power Surge','Violent Attraction','Event Horizon','Necrotic Shroud', 'Radiant Soul', 'Radiant Consumption', 'Healing Hands', 'Duergar Magic', 'Dancing Lights', 'Faerie Fire', 'Darkness', 'Blessing of the Raven Queen', 'Mingle with the Wind', 'Merge with Stone', 'Reach to the Blaze', 'Burning Hands', 'Githyanki Psionics', 'Jump', 'Misty Step', 'Githerazi Psionics', 'Shield', 'Detect Thoughts', 'Control Air and Water', 'Poison Spray', 'Animal Friendship', 'Hellish Rebuke', 'Relentless Endurance', 'Luck'];
                const ammoList = ['Crossbow bolts', 'Arrows', 'Bullets', 'Sling Bullets', 'Needles', 'Darts'];

    on('chat:message', (msg) => {
        if ('api' === msg.type && /!r-(short|long|charges|ammo|help|list)\b/i.test(msg.content) && msg.selected) {
            let idList = [];
            let nameList = [];
            var rReport = '';
            const header = "
";
            const footer = '
';

            function sendMessage(messageText) {
                sendChat("Rest and Recovery", "/w " + msg.who + header + messageText + footer);

            }

            function checkRestList(rName) {

                rRecoverName = rName;

                if (ammoList.includes(rName)) {
                    rRecoverName = rName + '+ammo';
                } else if (longRestList.includes(rName)) {
                    rRecoverName = rName + '+LR';
                } else if (shortRestList.includes(rName)) {
                    rRecoverName = rName + '+SR';
                } else {
                    rRecoverName = rName + '+x';
                }

                return rRecoverName;
            }

            const attrLookup = (characterid, name, caseSensitive) => {
                let match = name.match(/^(repeating_.*)_\$(\d+)_.*$/);
                if (match) {
                    let index = match[2],
                        attrMatcher = new RegExp(`^${name.replace(/_\$\d+_/,'_([-\\da-zA-Z]+)_')}$`, (caseSensitive ? '' : 'i')),
                        createOrderKeys = [],
                        attrs = _.chain(findObjs({
                            type: 'attribute',
                            characterid
                        }))
                        .map((a) => {
                            return {
                                attr: a,
                                match: a.get('name').match(attrMatcher)
                            };
                        })
                        .filter((o) => o.match)
                        .each((o) => createOrderKeys.push(o.match[1]))
                        .reduce((m, o) => {
                            m[o.match[1]] = o.attr;
                            return m;
                        }, {})
                        .value(),
                        sortOrderKeys = _.chain(((findObjs({
                            type: 'attribute',
                            characterid,
                            name: `_reporder_${match[1]}`
                        })[0] || {
                            get: _.noop
                        }).get('current') || '').split(/\s*,\s*/))
                        .intersection(createOrderKeys)
                        .union(createOrderKeys)
                        .value();
                    if (index < sortOrderKeys.length && _.has(attrs, sortOrderKeys[index])) {
                        return attrs[sortOrderKeys[index]];
                    }
                    return;
                }
                return findObjs({
                    type: 'attribute',
                    characterid,
                    name: name
                }, {
                    caseInsensitive: !caseSensitive
                })[0];
            };

            //Dice rolling function
            const parse = (txt) => {
                const tokenize = /(\d+d\d+|\d+|\+|-)/ig;
                const dieparts = /^(\d+)?d(\d+)$/i;
                const ops = {
                    '+': (m, n) => m + n,
                    '-': (m, n) => m - n
                };
                let op = '+';

                return (txt.replace(/\s+/g, '').match(tokenize) || []).reduce((m, t) => {
                    let matches = t.match(dieparts);
                    if (matches) {
                        return ops[op](m, [...Array(parseInt(matches[1]) || 1)].reduce(m => m + randomInteger(parseInt(matches[2])), 0));
                    } else if (/^\d+$/.test(t)) {
                        return ops[op](m, parseInt(t));
                    } else {
                        op = t;
                        return m;
                    }
                }, 0);
            };

            //Get character list. Future versions may handle multiple characters
            let TCData = msg.selected
                .map(o => getObj('graphic', o._id))
                .filter(o => undefined !== o)
                .filter(t => t.get('represents').length)
                .map(t => ({
                    token: t,
                    character: getObj('character', t.get('represents'))
                }))
                .filter(o => undefined !== o.character);
            var allIDs = TCData.map(n => n.character.get('_id'));

            var characterID = (allIDs[0]);
            var myCharacter = getObj('character', characterID);

            var resourceName_l = [],
                resourceRecovery_l = [],
                resourceCurrent_l = [],
                resourceMax_l = [],
                resourceName_r = [],
                resourceRecovery_r = [],
                resourceCurrent_r = [],
                resourceMax_r = [];

            resourceName_c = getAttrByName(characterID, "class_resource_name");
            resourceName_c = checkRestList(resourceName_c);
            resourceRecovery_c = resourceName_c.split(/\+(.+)/)[1]; //Regex splits on first occurence of '+', so the dice strings are preserved
            resourceCurrent_c = getAttrByName(characterID, "class_resource");
            resourceMax_c = getAttrByName(characterID, "class_resource", "max");

            resourceName_o = getAttrByName(characterID, "other_resource_name");
            resourceName_o = checkRestList(resourceName_o);
            resourceRecovery_o = resourceName_o.split(/\+(.+)/)[1];
            resourceCurrent_o = getAttrByName(characterID, "other_resource");
            resourceMax_o = getAttrByName(characterID, "other_resource", "max");

            //Get Repeating Resources
            var i = 0
            do {
                if (attrLookup(characterID, 'repeating_resource_$' + i + '_resource_left_name')) {
                    resourceName_l[i] = getAttrByName(characterID, 'repeating_resource_$' + i + '_resource_left_name');
                    resourceName_l[i] = checkRestList(resourceName_l[i]);
                    resourceRecovery_l[i] = resourceName_l[i].split(/\+(.+)/)[1];
                    resourceCurrent_l[i] = getAttrByName(characterID, 'repeating_resource_$' + i + '_resource_left');
                    resourceMax_l[i] = getAttrByName(characterID, 'repeating_resource_$' + i + '_resource_left', 'max');
                }

                if (attrLookup(characterID, 'repeating_resource_$' + i + '_resource_right_name')) {
                    resourceName_r[i] = getAttrByName(characterID, 'repeating_resource_$' + i + '_resource_right_name');
                    resourceName_r[i] = checkRestList(resourceName_r[i]);
                    resourceRecovery_r[i] = resourceName_r[i].split(/\+(.+)/)[1];
                    resourceCurrent_r[i] = getAttrByName(characterID, 'repeating_resource_$' + i + '_resource_right');
                    resourceMax_r[i] = getAttrByName(characterID, 'repeating_resource_$' + i + '_resource_right', 'max');
                }
                i++;
            }
            while (i < 10);

            //recover resources function
            function recoverResource(rFullName, rName, rRecover, rCurrent, rMax) {
                let r = attrLookup(characterID, rCurrent);

                if ((rRecover.includes("SR") && msg.content.includes("short")) || ((rRecover.includes("LR") || rRecover.includes("SR")) && msg.content.includes("long"))) {

                    if (rRecover.includes('d')) {

                        rRecover = rRecover.split(/SR|LR/)[1];
                        let recoverAmount = parse(rRecover);
                        r.set('current', Math.min(Number(r.get('current')) + recoverAmount, rMax));
                        rReport = rReport + getAttrByName(characterID, rName) + ' added ' + recoverAmount + 'charges.
';
                    } else {
                        r.set('current', rMax);
                        rReport = rReport + getAttrByName(characterID, rName) + ' has been fully restored.
';

                    }
                } else if (rRecover.includes("d") && !rRecover.includes('R') && msg.content.includes("charges")) {
                    let recoverAmount = parse(rRecover);
                    r.set('current', Math.min(Number(r.get('current')) + recoverAmount, rMax));
                    rReport = rReport + getAttrByName(characterID, rName) + ' added ' + recoverAmount + 'charges.
';
                } else if (rRecover.includes('ammo') && msg.content.includes("ammo")) {
                    if (rMax - Number(r.get('current')) > 0) {
                        let recoverAmount = (rMax - Number(r.get('current'))) + 'd2-' + (rMax - Number(r.get('current')));
                        recoverAmount = parse(recoverAmount);
                        r.set('current', Math.min(Number(r.get('current')) + recoverAmount, rMax));
                        r.set('max', r.get('current'));
                        rReport = rReport + recoverAmount + ' ' + getAttrByName(characterID, rName) + ' have been recovered.
';
                    }
                }
            }

            //Run Recoveries Here
            recoverResource(resourceName_c, "class_resource_name", resourceRecovery_c, "class_resource", resourceMax_c);
            recoverResource(resourceName_o, "other_resource_name", resourceRecovery_o, "other_resource", resourceMax_o);

            i = 0;
            do {
                if (resourceName_l[i]) {
                    recoverResource(resourceName_l[i], 'repeating_resource_$' + i + '_resource_left_name', resourceRecovery_l[i], 'repeating_resource_$' + i + '_resource_left', resourceMax_l[i]);

                }
                i++;
            }
            while (resourceName_l[i]);

            i = 0;
            do {
                if (resourceName_r[i]) {
                    recoverResource(resourceName_r[i], 'repeating_resource_$' + i + '_resource_right_name', resourceRecovery_r[i], 'repeating_resource_$' + i + '_resource_right', resourceMax_r[i]);
                }
                i++;
            }
            while (resourceName_r[i]);

            if (msg.content.includes("help")) {
                sendMessage('
Rest and Recovery

A Roll20 API script to handle recovery on the resource attributes on the D&D 5th Edition by Roll20 sheet.To use this script, resources must include a code in their name, separated from the name by a plus sign. You can include standard dice expressions as well. "1d6" is used in all examples, but you can do 2d6+3, 3d20, etc. Here are examples of the commands given and the codes that are affected.
!r-short
Used for Short Rest

+SR This resource will return to its maximumm value

+SR1d6 This resource will add 1d6 to the resource up to its maximum value
!r-long
Used for Long Rest

+LR This resource will return to its maximumm value

+LR1d6 This resource will add 1d6 to the resource up to its maximum value
!r-Charges
*used for restoring charges that are user-controlled, such as "at dawn" or "under a full moon".

+1d6
!r-Ammo
no code is used here. The script looks for common ammo types: Crossbow bolts, Arrows, Bullets, etc. It rolls 1d2 for each piece of ammo expended. If the result is a "2", the ammo is recovered. The max and current values are adjusted to reflect the new total.
Special Cases
Finally, the following special cases exist. Class Resources that have any of the following names are recognized and handled appropriately:
These are recovered on a Short or Long Rest:

Spell Slots, Warlock Spell Slots

Channel Divinity

Wild Shape

Superiority Dice

Ki Points, Ki
These are recovered on a Long Rest.

Rages

Lay on Hands

Sorcery Points

Bardic Inspiration needs a +SR or +LR code, since the recovery rate changes at fifth level
');
            } else {
             if (msg.content.includes("list")) {
                sendMessage('
Rest and Recovery

Short Rest Class Resources
'+shortRestList.join('

')+'

Long Rest Class Resources
'+longRestList.join('

')+'

Ammo Types
'+ammoList.join('

')+'
');
            } else {
                if (rReport) {
                    sendMessage(rReport);
                } else {
                    sendMessage('No resources were changed.');
}
                }
                log(rReport);
            }

        }
    });
});
<|endoftext|>


<commit_before>var args = arguments[0] || {},
	enabled = true;

// custom properties that can/should be set in the TSS of the view where you're putting the tabbed bar
var selectedButtonColor = args.selectedButtonColor || "#d9bc1b",
	unselectedButtonColor = args.unselectedButtonColor || "#226e92",
	selectedButtonTextColor = args.selectedButtonTextColor || "#fff",
	unselectedButtonTextColor = args.unselectedButtonTextColor || "#000",
	disabledTextColor = args.disabledTextColor || '#aaa',
	disabledButtonBackgroundColor = args.disabledButtonBackgroundColor || '#444',
	borderColor = args.borderColor || 'transparent',
	font = args.font || (OS_IOS ? {fontFamily: 'Avenir-Light', fontSize: 11} : {fontWeight: 'normal', fontSize: '15dp'});

args.borderColor = borderColor; // stuff this back in there in case it's not set in the tss

$.segCtrlWrapper.applyProperties(_.omit(args, 'id', '__parentSymbol', '__itemTemplate', '$model', 'selectedButtonColor', 'unselectedButtonColor', 'selectedButtonTextColor', 'unselectedButtonTextColor', 'font'));

var height = ((isNaN(parseInt($.segCtrlWrapper.height))) ? 40 : parseInt($.segCtrlWrapper.height)) - 2;
if (OS_ANDROID) {height += 'dp';}
$.segCtrlButtonContainer.height = Ti.UI.FILL;

var callback = function () {}; // empty function as placeholder

var buttons = [];
exports.init = function (labels, cb) {
	var wrapperWidthIsCalculated = false,
		calculatedWidth;
	if (typeof cb === 'function') {
		callback = cb;
	}
	if (!labels || !_.isArray(labels) || labels.length === 0) {
		labels = ['Yes', 'No'];
	}
	// calculate button width
	if (OS_ANDROID) {
		if ($.segCtrlWrapper.width.slice(-1) === '%') {
			calculatedWidth = Ti.Platform.displayCaps.platformWidth / Ti.Platform.displayCaps.logicalDensityFactor * parseInt($.segCtrlWrapper.width)/100;
		} else if (isNaN(parseInt($.segCtrlWrapper.width))) {
			calculatedWidth = Ti.Platform.displayCaps.platformWidth / Ti.Platform.displayCaps.logicalDensityFactor;
			wrapperWidthIsCalculated = true;
		}
	} else if (OS_IOS) {
		if ($.segCtrlWrapper.width.slice(-1) === '%') {
			calculatedWidth = Ti.Platform.displayCaps.platformWidth * parseInt($.segCtrlWrapper.width)/100;
			console.log('Calculated the width of ' + args.id + ' to be ' + calculatedWidth);
		} else if (isNaN(parseInt($.segCtrlWrapper.width))) {
			// iOS handles rotation, but the wrapper width needs to be calculated to be
			// the smaller of the height or width to avoid layout issues
			if (Ti.Platform.displayCaps.platformWidth < Ti.Platform.displayCaps.platformHeight) {
				calculatedWidth = Ti.Platform.displayCaps.platformWidth;
			} else {
				calculatedWidth = Ti.Platform.displayCaps.platformHeight;
			}
		}
	}

	var btnWidth = calculatedWidth / labels.length;
	if (OS_ANDROID) {btnWidth += 'dp';}

	// make our buttons
	for (var i = 0, j = labels.length; i < j; i++) {
		if (!wrapperWidthIsCalculated && i === j-1 && !isIphone6splus()) {
			// if an explicit width has been set, we need to shrink the last button
			// by 1 or it will be too wide for the container and won't be shown
			btnWidth = (parseInt(btnWidth) - 1);
			if (OS_ANDROID) {
				btnWidth += 'dp';
			}
		}
		var btn = Widget.createController('button', { // jshint ignore:line
			text: labels[i],
			width: btnWidth,
			height: Ti.UI.FILL,
			left: 0,
			top: 0,
			bottom: 0,
			color: unselectedButtonTextColor,
			backgroundColor: unselectedButtonColor,
			font: font
		}).getView();
		if (args.index === i) {
			_highlight(btn);
		}
		buttons.push(btn);
		$.segCtrlButtonContainer.add(btn);
	}

	// add the button dividers, if desired
	if(args.withDividers) {
		for (var i = 0, j = labels.length-1; i < j; i++) { // jshint ignore:line
			$.segCtrlWrapper.add(Ti.UI.createView({
				width: OS_ANDROID ? '1dp' : 1,
				height: height,
				left: OS_ANDROID ? ((parseInt(btnWidth) * (i+1) + 1) + 'dp') : (btnWidth * (i+1) + (!isIphone6splus() ? 1 : -1)),
				backgroundColor: selectedButtonColor,
				zIndex: 10
			}));
		}
	}

	// event listener on the wrapper determines button clicked by x coord of click location
	$.segCtrlWrapper.addEventListener('click', function (e) {
		var clickedButton,
			butWid;
		var point = e.source.convertPointToView({
			x: e.x,
			y: e.y
		}, $.segCtrlWrapper);
		if (OS_ANDROID) {
			butWid = parseFloat(btnWidth.replace('dp', '')) * Ti.Platform.displayCaps.logicalDensityFactor;
			clickedButton = Math.floor(point.x / butWid);
		} else {
			butWid = btnWidth;
			clickedButton = Math.floor(point.x / butWid);
		}
		if (buttons[clickedButton].disabled) {
			return;
		}
		_.each(buttons, function (element, index) {
			if (enabled) {
				if (index === clickedButton) {
					_highlight(element);
				} else {
					_unhighlight(element);
				}
			}
		});
		callback({
			index: clickedButton,
			source: {
				id: args.id || undefined
			}
		});
	});
};

function _highlight(btn) {
	if (btn) {
		btn.backgroundColor = selectedButtonColor;
		btn.color = selectedButtonTextColor;
	}
}

function _unhighlight(btn) {
	if (!btn.disabled) {
		btn.backgroundColor = unselectedButtonColor;
		btn.color = unselectedButtonTextColor;
	}
}

exports.select = function (num) {
	var btnNumber = parseInt(num) || 0;
	_highlight(buttons[btnNumber]);
};
exports.setIndex = function (num) {
	var btnNumber = parseInt(num) || 0;
	exports.deselectAll();
	_highlight(buttons[btnNumber]);
};
exports.deselect = function (num) {
	var btnNumber = parseInt(num) || 0;
	_unhighlight(buttons[btnNumber]);
};
exports.enable = function () {
	enabled = true;
};
exports.disable = function () {
	enabled = false;
};
exports.deselectAll = function () {
	_.each(buttons, _unhighlight);
};

exports.changeButtonLabels = function (arr) {
	if (!arr || !arr.length || arr.length !== buttons.length) {
		throw "You must pass an array with " + buttons.length + " members to this function";
	}
	for (var b = 0, c = buttons.length; b < c; b++) {
		buttons[b].text = arr[b];
	}
};

/*
Public function to disable a button (make it non-clickable)
*/
exports.disableButton = function (num) {
	if (typeof num !== 'number' || !buttons[num]) {
		return;
	}
	// turn it semi-transparent
	// disable the click handler
	_unhighlight(buttons[num]);
	buttons[num].opacity = 0.4;
	buttons[num].color = disabledTextColor;
	buttons[num].backgroundColor = disabledButtonBackgroundColor;
	buttons[num].disabled = true;
};
exports.disableAllButtons = function () {
	for (var i = 0, j = buttons.length; i < j; i++) {
		exports.disableButton(i);
	}
};
/*
Public function to enable a button (make it clickable)
*/
exports.enableButton = function (num) {
	// turn it fully opaque
	// enable the click handler
	if (typeof num !== 'number' || !buttons[num]) {
		return;
	}
	_unhighlight(buttons[num]);
	buttons[num].opacity = 1;
	buttons[num].color = unselectedButtonTextColor;
	buttons[num].disabled = false;
};
exports.enableAllButtons = function () {
	for (var i = 0, j = buttons.length; i < j; i++) {
		exports.enableButton(i);
	}
};

function isIphone6splus() {
	if (OS_ANDROID) {
		return false;
	}
	if (Ti.Gesture.isPortrait()) {
		return Ti.Platform.displayCaps.platformWidth >= 414;
	} else if (Ti.Gesture.isLandscape()) {
		return Ti.Platform.displayCaps.platformHeight >= 414;
	}
	return false;
}<commit_msg>Fixed Issue #5<commit_after>var args = arguments[0] || {},
	enabled = true;

// custom properties that can/should be set in the TSS of the view where you're putting the tabbed bar
var selectedButtonColor = args.selectedButtonColor || "#d9bc1b",
	unselectedButtonColor = args.unselectedButtonColor || "#226e92",
	selectedButtonTextColor = args.selectedButtonTextColor || "#fff",
	unselectedButtonTextColor = args.unselectedButtonTextColor || "#000",
	disabledTextColor = args.disabledTextColor || '#aaa',
	disabledButtonBackgroundColor = args.disabledButtonBackgroundColor || '#444',
	borderColor = args.borderColor || 'transparent',
	font = args.font || (OS_IOS ? {fontFamily: 'Avenir-Light', fontSize: 11} : {fontWeight: 'normal', fontSize: '15dp'});

args.borderColor = borderColor; // stuff this back in there in case it's not set in the tss

$.segCtrlWrapper.applyProperties(_.omit(args, 'id', '__parentSymbol', '__itemTemplate', '$model', 'selectedButtonColor', 'unselectedButtonColor', 'selectedButtonTextColor', 'unselectedButtonTextColor', 'font'));

var height = ((isNaN(parseInt($.segCtrlWrapper.height))) ? 40 : parseInt($.segCtrlWrapper.height)) - 2;
if (OS_ANDROID) {height += 'dp';}
$.segCtrlButtonContainer.height = Ti.UI.FILL;

var callback = function () {}; // empty function as placeholder

var buttons = [];
exports.init = function (labels, cb) {
	var wrapperWidthIsCalculated = false,
		calculatedWidth;
	if (typeof cb === 'function') {
		callback = cb;
	}
	if (!labels || !_.isArray(labels) || labels.length === 0) {
		labels = ['Yes', 'No'];
	}
	// calculate button width
	if (OS_ANDROID) {
		if ($.segCtrlWrapper.width.slice(-1) === '%') {
			calculatedWidth = Ti.Platform.displayCaps.platformWidth / Ti.Platform.displayCaps.logicalDensityFactor * parseInt($.segCtrlWrapper.width)/100;
		} else if (isNaN(parseInt($.segCtrlWrapper.width))) {
			calculatedWidth = Ti.Platform.displayCaps.platformWidth / Ti.Platform.displayCaps.logicalDensityFactor;
			wrapperWidthIsCalculated = true;
		}
	} else if (OS_IOS) {
		if ($.segCtrlWrapper.width.slice(-1) === '%') {
			calculatedWidth = Ti.Platform.displayCaps.platformWidth * parseInt($.segCtrlWrapper.width)/100;
			console.log('Calculated the width of ' + args.id + ' to be ' + calculatedWidth);
		} else if (isNaN(parseInt($.segCtrlWrapper.width))) {
			// iOS handles rotation, but the wrapper width needs to be calculated to be
			// the smaller of the height or width to avoid layout issues
			if (Ti.Platform.displayCaps.platformWidth < Ti.Platform.displayCaps.platformHeight) {
				calculatedWidth = Ti.Platform.displayCaps.platformWidth;
			} else {
				calculatedWidth = Ti.Platform.displayCaps.platformHeight;
			}
		}
	}

	var btnWidth = calculatedWidth / labels.length;
	if (OS_ANDROID) {btnWidth += 'dp';}

	// make our buttons
	for (var i = 0, j = labels.length; i < j; i++) {
		if (!wrapperWidthIsCalculated && i === j-1 && !isIphone6splus()) {
			// if an explicit width has been set, we need to shrink the last button
			// by 1 or it will be too wide for the container and won't be shown
			btnWidth = (parseInt(btnWidth) - 1);
			if (OS_ANDROID) {
				btnWidth += 'dp';
			}
		}
		var btn = Widget.createController('button', { // jshint ignore:line
			text: labels[i],
			width: btnWidth,
			height: Ti.UI.FILL,
			left: 0,
			top: 0,
			bottom: 0,
			color: unselectedButtonTextColor,
			backgroundColor: unselectedButtonColor,
			font: font
		}).getView();
		if (args.index === i) {
			_highlight(btn);
		}
		buttons.push(btn);
		$.segCtrlButtonContainer.add(btn);
	}

	// add the button dividers, if desired
	if(args.withDividers) {
		for (var i = 0, j = labels.length-1; i < j; i++) { // jshint ignore:line
			$.segCtrlWrapper.add(Ti.UI.createView({
				width: OS_ANDROID ? '1dp' : 1,
				height: height,
				left: OS_ANDROID ? ((parseInt(btnWidth) * (i+1) + 1) + 'dp') : (btnWidth * (i+1) + (!isIphone6splus() ? 1 : -1)),
				backgroundColor: selectedButtonColor,
				zIndex: 10
			}));
		}
	}

	// event listener on the wrapper determines button clicked by x coord of click location
	$.segCtrlWrapper.addEventListener('click', function (e) {
		var clickedButton,
			butWid;
		var point = e.source.convertPointToView({
			x: e.x,
			y: e.y
		}, $.segCtrlWrapper);
		if (OS_ANDROID) {
			butWid = parseFloat(btnWidth.replace('dp', '')) * Ti.Platform.displayCaps.logicalDensityFactor;
			clickedButton = Math.floor(point.x / butWid);
		} else {
			butWid = btnWidth;
			clickedButton = Math.floor(point.x / butWid);
		}
		if (buttons[clickedButton].disabled) {
			return;
		}
		_.each(buttons, function (element, index) {
			if (enabled) {
				if (index === clickedButton) {
					_highlight(element);
				} else {
					_unhighlight(element);
				}
			}
		});
		callback({
			index: clickedButton,
			source: {
				id: args.id || undefined
			}
		});
	});
};

function _highlight(btn) {
	if (btn) {
		btn.backgroundColor = selectedButtonColor;
		btn.color = selectedButtonTextColor;
	}
}

function _unhighlight(btn) {
	if (!btn.disabled) {
		btn.backgroundColor = unselectedButtonColor;
		btn.color = unselectedButtonTextColor;
	}
}

exports.select = function (num) {
	var btnNumber = parseInt(num) || 0;
	_highlight(buttons[btnNumber]);
};
exports.setIndex = function (num) {
	var btnNumber = parseInt(num) || 0;
	exports.deselectAll();
	_highlight(buttons[btnNumber]);
};
exports.deselect = function (num) {
	var btnNumber = parseInt(num) || 0;
	_unhighlight(buttons[btnNumber]);
};
exports.enable = function () {
	enabled = true;
};
exports.disable = function () {
	enabled = false;
};
exports.deselectAll = function () {
	_.each(buttons, _unhighlight);
};

exports.changeButtonLabels = function (arr) {
	if (!arr || !arr.length || arr.length !== buttons.length) {
		throw "You must pass an array with " + buttons.length + " members to this function";
	}
	for (var b = 0, c = buttons.length; b < c; b++) {
		buttons[b].text = arr[b];
	}
};

/*
Public function to disable a button (make it non-clickable)
*/
exports.disableButton = function (num) {
	if (typeof num !== 'number' || !buttons[num]) {
		return;
	}
	// turn it semi-transparent
	// disable the click handler
	_unhighlight(buttons[num]);
	buttons[num].opacity = 0.4;
	buttons[num].color = disabledTextColor;
	buttons[num].backgroundColor = disabledButtonBackgroundColor;
	buttons[num].disabled = true;
};
exports.disableAllButtons = function () {
	for (var i = 0, j = buttons.length; i < j; i++) {
		exports.disableButton(i);
	}
};
/*
Public function to enable a button (make it clickable)
*/
exports.enableButton = function (num) {
	// turn it fully opaque
	// enable the click handler
	if (typeof num !== 'number' || !buttons[num]) {
		return;
	}
	_unhighlight(buttons[num]);
	buttons[num].opacity = 1;
	buttons[num].color = unselectedButtonTextColor;
	buttons[num].disabled = false;
};
exports.enableAllButtons = function () {
	for (var i = 0, j = buttons.length; i < j; i++) {
		exports.enableButton(i);
	}
};

function isIphone6splus() {
	 if(OS_IOS && parseInt(Ti.Platform.version.split(".")[0]) >= 7){
		 return true;
	 }else{
		return false;
	 }
}
<|endoftext|>


<commit_before>/* (c) Copyright 2002-2013 by authors of the Tiki Wiki CMS Groupware Project
 *
 * All Rights Reserved. See copyright.txt for details and a complete list of authors.
 * Licensed under the GNU LESSER GENERAL PUBLIC LICENSE. See license.txt for details.
 * $Id$
 *
 * Tiki plugin integration plugin.
 *
 * Based on ckeditor flash plugin
 */

(function() {

	var protectTikiPluginsRegexp = /<(?:div|span)[^>]*syntax="([^"]*)"[\s\S]*?end tiki_plugin -->\s*?<\/(?:div|span)>/mig;

	CKEDITOR.plugins.add( 'tikiplugin', {

		init : function(editor) {

			var asplugin = this;

			editor.config.protectedSource.push( protectTikiPluginsRegexp );

			this.command = new CKEDITOR.command( editor , {
				modes: { wysiwyg:1 },
				exec: function(editor, data) {	// odd? elem param disappears from doubleclick
					var sel = editor.getSelection(), r;
					// Selection will be unavailable after context menu shows up - in IE, lock it now.
					if ( CKEDITOR.env.ie ) {
						if (sel) { sel.lock(); }
					}
					if (data === editor) {
						r = sel.getRanges();
						data = r[0].startContainer;
					}
					if (!$(data.$).hasClass("tiki_plugin")) {
						// problem here - wrong element sent in?
						debugger;		// intentionally left in to catch occasional IE edge cases
						return;
					}
					var args = {};
					var str = $('
').html(data.getAttribute("args")).text();	// decode html entities
					var pairs = str.split("&");
					for (var i = 0; i < pairs.length; i++) {
						if (pairs[i].indexOf("=") > -1) {
							var val = pairs[i].substring((pairs[i].indexOf("=") + 1));
							if (val.match(/^".*"$/)) {								// strip off extra quotes
								val = val.substring(1, val.length - 1)
							}
							args[pairs[i].substring(0, pairs[i].indexOf("="))] = val;
						}
					}
					sel.selectElement( data );
					popupPluginForm( editor.name, data.getAttribute("plugin"), 0, '', args, data.getAttribute("body"), '');
				},
				canUndo: false
			});

			editor.addCommand('tikiplugin', this.command);

			$window.find('header').append(
				''
			);

			// If the "menu" plugin is loaded, register the menu items.
			if (editor.addMenuItems) {
                editor.addMenuGroup("tiki", 999);
				editor.addMenuItems( {
					tikiplugin : {
						label : 'Tiki Plugin',
						command : 'tikiplugin',
						group : 'tiki',
						icon : CKEDITOR.config._TikiRoot + 'img/icons/plugin_edit.png'
					}
				});
				// remove the built in image menu item
				editor.removeMenuItem('image');
			}

			editor.on('contentDom', function() {	// re-apply the events etc when wysiwyg mode happens

				$(".tiki_plugin", editor.document.$).each(function() {
					var parentPlugin = new CKEDITOR.dom.element(this);
					$(this).find("*").each(function(){
						$(this).mousedown(function(){
							var sel = editor.getSelection();
							if (sel) {
								sel.selectElement(parentPlugin);
							}
							return false;
						}); //.attr("contenteditable", "false");
					});
				});

				editor.on( 'doubleclick', function(evt) {
					var element = evt.data.element;

					element = asplugin.getPluginParent( element );

					if (element) {
						evt.cancel();
						evt.stop();
						evt.data.dialog = null;
						this.plugins.tikiplugin.command.exec(element);
					}
				});
			});

			// If the "contextmenu" plugin is loaded, register the listeners.
			if (editor.contextMenu) {
				editor.contextMenu.addListener(function(element, selection) {

					element = asplugin.getPluginParent( element );

					if (element) {
						return {
							tikiplugin : CKEDITOR.TRISTATE_OFF
						};
					}
				});
			}
			if (jqueryTiki.autosave) {	// pref check
				if (typeof editor.plugins["tikiwiki"] === "undefined") {	// also defined in tikiwiki plugin
					this.ckToHtml = editor.dataProcessor.toHtml;		// reference to original ckeditor dataProcessor
					editor.dataProcessor.toHtml			= function ( data, fixForBody ) { return asplugin.toHtmlFormat( editor, data, fixForBody ); };
				}
				this.ckToData = editor.dataProcessor.toDataFormat;
				editor.dataProcessor.toDataFormat 	= function ( html, fixForBody ) { return asplugin.toHTMLSource( editor, html, fixForBody ); };
			}
		},			// end of init()

		afterInit : function( editor ) {
			// Register a filter to displaying placeholders after mode change. (taken from pagebreak plugin)

			var asplugin = this;

			var dataProcessor = editor.dataProcessor,
				dataFilter = dataProcessor && dataProcessor.dataFilter;

			if ( dataFilter ) {
				dataFilter.addRules( {
					elements : {
						div : function( element ) {
							return asplugin.processTikiPlugin( element, 'div', editor);
						},
						span : function( element ) {
							return asplugin.processTikiPlugin( element, 'span', editor);
						}
					}
				});
			}
		},

		requires : [ 'fakeobjects' ],

		// variation on CKEDITOR.editor.createFakeParserElement set up by fakeobject plugin
		processTikiPlugin : function ( element, tag, editor ) {
			var name = element.attributes && element.attributes.plugin;

			if (name) {	// a tiki plugin
				var html;

				var writer = new CKEDITOR.htmlParser.basicWriter();
				element.writeHtml( writer );
				html = writer.getHtml();

				element.attributes._cke_realelement = tiki_encodeURIComponent( html );
				element.attributes._cke_real_node_type = tag;

				element.attributes._cke_real_element_type = tag;
				element.attributes.contenteditable = false;

				if (name === 'img') {
					element.attributes._cke_resizable = true;
				} else {
					element.attributes._cke_resizable = false;
				}

				// Set onclick for the contents
				for ( var i = 0 ; i < element.children.length ; i++ ) {
					if ( element.children[ i ].attributes ) {
						element.children[ i ].attributes.contenteditable = false;
					} else {
						// seems to make the whole editor readonly
						//element.children[ i ].attributes = { contenteditable: false};
					}
				}

				return element;
			}
			return null;
		},

		getPluginParent: function ( element ) {
			while (element && element.getParent()) {
				if (element.getAttribute('plugin')) {
					return element;
				}
				element = element.getParent();
			}
			return null;
		},

		toHTMLSource: function( editor, html, fixForBody ) {
			// de-protect ck_protected comments
			var output = html;

			output = this.ckToData.call( editor.dataProcessor, output, fixForBody );

			if (typeof editor.plugins["tikiwiki"] === "undefined") {
				output = output.replace(/
([\s\S]*?)<\/pre>/ig, "$1");
			}
			output = output.replace(//ig, "");
			// replace visual plugins with syntax
			output = output.replace( protectTikiPluginsRegexp, function() {
				if (arguments.length > 0) {
					var plugCode = $("").html(arguments[1]).text();
					if (typeof editor.plugins["tikiwiki"] !== "undefined") {	// also defined in tikiwiki plugin
						plugCode = plugCode.replace(/\n/g, "
");
					}
					return plugCode;	// decode html entities
				} else {
					alert("ckeditor: error parsing to html source");
					return "";
				}
			});
			output = output.replace(/\s*
[\s]*<\/p>\s*/mgi, "");	// strip out empty
 tags

			return output;
		},

		toHtmlFormat: function( editor, data, fixForBody ) {
			if (!jqueryTiki.autosave) {
				alert(tr("AJAX Autosave preference not enabled. Please go to admin/features/interface to enable it."));
			}

			if (typeof editor.pluginReparsing !== "undefined" && editor.pluginReparsing) {
				editor.pluginReparsing = false;
				return data;
			}

			var output = "";
			var asplugin = this;
			var orig_data = $("#editwiki").val();	// just in case
			var isHtml = $("#allowhtml:checked").length || $("#allowhtml[type=hidden]").val();

			ajaxLoadingShow( "cke_contents_" + editor.name);
			$("#ajaxLoading").show();		// FIXME safari/chrome refuse to show until ajax finished
			jQuery.ajax({
				async: false, // wait for this one
				url: CKEDITOR.config.ajaxAutoSaveTargetUrl,
				type: "POST",
				data: {
					referer: editor.config.autoSaveSelf,
					editor_id: editor.name,
					data: tiki_encodeURIComponent(data),
					command: "toHtmlFormat",
					allowhtml: isHtml
				},
				// good callback
				success: function(data) {
					ajaxLoadingHide();
					try {
					    output = tiki_decodeURIComponent(jQuery(data).find('data').text());
					} catch (err) {
					    // Maybe it used escape to encode it? Try unescape
					    output = unescape(jQuery(data).find('data').text());
					}
					output = asplugin.ckToHtml.call(editor.dataProcessor, output, fixForBody);
					if (output.indexOf("") === -1) {
						output += ""; 	// workaround for cke 3.4 / tiki 6.0 FIXME
					}
				},
				// bad callback - no good info in the params :(
				error: function(req, status, error) {
					ajaxLoadingHide();
					output = orig_data;	//"ajax error";
					alert(tr("AJAX Error") + "\n" + tr("It may not be possible to edit this page reliably in WYSIWYG mode in this browser.") + "\n\n" + error.message);
				}
			});
			return output;
		},

		requires : [ '' ]	// TODO check req - autosave? (really)
	});

	if (typeof CKEDITOR.editor.prototype.reParse != 'function') {
		CKEDITOR.editor.prototype.pluginReparsing = false;

		CKEDITOR.editor.prototype.reParse = function() {

			// send the whole source off to the server to get reparsed?
			var myoutput = "";
			var mydata = this.getData();
			myoutput = this.dataProcessor.toHtml(mydata);

			this.pluginReparsing = true;
			this.setData(myoutput);
		};
	}
})();
<commit_msg>[FIX] wysiwyg: In Firefox, context menu item was finding the wrong element, so loop through parents until we find a tiki_plugin (thanks nkoth)<commit_after>/* (c) Copyright 2002-2013 by authors of the Tiki Wiki CMS Groupware Project
 *
 * All Rights Reserved. See copyright.txt for details and a complete list of authors.
 * Licensed under the GNU LESSER GENERAL PUBLIC LICENSE. See license.txt for details.
 * $Id$
 *
 * Tiki plugin integration plugin.
 *
 * Based on ckeditor flash plugin
 */

(function() {

	var protectTikiPluginsRegexp = /<(?:div|span)[^>]*syntax="([^"]*)"[\s\S]*?end tiki_plugin -->\s*?<\/(?:div|span)>/mig;

	CKEDITOR.plugins.add( 'tikiplugin', {

		init : function(editor) {

			var asplugin = this;

			editor.config.protectedSource.push( protectTikiPluginsRegexp );

			this.command = new CKEDITOR.command( editor , {
				modes: { wysiwyg:1 },
				exec: function(editor, data) {	// odd? elem param disappears from doubleclick
					var sel = editor.getSelection(), r;
					// Selection will be unavailable after context menu shows up - in IE, lock it now.
					if ( CKEDITOR.env.ie ) {
						if (sel) { sel.lock(); }
					}
					if (data === editor) {
						r = sel.getRanges();
						data = r[0].startContainer;
					}
					while (!$(data.$).hasClass("tiki_plugin") && data.$.parentElement) {	// try parents?
						data = new CKEDITOR.dom.element(data.$.parentElement);
						if ($(data.$).hasClass("tiki_plugin")) {
							break;
						}
					}
					if (!$(data.$).hasClass("tiki_plugin")) {
						// problem here - wrong element sent in?
						debugger;		// intentionally left in to catch occasional IE edge cases
						return;
					}
					var args = {};
					var str = $('
').html(data.getAttribute("args")).text();	// decode html entities
					var pairs = str.split("&");
					for (var i = 0; i < pairs.length; i++) {
						if (pairs[i].indexOf("=") > -1) {
							var val = pairs[i].substring((pairs[i].indexOf("=") + 1));
							if (val.match(/^".*"$/)) {								// strip off extra quotes
								val = val.substring(1, val.length - 1)
							}
							args[pairs[i].substring(0, pairs[i].indexOf("="))] = val;
						}
					}
					sel.selectElement( data );
					popupPluginForm( editor.name, data.getAttribute("plugin"), 0, '', args, data.getAttribute("body"), '');
				},
				canUndo: false
			});

			editor.addCommand('tikiplugin', this.command);

			$window.find('header').append(
				''
			);

			// If the "menu" plugin is loaded, register the menu items.
			if (editor.addMenuItems) {
                editor.addMenuGroup("tiki", 999);
				editor.addMenuItems( {
					tikiplugin : {
						label : 'Tiki Plugin',
						command : 'tikiplugin',
						group : 'tiki',
						icon : CKEDITOR.config._TikiRoot + 'img/icons/plugin_edit.png'
					}
				});
				// remove the built in image menu item
				editor.removeMenuItem('image');
			}

			editor.on('contentDom', function() {	// re-apply the events etc when wysiwyg mode happens

				$(".tiki_plugin", editor.document.$).each(function() {
					var parentPlugin = new CKEDITOR.dom.element(this);
					$(this).find("*").each(function(){
						$(this).mousedown(function(){
							var sel = editor.getSelection();
							if (sel) {
								sel.selectElement(parentPlugin);
							}
							return false;
						}); //.attr("contenteditable", "false");
					});
				});

				editor.on( 'doubleclick', function(evt) {
					var element = evt.data.element;

					element = asplugin.getPluginParent( element );

					if (element) {
						evt.cancel();
						evt.stop();
						evt.data.dialog = null;
						this.plugins.tikiplugin.command.exec(element);
					}
				});
			});

			// If the "contextmenu" plugin is loaded, register the listeners.
			if (editor.contextMenu) {
				editor.contextMenu.addListener(function(element, selection) {

					element = asplugin.getPluginParent( element );

					if (element) {
						return {
							tikiplugin : CKEDITOR.TRISTATE_OFF
						};
					}
				});
			}
			if (jqueryTiki.autosave) {	// pref check
				if (typeof editor.plugins["tikiwiki"] === "undefined") {	// also defined in tikiwiki plugin
					this.ckToHtml = editor.dataProcessor.toHtml;		// reference to original ckeditor dataProcessor
					editor.dataProcessor.toHtml			= function ( data, fixForBody ) { return asplugin.toHtmlFormat( editor, data, fixForBody ); };
				}
				this.ckToData = editor.dataProcessor.toDataFormat;
				editor.dataProcessor.toDataFormat 	= function ( html, fixForBody ) { return asplugin.toHTMLSource( editor, html, fixForBody ); };
			}
		},			// end of init()

		afterInit : function( editor ) {
			// Register a filter to displaying placeholders after mode change. (taken from pagebreak plugin)

			var asplugin = this;

			var dataProcessor = editor.dataProcessor,
				dataFilter = dataProcessor && dataProcessor.dataFilter;

			if ( dataFilter ) {
				dataFilter.addRules( {
					elements : {
						div : function( element ) {
							return asplugin.processTikiPlugin( element, 'div', editor);
						},
						span : function( element ) {
							return asplugin.processTikiPlugin( element, 'span', editor);
						}
					}
				});
			}
		},

		requires : [ 'fakeobjects' ],

		// variation on CKEDITOR.editor.createFakeParserElement set up by fakeobject plugin
		processTikiPlugin : function ( element, tag, editor ) {
			var name = element.attributes && element.attributes.plugin;

			if (name) {	// a tiki plugin
				var html;

				var writer = new CKEDITOR.htmlParser.basicWriter();
				element.writeHtml( writer );
				html = writer.getHtml();

				element.attributes._cke_realelement = tiki_encodeURIComponent( html );
				element.attributes._cke_real_node_type = tag;

				element.attributes._cke_real_element_type = tag;
				element.attributes.contenteditable = false;

				if (name === 'img') {
					element.attributes._cke_resizable = true;
				} else {
					element.attributes._cke_resizable = false;
				}

				// Set onclick for the contents
				for ( var i = 0 ; i < element.children.length ; i++ ) {
					if ( element.children[ i ].attributes ) {
						element.children[ i ].attributes.contenteditable = false;
					} else {
						// seems to make the whole editor readonly
						//element.children[ i ].attributes = { contenteditable: false};
					}
				}

				return element;
			}
			return null;
		},

		getPluginParent: function ( element ) {
			while (element && element.getParent()) {
				if (element.getAttribute('plugin')) {
					return element;
				}
				element = element.getParent();
			}
			return null;
		},

		toHTMLSource: function( editor, html, fixForBody ) {
			// de-protect ck_protected comments
			var output = html;

			output = this.ckToData.call( editor.dataProcessor, output, fixForBody );

			if (typeof editor.plugins["tikiwiki"] === "undefined") {
				output = output.replace(/
([\s\S]*?)<\/pre>/ig, "$1");
			}
			output = output.replace(//ig, "");
			// replace visual plugins with syntax
			output = output.replace( protectTikiPluginsRegexp, function() {
				if (arguments.length > 0) {
					var plugCode = $("").html(arguments[1]).text();
					if (typeof editor.plugins["tikiwiki"] !== "undefined") {	// also defined in tikiwiki plugin
						plugCode = plugCode.replace(/\n/g, "
");
					}
					return plugCode;	// decode html entities
				} else {
					alert("ckeditor: error parsing to html source");
					return "";
				}
			});
			output = output.replace(/\s*
[\s]*<\/p>\s*/mgi, "");	// strip out empty
 tags

			return output;
		},

		toHtmlFormat: function( editor, data, fixForBody ) {
			if (!jqueryTiki.autosave) {
				alert(tr("AJAX Autosave preference not enabled. Please go to admin/features/interface to enable it."));
			}

			if (typeof editor.pluginReparsing !== "undefined" && editor.pluginReparsing) {
				editor.pluginReparsing = false;
				return data;
			}

			var output = "";
			var asplugin = this;
			var orig_data = $("#editwiki").val();	// just in case
			var isHtml = $("#allowhtml:checked").length || $("#allowhtml[type=hidden]").val();

			ajaxLoadingShow( "cke_contents_" + editor.name);
			$("#ajaxLoading").show();		// FIXME safari/chrome refuse to show until ajax finished
			jQuery.ajax({
				async: false, // wait for this one
				url: CKEDITOR.config.ajaxAutoSaveTargetUrl,
				type: "POST",
				data: {
					referer: editor.config.autoSaveSelf,
					editor_id: editor.name,
					data: tiki_encodeURIComponent(data),
					command: "toHtmlFormat",
					allowhtml: isHtml
				},
				// good callback
				success: function(data) {
					ajaxLoadingHide();
					try {
					    output = tiki_decodeURIComponent(jQuery(data).find('data').text());
					} catch (err) {
					    // Maybe it used escape to encode it? Try unescape
					    output = unescape(jQuery(data).find('data').text());
					}
					output = asplugin.ckToHtml.call(editor.dataProcessor, output, fixForBody);
					if (output.indexOf("") === -1) {
						output += ""; 	// workaround for cke 3.4 / tiki 6.0 FIXME
					}
				},
				// bad callback - no good info in the params :(
				error: function(req, status, error) {
					ajaxLoadingHide();
					output = orig_data;	//"ajax error";
					alert(tr("AJAX Error") + "\n" + tr("It may not be possible to edit this page reliably in WYSIWYG mode in this browser.") + "\n\n" + error.message);
				}
			});
			return output;
		},

		requires : [ '' ]	// TODO check req - autosave? (really)
	});

	if (typeof CKEDITOR.editor.prototype.reParse != 'function') {
		CKEDITOR.editor.prototype.pluginReparsing = false;

		CKEDITOR.editor.prototype.reParse = function() {

			// send the whole source off to the server to get reparsed?
			var myoutput = "";
			var mydata = this.getData();
			myoutput = this.dataProcessor.toHtml(mydata);

			this.pluginReparsing = true;
			this.setData(myoutput);
		};
	}
})();
<|endoftext|>


<commit_before>import React from 'react';
import PropTypes from 'prop-types';
import path from 'path';

import {autobind, equalSets} from '../helpers';
import {addEvent} from '../reporter-proxy';
import {MultiFilePatchPropType} from '../prop-types';
import ChangedFileItem from '../items/changed-file-item';
import MultiFilePatchView from '../views/multi-file-patch-view';

export default class MultiFilePatchController extends React.Component {
  static propTypes = {
    repository: PropTypes.object.isRequired,
    stagingStatus: PropTypes.oneOf(['staged', 'unstaged']),
    multiFilePatch: MultiFilePatchPropType.isRequired,
    hasUndoHistory: PropTypes.bool,

    workspace: PropTypes.object.isRequired,
    commands: PropTypes.object.isRequired,
    keymaps: PropTypes.object.isRequired,
    tooltips: PropTypes.object.isRequired,
    config: PropTypes.object.isRequired,

    destroy: PropTypes.func.isRequired,
    discardLines: PropTypes.func,
    undoLastDiscard: PropTypes.func,
    surfaceFileAtPath: PropTypes.func,
    handleClick: PropTypes.func,
  }

  constructor(props) {
    super(props);
    autobind(
      this,
      'selectedRowsChanged',
      'undoLastDiscard', 'diveIntoMirrorPatch', 'surfaceFile', 'openFile',
      'toggleFile', 'toggleRows', 'toggleModeChange', 'toggleSymlinkChange', 'discardRows',
    );

    this.state = {
      lastMultiFilePatch: this.props.multiFilePatch,
      selectionMode: 'hunk',
      selectedRows: new Set(),
      hasMultipleFileSelections: false,
    };

    this.mouseSelectionInProgress = false;
    this.stagingOperationInProgress = false;

    this.patchChangePromise = new Promise(resolve => {
      this.resolvePatchChangePromise = resolve;
    });
  }

  componentDidUpdate(prevProps) {
    if (prevProps.multiFilePatch !== this.props.multiFilePatch) {
      this.resolvePatchChangePromise();
      this.patchChangePromise = new Promise(resolve => {
        this.resolvePatchChangePromise = resolve;
      });
    }
  }

  render() {
    return (
      <MultiFilePatchView
        {...this.props}

        selectedRows={this.state.selectedRows}
        selectionMode={this.state.selectionMode}
        hasMultipleFileSelections={this.state.hasMultipleFileSelections}
        selectedRowsChanged={this.selectedRowsChanged}

        diveIntoMirrorPatch={this.diveIntoMirrorPatch}
        surfaceFile={this.surfaceFile}
        openFile={this.openFile}
        toggleFile={this.toggleFile}
        toggleRows={this.toggleRows}
        toggleModeChange={this.toggleModeChange}
        toggleSymlinkChange={this.toggleSymlinkChange}
        undoLastDiscard={this.undoLastDiscard}
        discardRows={this.discardRows}
        selectNextHunk={this.selectNextHunk}
        selectPreviousHunk={this.selectPreviousHunk}
      />
    );
  }

  undoLastDiscard(filePatch, {eventSource} = {}) {
    addEvent('undo-last-discard', {
      package: 'github',
      component: this.constructor.name,
      eventSource,
    });

    return this.props.undoLastDiscard(filePatch.getPath(), this.props.repository);
  }

  diveIntoMirrorPatch(filePatch) {
    const mirrorStatus = this.withStagingStatus({staged: 'unstaged', unstaged: 'staged'});
    const workingDirectory = this.props.repository.getWorkingDirectoryPath();
    const uri = ChangedFileItem.buildURI(filePatch.getPath(), workingDirectory, mirrorStatus);

    this.props.destroy();
    return this.props.workspace.open(uri);
  }

  surfaceFile(filePatch) {
    return this.props.surfaceFileAtPath(filePatch.getPath(), this.props.stagingStatus);
  }

  async openFile(filePatch, positions) {
    const absolutePath = path.join(this.props.repository.getWorkingDirectoryPath(), filePatch.getPath());
    const editor = await this.props.workspace.open(absolutePath, {pending: true});
    if (positions.length > 0) {
      editor.setCursorBufferPosition(positions[0], {autoscroll: false});
      for (const position of positions.slice(1)) {
        editor.addCursorAtBufferPosition(position);
      }
      editor.scrollToBufferPosition(positions[positions.length - 1], {center: true});
    }
    return editor;
  }

  toggleFile(filePatch) {
    return this.stagingOperation(() => {
      const methodName = this.withStagingStatus({staged: 'unstageFiles', unstaged: 'stageFiles'});
      return this.props.repository[methodName]([filePatch.getPath()]);
    });
  }

  async toggleRows(rowSet, nextSelectionMode) {
    let chosenRows = rowSet;
    if (chosenRows) {
      await this.selectedRowsChanged(chosenRows, nextSelectionMode);
    } else {
      chosenRows = this.state.selectedRows;
    }

    if (chosenRows.size === 0) {
      return Promise.resolve();
    }

    return this.stagingOperation(() => {
      const patch = this.withStagingStatus({
        staged: () => this.props.multiFilePatch.getUnstagePatchForLines(chosenRows),
        unstaged: () => this.props.multiFilePatch.getStagePatchForLines(chosenRows),
      });
      return this.props.repository.applyPatchToIndex(patch);
    });
  }

  toggleModeChange(filePatch) {
    return this.stagingOperation(() => {
      const targetMode = this.withStagingStatus({
        unstaged: filePatch.getNewMode(),
        staged: filePatch.getOldMode(),
      });
      return this.props.repository.stageFileModeChange(filePatch.getPath(), targetMode);
    });
  }

  toggleSymlinkChange(filePatch) {
    return this.stagingOperation(() => {
      const relPath = filePatch.getPath();
      const repository = this.props.repository;
      return this.withStagingStatus({
        unstaged: () => {
          if (filePatch.hasTypechange() && filePatch.getStatus() === 'added') {
            return repository.stageFileSymlinkChange(relPath);
          }

          return repository.stageFiles([relPath]);
        },
        staged: () => {
          if (filePatch.hasTypechange() && filePatch.getStatus() === 'deleted') {
            return repository.stageFileSymlinkChange(relPath);
          }

          return repository.unstageFiles([relPath]);
        },
      });
    });
  }

  async discardRows(rowSet, nextSelectionMode, {eventSource} = {}) {
    // (kuychaco) For now we only support discarding rows for MultiFilePatches that contain a single file patch
    // The only way to access this method from the UI is to be in a ChangedFileItem, which only has a single file patch
    // This check is duplicated in RootController#discardLines. We also want it here to prevent us from sending metrics
    // unnecessarily
    if (this.props.multiFilePatch.getFilePatches().length !== 1) {
      return Promise.resolve(null);
    }

    let chosenRows = rowSet;
    if (chosenRows) {
      await this.selectedRowsChanged(chosenRows, nextSelectionMode);
    } else {
      chosenRows = this.state.selectedRows;
    }

    addEvent('discard-unstaged-changes', {
      package: 'github',
      component: this.constructor.name,
      lineCount: chosenRows.size,
      eventSource,
    });

    return this.props.discardLines(this.props.multiFilePatch, chosenRows, this.props.repository);
  }

  selectedRowsChanged(rows, nextSelectionMode, nextMultipleFileSelections) {
    if (
      equalSets(this.state.selectedRows, rows) &&
      this.state.selectionMode === nextSelectionMode &&
      this.state.hasMultipleFileSelections === nextMultipleFileSelections
    ) {
      return Promise.resolve();
    }

    return new Promise(resolve => {
      this.setState({
        selectedRows: rows,
        selectionMode: nextSelectionMode,
        hasMultipleFileSelections: nextMultipleFileSelections,
      }, resolve);
    });
  }

  withStagingStatus(callbacks) {
    const callback = callbacks[this.props.stagingStatus];
    /* istanbul ignore if */
    if (!callback) {
      throw new Error(`Unknown staging status: ${this.props.stagingStatus}`);
    }
    return callback instanceof Function ? callback() : callback;
  }

  stagingOperation(fn) {
    if (this.stagingOperationInProgress) {
      return null;
    }
    this.stagingOperationInProgress = true;
    this.patchChangePromise.then(() => {
      this.stagingOperationInProgress = false;
    });

    return fn();
  }
}
<commit_msg>Accept a pending argument in openFile action method<commit_after>import React from 'react';
import PropTypes from 'prop-types';
import path from 'path';

import {autobind, equalSets} from '../helpers';
import {addEvent} from '../reporter-proxy';
import {MultiFilePatchPropType} from '../prop-types';
import ChangedFileItem from '../items/changed-file-item';
import MultiFilePatchView from '../views/multi-file-patch-view';

export default class MultiFilePatchController extends React.Component {
  static propTypes = {
    repository: PropTypes.object.isRequired,
    stagingStatus: PropTypes.oneOf(['staged', 'unstaged']),
    multiFilePatch: MultiFilePatchPropType.isRequired,
    hasUndoHistory: PropTypes.bool,

    workspace: PropTypes.object.isRequired,
    commands: PropTypes.object.isRequired,
    keymaps: PropTypes.object.isRequired,
    tooltips: PropTypes.object.isRequired,
    config: PropTypes.object.isRequired,

    destroy: PropTypes.func.isRequired,
    discardLines: PropTypes.func,
    undoLastDiscard: PropTypes.func,
    surfaceFileAtPath: PropTypes.func,
    handleClick: PropTypes.func,
  }

  constructor(props) {
    super(props);
    autobind(
      this,
      'selectedRowsChanged',
      'undoLastDiscard', 'diveIntoMirrorPatch', 'surfaceFile', 'openFile',
      'toggleFile', 'toggleRows', 'toggleModeChange', 'toggleSymlinkChange', 'discardRows',
    );

    this.state = {
      lastMultiFilePatch: this.props.multiFilePatch,
      selectionMode: 'hunk',
      selectedRows: new Set(),
      hasMultipleFileSelections: false,
    };

    this.mouseSelectionInProgress = false;
    this.stagingOperationInProgress = false;

    this.patchChangePromise = new Promise(resolve => {
      this.resolvePatchChangePromise = resolve;
    });
  }

  componentDidUpdate(prevProps) {
    if (prevProps.multiFilePatch !== this.props.multiFilePatch) {
      this.resolvePatchChangePromise();
      this.patchChangePromise = new Promise(resolve => {
        this.resolvePatchChangePromise = resolve;
      });
    }
  }

  render() {
    return (
      <MultiFilePatchView
        {...this.props}

        selectedRows={this.state.selectedRows}
        selectionMode={this.state.selectionMode}
        hasMultipleFileSelections={this.state.hasMultipleFileSelections}
        selectedRowsChanged={this.selectedRowsChanged}

        diveIntoMirrorPatch={this.diveIntoMirrorPatch}
        surfaceFile={this.surfaceFile}
        openFile={this.openFile}
        toggleFile={this.toggleFile}
        toggleRows={this.toggleRows}
        toggleModeChange={this.toggleModeChange}
        toggleSymlinkChange={this.toggleSymlinkChange}
        undoLastDiscard={this.undoLastDiscard}
        discardRows={this.discardRows}
        selectNextHunk={this.selectNextHunk}
        selectPreviousHunk={this.selectPreviousHunk}
      />
    );
  }

  undoLastDiscard(filePatch, {eventSource} = {}) {
    addEvent('undo-last-discard', {
      package: 'github',
      component: this.constructor.name,
      eventSource,
    });

    return this.props.undoLastDiscard(filePatch.getPath(), this.props.repository);
  }

  diveIntoMirrorPatch(filePatch) {
    const mirrorStatus = this.withStagingStatus({staged: 'unstaged', unstaged: 'staged'});
    const workingDirectory = this.props.repository.getWorkingDirectoryPath();
    const uri = ChangedFileItem.buildURI(filePatch.getPath(), workingDirectory, mirrorStatus);

    this.props.destroy();
    return this.props.workspace.open(uri);
  }

  surfaceFile(filePatch) {
    return this.props.surfaceFileAtPath(filePatch.getPath(), this.props.stagingStatus);
  }

  async openFile(filePatch, positions, pending) {
    const absolutePath = path.join(this.props.repository.getWorkingDirectoryPath(), filePatch.getPath());
    const editor = await this.props.workspace.open(absolutePath, {pending});
    if (positions.length > 0) {
      editor.setCursorBufferPosition(positions[0], {autoscroll: false});
      for (const position of positions.slice(1)) {
        editor.addCursorAtBufferPosition(position);
      }
      editor.scrollToBufferPosition(positions[positions.length - 1], {center: true});
    }
    return editor;
  }

  toggleFile(filePatch) {
    return this.stagingOperation(() => {
      const methodName = this.withStagingStatus({staged: 'unstageFiles', unstaged: 'stageFiles'});
      return this.props.repository[methodName]([filePatch.getPath()]);
    });
  }

  async toggleRows(rowSet, nextSelectionMode) {
    let chosenRows = rowSet;
    if (chosenRows) {
      await this.selectedRowsChanged(chosenRows, nextSelectionMode);
    } else {
      chosenRows = this.state.selectedRows;
    }

    if (chosenRows.size === 0) {
      return Promise.resolve();
    }

    return this.stagingOperation(() => {
      const patch = this.withStagingStatus({
        staged: () => this.props.multiFilePatch.getUnstagePatchForLines(chosenRows),
        unstaged: () => this.props.multiFilePatch.getStagePatchForLines(chosenRows),
      });
      return this.props.repository.applyPatchToIndex(patch);
    });
  }

  toggleModeChange(filePatch) {
    return this.stagingOperation(() => {
      const targetMode = this.withStagingStatus({
        unstaged: filePatch.getNewMode(),
        staged: filePatch.getOldMode(),
      });
      return this.props.repository.stageFileModeChange(filePatch.getPath(), targetMode);
    });
  }

  toggleSymlinkChange(filePatch) {
    return this.stagingOperation(() => {
      const relPath = filePatch.getPath();
      const repository = this.props.repository;
      return this.withStagingStatus({
        unstaged: () => {
          if (filePatch.hasTypechange() && filePatch.getStatus() === 'added') {
            return repository.stageFileSymlinkChange(relPath);
          }

          return repository.stageFiles([relPath]);
        },
        staged: () => {
          if (filePatch.hasTypechange() && filePatch.getStatus() === 'deleted') {
            return repository.stageFileSymlinkChange(relPath);
          }

          return repository.unstageFiles([relPath]);
        },
      });
    });
  }

  async discardRows(rowSet, nextSelectionMode, {eventSource} = {}) {
    // (kuychaco) For now we only support discarding rows for MultiFilePatches that contain a single file patch
    // The only way to access this method from the UI is to be in a ChangedFileItem, which only has a single file patch
    // This check is duplicated in RootController#discardLines. We also want it here to prevent us from sending metrics
    // unnecessarily
    if (this.props.multiFilePatch.getFilePatches().length !== 1) {
      return Promise.resolve(null);
    }

    let chosenRows = rowSet;
    if (chosenRows) {
      await this.selectedRowsChanged(chosenRows, nextSelectionMode);
    } else {
      chosenRows = this.state.selectedRows;
    }

    addEvent('discard-unstaged-changes', {
      package: 'github',
      component: this.constructor.name,
      lineCount: chosenRows.size,
      eventSource,
    });

    return this.props.discardLines(this.props.multiFilePatch, chosenRows, this.props.repository);
  }

  selectedRowsChanged(rows, nextSelectionMode, nextMultipleFileSelections) {
    if (
      equalSets(this.state.selectedRows, rows) &&
      this.state.selectionMode === nextSelectionMode &&
      this.state.hasMultipleFileSelections === nextMultipleFileSelections
    ) {
      return Promise.resolve();
    }

    return new Promise(resolve => {
      this.setState({
        selectedRows: rows,
        selectionMode: nextSelectionMode,
        hasMultipleFileSelections: nextMultipleFileSelections,
      }, resolve);
    });
  }

  withStagingStatus(callbacks) {
    const callback = callbacks[this.props.stagingStatus];
    /* istanbul ignore if */
    if (!callback) {
      throw new Error(`Unknown staging status: ${this.props.stagingStatus}`);
    }
    return callback instanceof Function ? callback() : callback;
  }

  stagingOperation(fn) {
    if (this.stagingOperationInProgress) {
      return null;
    }
    this.stagingOperationInProgress = true;
    this.patchChangePromise.then(() => {
      this.stagingOperationInProgress = false;
    });

    return fn();
  }
}
<|endoftext|>


<commit_before>/**
* @license Apache-2.0
*
* Copyright (c) 2018 The Stdlib Authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
*    http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

'use strict';

// MODULES //

var EventEmitter = require( 'events' ).EventEmitter;
var logger = require( 'debug' );
var getKeys = require( 'object-keys' ).shim();
var inherit = require( '@stdlib/utils/inherit' );
var copy = require( '@stdlib/utils/copy' );
var mergeFcn = require( '@stdlib/utils/merge' ).factory;
var setReadOnly = require( '@stdlib/utils/define-read-only-property' );
var isObject = require( '@stdlib/assert/is-plain-object' );
var minstd = require( '@stdlib/random/base/minstd' );
var defaults = require( './defaults.js' );

// VARIABLES //

var debug = logger( 'hist:main' );
var PRIVATE_PROPS = [
	'_autoRender',
	'_autoView',
	'_barColor',
	'_barOpacity',
	'_binCounts',
	'_binMax',
	'_binMethod',
	'_binMin',
	'_binWidth',
	'_description',
	'_edges',
	'_engine',
	'_height',
	'_labels',
	'_lineColor',
	'_lineOpacity',
	'_lineStyle',
	'_lineWidth',
	'_nbins',
	'_normalization',
	'_orientation',
	'_paddingBottom',
	'_paddingLeft',
	'_paddingRight',
	'_paddingTop',
	'_renderFormat',
	'_title',
	'_viewer',
	'_width',
	'_xAxisOrient',
	'_xData',
	'_xLabel',
	'_xMax',
	'_xMin',
	'_xNumTicks',
	'_xScale',
	'_xTickFormat',
	'_yAxisOrient',
	'_yData',
	'_yLabel',
	'_yMax',
	'_yMin',
	'_yNumTicks',
	'_yScale',
	'_yTickFormat'
];

// FUNCTIONS //

var merge = mergeFcn({
	'extend': false
});

// MAIN //

/**
* Histogram constructor.
*
* @constructor
* @param {Array} [x] - values to bin
* @param {Array} [edges] - edges
* @param {Options} [options] - constructor options
* @returns {Histogram} Histogram instance
*
* @example
* TODO
*/
function Histogram() {
	var options;
	var nargs;
	var keys;
	var self;
	var opts;
	var key;
	var i;

	nargs = arguments.length;
	if ( !(this instanceof Histogram) ) {
		if ( nargs === 0 ) {
			return new Histogram();
		}
		if ( nargs === 1 ) {
			return new Histogram( arguments[0] );
		}
		if ( nargs === 2 ) {
			return new Histogram( arguments[0], arguments[1] );
		}
		return new Histogram( arguments[0], arguments[1], arguments[2] );
	}
	self = this;

	opts = defaults();
	if ( nargs === 0 ) {
		options = {};
	} else if ( nargs === 1 ) {
		options = arguments[ 0 ];
		if ( !isObject( options ) ) {
			throw new TypeError( 'invalid input argument. Options argument must be an `object`. Value: `' + options + '`.' );
		}
	} else if ( nargs === 2 ) {
		options = {};
		options.x = arguments[ 0 ];
		options.edges = arguments[ 1 ];
	} else if ( nargs > 2 ) {
		if ( !isObject( arguments[ 2 ] ) ) {
			throw new TypeError( 'invalid input argument. Options argument must be an `object`. Value: `' + arguments[2] + '`.' );
		}
		options = copy( arguments[ 2 ] ); // avoid mutation
		options.x = arguments[ 0 ];
		options.edges = arguments[ 1 ];
	}
	opts = merge( opts, options );

	debug( 'Creating an instance with the following configuration: %s.', JSON.stringify( opts ) );
	EventEmitter.call( this );

	for ( i = 0; i < PRIVATE_PROPS.length; i++ ) {
		Object.defineProperty( this, PRIVATE_PROPS[i], {
			'configurable': false,
			'enumerable': false,
			'writable': true,
			'value': null
		});
	}
	// Set a clipping path id:
	Object.defineProperty( this, '_clipPathId', {
		'configurable': false,
		'enumerable': false,
		'writable': false,
		'value': minstd().toString() // TODO: uuid
	});

	// Initialize an internal cache for renderers...
	Object.defineProperty( this, '$', {
		'configurable': false,
		'enumerable': false,
		'writable': false,
		'value': {}
	});
	Object.defineProperty( this.$, 'svg', {
		'configurable': false,
		'enumerable': false,
		'writable': false,
		'value': {}
	});

	// Set options...
	keys = getKeys( opts );
	for ( i = 0; i < keys.length; i++ ) {
		key = keys[ i ];
		this[ key ] = opts[ key ];
	}

	// Add event listeners:
	this.on( 'change', onChange );
	this.on( 'render', onRender );

	return this;

	/**
	* Callback invoked upon receiving a change event.
	*
	* @private
	*/
	function onChange() {
		/* eslint-disable no-underscore-dangle */
		debug( 'Received a change event.' );
		if ( self._autoRender ) {
			self.render();
		}
	}

	/**
	* Callback invoked upon receiving a render event.
	*
	* @private
	* @param {*} hist - rendered histogram
	*/
	function onRender( hist ) {
		/* eslint-disable no-underscore-dangle */
		debug( 'Received a render event.' );
		if ( self._autoView ) {
			debug( 'Viewer: %s.', self._viewer );
			debug( 'Generating view...' );
			view( self, self._viewer, hist );
		}
	}
}

/*
* Inherit from the `EventEmitter` prototype.
*/
inherit( Histogram, EventEmitter );

// EXPORTS //

module.exports = Histogram;
<commit_msg>Document constructor options<commit_after>/**
* @license Apache-2.0
*
* Copyright (c) 2018 The Stdlib Authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
*    http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

'use strict';

// MODULES //

var EventEmitter = require( 'events' ).EventEmitter;
var logger = require( 'debug' );
var getKeys = require( 'object-keys' ).shim();
var inherit = require( '@stdlib/utils/inherit' );
var copy = require( '@stdlib/utils/copy' );
var mergeFcn = require( '@stdlib/utils/merge' ).factory;
var setReadOnly = require( '@stdlib/utils/define-read-only-property' );
var isObject = require( '@stdlib/assert/is-plain-object' );
var minstd = require( '@stdlib/random/base/minstd' );
var defaults = require( './defaults.js' );

// VARIABLES //

var debug = logger( 'hist:main' );
var PRIVATE_PROPS = [
	'_autoRender',
	'_autoView',
	'_barColors',
	'_barOpacity',
	'_binCounts',
	'_binMethod',
	'_binWidth',
	'_description',
	'_edgeMax',
	'_edgeMin',
	'_edges',
	'_engine',
	'_height',
	'_isDefined',
	'_labels',
	'_lineColors',
	'_lineOpacity',
	'_lineStyle',
	'_lineWidth',
	'_nbins',
	'_normalization',
	'_orientation',
	'_paddingBottom',
	'_paddingLeft',
	'_paddingRight',
	'_paddingTop',
	'_renderFormat',
	'_title',
	'_viewer',
	'_width',
	'_xAxisOrient',
	'_xData',
	'_xLabel',
	'_xMax',
	'_xMin',
	'_xNumTicks',
	'_xScale',
	'_xTickFormat',
	'_yAxisOrient',
	'_yData',
	'_yLabel',
	'_yMax',
	'_yMin',
	'_yNumTicks',
	'_yScale',
	'_yTickFormat'
];

// FUNCTIONS //

var merge = mergeFcn({
	'extend': false
});

// MAIN //

/**
* Histogram constructor.
*
* @constructor
* @param {Array} [x] - values to bin
* @param {Array} [bins] - edges and/or number of bins
* @param {Options} [options] - constructor options
* @param {boolean} [options.autoRender=true] - indicates whether to re-render on a change event
* @param {boolean} [options.autoView=false] - indicates whether to generate an updated view on a render event
* @param {(string|StringArray)} [options.barColors='category10'] - bar face color(s)
* @param {(number|NumberArray)} [options.barOpacity=0.9] - bar face opacity
* @param {(null|string|Array)} [options.binMethod=null] - binning algorithm
* @param {(number|NumberArray)} [options.binWidth] - bin width
* @param {string} [options.description=''] - description
* @param {(null|number|Array)} [options.edgeMax=null] - maximum bin edge value
* @param {(null|number|Array)} [options.edgeMin=null] - minimum bin edge value
* @param {Array} [options.edges] - bin edges
* @param {string} [options.engine='svg'] - render engine
* @param {PositiveNumber} [options.height=400] - plot height
* @param {Function} [options.isDefined] - accessor indicating whether a datum is defined
* @param {(StringArray|EmptyArray)} [options.labels] - data labels
* @param {(string|StringArray)} [options.lineColors='#000'] - line color(s)
* @param {(number|NumberArray)} [options.lineOpacity=0.9] - line opacity
* @param {(string|StringArray)} [options.lineStyle='-'] - line style(s)
* @param {(NonNegativeInteger|Array<NonNegativeInteger>)} [options.lineWidth=2] - line width(s)
* @param {(null|number|Array)} [options.nbins] - number of bins
* @param {(string|StringArray)} [options.normalization='count'] - histogram normalization
* @param {string} [options.orientation='vertical'] - histogram orientation
* @param {NonNegativeInteger} [options.paddingBottom=80] - bottom padding
* @param {NonNegativeInteger} [options.paddingLeft=90] - left padding
* @param {NonNegativeInteger} [options.paddingRight=20] - right padding
* @param {NonNegativeInteger} [options.paddingTop=80] - top padding
* @param {string} [options.renderFormat='vdom'] - render format
* @param {string} [options.title=''] - title
* @param {string} [options.viewer='none'] - viewer
* @param {PositiveNumber} [options.width=400] - plot width
* @param {string} [options.xAxisOrient='bottom'] - x-axis orientation
* @param {string} [options.xLabel='x'] - x-axis label
* @param {(Date|FiniteNumber|null)} [options.xMax=null] - maximum value of x-axis domain
* @param {(Date|FiniteNumber|null)} [options.xMin=null] - minimum value of x-axis domain
* @param {(NonNegativeInteger|null)} [options.xNumTicks=5] - number of x-axis tick marks
* @param {(string|null)} [options.xTickFormat=null] - x-axis tick format
* @param {string} [options.yAxisOrient='left'] - y-axis orientation
* @param {string} [options.yLabel='y'] - y-axis label
* @param {(FiniteNumber|null)} [options.yMax=null] - maximum value of y-axis domain
* @param {(FiniteNumber|null)} [options.yMin=null] - minimum value of y-axis domain
* @param {(NonNegativeInteger|null)} [options.yNumTicks=5] - number of y-axis tick marks
* @param {(string|null)} [options.yTickFormat=null] - y-axis tick format
* @throws {TypeError} must provide valid options
* @returns {Histogram} Histogram instance
*
* @example
* TODO
*/
function Histogram() {
	var options;
	var nargs;
	var keys;
	var self;
	var opts;
	var key;
	var i;

	nargs = arguments.length;
	if ( !(this instanceof Histogram) ) {
		if ( nargs === 0 ) {
			return new Histogram();
		}
		if ( nargs === 1 ) {
			return new Histogram( arguments[0] );
		}
		if ( nargs === 2 ) {
			return new Histogram( arguments[0], arguments[1] );
		}
		return new Histogram( arguments[0], arguments[1], arguments[2] );
	}
	self = this;

	opts = defaults();
	if ( nargs === 0 ) {
		options = {};
	} else if ( nargs === 1 ) {
		options = arguments[ 0 ];
		if ( !isObject( options ) ) {
			throw new TypeError( 'invalid input argument. Options argument must be an `object`. Value: `' + options + '`.' );
		}
	} else if ( nargs === 2 ) {
		options = {};
		options.x = arguments[ 0 ];
		options.bins = arguments[ 1 ];
	} else if ( nargs > 2 ) {
		if ( !isObject( arguments[ 2 ] ) ) {
			throw new TypeError( 'invalid input argument. Options argument must be an `object`. Value: `' + arguments[2] + '`.' );
		}
		options = copy( arguments[ 2 ] ); // avoid mutation
		options.x = arguments[ 0 ];
		options.bins = arguments[ 1 ];
	}
	opts = merge( opts, options );

	debug( 'Creating an instance with the following configuration: %s.', JSON.stringify( opts ) );
	EventEmitter.call( this );

	for ( i = 0; i < PRIVATE_PROPS.length; i++ ) {
		Object.defineProperty( this, PRIVATE_PROPS[i], {
			'configurable': false,
			'enumerable': false,
			'writable': true,
			'value': null
		});
	}
	// Set a clipping path id:
	Object.defineProperty( this, '_clipPathId', {
		'configurable': false,
		'enumerable': false,
		'writable': false,
		'value': minstd().toString() // TODO: uuid
	});

	// Initialize an internal cache for renderers...
	Object.defineProperty( this, '$', {
		'configurable': false,
		'enumerable': false,
		'writable': false,
		'value': {}
	});
	Object.defineProperty( this.$, 'svg', {
		'configurable': false,
		'enumerable': false,
		'writable': false,
		'value': {}
	});

	// Set options...
	keys = getKeys( opts );
	for ( i = 0; i < keys.length; i++ ) {
		key = keys[ i ];
		this[ key ] = opts[ key ];
	}

	// Add event listeners:
	this.on( 'change', onChange );
	this.on( 'render', onRender );

	return this;

	/**
	* Callback invoked upon receiving a change event.
	*
	* @private
	*/
	function onChange() {
		/* eslint-disable no-underscore-dangle */
		debug( 'Received a change event.' );
		if ( self._autoRender ) {
			self.render();
		}
	}

	/**
	* Callback invoked upon receiving a render event.
	*
	* @private
	* @param {*} hist - rendered histogram
	*/
	function onRender( hist ) {
		/* eslint-disable no-underscore-dangle */
		debug( 'Received a render event.' );
		if ( self._autoView ) {
			debug( 'Viewer: %s.', self._viewer );
			debug( 'Generating view...' );
			view( self, self._viewer, hist );
		}
	}
}

/*
* Inherit from the `EventEmitter` prototype.
*/
inherit( Histogram, EventEmitter );

// EXPORTS //

module.exports = Histogram;
<|endoftext|>


<commit_before>// add methods to a form element so it can be easily manipulated
//
// add client side validation
// understands:
//   - rails form structures
//   - rails form errors in JSON
//   - etc
(function($) {
$.fn.edifice_form = function() {
  $.extend(this, methods);
  return this;
};

// VALIDATORS:
  // validators expect a form element and return true or an error message
$.fn.edifice_form.validators = {
  not_empty: function($input) {
    return $input.val() ? true : 'must not be empty';
  }
};

// in these 'this' is the $form
var methods = {
  fields: function() {
    return this.find('input, textarea, select');
  },

  error_fields: function() {
    var $form = this;
    return this.fields().filter(function() {
      return $form.has_error($(this));
    });
  },

  submits: function() {
    return this.find('input[type=submit], button[type=submit]');
  },

  valid: function() {
    var $form = this, valid = true;

    this.fields().each(function() {
      if (!$form.validate($(this))) { valid = false; }
    });

    return valid;
  },

  // 'prepare' a validator --> validator returns true or an error string
  set_validator: function($field, validator) {
    if (typeof validator === 'string') {
      validator = $.fn.edifice_form.validators[validator] || validator;
      if (typeof validator !== 'function') { throw "Validator not defined: '" + validator + "'"; }
    };
    $field.data('validator', validator);
  },

  // validate a single field
  validate: function($field, validator) {
    if (typeof validator === 'undefined') { validator = $field.data('validator'); }
    if (validator) {
      var error = validator($field), valid = error === true;
      this.clear_error($field);
      if (!valid) {
        this.add_error($field, error);
      }

      return valid;
    } else {
      return true;
    }
  },

  has_error: function($field) {
    return $field.parent('.field_with_errors').length > 0;
  },

  clear_error: function($field) {
   var id = $field.attr('id');
   if (this.has_error($field)) { $field.unwrap() }

   this.find('.field_with_errors label[for=' + id + ']')
    .unwrap()
    .next('.formError').remove();
  },

  add_error: function($field, error) {
    var id = $field.attr('id');
    $field.wrap('
');
    this.find('label[for=' + id + ']')
      .wrap('
')
      .after('
' + error + '
');
  },

  set_errors: function(errors) {
    for (var name in errors) {
      this.add_error(this.fields().filter('[name*=' + name + ']'), errors[name][0]);
    }
  }
};

}(jQuery));<commit_msg>Fixed bug where we don't mirror rails exactly<commit_after>// add methods to a form element so it can be easily manipulated
//
// add client side validation
// understands:
//   - rails form structures
//   - rails form errors in JSON
//   - etc
(function($) {
$.fn.edifice_form = function() {
  $.extend(this, methods);
  return this;
};

// VALIDATORS:
  // validators expect a form element and return true or an error message
$.fn.edifice_form.validators = {
  not_empty: function($input) {
    return $input.val() ? true : 'must not be empty';
  }
};

// in these 'this' is the $form
var methods = {
  fields: function() {
    return this.find('input, textarea, select');
  },

  error_fields: function() {
    var $form = this;
    return this.fields().filter(function() {
      return $form.has_error($(this));
    });
  },

  submits: function() {
    return this.find('input[type=submit], button[type=submit]');
  },

  valid: function() {
    var $form = this, valid = true;

    this.fields().each(function() {
      if (!$form.validate($(this))) { valid = false; }
    });

    return valid;
  },

  // 'prepare' a validator --> validator returns true or an error string
  set_validator: function($field, validator) {
    if (typeof validator === 'string') {
      validator = $.fn.edifice_form.validators[validator] || validator;
      if (typeof validator !== 'function') { throw "Validator not defined: '" + validator + "'"; }
    };
    $field.data('validator', validator);
  },

  // validate a single field
  validate: function($field, validator) {
    if (typeof validator === 'undefined') { validator = $field.data('validator'); }
    if (validator) {
      var error = validator($field), valid = error === true;
      this.clear_error($field);
      if (!valid) {
        this.add_error($field, error);
      }

      return valid;
    } else {
      return true;
    }
  },

  has_error: function($field) {
    return $field.parent('.field_with_errors').length > 0;
  },

  clear_error: function($field) {
   var id = $field.attr('id');
   if (this.has_error($field)) { $field.unwrap() }

   this.find('.field_with_errors label[for=' + id + ']')
    .unwrap()
    .next('.formError').remove();
  },

  add_error: function($field, error) {
    var id = $field.attr('id');
    $field.wrap('
');
    this.find('label[for=' + id + ']')
      .after('
' + error + '
')
      .wrap('
');

  },

  set_errors: function(errors) {
    for (var name in errors) {
      this.add_error(this.fields().filter('[name*=' + name + ']'), errors[name][0]);
    }
  }
};

}(jQuery));<|endoftext|>