<commit_before>define([
    'specimenTools/_BaseWidget'
  , '!require/text!specimenTools/services/languageCharSets.json'
], function(
    Parent
  , languageCharSetsJson
) {
    "use strict";
    /*jshint esnext:true*/

    var weight2weightName = {
            250: 'Thin'
          , 275: 'ExtraLight'
          , 300: 'Light'
          , 400: 'Regular'
          , 500: 'Medium'
          , 600: 'SemiBold'
          , 700: 'Bold'
          , 800: 'ExtraBold'
          , 900: 'Black'
          // bad values (found first in WorkSans)
          , 260: 'Thin'
          , 280: 'ExtraLight'
        }
      , weight2cssWeight = {
            250: '100'
          , 275: '200'
          , 300: '300'
          , 400: '400'
          , 500: '500'
          , 600: '600'
          , 700: '700'
          , 800: '800'
          , 900: '900'
          // bad values (found first in WorkSans)
          , 260: '100'
          , 280: '200'
        }
      ;

    function FontsData(pubsub, options) {
        Parent.call(this, options);
        this._pubSub = pubsub;
        this._pubSub.subscribe('loadFont', this._onLoadFont.bind(this));
        this._data = [];
        Object.defineProperty(this._data, 'globalCache', {
            value: Object.create(null)
        });
    }

    var _p = FontsData.prototype = Object.create(Parent.prototype);
    _p.constructor = FontsData;

    FontsData.defaultOptions = {
        // This should be set explicitly to true (or a string containing
        // glyphs that are allowed to miss despite of being required in
        // languageCharSetsJson.
        // The builtin FontsData.DEFAULT_LAX_CHAR_LIST is there for
        // convenience but may cause trouble!
        useLaxDetection: false
    };

    FontsData._cacheDecorator = function (k) {
        return function(fontIndex) {
            /*jshint validthis:true*/
            var args = [], i, l, data, cached;

            for(i=0,l=arguments.length;i<l;i++)
                args[i] = arguments[i];

            data = this._aquireFontData(fontIndex);
            if(!(k in data.cache))
                cached = data.cache[k] = this[k].apply(this, args);
            else
                cached = data.cache[k];
            return cached;
        };
    };

    FontsData._installPublicCachedInterface = function(_p) {
        var k, newk;
        for(k in _p) {
            newk = k.slice(1);
            if(k.indexOf('_get') !== 0
                        || typeof _p[k] !== 'function'
                        // don't override if it is defined
                        || newk in _p)
                continue;
            _p[newk] = FontsData._cacheDecorator(k);
        }
    };

    FontsData._getFeatures = function _getFeatures(features, langSys, featureIndexes) {
        /*jshint validthis:true*/
        var i,l, idx, tag;
        for(i=0,l=featureIndexes.length;i<l;i++) {
            idx = featureIndexes[i];
            tag = features[idx].tag;
            if(!this[tag])
                this[tag] = [];
            this[tag].push(langSys);
        }
    };

    FontsData.getFeatures = function getFeatures(font) {
        // get all gsub features:
        var features = {/*tag: ["{script:lang}", {script:lang}]*/}
          ,  table, scripts, i, l, j, m, script, scriptTag, lang
          ;
        if(!('gsub' in font.tables) || !font.tables.gsub.scripts)
            return features;
        table = font.tables.gsub;
        scripts = font.tables.gsub.scripts;
        for(i=0,l=scripts.length;i<l;i++) {
            script = scripts[i].script;
            scriptTag = scripts[i].tag;
            if(script.defaultLangSys) {
                lang = 'Default';
                FontsData._getFeatures.call(features
                  , table.features
                  , [scriptTag, lang].join(':')
                  , script.defaultLangSys.featureIndexes
                );
            }
            if(script.langSysRecords) {
                for(j = 0, m = script.langSysRecords.length; j < m; j++) {
                    lang = script.langSysRecords[j].tag;
                    FontsData._getFeatures.call(features
                      , table.features
                      , [scriptTag, lang].join(':')
                      , script.langSysRecords[j].langSys.featureIndexes
                    );
                }
            }
            return features;
        }
        // when supported by opentype.js, get all gpos features:
    };

    FontsData.languageCharSets = JSON.parse(languageCharSetsJson);

    FontsData.sortCoverage = function sortCoverage(a, b) {
        if(a[1] === b[1])
            // compare the names of the languages, to sort alphabetical;
            return a[0].localeCompare(b[0]);
        return b[1] - a[1] ;
    };

    // These are characters that appear in the CLDR data as needed for
    // some languages, but we decided that they are not exactly needed
    // for language support.
    // These are all punctuation characters currently.
    // Don't just trust this list, and if something is terribly wrong
    // for your language, please complain!
    FontsData.DEFAULT_LAX_CHAR_LIST = new Set([
        0x2010 // HYPHEN -> we usually use/include HYPHEN-MINUS: 0x002D
      , 0x2032 // PRIME
      , 0x2033 // DOUBLE PRIME
      , 0x27e8 // MATHEMATICAL LEFT ANGLE BRACKET
      , 0x27e9 // MATHEMATICAL RIGHT ANGLE BRACKET
      , 0x2052 // COMMERCIAL MINUS SIGN
    ]);

    FontsData.getLanguageCoverage = function getLanguageCoverage(font, useLaxDetection) {
        var result = []
          , included, missing, laxSkipped
          , language, chars, charCode, found, i, l, total
          , laxCharList
          ;

        if(typeof useLaxDetection === 'string') {
            laxCharList = new Set();
            for(i=0,l=useLaxDetection.length;i<l;i++)
                laxCharList.add(useLaxDetection.codePointAt(i));
        }
        else
            laxCharList = FontsData.DEFAULT_LAX_CHAR_LIST;

        for(language in FontsData.languageCharSets) {
            // chars is a string
            chars = FontsData.languageCharSets[language];
            found = 0;
            total = l = chars.length;
            included = [];
            missing = [];
            laxSkipped = [];
            for(i=0;i<l;i++) {
                charCode = chars.codePointAt(i);
                if(charCode in font.encoding.cmap.glyphIndexMap) {
                    found += 1;
                    included.push(charCode);
                }
                else if(useLaxDetection && laxCharList.has(charCode)) {
                    total = total-1;
                    laxSkipped.push(charCode);
                }
                else
                    missing.push(charCode);
            }
            result.push([language, found/total, found, total, missing, included, laxSkipped]);
        }

        result.sort(FontsData.sortCoverage);
        return result;
    };

    _p._aquireFontData = function(fontIndex) {
        var data = this._data[fontIndex];
        if(!data)
            throw new Error('FontIndex "'+fontIndex+'" is not available.');
        return data;
    };

    _p._onLoadFont = function(fontIndex, fontFileName, font, originalArraybuffer) {
        this._data[fontIndex] = {
            font: font
          , fileName: fontFileName
          , originalArraybuffer: originalArraybuffer
          , cache: Object.create(null)
        };
    };

    _p._getLanguageCoverage = function(fontIndex) {
        return FontsData.getLanguageCoverage(this._data[fontIndex].font, this._options.useLaxDetection);
    };

    _p._getLanguageCoverageStrict = function(fontIndex) {
        return FontsData.getLanguageCoverage(this._data[fontIndex].font, false);
    };

    _p._getLanguageCoverageLax = function(fontIndex) {
        return FontsData.getLanguageCoverage(this._data[fontIndex].font, true);
    };

    _p._getSupportedLanguages = function(fontIndex) {
        var coverage = this.getLanguageCoverage(fontIndex)
          , i, l
          , result = [], language, support
          ;
        for(i=0,l=coverage.length;i<l;i++) {
            language = coverage[i][0];
            support = coverage[i][1];
            if(support === 1)
                result.push(language);
        }
        result.sort();
        return result;
    };

    _p._getNumberGlyphs = function(fontIndex) {
        return this._data[fontIndex].font.glyphNames.names.length;
    };

    _p._getFeatures = function(fontIndex) {
        return FontsData.getFeatures(this._data[fontIndex].font);
    };

    _p._getFamilyName  = function(fontIndex) {
        var font = this._data[fontIndex].font
          , fontFamily
          ;

        fontFamily = font.names.postScriptName.en
                        || Object.values(font.names.postScriptName)[0]
                        || font.names.fontFamily
                        ;
        fontFamily = fontFamily.split('-')[0];
        return fontFamily;
    };

    _p._getOS2FontWeight = function(fontIndex) {
        var font = this._data[fontIndex].font;
        return font.tables.os2.usWeightClass;
    };

    // Keeping this, maybe we'll have to transform this name further for CSS?
    _p._getCSSFamilyName = _p._getFamilyName;

    _p._getIsItalic = function(fontIndex) {
        var font = this._data[fontIndex].font;
        return !!(font.tables.os2.fsSelection & font.fsSelectionValues.ITALIC);
    };

    _p.getFamiliesData = function() {
        var cacheKey = 'getFamiliesData';
        if(cacheKey in this._data.globalCache)
            return this._data.globalCache[cacheKey];

        var families = Object.create(null)
          , weightDict, styleDict
          , fontFamily, fontWeight, fontStyle
          , fontIndex, l
          , result
          ;
        for(fontIndex=0,l=this._data.length;fontIndex<l;fontIndex++) {
            fontFamily  = this.getFamilyName(fontIndex);
            fontWeight = this.getCSSWeight(fontIndex);
            fontStyle = this.getCSSStyle(fontIndex);

            weightDict = families[fontFamily];
            if(!weightDict)
                families[fontFamily] = weightDict = Object.create(null);

            styleDict = weightDict[fontWeight];
            if(!styleDict)
                weightDict[fontWeight] = styleDict = Object.create(null);

            if(fontStyle in styleDict) {
                console.warn('A font with weight ' + fontWeight
                                + ' and style "'+fontStyle+'"'
                                + ' has already appeared for '
                                +'"' +fontFamily+'".\nFirst was the file: '
                                + styleDict[fontStyle] + ' '
                                + this.getFileName(styleDict[fontStyle])
                                + '.\nNow the file: ' + fontIndex + ' '
                                +  this.getFileName(fontIndex)
                                + ' is in conflict.\nThis may hint to a bad '
                                + 'OS/2 table entry.\nSkipping.'
                                );
                continue;
            }
            // assert(fontStyle not in weightDict)
            styleDict[fontStyle] = fontIndex;
        }

        result =  Object.keys(families).sort()
              .map(function(key){ return [key, this[key]];}, families);
        this._data.globalCache[cacheKey] = result;
        return result;
    };

    // no need to cache these: No underscore will prevent
    //_installPublicCachedInterface from doing anything.
    _p.getNumberSupportedLanguages = function(fontIndex) {
        return this.getSupportedLanguages(fontIndex).length;
    };

    _p.getFont = function(fontIndex) {
        return this._aquireFontData(fontIndex).font;
    };

    _p.getFileName = function(fontIndex) {
        return this._aquireFontData(fontIndex).fileName;
    };

    _p.getOriginalArraybuffer = function(fontIndex) {
        return this._aquireFontData(fontIndex).originalArraybuffer;
    };

    _p.getCSSWeight = function(fontIndex) {
        return weight2cssWeight[this.getOS2FontWeight(fontIndex)];
    };

    _p.getWeightName = function(fontIndex) {
        return weight2weightName[this.getOS2FontWeight(fontIndex)];
    };

    _p.getCSSStyle = function(fontIndex) {
        return this.getIsItalic(fontIndex) ? 'italic' : 'normal';
    };

    _p.getStyleName = function(fontIndex) {
        return this.getWeightName(fontIndex) + (this.getIsItalic(fontIndex) ? ' Italic' : '');
    };

    _p.getPostScriptName = function(fontIndex) {
        return this._aquireFontData(fontIndex).font.names.postScriptName;
    };

    _p.getGlyphByName = function(fontIndex, name) {
        var font = this._aquireFontData(fontIndex).font
          , glyphIndex = font.glyphNames.nameToGlyphIndex(name)
          , glyph = font.glyphs.get(glyphIndex)
          ;
        return glyph;
    };

    _p.getFontValue = function(fontIndex, name /* like: "xHeight" */) {
        var font = this._aquireFontData(fontIndex).font;
        switch(name){
            case('xHeight'):
                return font.tables.os2.sxHeight;
            default:
                console.warn('getFontValue: don\'t know how to get "'+ name +'".');
        }
    };

    function familiesDataReducer(all, item) {
        var i, l, weightDict, weights, styles, result = [];
        weightDict = item[1];
        weights = Object.keys(weightDict).sort();
        for(i=0,l=weights.length;i<l;i++) {
            styles = weightDict[weights[i]];
            if('normal' in styles)
                result.push(styles.normal);
            if('italic' in styles)
                result.push(styles.italic);
        }
        return all.concat(result);
    }

    _p.getFontIndexesInFamilyOrder = function(){
        var familiesData = this.getFamiliesData();
        return familiesData.reduce(familiesDataReducer, []);
    }

    FontsData._installPublicCachedInterface(_p);
    return FontsData;
});
<commit_msg>Add more values to getFontValue<commit_after>define([
    'specimenTools/_BaseWidget'
  , '!require/text!specimenTools/services/languageCharSets.json'
], function(
    Parent
  , languageCharSetsJson
) {
    "use strict";
    /*jshint esnext:true*/

    var weight2weightName = {
            250: 'Thin'
          , 275: 'ExtraLight'
          , 300: 'Light'
          , 400: 'Regular'
          , 500: 'Medium'
          , 600: 'SemiBold'
          , 700: 'Bold'
          , 800: 'ExtraBold'
          , 900: 'Black'
          // bad values (found first in WorkSans)
          , 260: 'Thin'
          , 280: 'ExtraLight'
        }
      , weight2cssWeight = {
            250: '100'
          , 275: '200'
          , 300: '300'
          , 400: '400'
          , 500: '500'
          , 600: '600'
          , 700: '700'
          , 800: '800'
          , 900: '900'
          // bad values (found first in WorkSans)
          , 260: '100'
          , 280: '200'
        }
      ;

    function FontsData(pubsub, options) {
        Parent.call(this, options);
        this._pubSub = pubsub;
        this._pubSub.subscribe('loadFont', this._onLoadFont.bind(this));
        this._data = [];
        Object.defineProperty(this._data, 'globalCache', {
            value: Object.create(null)
        });
    }

    var _p = FontsData.prototype = Object.create(Parent.prototype);
    _p.constructor = FontsData;

    FontsData.defaultOptions = {
        // This should be set explicitly to true (or a string containing
        // glyphs that are allowed to miss despite of being required in
        // languageCharSetsJson.
        // The builtin FontsData.DEFAULT_LAX_CHAR_LIST is there for
        // convenience but may cause trouble!
        useLaxDetection: false
    };

    FontsData._cacheDecorator = function (k) {
        return function(fontIndex) {
            /*jshint validthis:true*/
            var args = [], i, l, data, cached;

            for(i=0,l=arguments.length;i<l;i++)
                args[i] = arguments[i];

            data = this._aquireFontData(fontIndex);
            if(!(k in data.cache))
                cached = data.cache[k] = this[k].apply(this, args);
            else
                cached = data.cache[k];
            return cached;
        };
    };

    FontsData._installPublicCachedInterface = function(_p) {
        var k, newk;
        for(k in _p) {
            newk = k.slice(1);
            if(k.indexOf('_get') !== 0
                        || typeof _p[k] !== 'function'
                        // don't override if it is defined
                        || newk in _p)
                continue;
            _p[newk] = FontsData._cacheDecorator(k);
        }
    };

    FontsData._getFeatures = function _getFeatures(features, langSys, featureIndexes) {
        /*jshint validthis:true*/
        var i,l, idx, tag;
        for(i=0,l=featureIndexes.length;i<l;i++) {
            idx = featureIndexes[i];
            tag = features[idx].tag;
            if(!this[tag])
                this[tag] = [];
            this[tag].push(langSys);
        }
    };

    FontsData.getFeatures = function getFeatures(font) {
        // get all gsub features:
        var features = {/*tag: ["{script:lang}", {script:lang}]*/}
          ,  table, scripts, i, l, j, m, script, scriptTag, lang
          ;
        if(!('gsub' in font.tables) || !font.tables.gsub.scripts)
            return features;
        table = font.tables.gsub;
        scripts = font.tables.gsub.scripts;
        for(i=0,l=scripts.length;i<l;i++) {
            script = scripts[i].script;
            scriptTag = scripts[i].tag;
            if(script.defaultLangSys) {
                lang = 'Default';
                FontsData._getFeatures.call(features
                  , table.features
                  , [scriptTag, lang].join(':')
                  , script.defaultLangSys.featureIndexes
                );
            }
            if(script.langSysRecords) {
                for(j = 0, m = script.langSysRecords.length; j < m; j++) {
                    lang = script.langSysRecords[j].tag;
                    FontsData._getFeatures.call(features
                      , table.features
                      , [scriptTag, lang].join(':')
                      , script.langSysRecords[j].langSys.featureIndexes
                    );
                }
            }
            return features;
        }
        // when supported by opentype.js, get all gpos features:
    };

    FontsData.languageCharSets = JSON.parse(languageCharSetsJson);

    FontsData.sortCoverage = function sortCoverage(a, b) {
        if(a[1] === b[1])
            // compare the names of the languages, to sort alphabetical;
            return a[0].localeCompare(b[0]);
        return b[1] - a[1] ;
    };

    // These are characters that appear in the CLDR data as needed for
    // some languages, but we decided that they are not exactly needed
    // for language support.
    // These are all punctuation characters currently.
    // Don't just trust this list, and if something is terribly wrong
    // for your language, please complain!
    FontsData.DEFAULT_LAX_CHAR_LIST = new Set([
        0x2010 // HYPHEN -> we usually use/include HYPHEN-MINUS: 0x002D
      , 0x2032 // PRIME
      , 0x2033 // DOUBLE PRIME
      , 0x27e8 // MATHEMATICAL LEFT ANGLE BRACKET
      , 0x27e9 // MATHEMATICAL RIGHT ANGLE BRACKET
      , 0x2052 // COMMERCIAL MINUS SIGN
    ]);

    FontsData.getLanguageCoverage = function getLanguageCoverage(font, useLaxDetection) {
        var result = []
          , included, missing, laxSkipped
          , language, chars, charCode, found, i, l, total
          , laxCharList
          ;

        if(typeof useLaxDetection === 'string') {
            laxCharList = new Set();
            for(i=0,l=useLaxDetection.length;i<l;i++)
                laxCharList.add(useLaxDetection.codePointAt(i));
        }
        else
            laxCharList = FontsData.DEFAULT_LAX_CHAR_LIST;

        for(language in FontsData.languageCharSets) {
            // chars is a string
            chars = FontsData.languageCharSets[language];
            found = 0;
            total = l = chars.length;
            included = [];
            missing = [];
            laxSkipped = [];
            for(i=0;i<l;i++) {
                charCode = chars.codePointAt(i);
                if(charCode in font.encoding.cmap.glyphIndexMap) {
                    found += 1;
                    included.push(charCode);
                }
                else if(useLaxDetection && laxCharList.has(charCode)) {
                    total = total-1;
                    laxSkipped.push(charCode);
                }
                else
                    missing.push(charCode);
            }
            result.push([language, found/total, found, total, missing, included, laxSkipped]);
        }

        result.sort(FontsData.sortCoverage);
        return result;
    };

    _p._aquireFontData = function(fontIndex) {
        var data = this._data[fontIndex];
        if(!data)
            throw new Error('FontIndex "'+fontIndex+'" is not available.');
        return data;
    };

    _p._onLoadFont = function(fontIndex, fontFileName, font, originalArraybuffer) {
        this._data[fontIndex] = {
            font: font
          , fileName: fontFileName
          , originalArraybuffer: originalArraybuffer
          , cache: Object.create(null)
        };
    };

    _p._getLanguageCoverage = function(fontIndex) {
        return FontsData.getLanguageCoverage(this._data[fontIndex].font, this._options.useLaxDetection);
    };

    _p._getLanguageCoverageStrict = function(fontIndex) {
        return FontsData.getLanguageCoverage(this._data[fontIndex].font, false);
    };

    _p._getLanguageCoverageLax = function(fontIndex) {
        return FontsData.getLanguageCoverage(this._data[fontIndex].font, true);
    };

    _p._getSupportedLanguages = function(fontIndex) {
        var coverage = this.getLanguageCoverage(fontIndex)
          , i, l
          , result = [], language, support
          ;
        for(i=0,l=coverage.length;i<l;i++) {
            language = coverage[i][0];
            support = coverage[i][1];
            if(support === 1)
                result.push(language);
        }
        result.sort();
        return result;
    };

    _p._getNumberGlyphs = function(fontIndex) {
        return this._data[fontIndex].font.glyphNames.names.length;
    };

    _p._getFeatures = function(fontIndex) {
        return FontsData.getFeatures(this._data[fontIndex].font);
    };

    _p._getFamilyName  = function(fontIndex) {
        var font = this._data[fontIndex].font
          , fontFamily
          ;

        fontFamily = font.names.postScriptName.en
                        || Object.values(font.names.postScriptName)[0]
                        || font.names.fontFamily
                        ;
        fontFamily = fontFamily.split('-')[0];
        return fontFamily;
    };

    _p._getOS2FontWeight = function(fontIndex) {
        var font = this._data[fontIndex].font;
        return font.tables.os2.usWeightClass;
    };

    // Keeping this, maybe we'll have to transform this name further for CSS?
    _p._getCSSFamilyName = _p._getFamilyName;

    _p._getIsItalic = function(fontIndex) {
        var font = this._data[fontIndex].font;
        return !!(font.tables.os2.fsSelection & font.fsSelectionValues.ITALIC);
    };

    _p.getFamiliesData = function() {
        var cacheKey = 'getFamiliesData';
        if(cacheKey in this._data.globalCache)
            return this._data.globalCache[cacheKey];

        var families = Object.create(null)
          , weightDict, styleDict
          , fontFamily, fontWeight, fontStyle
          , fontIndex, l
          , result
          ;
        for(fontIndex=0,l=this._data.length;fontIndex<l;fontIndex++) {
            fontFamily  = this.getFamilyName(fontIndex);
            fontWeight = this.getCSSWeight(fontIndex);
            fontStyle = this.getCSSStyle(fontIndex);

            weightDict = families[fontFamily];
            if(!weightDict)
                families[fontFamily] = weightDict = Object.create(null);

            styleDict = weightDict[fontWeight];
            if(!styleDict)
                weightDict[fontWeight] = styleDict = Object.create(null);

            if(fontStyle in styleDict) {
                console.warn('A font with weight ' + fontWeight
                                + ' and style "'+fontStyle+'"'
                                + ' has already appeared for '
                                +'"' +fontFamily+'".\nFirst was the file: '
                                + styleDict[fontStyle] + ' '
                                + this.getFileName(styleDict[fontStyle])
                                + '.\nNow the file: ' + fontIndex + ' '
                                +  this.getFileName(fontIndex)
                                + ' is in conflict.\nThis may hint to a bad '
                                + 'OS/2 table entry.\nSkipping.'
                                );
                continue;
            }
            // assert(fontStyle not in weightDict)
            styleDict[fontStyle] = fontIndex;
        }

        result =  Object.keys(families).sort()
              .map(function(key){ return [key, this[key]];}, families);
        this._data.globalCache[cacheKey] = result;
        return result;
    };

    // no need to cache these: No underscore will prevent
    //_installPublicCachedInterface from doing anything.
    _p.getNumberSupportedLanguages = function(fontIndex) {
        return this.getSupportedLanguages(fontIndex).length;
    };

    _p.getFont = function(fontIndex) {
        return this._aquireFontData(fontIndex).font;
    };

    _p.getFileName = function(fontIndex) {
        return this._aquireFontData(fontIndex).fileName;
    };

    _p.getOriginalArraybuffer = function(fontIndex) {
        return this._aquireFontData(fontIndex).originalArraybuffer;
    };

    _p.getCSSWeight = function(fontIndex) {
        return weight2cssWeight[this.getOS2FontWeight(fontIndex)];
    };

    _p.getWeightName = function(fontIndex) {
        return weight2weightName[this.getOS2FontWeight(fontIndex)];
    };

    _p.getCSSStyle = function(fontIndex) {
        return this.getIsItalic(fontIndex) ? 'italic' : 'normal';
    };

    _p.getStyleName = function(fontIndex) {
        return this.getWeightName(fontIndex) + (this.getIsItalic(fontIndex) ? ' Italic' : '');
    };

    _p.getPostScriptName = function(fontIndex) {
        return this._aquireFontData(fontIndex).font.names.postScriptName;
    };

    _p.getGlyphByName = function(fontIndex, name) {
        var font = this._aquireFontData(fontIndex).font
          , glyphIndex = font.glyphNames.nameToGlyphIndex(name)
          , glyph = font.glyphs.get(glyphIndex)
          ;
        return glyph;
    };

    _p.getFontValue = function(fontIndex, name /* like: "xHeight" */) {
        var font = this._aquireFontData(fontIndex).font;
        switch(name){
            case('xHeight'):
                return font.tables.os2.sxHeight;
            case('capHeight'):
                 return font.tables.os2.sCapHeight;
            case('ascender'):
            /*falls through*/
            case('descender'):
                return font[name];
            default:
                console.warn('getFontValue: don\'t know how to get "'+ name +'".');
        }
    };

    function familiesDataReducer(all, item) {
        var i, l, weightDict, weights, styles, result = [];
        weightDict = item[1];
        weights = Object.keys(weightDict).sort();
        for(i=0,l=weights.length;i<l;i++) {
            styles = weightDict[weights[i]];
            if('normal' in styles)
                result.push(styles.normal);
            if('italic' in styles)
                result.push(styles.italic);
        }
        return all.concat(result);
    }

    _p.getFontIndexesInFamilyOrder = function(){
        var familiesData = this.getFamiliesData();
        return familiesData.reduce(familiesDataReducer, []);
    };

    FontsData._installPublicCachedInterface(_p);
    return FontsData;
});
<|endoftext|>


