* add Patch CSP option * show style version, size, and update age in manager * add scope selector to style search in manager * keep scroll position and selections in tab's session * directly install usercss from raw github links * ditch localStorage, use on-demand SessionStore proxy * simplify localization * allow <code> tag in i18n-html * keep   nodes in HTML templates * API.getAllStyles is actually faster with code untouched * fix fitToContent when applies-to is taller than window * dedupe linter.enableForEditor calls * prioritize visible CMs in refreshOnViewListener * don't scroll to last style on editing a new one * delay colorview for invisible CMs * eslint comma-dangle error + autofix files * styleViaXhr: also toggle for disableAll pref * styleViaXhr: allow cookies for sandbox CSP * simplify notes in options * simplify getStylesViaXhr * oldUI fixups: * remove separator before 1st applies-to * center name bubbles * fix updateToc focus on a newly added section * fix fitToContent when cloning section * remove CSS `contain` as it makes no difference * replace overrides with declarative CSS + code cosmetics * simplify adjustWidth and make it work in FF
		
			
				
	
	
		
			220 lines
		
	
	
		
			5.4 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			220 lines
		
	
	
		
			5.4 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| /* global
 | |
|   $create
 | |
|   CodeMirror
 | |
|   prefs
 | |
| */
 | |
| 'use strict';
 | |
| 
 | |
| /* exported DirtyReporter */
 | |