<commit_before>'use strict';

(function() {
  angular.module('ncsaas')
    .service('cloudsService', ['ENV', '$resource', 'projectsService', cloudsService]);

  function cloudsService(ENV, $resource, projectsService, $cookies) {
    /*jshint validthis: true */
    var vm = this;
    vm.cloudResource = $resource(ENV.apiEndpoint + 'api/clouds/:cloudUUID/', {cloudUUID:'@uuid'});

  }

})();

(function() {
  angular.module('ncsaas')
    .factory('RawCloud', ['ENV', '$resource', RawCloud]);

    function RawCloud(ENV, $resource) {
      return $resource(ENV.apiEndpoint + 'api/clouds/:cloudUUID/', {cloudUUID:'@uuid'});
    }

})();
<commit_msg>Creating a cloud service under new project rules.<commit_after>'use strict';

(function() {
  angular.module('ncsaas')
    .service('cloudsService', ['RawCloud', cloudsService]);

  function cloudsService(RawCloud) {
    /*jshint validthis: true */
    var vm = this;

    vm.getCloudList = getCloudList;
    vm.getCloud = getCloud;

    function getCloudList() {
      return RawCloud.query();
    }

    function getCloud(uuid) {
      return RawCloud.get({cloudUUID: uuid});
    }

  }

})();

(function() {
  angular.module('ncsaas')
    .factory('RawCloud', ['ENV', '$resource', RawCloud]);

    function RawCloud(ENV, $resource) {
      return $resource(ENV.apiEndpoint + 'api/clouds/:cloudUUID/', {cloudUUID:'@uuid'});
    }

})();
<|endoftext|>


<commit_before>/**
 * Copyright reelyActive 2014
 * We believe in an open Internet of Things
 */

var http = require('http');

var DEFAULT_HOSTNAME = "webroad66-demo.azurewebsites.net";
var DEFAULT_PORT = 80;

/**
 * WebRoad66 Class
 * Sends notifications to the Web Road 66 service.
 * @param {Object} options The options as a JSON object.
 * @constructor
 */
function WebRoad66(options) {
  options = options || {};
  var self = this;

  this.eventsManager = options.eventsManager;
  this.hostname = options.hostname || DEFAULT_HOSTNAME;
  this.port = options.port || DEFAULT_PORT;
  //this.authorization = options.authorization;
  //this.clientId = options.clientId;

  this.eventsManager.on('appearance', function(tiraid) {
    postUpdate(self, 'appearance', tiraid);
  });
  this.eventsManager.on('displacement', function(tiraid) {
    postUpdate(self, 'displacement', tiraid);
  });
  this.eventsManager.on('disappearance', function(tiraid) {
    //postUpdate(self, 'disappearance', tiraid);
  });
};

/**
 * Post an update to the WebRoad66 service
 * @param {WebRoad66} instance The given instance.
 * @param {String} event The type of event.
 * @param {Object} tiraid The tiraid representing the event.
 */
function postUpdate(instance, event, tiraid) {
  var options = {
    hostname: instance.hostname,
    port: instance.port,
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    path: '/api/Statistics?bid=' + tiraid.radioDecodings[0].identifier.value +
          '&did=' + tiraid.identifier.value
  };

  var payload = tiraid;

  // TODO: move this to a utils file
  var req = http.request(options, function(res) {
    var data = '';
    res.on('data', function(chunk) {
      data += chunk;
    });
    res.on('end', function() {
      console.log("Web Road 66 says: " + data);
    });
    console.log("Web Road 66 status code: " + res.statusCode);
  });

  req.write(JSON.stringify(payload));
  req.end();
}

module.exports = WebRoad66;
<commit_msg>Completed initial integration<commit_after>/**
 * Copyright reelyActive 2014
 * We believe in an open Internet of Things
 */

var http = require('http');

var DEFAULT_HOSTNAME = "webroad66-demo.azurewebsites.net";
var DEFAULT_PORT = 80;

/**
 * WebRoad66 Class
 * Sends notifications to the Web Road 66 service.
 * @param {Object} options The options as a JSON object.
 * @constructor
 */
function WebRoad66(options) {
  options = options || {};
  var self = this;

  this.eventsManager = options.eventsManager;
  this.hostname = options.hostname || DEFAULT_HOSTNAME;
  this.port = options.port || DEFAULT_PORT;

  this.eventsManager.on('appearance', function(tiraid) {
    postUpdate(self, 'appearance', tiraid);
  });
  this.eventsManager.on('displacement', function(tiraid) {
    postUpdate(self, 'displacement', tiraid);
  });
  this.eventsManager.on('disappearance', function(tiraid) {
    //postUpdate(self, 'disappearance', tiraid);
  });
};

/**
 * Post an update to the WebRoad66 service
 * @param {WebRoad66} instance The given instance.
 * @param {String} event The type of event.
 * @param {Object} tiraid The tiraid representing the event.
 */
function postUpdate(instance, event, tiraid) {
  var options = {
    hostname: instance.hostname,
    port: instance.port,
    method: 'POST',
    headers: { 'Content-Type': 'application/json',
               'Referer': 'http://reelyactive.com' },
    path: '/api/Statistics/SendStatistics/' +
          tiraid.radioDecodings[0].identifier.value + '/' +
          tiraid.identifier.value
  };

  var payload = tiraid;

  // TODO: move this to a utils file
  var req = http.request(options, function(res) {
    var data = '';
    res.on('data', function(chunk) {
      data += chunk;
    });
    res.on('end', function() {
      console.log("Web Road 66 says: " + data);
    });
    console.log("Web Road 66 status code: " + res.statusCode);
  });

  req.write(JSON.stringify(payload));
  req.end();
}

module.exports = WebRoad66;
<|endoftext|>


<commit_before>'use strict';

/**
 * @ngdoc service
 * @name labelsApp.TooltipService
 * @description
 * # TooltipService
 * Service in the labelsApp.
 */
angular.module('labelsApp')
  .service('TooltipService', function () {
    // AngularJS will instantiate a singleton by calling "new" on this function

    this.buttons = {
        description: {
            active: "this is the active description tooltip",
            inactive: "this is the inactive description tooltip",
        }
    };

    this.icons = {
        types: {
            concept: "this is a concept!"
        },
        relations: {
            closeMatch: "closeMatch!",
            exactMatch: "exactMatch!",
            relatedMatch: "relatedMatch!"
        },
        translation: "this is a translation",
        description: "This is a description!",
        internalLink: "internal links",
        externalLink: "external links",
        thesauriSettings: "change thesauri settings",
        vocabSettings: "change vocab",
        minipreview: "this is the minipreview"
    };//

    this.comments = {
        description: "Describe current label with a few, clear words.",
        exactMatch: "skos:exactMatch is used to link two concepts, indicating a high degree of confidence that the concepts can be used interchangeably across a wide range of information retrieval applications.",
        relatedMatch: "skos:relatedMatch is used to state an associative mapping link between two conceptual resources in different concept schemes.",
        closeMatch: "skos:closeMatch is used to link two concepts that are sufficiently similar that they can be used interchangeably in some information retrieval applications.",
        broadMatch: "skos:broadMatch is used to state a hierarchical mapping link between two conceptual resources in different concept schemes.",
        narrowMatch: "skos:narrowMatch is used to state a hierarchical mapping link between two conceptual resources in different concept schemes.",
        related: "skos:related relates a concept to a concept with which there is an associative semantic relationship.",
        narrower: "skos:narrower relates a concept to a concept that is more specific in meaning.",
        broader: "skos:broader relates a concept to a concept that is more general in meaning."
    };

    this.explaination = {
        wayback: "Use Wayback links",
        publication: "By pressing this button you make this vocabulary public. The concepts of this vocabulary can no longer be edited. Furthermore, you may not delete concepts of this vocabulary."
    }

  });
<commit_msg>tooltip modified<commit_after>'use strict';

/**
 * @ngdoc service
 * @name labelsApp.TooltipService
 * @description
 * # TooltipService
 * Service in the labelsApp.
 */
angular.module('labelsApp')
    .service('TooltipService', function() {
        // AngularJS will instantiate a singleton by calling "new" on this function

        this.buttons = {
            description: {
                active: "this is the active description tooltip",
                inactive: "this is the inactive description tooltip",
            }
        };

        this.icons = {
            types: {
                concept: "this is a concept!"
            },
            relations: {
                closeMatch: "closeMatch!",
                exactMatch: "exactMatch!",
                relatedMatch: "relatedMatch!"
            },
            translation: "this is a translation",
            description: "This is a description!",
            internalLink: "internal links",
            externalLink: "external links",
            thesauriSettings: "change thesauri settings",
            vocabSettings: "change vocabulary",
            minipreview: "this is the minipreview"
        }; //

        this.comments = {
            description: "Describe current concept with a few, clear words.",
            exactMatch: "skos:exactMatch is used to link two concepts, indicating a high degree of confidence that the concepts can be used interchangeably across a wide range of information retrieval applications.",
            relatedMatch: "skos:relatedMatch is used to state an associative mapping link between two conceptual resources in different concept schemes.",
            closeMatch: "skos:closeMatch is used to link two concepts that are sufficiently similar that they can be used interchangeably in some information retrieval applications.",
            broadMatch: "skos:broadMatch is used to state a hierarchical mapping link between two conceptual resources in different concept schemes.",
            narrowMatch: "skos:narrowMatch is used to state a hierarchical mapping link between two conceptual resources in different concept schemes.",
            related: "skos:related relates a concept to a concept with which there is an associative semantic relationship.",
            narrower: "skos:narrower relates a concept to a concept that is more specific in meaning.",
            broader: "skos:broader relates a concept to a concept that is more general in meaning."
        };

        this.explaination = {
            wayback: "Use Wayback links",
            publication: "By pressing this button you make this vocabulary and all its concepts public. You may not delete concepts of this vocabulary anymore. Furthermore, the concepts of this vocabulary can be still edited and a revision history will be created. In this case, be carefull and do not change the meaning of the concept. Somebody could have used it as a reference."
        }

    });<|endoftext|>