| class DirtyReporter {
 | |
|   constructor() {
 | |
|     this._dirty = new Map();
 | |
|     this._onchange = new Set();
 | |
|   }
 | |
| 
 | |
|   add(obj, value) {
 | |
|     const wasDirty = this.isDirty();
 | |
|     const saved = this._dirty.get(obj);
 | |
|     if (!saved) {
 | |
|       this._dirty.set(obj, {type: 'add', newValue: value});
 | |
|     } else if (saved.type === 'remove') {
 | |
|       if (saved.savedValue === value) {
 | |
|         this._dirty.delete(obj);
 | |
|       } else {
 | |
|         saved.newValue = value;
 | |
|         saved.type = 'modify';
 | |
|       }
 | |
|     }
 | |
|     this.notifyChange(wasDirty);
 | |
|   }
 | |
| 
 | |
|   remove(obj, value) {
 | |
|     const wasDirty = this.isDirty();
 | |
|     const saved = this._dirty.get(obj);
 | |
|     if (!saved) {
 | |
|       this._dirty.set(obj, {type: 'remove', savedValue: value});
 | |
|     } else if (saved.type === 'add') {
 | |
|       this._dirty.delete(obj);
 | |
|     } else if (saved.type === 'modify') {
 | |
|       saved.type = 'remove';
 | |
|     }
 | |
|     this.notifyChange(wasDirty);
 | |
|   }
 | |
| 
 | |
|   modify(obj, oldValue, newValue) {
 | |
|     const wasDirty = this.isDirty();
 | |
|     const saved = this._dirty.get(obj);
 | |
|     if (!saved) {
 | |
|       if (oldValue !== newValue) {
 | |
|         this._dirty.set(obj, {type: 'modify', savedValue: oldValue, newValue});
 | |
|       }
 | |
|     } else if (saved.type === 'modify') {
 | |
|       if (saved.savedValue === newValue) {
 | |
|         this._dirty.delete(obj);
 | |
|       } else {
 | |
|         saved.newValue = newValue;
 | |
|       }
 | |
|     } else if (saved.type === 'add') {
 | |
|       saved.newValue = newValue;
 | |
|     }
 | |
|     this.notifyChange(wasDirty);
 | |
|   }
 | |
| 
 | |
|   clear(obj) {
 | |
|     const wasDirty = this.isDirty();
 | |
|     if (obj === undefined) {
 | |
|       this._dirty.clear();
 | |
|     } else {
 | |
|       this._dirty.delete(obj);
 | |
|     }
 | |
|     this.notifyChange(wasDirty);
 | |
|   }
 | |
| 
 | |
|   isDirty() {
 | |
|     return this._dirty.size > 0;
 | |
|   }
 | |
| 
 | |
|   onChange(cb, add = true) {
 | |
|     this._onchange[add ? 'add' : 'delete'](cb);
 | |
|   }
 | |
| 
 | |
|   notifyChange(wasDirty) {
 | |
|     if (wasDirty !== this.isDirty()) {
 | |
|       this._onchange.forEach(cb => cb());
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   has(key) {
 | |
|     return this._dirty.has(key);
 | |
|   }
 | |
| }
 | |
| 
 | |
| /* exported DocFuncMapper */
 | |
| const DocFuncMapper = {
 | |
|   TO_CSS: {
 | |
|     urls: 'url',
 | |
|     urlPrefixes: 'url-prefix',
 | |
|     domains: 'domain',
 | |
|     regexps: 'regexp',
 | |
|   },
 | |
|   FROM_CSS: {
 | |
|     'url': 'urls',
 | |
|     'url-prefix': 'urlPrefixes',
 | |
|     'domain': 'domains',
 | |
|     'regexp': 'regexps',
 | |
|   },
 | |
|   /**
 | |
|    * @param {Object} section
 | |
|    * @param {function(func:string, value:string)} fn
 | |
|    */
 | |
|   forEachProp(section, fn) {
 | |
|     for (const [propName, func] of Object.entries(DocFuncMapper.TO_CSS)) {
 | |
|       const props = section[propName];
 | |
|       if (props) props.forEach(value => fn(func, value));
 | |
|     }
 | |
|   },
 | |
|   /**
 | |
|    * @param {Array<?[type,value]>} funcItems
 | |
|    * @param {?Object} [section]
 | |
|    * @returns {Object} section
 | |
|    */
 | |
|   toSection(funcItems, section = {}) {
 | |
|     for (const item of funcItems) {
 | |
|       const [func, value] = item || [];
 | |
|       const propName = DocFuncMapper.FROM_CSS[func];
 | |
|       if (propName) {
 | |
|         const props = section[propName] || (section[propName] = []);
 | |
|         if (Array.isArray(value)) props.push(...value);
 | |
|         else props.push(value);
 | |
|       }
 | |
|     }
 | |
|     return section;
 | |
|   },
 | |
| };
 | |
| 
 | |
| /* exported sectionsToMozFormat */
 | |
| function sectionsToMozFormat(style) {
 | |
|   return style.sections.map(section => {
 | |
|     const cssFuncs = [];
 | |
|     DocFuncMapper.forEachProp(section, (type, value) =>
 | |
|       cssFuncs.push(`${type}("${value.replace(/\\/g, '\\\\')}")`));
 | |
|     return cssFuncs.length ?
 | |
|       `@-moz-document ${cssFuncs.join(', ')} {\n${section.code}\n}` :
 | |
|       section.code;
 | |
|   }).join('\n\n');
 | |
| }
 | |
| 
 | |
| /* exported trimCommentLabel */
 | |
| function trimCommentLabel(str, limit = 1000) {
 | |
|   // stripping /*** foo ***/ to foo
 | |
|   return clipString(str.replace(/^[!-/:;=\s]*|[-#$&(+,./:;<=>\s*]*$/g, ''), limit);
 | |
| }
 | |
| 
 | |
| /* exported clipString */
 | |
| function clipString(str, limit = 100) {
 | |
|   return str.length <= limit ? str : str.substr(0, limit) + '...';
 | |
| }
 | |
| 
 | |
| /* exported memoize */
 | |
| function memoize(fn) {
 | |
|   let cached = false;
 | |
|   let result;
 | |
|   return (...args) => {
 | |
|     if (!cached) {
 | |
|       result = fn(...args);
 | |
|       cached = true;
 | |
|     }
 | |
|     return result;
 | |
|   };
 | |
| }
 | |
| 
 | |
| /* exported createHotkeyInput */
 | |
| /**
 | |
|  * @param {!string} prefId
 | |
|  * @param {?function(isEnter:boolean)} onDone
 | |
|  */
 | |
| function createHotkeyInput(prefId, onDone = () => {}) {
 | |
|   return $create('input', {
 | |
|     type: 'search',
 | |
|     spellcheck: false,
 | |
|     value: prefs.get(prefId),
 | |
|     onkeydown(event) {
 | |
|       const key = CodeMirror.keyName(event);
 | |
|       if (key === 'Tab' || key === 'Shift-Tab') {
 | |
|         return;
 | |
|       }
 | |
|       event.preventDefault();
 | |
|       event.stopPropagation();
 | |
|       switch (key) {
 | |
|         case 'Enter':
 | |
|           if (this.checkValidity()) onDone(true);
 | |
|           return;
 | |
|         case 'Esc':
 | |
|           onDone(false);
 | |
|           return;
 | |
|         default:
 | |
|           // disallow: [Shift?] characters, modifiers-only, [modifiers?] + Esc, Tab, nav keys
 | |
|           if (!key || new RegExp('^(' + [
 | |
|             '(Back)?Space',
 | |
|             '(Shift-)?.', // a single character
 | |
|             '(Shift-?|Ctrl-?|Alt-?|Cmd-?){0,2}(|Esc|Tab|(Page)?(Up|Down)|Left|Right|Home|End|Insert|Delete)',
 | |
|           ].join('|') + ')$', 'i').test(key)) {
 | |
|             this.value = key || this.value;
 | |
|             this.setCustomValidity('Not allowed');
 | |
|             return;
 | |
|           }
 | |
|       }
 | |
|       this.value = key;
 | |
|       this.setCustomValidity('');
 | |
|       prefs.set(prefId, key);
 | |
|     },
 | |
|     oninput() {
 | |
|       // fired on pressing "x" to clear the field
 | |
|       prefs.set(prefId, '');
 | |
|     },
 | |
|     onpaste(event) {
 | |
|       event.preventDefault();
 | |
|     },
 | |
|   });
 | |
| }
 |