<commit_before>'use strict';

describe('Filter: reportUrl', function () {

  // load the filter's module
  beforeEach(module('polestar'));

  // initialize a new instance of the filter before each test
  var reportUrl;

  beforeEach(module('polestar', function($provide) {
    $provide.constant('consts', {report: 'voyager'});
  }));

  beforeEach(inject(function ($filter) {
    reportUrl = $filter('reportUrl');
  }));

  it('should return url for error report', function () {
    expect(reportUrl({})).toBe('https://docs.google.com/forms/d/1T9ZA14F3mmzrHR7JJVUKyPXzrMqF54CjLIOjv2E7ZEM/viewform?');
  });

});<commit_msg>fix spec<commit_after>'use strict';

describe('Filter: reportUrl', function () {

  // load the filter's module
  beforeEach(module('polestar'));

  // initialize a new instance of the filter before each test
  var reportUrl;

  beforeEach(module('polestar', function($provide) {
    $provide.constant('consts', {report: 'voyager'});
  }));

  beforeEach(inject(function ($filter) {
    reportUrl = $filter('reportUrl');
  }));

  it('should return url for error report', function () {
    expect(reportUrl({})).toBe('https://docs.google.com/forms/d/1xKs-qGaLZEUfbTmhdmSoS13OKOEpuu_NNWE5TAAml_Y/viewform?');
  });

});<|endoftext|>


<commit_before>;(function() {

  var config = [

    // Stream Pins
    {
      placement: 'pinterest-stream-pins',
      selector: '.Pin.Module:not(.buffer-inserted, .hideHoverUI)',
      $button: $([
        '
',
          '',
            '',
          '',
        '
'
      ].join('')),
      insert: function(el) {
        var $actions = $(el);
        var $newActionItem = this.$button.clone();

        $actions
          .addClass('buffer-inserted');

        $actions
          .find('.pinImageActionButtonWrapper')
          .prepend($newActionItem);

        return $newActionItem;
      },
      getData: function(el) {
        var $img = $(el).find('.pinImg');
        var image = $img.attr('src');
        // Grab text from image alt attribute
        var text = $img.attr('alt');
        return {
          text: text,
          url: "https://www.pinterest.com",
          picture: image,
          placement: this.placement
        };
      }
    },
    // Single Pins
    {
      placement: 'pinterest-single-pin',
      selector: '.PinActionBar:not(.buffer-inserted)',
      $button: $([
        '
',
          '',
            '',
          '',
        '
'
      ].join('')),
      insert: function(el) {
        var $actions = $(el);
        var $newActionItem = this.$button.clone();

        $actions
          .addClass('buffer-inserted')
          .prepend($newActionItem);

        return $newActionItem;
      },
      getData: function(el) {
        var $img = $(el).parents('.closeupActionBarContainer')
                        .siblings('.closeupContainer')
                        .find('.pinImage');

        var image = $img.attr('src');
        // Grab text from image alt attribute
        var text = $img.attr('alt');

        return {
          text: text,
          url: "https://www.pinterest.com",
          picture: image,
          placement: this.placement
        };
      }
    },
    // Pin Action Popup Pin
    {
      placement: 'pinterest-pin-popup',
      selector: '.SelectList li.item .BoardLabel:not(.buffer-inserted)',
      $button: $([
        '
',
          '',
            '',
          '',
        '
'
      ].join('')),
      insert: function(el) {
        var $actions = $(el);
        var $newActionItem = this.$button.clone();

        $actions
          .addClass('buffer-inserted')
          .prepend($newActionItem);

        return $newActionItem;
      },
      getData: function(el) {
        var $img = $(el).parents('.Module.inModal').children('.pinContainer').find('.pinImg');

        var image = $img.attr('src');
        // Grab text from image alt attribute
        var text = $img.attr('alt');

        var $source = $(el).parents('.Module.inModal').children('.pinContainer').find('.pinDomain');
        var source = $source.text();

        return {
          text: text,
          url: source,
          picture: image,
          placement: this.placement
        };
      }
    }
  ];

  var insertButton = function(target) {
    $(target.selector).each(function(i, el) {
      var $button = target.insert(el);
      $button.on('click', function(e) {
        e.preventDefault();
        e.stopImmediatePropagation();
        xt.port.emit('buffer_click', target.getData(el));
      })
    });
  };

  var insertButtons = function() {
    config.forEach(insertButton);
  };

  var pinterestLoop = function() {
    insertButtons();
    setTimeout(pinterestLoop, 500);
  };

  var start = function() {

    // Add class for css scoping
    document.body.classList.add('buffer-pinterest');

    // Start the loop that will watch for new DOM elements
    pinterestLoop();
  }

  // Wait for xt.options to be set
  ;(function check() {
    start();

    // If Pinterest is switched on, start the main loop
    if (!xt.options) {
      setTimeout(check, 0);
    } else if (xt.options['buffer.op.pinterest'] === 'pinterest') {
      start();
    } else {
      setTimeout(check, 2000);
    }
  }());

}());
<commit_msg>Grab correct source from stream<commit_after>;(function() {

  var config = [

    // Stream Pins
    {
      placement: 'pinterest-stream-pins',
      selector: '.Pin.Module:not(.buffer-inserted, .hideHoverUI)',
      $button: $([
        '
',
          '',
            '',
          '',
        '
'
      ].join('')),
      insert: function(el) {
        var $actions = $(el);
        var $newActionItem = this.$button.clone();

        $actions
          .addClass('buffer-inserted');

        $actions
          .find('.pinImageActionButtonWrapper')
          .prepend($newActionItem);

        return $newActionItem;
      },
      getData: function(el) {
        var $img = $(el).find('.pinImg');
        var image = $img.attr('src');
        // Grab text from image alt attribute
        var text = $img.attr('alt');

        var $source = $(el).find('.pinNavLink');
        var source = $source.attr('href');
        return {
          text: text,
          url: source,
          picture: image,
          placement: this.placement
        };
      }
    },
    // Single Pins
    {
      placement: 'pinterest-single-pin',
      selector: '.PinActionBar:not(.buffer-inserted)',
      $button: $([
        '
',
          '',
            '',
          '',
        '
'
      ].join('')),
      insert: function(el) {
        var $actions = $(el);
        var $newActionItem = this.$button.clone();

        $actions
          .addClass('buffer-inserted')
          .prepend($newActionItem);

        return $newActionItem;
      },
      getData: function(el) {
        var $img = $(el).parents('.closeupActionBarContainer')
                        .siblings('.closeupContainer')
                        .find('.pinImage');

        var image = $img.attr('src');
        // Grab text from image alt attribute
        var text = $img.attr('alt');

        return {
          text: text,
          url: "https://www.pinterest.com",
          picture: image,
          placement: this.placement
        };
      }
    },
    // Pin Action Popup Pin
    {
      placement: 'pinterest-pin-popup',
      selector: '.SelectList li.item .BoardLabel:not(.buffer-inserted)',
      $button: $([
        '
',
          '',
            '',
          '',
        '
'
      ].join('')),
      insert: function(el) {
        var $actions = $(el);
        var $newActionItem = this.$button.clone();

        $actions
          .addClass('buffer-inserted')
          .prepend($newActionItem);

        return $newActionItem;
      },
      getData: function(el) {
        var $img = $(el).parents('.Module.inModal').children('.pinContainer').find('.pinImg');

        var image = $img.attr('src');
        // Grab text from image alt attribute
        var text = $img.attr('alt');

        var $source = $(el).parents('.Module.inModal').children('.pinContainer').find('.pinDomain');
        var source = $source.text();

        return {
          text: text,
          url: source,
          picture: image,
          placement: this.placement
        };
      }
    }
  ];

  var insertButton = function(target) {
    $(target.selector).each(function(i, el) {
      var $button = target.insert(el);
      $button.on('click', function(e) {
        e.preventDefault();
        e.stopImmediatePropagation();
        xt.port.emit('buffer_click', target.getData(el));
      })
    });
  };

  var insertButtons = function() {
    config.forEach(insertButton);
  };

  var pinterestLoop = function() {
    insertButtons();
    setTimeout(pinterestLoop, 500);
  };

  var start = function() {

    // Add class for css scoping
    document.body.classList.add('buffer-pinterest');

    // Start the loop that will watch for new DOM elements
    pinterestLoop();
  }

  // Wait for xt.options to be set
  ;(function check() {
    start();

    // If Pinterest is switched on, start the main loop
    if (!xt.options) {
      setTimeout(check, 0);
    } else if (xt.options['buffer.op.pinterest'] === 'pinterest') {
      start();
    } else {
      setTimeout(check, 2000);
    }
  }());

}());
<|endoftext|>


<commit_before>import Vue from '../../utils/vue'
import { BVFormBtnLabelControl, dropdownProps } from '../../utils/bv-form-btn-label-control'
import { getComponentConfig } from '../../utils/config'
import { createDate, formatYMD, parseYMD } from '../../utils/date'
import { isUndefinedOrNull } from '../../utils/inspect'
import idMixin from '../../mixins/id'
import { BButton } from '../button/button'
import { BCalendar } from '../calendar/calendar'
import { BIconCalendar, BIconCalendarFill } from '../../icons/icons'

const NAME = 'BFormDatepicker'

// Fallback to BCalendar prop if no value found
const getConfigFallback = prop => {
  return getComponentConfig(NAME, prop) || getComponentConfig('BCalendar', prop)
}

// We create our props as a mixin so that we can control
// where they appear in the props listing reference section
const propsMixin = {
  props: {
    value: {
      type: [String, Date],
      default: null
    },
    valueAsDate: {
      type: Boolean,
      default: false
    },
    resetValue: {
      type: [String, Date],
      default: ''
    },
    placeholder: {
      type: String,
      // Defaults to `labelNoDateSelected` from calendar context
      default: null
    },
    size: {
      type: String,
      default: null
    },
    min: {
      type: [String, Date],
      default: null
    },
    max: {
      type: [String, Date],
      default: null
    },
    disabled: {
      type: Boolean,
      default: false
    },
    readonly: {
      type: Boolean,
      default: false
    },
    required: {
      // If true adds the `aria-required` attribute
      type: Boolean,
      default: false
    },
    name: {
      type: String,
      default: null
    },
    form: {
      type: String,
      default: null
    },
    state: {
      // Tri-state prop: `true`, `false` or `null`
      type: Boolean,
      default: null
    },
    dateDisabledFn: {
      type: Function,
      default: null
    },
    noCloseOnSelect: {
      type: Boolean,
      default: false
    },
    hideHeader: {
      type: Boolean,
      default: false
    },
    locale: {
      type: [String, Array],
      default: null
    },
    startWeekday: {
      // `0` (Sunday), `1` (Monday), ... `6` (Saturday)
      // Day of week to start calendar on
      type: [Number, String],
      default: 0
    },
    direction: {
      type: String,
      default: null
    },
    calendarWidth: {
      // Width of the calendar dropdown
      type: String,
      default: '270px'
    },
    selectedVariant: {
      // Variant color to use for the selected date
      type: String,
      default: 'primary'
    },
    todayVariant: {
      // Variant color to use for today's date (defaults to `variant`)
      type: String,
      default: null
    },
    noHighlightToday: {
      // Disable highlighting today's date
      type: Boolean,
      default: false
    },
    todayButton: {
      type: Boolean,
      default: false
    },
    labelTodayButton: {
      type: String,
      default: () => getComponentConfig(NAME, 'labelTodayButton')
    },
    todayButtonVariant: {
      type: String,
      default: 'outline-primary'
    },
    resetButton: {
      type: Boolean,
      default: false
    },
    labelResetButton: {
      type: String,
      default: () => getComponentConfig(NAME, 'labelResetButton')
    },
    resetButtonVariant: {
      type: String,
      default: 'outline-danger'
    },
    closeButton: {
      type: Boolean,
      default: false
    },
    labelCloseButton: {
      type: String,
      default: () => getComponentConfig(NAME, 'labelCloseButton')
    },
    closeButtonVariant: {
      type: String,
      default: 'outline-secondary'
    },
    // Labels for buttons and keyboard shortcuts
    // These pick BCalendar global config if no BFormDate global config
    labelPrevYear: {
      type: String,
      default: () => getConfigFallback('labelPrevYear')
    },
    labelPrevMonth: {
      type: String,
      default: () => getConfigFallback('labelPrevMonth')
    },
    labelCurrentMonth: {
      type: String,
      default: () => getConfigFallback('labelCurrentMonth')
    },
    labelNextMonth: {
      type: String,
      default: () => getConfigFallback('labelNextMonth')
    },
    labelNextYear: {
      type: String,
      default: () => getConfigFallback('labelNextYear')
    },
    labelToday: {
      type: String,
      default: () => getConfigFallback('labelToday')
    },
    labelSelected: {
      type: String,
      default: () => getConfigFallback('labelSelected')
    },
    labelNoDateSelected: {
      type: String,
      default: () => getConfigFallback('labelNoDateSelected')
    },
    labelCalendar: {
      type: String,
      default: () => getConfigFallback('labelCalendar')
    },
    labelNav: {
      type: String,
      default: () => getConfigFallback('labelNav')
    },
    labelHelp: {
      type: String,
      default: () => getConfigFallback('labelHelp')
    },
    dateFormatOptions: {
      // `Intl.DateTimeFormat` object
      type: Object,
      default: () => ({
        year: 'numeric',
        month: 'long',
        day: 'numeric',
        weekday: 'long'
      })
    },
    // Dark mode
    dark: {
      type: Boolean,
      default: false
    },
    // extra dropdown stuff
    menuClass: {
      type: [String, Array, Object],
      default: null
    },
    ...dropdownProps
  }
}

// --- BFormDate component ---

// @vue/component
export const BFormDatepicker = /*#__PURE__*/ Vue.extend({
  name: NAME,
  // The mixins order determines the order of appearance in the props reference section
  mixins: [idMixin, propsMixin],
  model: {
    prop: 'value',
    event: 'input'
  },
  data() {
    return {
      // We always use `YYYY-MM-DD` value internally
      localYMD: formatYMD(this.value) || '',
      // Context data from BCalendar
      localLocale: null,
      isRTL: false,
      formattedValue: '',
      activeYMD: '',
      // If the popup is open
      isVisible: false
    }
  },
  computed: {
    calendarYM() {
      // Returns the calendar year/month
      // Returns the `YYYY-MM` portion of the active calendar date
      return this.activeYMD.slice(0, -3)
    },
    calendarProps() {
      // Use self for better minification, as `this` won't
      // minimize and we reference it many times below
      const self = this
      return {
        hidden: !self.isVisible,
        value: self.localYMD,
        min: self.min,
        max: self.max,
        readonly: self.readonly,
        disabled: self.disabled,
        locale: self.locale,
        startWeekday: self.startWeekday,
        direction: self.direction,
        width: self.calendarWidth,
        dateDisabledFn: self.dateDisabledFn,
        selectedVariant: self.selectedVariant,
        todayVariant: self.todayVariant,
        hideHeader: self.hideHeader,
        labelPrevYear: self.labelPrevYear,
        labelPrevMonth: self.labelPrevMonth,
        labelCurrentMonth: self.labelCurrentMonth,
        labelNextMonth: self.labelNextMonth,
        labelNextYear: self.labelNextYear,
        labelToday: self.labelToday,
        labelSelected: self.labelSelected,
        labelNoDateSelected: self.labelNoDateSelected,
        labelCalendar: self.labelCalendar,
        labelNav: self.labelNav,
        labelHelp: self.labelHelp,
        dateFormatOptions: self.dateFormatOptions
      }
    },
    computedLang() {
      return (this.localLocale || '').replace(/-u-.*$/i, '') || null
    },
    computedResetValue() {
      return parseYMD(this.resetValue) || ''
    }
  },
  watch: {
    value(newVal) {
      this.localYMD = formatYMD(newVal) || ''
    },
    localYMD(newVal) {
      this.$emit('input', this.valueAsDate ? parseYMD(newVal) || null : newVal || '')
    },
    calendarYM(newVal, oldVal) /* istanbul ignore next */ {
      // Displayed calendar month has changed
      // So possibly the calendar height has changed...
      // We need to update popper computed position
      if (newVal !== oldVal && oldVal) {
        try {
          this.$refs.control.updatePopper()
        } catch {}
      }
    }
  },
  methods: {
    // Public methods
    focus() {
      if (!this.disabled) {
        try {
          this.$refs.control.focus()
        } catch {}
      }
    },
    blur() {
      if (!this.disabled) {
        try {
          this.$refs.control.blur()
        } catch {}
      }
    },
    // Private methods
    setAndClose(ymd) {
      this.localYMD = ymd
      // Close calendar popup, unless `noCloseOnSelect`
      if (!this.noCloseOnSelect) {
        this.$nextTick(() => {
          this.$refs.control.hide(true)
        })
      }
    },
    onSelected(ymd) {
      this.$nextTick(() => {
        this.setAndClose(ymd)
      })
    },
    onInput(ymd) {
      if (this.localYMD !== ymd) {
        this.localYMD = ymd
      }
    },
    onContext(ctx) {
      const { activeYMD, isRTL, locale, selectedYMD, selectedFormatted } = ctx
      this.isRTL = isRTL
      this.localLocale = locale
      this.formattedValue = selectedFormatted
      this.localYMD = selectedYMD
      this.activeYMD = activeYMD
      // Re-emit the context event
      this.$emit('context', ctx)
    },
    onTodayButton() {
      this.setAndClose(formatYMD(createDate()))
    },
    onResetButton() {
      this.setAndClose(this.computedResetValue)
    },
    onCloseButton() {
      this.$refs.control.hide(true)
    },
    // Menu handlers
    onShow() {
      this.isVisible = true
    },
    onShown() {
      this.$nextTick(() => {
        try {
          this.$refs.calendar.focus()
        } catch {}
      })
    },
    onHidden() {
      this.isVisible = false
    },
    // Render helpers
    defaultButtonFn({ isHovered, hasFocus }) {
      return this.$createElement(isHovered || hasFocus ? BIconCalendarFill : BIconCalendar, {
        props: { scale: 1.25 },
        attrs: { 'aria-hidden': 'true' }
      })
    }
  },
  render(h) {
    const localYMD = this.localYMD
    const disabled = this.disabled
    const readonly = this.readonly
    const placeholder = isUndefinedOrNull(this.placeholder)
      ? this.labelNoDateSelected
      : this.placeholder

    // Optional footer buttons
    let $footer = []

    if (this.todayButton) {
      const label = this.labelTodayButton
      $footer.push(
        h(
          BButton,
          {
            props: { size: 'sm', disabled: disabled || readonly, variant: this.todayButtonVariant },
            attrs: { 'aria-label': label || null },
            on: { click: this.onTodayButton }
          },
          label
        )
      )
    }

    if (this.resetButton) {
      const label = this.labelResetButton
      $footer.push(
        h(
          BButton,
          {
            props: { size: 'sm', disabled: disabled || readonly, variant: this.resetButtonVariant },
            attrs: { 'aria-label': label || null },
            on: { click: this.onResetButton }
          },
          label
        )
      )
    }

    if (this.closeButton) {
      const label = this.labelCloseButton
      $footer.push(
        h(
          BButton,
          {
            props: { size: 'sm', disabled, variant: this.closeButtonVariant },
            attrs: { 'aria-label': label || null },
            on: { click: this.onCloseButton }
          },
          label
        )
      )
    }

    if ($footer.length > 0) {
      $footer = [
        h(
          'div',
          {
            staticClass: 'b-form-date-controls d-flex flex-wrap',
            class: {
              'justify-content-between': $footer.length > 1,
              'justify-content-end': $footer.length < 2
            }
          },
          $footer
        )
      ]
    }

    const $calendar = h(
      BCalendar,
      {
        key: 'calendar',
        ref: 'calendar',
        staticClass: 'b-form-date-calendar',
        props: this.calendarProps,
        on: {
          selected: this.onSelected,
          input: this.onInput,
          context: this.onContext
        }
      },
      $footer
    )

    return h(
      BVFormBtnLabelControl,
      {
        ref: 'control',
        staticClass: 'b-form-datepicker',
        props: {
          // This adds unneeded props, but reduces code size:
          ...this.$props,
          // Overridden / computed props
          id: this.safeId(),
          rtl: this.isRTL,
          lang: this.computedLang,
          value: localYMD || '',
          formattedValue: localYMD ? this.formattedValue : '',
          placeholder: placeholder || '',
          menuClass: [{ 'bg-dark': !!this.dark, 'text-light': !!this.dark }, this.menuClass]
        },
        on: {
          show: this.onShow,
          shown: this.onShown,
          hidden: this.onHidden
        },
        scopedSlots: {
          'button-content': this.$scopedSlots['button-content'] || this.defaultButtonFn
        }
      },
      [$calendar]
    )
  }
})
<commit_msg>chore(b-form-datepicker): fix typecheck during tests<commit_after>import Vue from '../../utils/vue'
import { BVFormBtnLabelControl, dropdownProps } from '../../utils/bv-form-btn-label-control'
import { getComponentConfig } from '../../utils/config'
import { createDate, formatYMD, parseYMD } from '../../utils/date'
import { isUndefinedOrNull } from '../../utils/inspect'
import idMixin from '../../mixins/id'
import { BButton } from '../button/button'
import { BCalendar } from '../calendar/calendar'
import { BIconCalendar, BIconCalendarFill } from '../../icons/icons'

const NAME = 'BFormDatepicker'

// Fallback to BCalendar prop if no value found
const getConfigFallback = prop => {
  return getComponentConfig(NAME, prop) || getComponentConfig('BCalendar', prop)
}

// We create our props as a mixin so that we can control
// where they appear in the props listing reference section
const propsMixin = {
  props: {
    value: {
      type: [String, Date],
      default: null
    },
    valueAsDate: {
      type: Boolean,
      default: false
    },
    resetValue: {
      type: [String, Date],
      default: ''
    },
    placeholder: {
      type: String,
      // Defaults to `labelNoDateSelected` from calendar context
      default: null
    },
    size: {
      type: String,
      default: null
    },
    min: {
      type: [String, Date],
      default: null
    },
    max: {
      type: [String, Date],
      default: null
    },
    disabled: {
      type: Boolean,
      default: false
    },
    readonly: {
      type: Boolean,
      default: false
    },
    required: {
      // If true adds the `aria-required` attribute
      type: Boolean,
      default: false
    },
    name: {
      type: String,
      default: null
    },
    form: {
      type: String,
      default: null
    },
    state: {
      // Tri-state prop: `true`, `false` or `null`
      type: Boolean,
      default: null
    },
    dateDisabledFn: {
      type: Function,
      default: null
    },
    noCloseOnSelect: {
      type: Boolean,
      default: false
    },
    hideHeader: {
      type: Boolean,
      default: false
    },
    locale: {
      type: [String, Array],
      default: null
    },
    startWeekday: {
      // `0` (Sunday), `1` (Monday), ... `6` (Saturday)
      // Day of week to start calendar on
      type: [Number, String],
      default: 0
    },
    direction: {
      type: String,
      default: null
    },
    calendarWidth: {
      // Width of the calendar dropdown
      type: String,
      default: '270px'
    },
    selectedVariant: {
      // Variant color to use for the selected date
      type: String,
      default: 'primary'
    },
    todayVariant: {
      // Variant color to use for today's date (defaults to `variant`)
      type: String,
      default: null
    },
    noHighlightToday: {
      // Disable highlighting today's date
      type: Boolean,
      default: false
    },
    todayButton: {
      type: Boolean,
      default: false
    },
    labelTodayButton: {
      type: String,
      default: () => getComponentConfig(NAME, 'labelTodayButton')
    },
    todayButtonVariant: {
      type: String,
      default: 'outline-primary'
    },
    resetButton: {
      type: Boolean,
      default: false
    },
    labelResetButton: {
      type: String,
      default: () => getComponentConfig(NAME, 'labelResetButton')
    },
    resetButtonVariant: {
      type: String,
      default: 'outline-danger'
    },
    closeButton: {
      type: Boolean,
      default: false
    },
    labelCloseButton: {
      type: String,
      default: () => getComponentConfig(NAME, 'labelCloseButton')
    },
    closeButtonVariant: {
      type: String,
      default: 'outline-secondary'
    },
    // Labels for buttons and keyboard shortcuts
    // These pick BCalendar global config if no BFormDate global config
    labelPrevYear: {
      type: String,
      default: () => getConfigFallback('labelPrevYear')
    },
    labelPrevMonth: {
      type: String,
      default: () => getConfigFallback('labelPrevMonth')
    },
    labelCurrentMonth: {
      type: String,
      default: () => getConfigFallback('labelCurrentMonth')
    },
    labelNextMonth: {
      type: String,
      default: () => getConfigFallback('labelNextMonth')
    },
    labelNextYear: {
      type: String,
      default: () => getConfigFallback('labelNextYear')
    },
    labelToday: {
      type: String,
      default: () => getConfigFallback('labelToday')
    },
    labelSelected: {
      type: String,
      default: () => getConfigFallback('labelSelected')
    },
    labelNoDateSelected: {
      type: String,
      default: () => getConfigFallback('labelNoDateSelected')
    },
    labelCalendar: {
      type: String,
      default: () => getConfigFallback('labelCalendar')
    },
    labelNav: {
      type: String,
      default: () => getConfigFallback('labelNav')
    },
    labelHelp: {
      type: String,
      default: () => getConfigFallback('labelHelp')
    },
    dateFormatOptions: {
      // `Intl.DateTimeFormat` object
      type: Object,
      default: () => ({
        year: 'numeric',
        month: 'long',
        day: 'numeric',
        weekday: 'long'
      })
    },
    // Dark mode
    dark: {
      type: Boolean,
      default: false
    },
    // extra dropdown stuff
    menuClass: {
      type: [String, Array, Object],
      default: null
    },
    ...dropdownProps
  }
}

// --- BFormDate component ---

// @vue/component
export const BFormDatepicker = /*#__PURE__*/ Vue.extend({
  name: NAME,
  // The mixins order determines the order of appearance in the props reference section
  mixins: [idMixin, propsMixin],
  model: {
    prop: 'value',
    event: 'input'
  },
  data() {
    return {
      // We always use `YYYY-MM-DD` value internally
      localYMD: formatYMD(this.value) || '',
      // Context data from BCalendar
      localLocale: null,
      isRTL: false,
      formattedValue: '',
      activeYMD: '',
      // If the popup is open
      isVisible: false
    }
  },
  computed: {
    calendarYM() {
      // Returns the calendar year/month
      // Returns the `YYYY-MM` portion of the active calendar date
      return this.activeYMD.slice(0, -3)
    },
    calendarProps() {
      // Use self for better minification, as `this` won't
      // minimize and we reference it many times below
      const self = this
      return {
        hidden: !self.isVisible,
        value: self.localYMD,
        min: self.min,
        max: self.max,
        readonly: self.readonly,
        disabled: self.disabled,
        locale: self.locale,
        startWeekday: self.startWeekday,
        direction: self.direction,
        width: self.calendarWidth,
        dateDisabledFn: self.dateDisabledFn,
        selectedVariant: self.selectedVariant,
        todayVariant: self.todayVariant,
        hideHeader: self.hideHeader,
        labelPrevYear: self.labelPrevYear,
        labelPrevMonth: self.labelPrevMonth,
        labelCurrentMonth: self.labelCurrentMonth,
        labelNextMonth: self.labelNextMonth,
        labelNextYear: self.labelNextYear,
        labelToday: self.labelToday,
        labelSelected: self.labelSelected,
        labelNoDateSelected: self.labelNoDateSelected,
        labelCalendar: self.labelCalendar,
        labelNav: self.labelNav,
        labelHelp: self.labelHelp,
        dateFormatOptions: self.dateFormatOptions
      }
    },
    computedLang() {
      return (this.localLocale || '').replace(/-u-.*$/i, '') || null
    },
    computedResetValue() {
      return formatYMD(this.resetValue) || ''
    }
  },
  watch: {
    value(newVal) {
      this.localYMD = formatYMD(newVal) || ''
    },
    localYMD(newVal) {
      this.$emit('input', this.valueAsDate ? parseYMD(newVal) || null : newVal || '')
    },
    calendarYM(newVal, oldVal) /* istanbul ignore next */ {
      // Displayed calendar month has changed
      // So possibly the calendar height has changed...
      // We need to update popper computed position
      if (newVal !== oldVal && oldVal) {
        try {
          this.$refs.control.updatePopper()
        } catch {}
      }
    }
  },
  methods: {
    // Public methods
    focus() {
      if (!this.disabled) {
        try {
          this.$refs.control.focus()
        } catch {}
      }
    },
    blur() {
      if (!this.disabled) {
        try {
          this.$refs.control.blur()
        } catch {}
      }
    },
    // Private methods
    setAndClose(ymd) {
      this.localYMD = ymd
      // Close calendar popup, unless `noCloseOnSelect`
      if (!this.noCloseOnSelect) {
        this.$nextTick(() => {
          this.$refs.control.hide(true)
        })
      }
    },
    onSelected(ymd) {
      this.$nextTick(() => {
        this.setAndClose(ymd)
      })
    },
    onInput(ymd) {
      if (this.localYMD !== ymd) {
        this.localYMD = ymd
      }
    },
    onContext(ctx) {
      const { activeYMD, isRTL, locale, selectedYMD, selectedFormatted } = ctx
      this.isRTL = isRTL
      this.localLocale = locale
      this.formattedValue = selectedFormatted
      this.localYMD = selectedYMD
      this.activeYMD = activeYMD
      // Re-emit the context event
      this.$emit('context', ctx)
    },
    onTodayButton() {
      this.setAndClose(formatYMD(createDate()))
    },
    onResetButton() {
      this.setAndClose(this.computedResetValue)
    },
    onCloseButton() {
      this.$refs.control.hide(true)
    },
    // Menu handlers
    onShow() {
      this.isVisible = true
    },
    onShown() {
      this.$nextTick(() => {
        try {
          this.$refs.calendar.focus()
        } catch {}
      })
    },
    onHidden() {
      this.isVisible = false
    },
    // Render helpers
    defaultButtonFn({ isHovered, hasFocus }) {
      return this.$createElement(isHovered || hasFocus ? BIconCalendarFill : BIconCalendar, {
        props: { scale: 1.25 },
        attrs: { 'aria-hidden': 'true' }
      })
    }
  },
  render(h) {
    const localYMD = this.localYMD
    const disabled = this.disabled
    const readonly = this.readonly
    const placeholder = isUndefinedOrNull(this.placeholder)
      ? this.labelNoDateSelected
      : this.placeholder

    // Optional footer buttons
    let $footer = []

    if (this.todayButton) {
      const label = this.labelTodayButton
      $footer.push(
        h(
          BButton,
          {
            props: { size: 'sm', disabled: disabled || readonly, variant: this.todayButtonVariant },
            attrs: { 'aria-label': label || null },
            on: { click: this.onTodayButton }
          },
          label
        )
      )
    }

    if (this.resetButton) {
      const label = this.labelResetButton
      $footer.push(
        h(
          BButton,
          {
            props: { size: 'sm', disabled: disabled || readonly, variant: this.resetButtonVariant },
            attrs: { 'aria-label': label || null },
            on: { click: this.onResetButton }
          },
          label
        )
      )
    }

    if (this.closeButton) {
      const label = this.labelCloseButton
      $footer.push(
        h(
          BButton,
          {
            props: { size: 'sm', disabled, variant: this.closeButtonVariant },
            attrs: { 'aria-label': label || null },
            on: { click: this.onCloseButton }
          },
          label
        )
      )
    }

    if ($footer.length > 0) {
      $footer = [
        h(
          'div',
          {
            staticClass: 'b-form-date-controls d-flex flex-wrap',
            class: {
              'justify-content-between': $footer.length > 1,
              'justify-content-end': $footer.length < 2
            }
          },
          $footer
        )
      ]
    }

    const $calendar = h(
      BCalendar,
      {
        key: 'calendar',
        ref: 'calendar',
        staticClass: 'b-form-date-calendar',
        props: this.calendarProps,
        on: {
          selected: this.onSelected,
          input: this.onInput,
          context: this.onContext
        }
      },
      $footer
    )

    return h(
      BVFormBtnLabelControl,
      {
        ref: 'control',
        staticClass: 'b-form-datepicker',
        props: {
          // This adds unneeded props, but reduces code size:
          ...this.$props,
          // Overridden / computed props
          id: this.safeId(),
          rtl: this.isRTL,
          lang: this.computedLang,
          value: localYMD || '',
          formattedValue: localYMD ? this.formattedValue : '',
          placeholder: placeholder || '',
          menuClass: [{ 'bg-dark': !!this.dark, 'text-light': !!this.dark }, this.menuClass]
        },
        on: {
          show: this.onShow,
          shown: this.onShown,
          hidden: this.onHidden
        },
        scopedSlots: {
          'button-content': this.$scopedSlots['button-content'] || this.defaultButtonFn
        }
      },
      [$calendar]
    )
  }
})
<|endoftext|>


<commit_before>;(function() {

  // Only run this script on tweetdeck:
  if ( window.location.host.indexOf('tweetdeck.twitter.com') !== 0 ) return;

  var conifg = [

    // Streams
    {
      placement: 'tweekdeck-stream',
      selector: '.js-stream-item .js-tweet-actions:not(.buffer-inserted)',
      $button: $([
        '
',
          '',
            '',
            'Buffer ',
          '',
        '
'
      ].join('')),
      insert: function(el) {
        var $actions = $(el);
        var $newActionItem = this.$button.clone();

        $actions
          .addClass('buffer-inserted')
          .prepend($newActionItem);

        return $newActionItem;
      },
      getData: function(el) {
        var $streamItem = $(el).closest('.js-stream-item');

        var $text = $streamItem.find('.js-tweet-text');
        var $screenname = $streamItem.find('.username').first();
        var screenname = $screenname.text().trim().replace(/^@/, '');
        var text = getFullTweetText($text, screenname);
        var displayName = $streamItem.find('.fullname').first().text().trim();
        var tweetKey = $streamItem.attr('data-key');
        var $tweetAction;
        var retweeted_tweet_id;

        // Retweet
        if (tweetKey.indexOf('quoted_tweet') == 0) {
          $tweetAction = $streamItem.find('.tweet-action[data-user-id]');
          retweeted_tweet_id = $tweetAction.attr('data-chirp-id');
        // Regular tweet
        } else {
          retweeted_tweet_id = tweetKey;
        }

        return {
          text: text,
          placement: this.placement,
          retweeted_tweet_id: retweeted_tweet_id,
          retweeted_user_name: screenname,
          retweeted_user_display_name: displayName
        };
      }
    },

    // Tweet Detail
    {
      placement: 'tweetdeck-detail',
      selector: '.js-tweet-detail .tweet-detail-actions:not(.buffer-inserted)',
      $button: $([
        '
',
          '',
            '',
            'Buffer ',
          '',
        '
'
      ].join('')),
      insert: function(el) {
        var $actions = $(el);
        var $newActionItem = this.$button.clone();

        $actions
          .addClass('buffer-inserted')
          .prepend($newActionItem);

        return $newActionItem;
      },
      getData: function(el) {
        var $streamItem = $(el).closest('.js-stream-item');

        var $text = $streamItem.find('.js-tweet-text');
        var $screenname = $streamItem.find('.username').first();
        var screenname = $screenname.text().trim().replace(/^@/, '');
        var text = getFullTweetText($text, screenname);
        var displayName = $streamItem.find('.fullname').first().text().trim();

        return {
          text: text,
          placement: this.placement,
          retweeted_tweet_id: $streamItem.attr('data-key'),
          retweeted_user_name: screenname,
          retweeted_user_display_name: displayName
        };
      }
    },

    // Composer slide-out
    {
      placement: 'tweetdeck-composer',
      selector: '.js-docked-compose .js-send-button-container:not(.buffer-inserted)',
      $button: $([
        '<button class="js-buffer-button btn-buffer is-disabled btn btn-extra-height">',
          'Buffer',
        '</button>'
      ].join('')),
      insert: function(el) {
        var $actions = $(el);
        var $newActionItem = this.$button.clone();

        $actions
          .addClass('buffer-inserted')
          .prepend($newActionItem);

        var $container = $(el).closest('.js-docked-compose');
        this.$newActionItem = $newActionItem;
        this.$textarea = $container.find('.js-compose-text');
        this.$charCount = $container.find('.js-character-count');

        // Listen to changes in the textarea
        this.$textarea.on('keyup focus blur change paste cut', this.onKeyup.bind(this));

        return $newActionItem;
      },
      onKeyup: function(e) {
        // NOTE - We check the live character count b/c the URL will be shortened
        var count = parseInt(this.$charCount.val(), 10);
        if (count < 140 && count >= 0) {
          this.$newActionItem.removeClass('is-disabled');
        } else {
          this.$newActionItem.addClass('is-disabled');
        }
      },
      getData: function(el) {
        var $container = $(el).closest('.js-docked-compose');
        var $quotedTweet = $container.find('.quoted-tweet');
        var isRTwcomment = !!$quotedTweet.length;
        var composerText = this.$textarea.val().trim();
        var $text;
        var username;

        if (isRTwcomment) {
          $text = $quotedTweet.find('.js-quoted-tweet-text');
          username = $quotedTweet.find('.username').text().trim().replace(/^@/, '');

          return {
            text: getFullTweetText($text, username),
            placement: this.placement,
            retweeted_tweet_id: $quotedTweet.attr('data-tweet-id'),
            retweeted_user_name: username,
            retweeted_user_display_name: $quotedTweet.find('.fullname').text().trim(),
            retweet_comment: composerText
          };
        } else {
          return {
            text: composerText,
            placement: this.placement
          };
        }
      }
    }

  ];

  // RT modal - This does not work on the loop
  var RTConfig = {
    placement: 'tweetdeck-rt-modal',
    selector: '#actions-modal .js-retweet-button:not(.buffer-inserted)',
    $button: $([
      '<button data-action="buffer" class="js-buffer-button btn btn-buffer">',
        'Buffer Retweet',
      '</button>'
    ].join('')),
    insert: function(el) {
      var $rtButton = $(el);
      $rtButton.addClass('buffer-inserted');
      var $actions = $rtButton.parent();

      var $newActionItem = this.$button.clone();
      $actions.append($newActionItem);

      return $newActionItem;
    },
    getData: function(el) {
      var $tweet = $('#actions-modal .js-tweet');

      var $text = $tweet.find('.js-tweet-text');
      var $screenname = $tweet.find('.username');
      var screenname = $screenname.text().trim().replace(/^@/, '');
      var text = getFullTweetText($text, screenname);
      var displayName = $tweet.find('.fullname').text().trim();
      var tweetURL = $tweet.find('.js-timestamp a').attr('href');

      // NOTE - this is kind of a strict regex, may need to change if URLs change
      var tweetId = tweetURL.replace(/https:\/\/twitter.com\/\w+\/status\//, '');

      return {
        text: text,
        placement: this.placement,
        retweeted_tweet_id: tweetId,
        retweeted_user_name: screenname,
        retweeted_user_display_name: displayName
      };
    }
  };

  var getFullTweetText = function($text, screenname) {
    var $clone = $text.clone();

    // Replace any shortened URL with the full url
    $clone.find('a').each(function(i, el) {
      el.textContent = el.getAttribute('data-full-url');
    });

    return 'RT @' + screenname + ': ' + $clone.text().trim() + '';
  };

  var insertButton = function(target) {
    $(target.selector).each(function(i, el) {
      var $button = target.insert(el);
      $button.on('click', function(e) {
        e.preventDefault();
        xt.port.emit('buffer_click', target.getData(el));
      })
    });
  };

  var insertButtons = function() {
    conifg.forEach(insertButton);
  };

  var tweetdeckLoop = function() {
    insertButtons();
    setTimeout(tweetdeckLoop, 500);
  };

  var darkStylesheet = document.head.querySelector('link[title="dark"]');

  var checkDarkTheme = function() {
    if (!!darkStylesheet && !darkStylesheet.disabled) {
      document.body.classList.add('buffer-tweetdeck-dark');
    } else {
      document.body.classList.remove('buffer-tweetdeck-dark');
    }
  };

  var start = function() {

    // Add class for css scoping
    document.body.classList.add('buffer-tweetdeck');

    $(document).ready(function() {
      checkDarkTheme();

      // Observe future changes to the 'disabled' attribute of link[title=dark]
      // Useful if the app hasn't initialized, or if settings are changed later on
      if (window.MutationObserver) {
        var observer = new MutationObserver(checkDarkTheme);
        observer.observe(darkStylesheet, { attributes: true, attributeFilter: ['disabled'] });
      }
    });

    // Start the loop that will watch for new DOM elements
    tweetdeckLoop();

    // Listen for a RT click so we can inject into the modal instantly
    // This addresses the potentially 500ms delay
    $('body').on('click',
      '.js-stream-item .js-tweet-actions .tweet-action[rel="retweet"],' +
      '.js-tweet-detail .tweet-detail-actions .tweet-detail-action[rel="retweet"]',
      function() {
        // 10ms delay for modal to open
        setTimeout(function() {
          insertButton(RTConfig);
        }, 10);
      });
  }

  // Wait for xt.options to be set
  ;(function check() {
    // If tweetdeck is switched on, start the main loop
    if (!xt.options) {
      setTimeout(check, 0);
    } else if (xt.options['buffer.op.tweetdeck'] === 'tweetdeck') {
      start();
    } else {
      setTimeout(check, 2000);
    }
  }());

}());
<commit_msg>Add RT w/ comment support to Tweetdeck mentions and favs<commit_after>;(function() {

  // Only run this script on tweetdeck:
  if ( window.location.host.indexOf('tweetdeck.twitter.com') !== 0 ) return;

  var conifg = [

    // Streams
    {
      placement: 'tweekdeck-stream',
      selector: '.js-stream-item .js-tweet-actions:not(.buffer-inserted)',
      $button: $([
        '
',
          '',
            '',
            'Buffer ',
          '',
        '
'
      ].join('')),
      insert: function(el) {
        var $actions = $(el);
        var $newActionItem = this.$button.clone();

        $actions
          .addClass('buffer-inserted')
          .prepend($newActionItem);

        return $newActionItem;
      },
      getData: function(el) {
        var $streamItem = $(el).closest('.js-stream-item');

        var $text = $streamItem.find('.js-tweet-text');
        var $screenname = $streamItem.find('.username').first();
        var screenname = $screenname.text().trim().replace(/^@/, '');
        var text = getFullTweetText($text, screenname);
        var displayName = $streamItem.find('.fullname').first().text().trim();
        var tweetKey = $streamItem.attr('data-key');
        var $tweetAction;
        var retweeted_tweet_id;

        // Retweet, mention, favorite: non-regular tweet whose data-key attr
        // is formatted differently, and for which we need to get the tweet_id
        // from somewhere else
        if (/^(?:quoted_tweet|favorite|mention)/.test(tweetKey)) {
          $tweetAction = $streamItem.find('.tweet-action[data-user-id]');
          retweeted_tweet_id = $tweetAction.attr('data-chirp-id');
        // Regular tweet
        } else {
          retweeted_tweet_id = tweetKey;
        }

        return {
          text: text,
          placement: this.placement,
          retweeted_tweet_id: retweeted_tweet_id,
          retweeted_user_name: screenname,
          retweeted_user_display_name: displayName
        };
      }
    },

    // Tweet Detail
    {
      placement: 'tweetdeck-detail',
      selector: '.js-tweet-detail .tweet-detail-actions:not(.buffer-inserted)',
      $button: $([
        '
',
          '',
            '',
            'Buffer ',
          '',
        '
'
      ].join('')),
      insert: function(el) {
        var $actions = $(el);
        var $newActionItem = this.$button.clone();

        $actions
          .addClass('buffer-inserted')
          .prepend($newActionItem);

        return $newActionItem;
      },
      getData: function(el) {
        var $streamItem = $(el).closest('.js-stream-item');

        var $text = $streamItem.find('.js-tweet-text');
        var $screenname = $streamItem.find('.username').first();
        var screenname = $screenname.text().trim().replace(/^@/, '');
        var text = getFullTweetText($text, screenname);
        var displayName = $streamItem.find('.fullname').first().text().trim();

        return {
          text: text,
          placement: this.placement,
          retweeted_tweet_id: $streamItem.attr('data-key'),
          retweeted_user_name: screenname,
          retweeted_user_display_name: displayName
        };
      }
    },

    // Composer slide-out
    {
      placement: 'tweetdeck-composer',
      selector: '.js-docked-compose .js-send-button-container:not(.buffer-inserted)',
      $button: $([
        '<button class="js-buffer-button btn-buffer is-disabled btn btn-extra-height">',
          'Buffer',
        '</button>'
      ].join('')),
      insert: function(el) {
        var $actions = $(el);
        var $newActionItem = this.$button.clone();

        $actions
          .addClass('buffer-inserted')
          .prepend($newActionItem);

        var $container = $(el).closest('.js-docked-compose');
        this.$newActionItem = $newActionItem;
        this.$textarea = $container.find('.js-compose-text');
        this.$charCount = $container.find('.js-character-count');

        // Listen to changes in the textarea
        this.$textarea.on('keyup focus blur change paste cut', this.onKeyup.bind(this));

        return $newActionItem;
      },
      onKeyup: function(e) {
        // NOTE - We check the live character count b/c the URL will be shortened
        var count = parseInt(this.$charCount.val(), 10);
        if (count < 140 && count >= 0) {
          this.$newActionItem.removeClass('is-disabled');
        } else {
          this.$newActionItem.addClass('is-disabled');
        }
      },
      getData: function(el) {
        var $container = $(el).closest('.js-docked-compose');
        var $quotedTweet = $container.find('.quoted-tweet');
        var isRTwcomment = !!$quotedTweet.length;
        var composerText = this.$textarea.val().trim();
        var $text;
        var username;

        if (isRTwcomment) {
          $text = $quotedTweet.find('.js-quoted-tweet-text');
          username = $quotedTweet.find('.username').text().trim().replace(/^@/, '');

          return {
            text: getFullTweetText($text, username),
            placement: this.placement,
            retweeted_tweet_id: $quotedTweet.attr('data-tweet-id'),
            retweeted_user_name: username,
            retweeted_user_display_name: $quotedTweet.find('.fullname').text().trim(),
            retweet_comment: composerText
          };
        } else {
          return {
            text: composerText,
            placement: this.placement
          };
        }
      }
    }

  ];

  // RT modal - This does not work on the loop
  var RTConfig = {
    placement: 'tweetdeck-rt-modal',
    selector: '#actions-modal .js-retweet-button:not(.buffer-inserted)',
    $button: $([
      '<button data-action="buffer" class="js-buffer-button btn btn-buffer">',
        'Buffer Retweet',
      '</button>'
    ].join('')),
    insert: function(el) {
      var $rtButton = $(el);
      $rtButton.addClass('buffer-inserted');
      var $actions = $rtButton.parent();

      var $newActionItem = this.$button.clone();
      $actions.append($newActionItem);

      return $newActionItem;
    },
    getData: function(el) {
      var $tweet = $('#actions-modal .js-tweet');

      var $text = $tweet.find('.js-tweet-text');
      var $screenname = $tweet.find('.username');
      var screenname = $screenname.text().trim().replace(/^@/, '');
      var text = getFullTweetText($text, screenname);
      var displayName = $tweet.find('.fullname').text().trim();
      var tweetURL = $tweet.find('.js-timestamp a').attr('href');

      // NOTE - this is kind of a strict regex, may need to change if URLs change
      var tweetId = tweetURL.replace(/https:\/\/twitter.com\/\w+\/status\//, '');

      return {
        text: text,
        placement: this.placement,
        retweeted_tweet_id: tweetId,
        retweeted_user_name: screenname,
        retweeted_user_display_name: displayName
      };
    }
  };

  var getFullTweetText = function($text, screenname) {
    var $clone = $text.clone();

    // Replace any shortened URL with the full url
    $clone.find('a').each(function(i, el) {
      el.textContent = el.getAttribute('data-full-url');
    });

    return 'RT @' + screenname + ': ' + $clone.text().trim() + '';
  };

  var insertButton = function(target) {
    $(target.selector).each(function(i, el) {
      var $button = target.insert(el);
      $button.on('click', function(e) {
        e.preventDefault();
        xt.port.emit('buffer_click', target.getData(el));
      })
    });
  };

  var insertButtons = function() {
    conifg.forEach(insertButton);
  };

  var tweetdeckLoop = function() {
    insertButtons();
    setTimeout(tweetdeckLoop, 500);
  };

  var darkStylesheet = document.head.querySelector('link[title="dark"]');

  var checkDarkTheme = function() {
    if (!!darkStylesheet && !darkStylesheet.disabled) {
      document.body.classList.add('buffer-tweetdeck-dark');
    } else {
      document.body.classList.remove('buffer-tweetdeck-dark');
    }
  };

  var start = function() {

    // Add class for css scoping
    document.body.classList.add('buffer-tweetdeck');

    $(document).ready(function() {
      checkDarkTheme();

      // Observe future changes to the 'disabled' attribute of link[title=dark]
      // Useful if the app hasn't initialized, or if settings are changed later on
      if (window.MutationObserver) {
        var observer = new MutationObserver(checkDarkTheme);
        observer.observe(darkStylesheet, { attributes: true, attributeFilter: ['disabled'] });
      }
    });

    // Start the loop that will watch for new DOM elements
    tweetdeckLoop();

    // Listen for a RT click so we can inject into the modal instantly
    // This addresses the potentially 500ms delay
    $('body').on('click',
      '.js-stream-item .js-tweet-actions .tweet-action[rel="retweet"],' +
      '.js-tweet-detail .tweet-detail-actions .tweet-detail-action[rel="retweet"]',
      function() {
        // 10ms delay for modal to open
        setTimeout(function() {
          insertButton(RTConfig);
        }, 10);
      });
  }

  // Wait for xt.options to be set
  ;(function check() {
    // If tweetdeck is switched on, start the main loop
    if (!xt.options) {
      setTimeout(check, 0);
    } else if (xt.options['buffer.op.tweetdeck'] === 'tweetdeck') {
      start();
    } else {
      setTimeout(check, 2000);
    }
  }());

}());
<|endoftext|>


<commit_before>import React from 'react';
import { addStoryInGroup, MID_LEVEL_BLOCKS } from '../../../.storybook/utils';
import { select, boolean } from '@storybook/addon-knobs';
import SplitButton from './SplitButton';
import MenuDivider from '../menu/MenuDivider';
import MenuItem from '../menu/MenuItem';

const levels = ['primary', 'secondary', 'destructive'];
const sizes = ['small', 'medium', 'large'];

const handleButtonClick = () => {
  console.log('clicked main button');
};

const handleMenuItemClick = () => {
  console.log('clicked menu item');
};

export default {
  component: SplitButton,
  title: addStoryInGroup(MID_LEVEL_BLOCKS, 'Split button'),
};

export const basic = () => (
  <SplitButton
    level={select('Level', levels, 'primary')}
    onButtonClick={handleButtonClick}
    size={select('Size', sizes, 'medium')}
    disabled={boolean('Disabled', false)}
  >
    <MenuItem onClick={handleMenuItemClick} label="Main action" />
    <MenuItem onClick={handleMenuItemClick} label="Via file upload" caption="(.CVS, . XLS & .XLSX)" />
    <MenuDivider />
    <MenuItem onClick={handleMenuItemClick} label="Via Marketplace integrations" />
  </SplitButton>
);
<commit_msg>SplitButton: update story<commit_after>import React from 'react';
import { addStoryInGroup, MID_LEVEL_BLOCKS } from '../../../.storybook/utils';
import SplitButton from './SplitButton';
import MenuDivider from '../menu/MenuDivider';
import MenuItem from '../menu/MenuItem';

const handleButtonClick = () => {
  console.log('clicked main button');
};

const handleMenuItemClick = () => {
  console.log('clicked menu item');
};

export default {
  component: SplitButton,
  title: addStoryInGroup(MID_LEVEL_BLOCKS, 'Split button'),
};

export const DefaultStory = (args) => (
  <SplitButton {...args} onButtonClick={handleButtonClick}>
    <MenuItem onClick={handleMenuItemClick} label="Main action" />
    <MenuItem onClick={handleMenuItemClick} label="Via file upload" caption="(.CVS, . XLS & .XLSX)" />
    <MenuDivider />
    <MenuItem onClick={handleMenuItemClick} label="Via Marketplace integrations" />
  </SplitButton>
);
<|endoftext|>


<commit_before>'use strict';

/**
 * Module for communication with Maloja
 */
define((require) => {
	const Util = require('util/util');
	const BaseScrobbler = require('scrobbler/base-scrobbler');
	const ServiceCallResult = require('object/service-call-result');

	class Maloja extends BaseScrobbler {

		/** @override */
		getStorageName() {
			return 'Maloja';
		}

		/** @override */
		getId() {
			return 'maloja';
		}

		/** @override */
		getLabel() {
			return 'Maloja';
		}

		/** @override */
		getStatusUrl() {
			return null;
		}

		/** @override */
		getUsedDefinedProperties() {
			return ['userApiUrl', 'userToken'];
		}

		/** @override */
		getProfileUrl() {
			return null;
		}

		/** @override */
		async getAuthUrl() {
			return null;
		}

		/** @override */
		async getSession() {
			if (!this.userToken) {
				throw ServiceCallResult.ERROR_AUTH;
			}
			return { sessionID: this.userToken };
		}

		/** @override */
		async isReadyForGrantAccess() {
			return false;
		}

		/** @override */
		async sendNowPlaying() {
			// Maloja does not support "now playing" scrobbles.
			return ServiceCallResult.RESULT_OK;
		}

		/** @override */
		async scrobble(song) {
			const songData = this.makeTrackMetadata(song);

			return this.sendRequest(songData, this.userToken);
		}

		/** Private methods */

		async sendRequest(params, sessionID) {
			params.key = sessionID;

			const requestInfo = {
				method: 'POST',
				headers: {
					'Content-Type': 'application/json',
				},
				body: JSON.stringify(params),
			};

			const promise = fetch(this.userApiUrl, requestInfo);
			const timeout = BaseScrobbler.REQUEST_TIMEOUT;

			let response = null;
			try {
				response = await Util.timeoutPromise(timeout, promise);
				if (response.status !== 200) {
					return ServiceCallResult.ERROR_OTHER;
				}
			} catch (e) {
				this.debugLog('Error while sending request', 'error');
				return ServiceCallResult.ERROR_OTHER;
			}

			return ServiceCallResult.RESULT_OK;
		}

		makeTrackMetadata(song) {
			const trackMeta = {
				artist: song.getArtist(),
				title: song.getTrack(),
				time: song.metadata.startTimestamp,
			};

			return trackMeta;
		}
	}

	return Maloja;
});
<commit_msg>Send album and album artist data to maloja (#3307)<commit_after>'use strict';

/**
 * Module for communication with Maloja
 */
define((require) => {
	const Util = require('util/util');
	const BaseScrobbler = require('scrobbler/base-scrobbler');
	const ServiceCallResult = require('object/service-call-result');

	class Maloja extends BaseScrobbler {

		/** @override */
		getStorageName() {
			return 'Maloja';
		}

		/** @override */
		getId() {
			return 'maloja';
		}

		/** @override */
		getLabel() {
			return 'Maloja';
		}

		/** @override */
		getStatusUrl() {
			return null;
		}

		/** @override */
		getUsedDefinedProperties() {
			return ['userApiUrl', 'userToken'];
		}

		/** @override */
		getProfileUrl() {
			return null;
		}

		/** @override */
		async getAuthUrl() {
			return null;
		}

		/** @override */
		async getSession() {
			if (!this.userToken) {
				throw ServiceCallResult.ERROR_AUTH;
			}
			return { sessionID: this.userToken };
		}

		/** @override */
		async isReadyForGrantAccess() {
			return false;
		}

		/** @override */
		async sendNowPlaying() {
			// Maloja does not support "now playing" scrobbles.
			return ServiceCallResult.RESULT_OK;
		}

		/** @override */
		async scrobble(song) {
			const songData = this.makeTrackMetadata(song);

			return this.sendRequest(songData, this.userToken);
		}

		/** Private methods */

		async sendRequest(params, sessionID) {
			params.key = sessionID;

			const requestInfo = {
				method: 'POST',
				headers: {
					'Content-Type': 'application/json',
				},
				body: JSON.stringify(params),
			};

			const promise = fetch(this.userApiUrl, requestInfo);
			const timeout = BaseScrobbler.REQUEST_TIMEOUT;

			let response = null;
			try {
				response = await Util.timeoutPromise(timeout, promise);
				if (response.status !== 200) {
					return ServiceCallResult.ERROR_OTHER;
				}
			} catch (e) {
				this.debugLog('Error while sending request', 'error');
				return ServiceCallResult.ERROR_OTHER;
			}

			return ServiceCallResult.RESULT_OK;
		}

		makeTrackMetadata(song) {
			const trackMeta = {
				artist: song.getArtist(),
				title: song.getTrack(),
				time: song.metadata.startTimestamp,
			};

			if (song.getAlbum()) {
				trackMeta.album = song.getAlbum();
			}

			if (song.getAlbumArtist()) {
				trackMeta.albumartists = [song.getAlbumArtist()];
			}

			return trackMeta;
		}
	}

	return Maloja;
});
<|endoftext|>