Refactor the entire storage system and the section editor (#518)
* Squashed commit of the following: commitd84c4dc3fe
Author: eight <eight04@gmail.com> Date: Sun Oct 14 19:13:29 2018 +0800 Fix: remove unused comment commit46027120ec
Author: eight <eight04@gmail.com> Date: Sun Oct 14 19:09:06 2018 +0800 Add: handle styleUpdated message commitf85d4de39b
Author: eight <eight04@gmail.com> Date: Sun Oct 14 18:59:29 2018 +0800 Fix: handle styleAdded message in popup commit81f3e69574
Author: eight <eight04@gmail.com> Date: Sun Oct 14 18:50:54 2018 +0800 Change: getStylesInfoByUrl -> getStylesByUrl commitf9dc04558f
Author: eight <eight04@gmail.com> Date: Sun Oct 14 18:48:20 2018 +0800 Fix: drop getStylesInfo commitfea04d591f
Author: eight <eight04@gmail.com> Date: Sun Oct 14 18:39:28 2018 +0800 Fix: remove unused ignoreChromeError commit2aff14e213
Author: eight <eight04@gmail.com> Date: Sun Oct 14 18:09:53 2018 +0800 Fix: don't dup promisify in prefs commitd4ddfcc713
Author: eight <eight04@gmail.com> Date: Sun Oct 14 17:56:16 2018 +0800 Change: drop .last and .rotate commit85e70491e4
Author: eight <eight04@gmail.com> Date: Sun Oct 14 17:36:00 2018 +0800 Fix: unused renderIndex commit7acb131642
Author: eight <eight04@gmail.com> Date: Sun Oct 14 17:32:49 2018 +0800 Fix: update title on input commita39405ac4c
Author: eight <eight04@gmail.com> Date: Sun Oct 14 17:17:20 2018 +0800 Fix: remove unused messages commit14c2fdbb58
Author: eight <eight04@gmail.com> Date: Sun Oct 14 16:36:12 2018 +0800 Fix: dirty state for new added applies commitfb1b49b8bb
Author: eight <eight04@gmail.com> Date: Sun Oct 14 16:27:17 2018 +0800 Fix: minor commit2c2d849fa4
Author: eight <eight04@gmail.com> Date: Sun Oct 14 16:20:14 2018 +0800 Fix: drop unused getCode commitf133c3e67a
Author: eight <eight04@gmail.com> Date: Sun Oct 14 16:18:14 2018 +0800 Fix: drop unused lastActive commit05a6208f5c
Author: eight <eight04@gmail.com> Date: Sun Oct 14 16:17:45 2018 +0800 Fix: minor commit05a87ed00f
Author: eight <eight04@gmail.com> Date: Sun Oct 14 15:58:33 2018 +0800 Fix: minor commit576f73f333
Author: eight <eight04@gmail.com> Date: Sun Oct 14 03:03:35 2018 +0800 Fix: always register listeners commite93819deb4
Author: eight <eight04@gmail.com> Date: Sun Oct 14 02:58:49 2018 +0800 Fix: unused statement commit39b11685b4
Author: eight <eight04@gmail.com> Date: Sun Oct 14 02:54:29 2018 +0800 Fix: minor commit9dd3cd43c1
Author: eight <eight04@gmail.com> Date: Sun Oct 14 02:49:22 2018 +0800 Fix: don't reorder options commit90aadfd728
Author: eight <eight04@gmail.com> Date: Sun Oct 14 02:43:52 2018 +0800 Fix: drop __ERROR__ commit838c21e3b3
Author: eight <eight04@gmail.com> Date: Sun Oct 14 02:36:20 2018 +0800 Fix: use findStyle API commit93a4cdf595
Author: eight <eight04@gmail.com> Date: Sun Oct 14 02:34:05 2018 +0800 Add: findStyle API commit8e75871b9b
Author: eight <eight04@gmail.com> Date: Sun Oct 14 02:19:01 2018 +0800 Breaking: drop getStylesFallback commitad06551440
Author: eight <eight04@gmail.com> Date: Sun Oct 14 02:16:48 2018 +0800 Fix: use dataurl to inject page script commitcb5cbb4d10
Author: eight <eight04@gmail.com> Date: Sun Oct 14 01:39:50 2018 +0800 Fix: various commit53efd78b89
Author: eight <eight04@gmail.com> Date: Sun Oct 14 01:12:57 2018 +0800 Update doc commit7d005f3eaa
Author: eight <eight04@gmail.com> Date: Sun Oct 14 01:09:22 2018 +0800 Change: kill style.reason commitfc53bed3de
Author: eight <eight04@gmail.com> Date: Sun Oct 14 00:56:04 2018 +0800 Fix: doo many indents commit14e321d258
Author: eight <eight04@gmail.com> Date: Sun Oct 14 00:40:23 2018 +0800 Fix: don't update icon for popup and options commit01bdd529bc
Author: eight <eight04@gmail.com> Date: Sun Oct 14 00:39:17 2018 +0800 Fix: updateCount commitb9968830d3
Author: eight <eight04@gmail.com> Date: Sun Oct 14 00:38:49 2018 +0800 Fix: don't send null value commitff3bf6f52d
Author: eight <eight04@gmail.com> Date: Sun Oct 14 00:03:34 2018 +0800 Add: styleViaAPI updateCount commit39d21c3d29
Author: eight <eight04@gmail.com> Date: Sat Oct 13 23:57:45 2018 +0800 Fix: broadcastError -> ignoreError commitecb622c93c
Author: eight <eight04@gmail.com> Date: Sat Oct 13 21:29:06 2018 +0800 Fix: implement styleViaAPI commit7c3d49c005
Author: eight <eight04@gmail.com> Date: Sat Oct 13 17:50:28 2018 +0800 Fix: ROOT may change in XML pages commit3fd8d937f3
Author: eight <eight04@gmail.com> Date: Sat Oct 13 16:49:43 2018 +0800 Fix: various commit859afc8ee9
Author: eight <eight04@gmail.com> Date: Sat Oct 13 16:39:54 2018 +0800 Enhance: don't cache enabled state commitfbe77a8d15
Author: eight <eight04@gmail.com> Date: Sat Oct 13 16:15:07 2018 +0800 Fix: various commita4fc3e9162
Author: eight <eight04@gmail.com> Date: Sat Oct 13 16:11:38 2018 +0800 Fix: various commit7e0eddeb8f
Author: eight <eight04@gmail.com> Date: Sat Oct 13 15:58:31 2018 +0800 Fix: various commit8b4ab47d89
Author: eight <eight04@gmail.com> Date: Sat Oct 13 15:20:10 2018 +0800 Add: some type hint commit7d340d62dc
Author: eight <eight04@gmail.com> Date: Sat Oct 13 15:13:11 2018 +0800 Change: drop storage.js, some functions are moved to sections-util commitd286997d6a
Author: eight <eight04@gmail.com> Date: Sat Oct 13 15:12:00 2018 +0800 Fix: minor commitd60db9dbef
Author: eight <eight04@gmail.com> Date: Sat Oct 13 15:03:10 2018 +0800 Fix: minor commit43afa31fa0
Author: eight <eight04@gmail.com> Date: Sat Oct 13 14:50:31 2018 +0800 Fix: update tab icon on forward/backward commitf08faea149
Author: eight <eight04@gmail.com> Date: Sat Oct 13 13:50:03 2018 +0800 Fix: parallel import commit4d06435486
Author: eight <eight04@gmail.com> Date: Fri Oct 12 23:32:03 2018 +0800 Add: importStyle API commitc55675912e
Author: eight <eight04@gmail.com> Date: Fri Oct 12 23:14:46 2018 +0800 Fix: refactor import-export commit86ea846a89
Author: eight <eight04@gmail.com> Date: Fri Oct 12 17:34:36 2018 +0800 Fix: search db is broken commit831ca07c2d
Author: eight <eight04@gmail.com> Date: Fri Oct 12 17:29:35 2018 +0800 fixup! Add: implement sloppy regexp indicator commite67b7f4f36
Author: eight <eight04@gmail.com> Date: Fri Oct 12 17:27:19 2018 +0800 Add: implement sloppy regexp indicator commit36e13f88f0
Author: eight <eight04@gmail.com> Date: Fri Oct 12 16:59:23 2018 +0800 Add: return excluded/sloppy state in getStylesInfoByUrl commitf6ce78f55b
Author: eight <eight04@gmail.com> Date: Fri Oct 12 16:39:47 2018 +0800 Fix: dead object commit5ae95a1ad9
Author: eight <eight04@gmail.com> Date: Fri Oct 12 16:27:54 2018 +0800 Fix: don't reinit all editors on save commit1a5a206fe6
Author: eight <eight04@gmail.com> Date: Fri Oct 12 16:18:40 2018 +0800 Refactor: pull out sections editor section commit8016346035
Author: eight <eight04@gmail.com> Date: Fri Oct 12 15:30:35 2018 +0800 Fix: replaceStyle make style name undefined commitfa080d1913
Author: eight <eight04@gmail.com> Date: Fri Oct 12 15:21:36 2018 +0800 Fix: catch csp error commite0b064115d
Author: eight <eight04@gmail.com> Date: Fri Oct 12 15:03:00 2018 +0800 Fix: use a simple eval to execute page scripts commit405b7f8f06
Author: eight <eight04@gmail.com> Date: Fri Oct 12 03:48:13 2018 +0800 Fix: removed unused API commit1b2c88f926
Author: eight <eight04@gmail.com> Date: Fri Oct 12 03:46:51 2018 +0800 Fix: no need to access db commita8131fc9c5
Author: eight <eight04@gmail.com> Date: Fri Oct 12 03:43:31 2018 +0800 Fix: remove unused methods commit3ae0c4dd13
Author: eight <eight04@gmail.com> Date: Fri Oct 12 03:10:26 2018 +0800 Enhance: allow matcher to return verbose info commit0ea7ada48f
Author: eight <eight04@gmail.com> Date: Fri Oct 12 02:02:14 2018 +0800 Fix: content script may load before the background is ready commit04c2d6bbf6
Author: eight <eight04@gmail.com> Date: Fri Oct 12 01:49:52 2018 +0800 Fix: throw receiving end doesn't exist message commitf0c0bc4d6a
Author: eight <eight04@gmail.com> Date: Fri Oct 12 01:11:17 2018 +0800 Fix: unwrap error commit4d42765d6c
Author: eight <eight04@gmail.com> Date: Thu Oct 11 23:55:16 2018 +0800 fixup! Fix: match subdomain commit99626e4a48
Author: eight <eight04@gmail.com> Date: Thu Oct 11 23:54:58 2018 +0800 Fix: match subdomain commita57b3b2716
Author: eight <eight04@gmail.com> Date: Thu Oct 11 23:39:11 2018 +0800 Fix: firefox commit5cfea3933f
Author: eight <eight04@gmail.com> Date: Thu Oct 11 22:46:34 2018 +0800 Add some comment to db.js commit25fd3a1c2b
Author: eight <eight04@gmail.com> Date: Thu Oct 11 22:14:56 2018 +0800 Fix: remove unused prop commitbdae1c3697
Author: eight <eight04@gmail.com> Date: Thu Oct 11 20:00:25 2018 +0800 Change: simpler styleCodeEmpty commitbd4a453f45
Merge:c1bf9f5
9058c06
Author: eight <eight04@gmail.com> Date: Thu Oct 11 19:49:37 2018 +0800 Merge branch 'dev-usercss-meta' into dev-exclusions commitc1bf9f57e9
Author: eight <eight04@gmail.com> Date: Thu Oct 11 19:29:17 2018 +0800 Fix: minor commitfd5eeb4b81
Author: eight <eight04@gmail.com> Date: Thu Oct 11 19:00:05 2018 +0800 Add: refresh on view commit3e38810a49
Author: eight <eight04@gmail.com> Date: Thu Oct 11 18:13:24 2018 +0800 Fix: make sure icons are refreshed at startup commitc657d7e55c
Author: eight <eight04@gmail.com> Date: Thu Oct 11 17:32:27 2018 +0800 Add: implement bug 461 commit7ed39ab6ef
Author: eight <eight04@gmail.com> Date: Thu Oct 11 15:42:44 2018 +0800 fixup! Add: icon-util commit30e494eda9
Author: eight <eight04@gmail.com> Date: Thu Oct 11 15:42:23 2018 +0800 Add: icon-util commit510a886e14
Author: eight <eight04@gmail.com> Date: Thu Oct 11 03:21:38 2018 +0800 Fix: exposeIframes commitc7f81662c4
Author: eight <eight04@gmail.com> Date: Thu Oct 11 02:19:14 2018 +0800 Fix: autoCloseBrackets is true by default commitf3a103645d
Author: eight <eight04@gmail.com> Date: Thu Oct 11 02:11:14 2018 +0800 Fix: various commitd4436cde20
Author: eight <eight04@gmail.com> Date: Thu Oct 11 01:39:10 2018 +0800 Add: implement exposeIframe commit43db875fd8
Author: eight <eight04@gmail.com> Date: Thu Oct 11 01:26:24 2018 +0800 Kill more globals commitdc491e9be3
Author: eight <eight04@gmail.com> Date: Thu Oct 11 01:22:13 2018 +0800 Kill old storage, storage-dummy commitba64b95575
Author: eight <eight04@gmail.com> Date: Thu Oct 11 00:54:38 2018 +0800 WIP: kill cachedStyles commit7eba890a21
Merge:d2b36a1
81e4823
Author: eight <eight04@gmail.com> Date: Wed Oct 10 23:15:14 2018 +0800 Merge branch 'dev-private-prefs' into dev-exclusions commitd2b36a168e
Author: eight <eight04@gmail.com> Date: Wed Oct 10 23:05:20 2018 +0800 Kill hidden globals commit22d4767511
Author: eight <eight04@gmail.com> Date: Wed Oct 10 19:23:34 2018 +0800 Fix: margin for deleted sections commit00687983f0
Author: eight <eight04@gmail.com> Date: Wed Oct 10 18:21:07 2018 +0800 Fix: default value commitff6fd8cad3
Author: eight <eight04@gmail.com> Date: Wed Oct 10 18:02:51 2018 +0800 Fix: default options commitc23f315c52
Author: eight <eight04@gmail.com> Date: Wed Oct 10 17:40:07 2018 +0800 Refactor: use CodeMirror.defineOption commit4419c5dc1e
Author: eight <eight04@gmail.com> Date: Wed Oct 10 16:32:39 2018 +0800 Change: kill editors, styleId commit6494985b50
Author: eight <eight04@gmail.com> Date: Wed Oct 10 16:14:51 2018 +0800 Fix: various commit37e1f43f75
Author: eight <eight04@gmail.com> Date: Wed Oct 10 15:04:03 2018 +0800 Fix: minor commitd26ce3238e
Author: eight <eight04@gmail.com> Date: Wed Oct 10 14:49:37 2018 +0800 Add: codemirror-factory commit15a1f552f6
Author: eight <eight04@gmail.com> Date: Wed Oct 10 12:08:35 2018 +0800 WIP: kill getSection commitba6159e067
Author: eight <eight04@gmail.com> Date: Wed Oct 10 02:43:09 2018 +0800 WIP: edit page commitfd9ab5d6e5
Author: eight <eight04@gmail.com> Date: Wed Oct 10 00:41:07 2018 +0800 Fix: switch to editor commit06e22d0d18
Author: eight <eight04@gmail.com> Date: Tue Oct 9 23:38:29 2018 +0800 Change: add sections-editor commit30e8662946
Author: eight <eight04@gmail.com> Date: Mon Oct 8 20:12:39 2018 +0800 Add: preview error commit47b2b4fc49
Author: eight <eight04@gmail.com> Date: Mon Oct 8 18:38:01 2018 +0800 Add: livePreview.show commit7b5e7c96d5
Author: eight <eight04@gmail.com> Date: Mon Oct 8 18:16:45 2018 +0800 Hook up live preview commit15efafff3c
Author: eight <eight04@gmail.com> Date: Mon Oct 8 17:49:57 2018 +0800 Add: live preview commita38558ef78
Author: eight <eight04@gmail.com> Date: Mon Oct 8 15:30:39 2018 +0800 WIP: make notifyAllTabs a noop commit582e9078af
Author: eight <eight04@gmail.com> Date: Mon Oct 8 14:39:08 2018 +0800 Fix: inject all scripts commitf4651da8d8
Author: eight <eight04@gmail.com> Date: Sun Oct 7 23:41:46 2018 +0800 Drop deleteStyle commit0489fb3b2f
Author: eight <eight04@gmail.com> Date: Sun Oct 7 23:33:51 2018 +0800 Drop saveStyle commit02f471f077
Author: eight <eight04@gmail.com> Date: Sun Oct 7 23:28:41 2018 +0800 Fix: usercss API commit057111b171
Author: eight <eight04@gmail.com> Date: Sun Oct 7 22:59:31 2018 +0800 Update usercss API commit69cae02381
Author: eight <eight04@gmail.com> Date: Sun Oct 7 21:40:29 2018 +0800 Drop getStyles commitc5d41529d9
Author: eight <eight04@gmail.com> Date: Sun Oct 7 21:28:51 2018 +0800 Minor fixes commit5b3b4e680f
Author: eight <eight04@gmail.com> Date: Sun Oct 7 21:20:39 2018 +0800 Add: navigator-util commitb5107b78a5
Author: eight <eight04@gmail.com> Date: Sun Oct 7 01:42:43 2018 +0800 Add: broadcast messages with reasons commite7ef4948cd
Author: eight <eight04@gmail.com> Date: Sat Oct 6 18:10:47 2018 +0800 Fix: observer is unavailable? commit1c635b5bc1
Author: eight <eight04@gmail.com> Date: Sat Oct 6 17:47:43 2018 +0800 Drop requestStyles commit75f2561154
Author: eight <eight04@gmail.com> Date: Sat Oct 6 16:38:04 2018 +0800 Fix: don't recreate element when style update in popup commit583ca31d97
Author: eight <eight04@gmail.com> Date: Sat Oct 6 15:40:07 2018 +0800 fixup! Add: isCodeEmpty commit1cf6008514
Author: eight <eight04@gmail.com> Date: Sat Oct 6 15:33:18 2018 +0800 Add: isCodeEmpty commit450cd60aeb
Author: eight <eight04@gmail.com> Date: Sat Oct 6 15:22:04 2018 +0800 Fix: ignore comment block commit196b6aac63
Author: eight <eight04@gmail.com> Date: Sat Oct 6 15:16:00 2018 +0800 Fix: the return value of getSectionsByUrl is changed commit3122d28c1a
Author: eight <eight04@gmail.com> Date: Sat Oct 6 15:14:05 2018 +0800 Fix: always use promise in API call commite594b8ccb1
Author: eight <eight04@gmail.com> Date: Sat Oct 6 15:11:01 2018 +0800 Cache enabled state commit1f18b13a92
Author: eight <eight04@gmail.com> Date: Sat Oct 6 13:48:46 2018 +0800 Add: match global sections commitfedf844ddd
Author: eight <eight04@gmail.com> Date: Sat Oct 6 13:45:37 2018 +0800 Add: getStylesInfoByUrl commit095998f07c
Author: eight <eight04@gmail.com> Date: Sat Oct 6 13:27:58 2018 +0800 Change: switch to msg.js commitfa3127d988
Author: eight <eight04@gmail.com> Date: Sat Oct 6 13:02:45 2018 +0800 Change: switch to msg.js commit05d582c726
Author: eight <eight04@gmail.com> Date: Sat Oct 6 11:43:42 2018 +0800 Add: msg.sendBg commit171339f710
Author: eight <eight04@gmail.com> Date: Sat Oct 6 04:39:48 2018 +0800 WIP: drop api.js commit3a618aca2a
Author: eight <eight04@gmail.com> Date: Sat Oct 6 03:19:51 2018 +0800 WIP: use deepCopy commitbb1cb58024
Author: eight <eight04@gmail.com> Date: Sat Oct 6 03:10:04 2018 +0800 WIP: msg.js commit2472e91f57
Author: eight <eight04@gmail.com> Date: Fri Oct 5 21:28:19 2018 +0800 WIP: emitChangesToTabs commit34497ebe16
Author: eight <eight04@gmail.com> Date: Fri Oct 5 18:47:52 2018 +0800 WIP: switch to API commitf1639cc33e
Author: eight <eight04@gmail.com> Date: Fri Oct 5 01:03:40 2018 +0800 WIP: broadcastMessage commit81e4823f46
Author: eight <eight04@gmail.com> Date: Thu Oct 4 19:39:59 2018 +0800 Debounce updateAllTabsIcon commitdc5f3e209f
Author: eight <eight04@gmail.com> Date: Thu Oct 4 19:34:36 2018 +0800 Fix: settings could be empty on the first install commit2328cf623a
Author: eight <eight04@gmail.com> Date: Thu Oct 4 19:34:22 2018 +0800 Change: start-firefox -> start commit7be6a1cba9
Author: eight <eight04@gmail.com> Date: Thu Oct 4 19:24:35 2018 +0800 Add: applications commit630725196f
Author: eight <eight04@gmail.com> Date: Thu Oct 4 19:22:44 2018 +0800 fixup! Fix: update all icons when some prefs changed commit0d0e1b4dc0
Author: eight <eight04@gmail.com> Date: Thu Oct 4 19:20:36 2018 +0800 Fix: update all icons when some prefs changed commit5c0288e9ba
Author: eight <eight04@gmail.com> Date: Thu Oct 4 19:20:11 2018 +0800 fixup! Remove unused FIREFOX_NO_DOM_STORAGE commit56b737b65a
Author: eight <eight04@gmail.com> Date: Thu Oct 4 18:14:57 2018 +0800 Remove unused FIREFOX_NO_DOM_STORAGE commit829a134ed1
Author: eight <eight04@gmail.com> Date: Thu Oct 4 18:10:53 2018 +0800 Fix: this -> prefs commitd35f92250e
Author: eight <eight04@gmail.com> Date: Thu Oct 4 18:08:19 2018 +0800 Fixme: styleViaAPI commit8a6e8ac03a
Author: eight <eight04@gmail.com> Date: Thu Oct 4 18:05:41 2018 +0800 Change: drop prefChanged, use prefs service commit10f9449144
Author: eight <eight04@gmail.com> Date: Thu Oct 4 17:46:45 2018 +0800 Change: move setupLivePrefs to dom.js. Remove prefs.js dependencies commitdd2b8ed091
Author: eight <eight04@gmail.com> Date: Thu Oct 4 17:18:38 2018 +0800 Fix: type error commit3af310c341
Author: eight <eight04@gmail.com> Date: Thu Oct 4 17:09:26 2018 +0800 Fix: open-manager has no default value commit874a2da33e
Author: eight <eight04@gmail.com> Date: Thu Oct 4 17:04:23 2018 +0800 Enhance: make prefs use storage.sync commitc01f93f62c
Author: eight <eight04@gmail.com> Date: Thu Oct 4 15:57:02 2018 +0800 WIP commit6d32ffb76b
Author: eight <eight04@gmail.com> Date: Thu Oct 4 12:46:19 2018 +0800 WIP commit0f148eac32
Author: eight <eight04@gmail.com> Date: Thu Oct 4 03:35:07 2018 +0800 WIP commit282bdf7706
Author: eight <eight04@gmail.com> Date: Wed Oct 3 20:24:06 2018 +0800 Fix: numbers are not compared correctly commit24b1eea8a4
Merge:8a6011d
5cbe8a8
Author: eight <eight04@gmail.com> Date: Wed Oct 3 15:00:07 2018 +0800 Merge branch 'master' of https://github.com/openstyles/stylus into dev-exclusions commit5cbe8a8d78
Author: eight <eight04@gmail.com> Date: Tue Oct 2 20:22:18 2018 +0800 Add: fetch style object from DB directly in the editor (#507) commit9058c06c54
Author: eight <eight04@gmail.com> Date: Mon Oct 1 23:24:29 2018 +0800 Fix: bad API commit1f2d116aae
Author: eight <eight04@gmail.com> Date: Mon Oct 1 23:14:56 2018 +0800 Fix: use meta parser commit918e47b1ed
Author: eight <eight04@gmail.com> Date: Mon Oct 1 23:01:21 2018 +0800 Fix: emit update event if no fatal errors commit81a7bb9ac9
Author: eight <eight04@gmail.com> Date: Mon Oct 1 22:56:25 2018 +0800 Add: editorWorker.metalint commitf47d57aea8
Author: eight <eight04@gmail.com> Date: Mon Oct 1 22:49:16 2018 +0800 Change: use editorWorker.metalint commit5778d5c858
Author: eight <eight04@gmail.com> Date: Mon Oct 1 22:39:01 2018 +0800 Change: editor-worker-body -> editor-worker commit268e1716b4
Author: eight <eight04@gmail.com> Date: Mon Oct 1 22:38:06 2018 +0800 Change: switch to worker-util commitcc2980b647
Author: eight <eight04@gmail.com> Date: Mon Oct 1 22:30:16 2018 +0800 Drop: parserlib-loader commit08adcb60f2
Merge:6909c73
2fd531e
Author: eight <eight04@gmail.com> Date: Mon Oct 1 22:29:39 2018 +0800 Merge branch 'master' into dev-usercss-meta commite4135ce35d
Author: eight <eight04@gmail.com> Date: Fri Sep 28 11:57:34 2018 +0800 Fix: remove unused function commit39a6d1909f
Author: eight <eight04@gmail.com> Date: Fri Sep 28 00:26:29 2018 +0800 Fix: prefs doesn't work in FF's private windows. Add web-ext. Drop prefs.readOnlyValues commit6909c73c69
Author: eight <eight04@gmail.com> Date: Wed Sep 26 12:16:33 2018 +0800 Fix: minor commit79833d8bba
Author: eight <eight04@gmail.com> Date: Wed Sep 26 11:40:04 2018 +0800 Fix: a better way to draw list? commita849fd6dda
Author: eight <eight04@gmail.com> Date: Wed Sep 26 11:39:53 2018 +0800 Fix: missing placeholders commitd5ee31a080
Author: eight <eight04@gmail.com> Date: Wed Sep 26 11:37:50 2018 +0800 Fix: a better way to draw character list? commit7b959af3e3
Author: eight <eight04@gmail.com> Date: Wed Sep 26 11:30:10 2018 +0800 Update usercss-meta commitfefa987c4d
Author: eight <eight04@gmail.com> Date: Wed Sep 26 10:37:28 2018 +0800 Change: sections-equal -> sections-util commit2abbf670d8
Author: eight <eight04@gmail.com> Date: Wed Sep 26 10:37:14 2018 +0800 Fix: check err.code commit1fe0586b29
Author: eight <eight04@gmail.com> Date: Wed Sep 26 10:33:02 2018 +0800 Add: i18n error message commitab0ef239cf
Author: eight <eight04@gmail.com> Date: Wed Sep 26 09:34:57 2018 +0800 Change: move styleCodeEmpty to sections-util, load colorConverter in background worker commitd5ade807f0
Author: eight <eight04@gmail.com> Date: Wed Sep 26 09:27:30 2018 +0800 Fix: display error message commit4f5337e51d
Author: eight <eight04@gmail.com> Date: Wed Sep 26 09:26:55 2018 +0800 Fix: remove unused colorconverter commit29b8f51292
Author: eight <eight04@gmail.com> Date: Tue Sep 25 23:21:44 2018 +0800 Fix: vars could be undefined commita7cfeb22e4
Author: eight <eight04@gmail.com> Date: Tue Sep 25 22:54:40 2018 +0800 Fix: window is undefined commit9713c6a3be
Author: eight <eight04@gmail.com> Date: Tue Sep 25 21:56:38 2018 +0800 Fix: throw an error for unparsable color commit3c30bc3eb0
Author: eight <eight04@gmail.com> Date: Tue Sep 25 21:55:55 2018 +0800 Fix: try to get error message commit3d32b0428b
Author: eight <eight04@gmail.com> Date: Tue Sep 25 21:38:40 2018 +0800 Fix: vars might be empty commit7d75dd8754
Author: eight <eight04@gmail.com> Date: Tue Sep 25 21:18:39 2018 +0800 Add: meta-parser commita4df641b96
Author: eight <eight04@gmail.com> Date: Tue Sep 25 21:18:18 2018 +0800 Enhance: set flag in parserlib so we don't need another loader commit8028a3529f
Author: eight <eight04@gmail.com> Date: Tue Sep 25 21:17:40 2018 +0800 Include util, worker-util in background commitba5d6cc31a
Author: eight <eight04@gmail.com> Date: Tue Sep 25 21:16:59 2018 +0800 Fix: use spread syntax in loadScript commitb853be13f8
Author: eight <eight04@gmail.com> Date: Tue Sep 25 21:14:46 2018 +0800 Enhance: swith to usercss-meta (in worker) commita3e7915199
Author: eight <eight04@gmail.com> Date: Tue Sep 25 21:11:54 2018 +0800 Fix: use promise API commit5d07a8cd4e
Author: eight <eight04@gmail.com> Date: Tue Sep 25 21:11:09 2018 +0800 Fix: buildMeta now returns a promise commita004bc3c7d
Author: eight <eight04@gmail.com> Date: Tue Sep 25 21:10:35 2018 +0800 Move styleCodeEmpty to util commit41ac66a137
Author: eight <eight04@gmail.com> Date: Tue Sep 25 21:09:40 2018 +0800 Add: background worker commitffb13bf1db
Author: eight <eight04@gmail.com> Date: Tue Sep 25 21:09:04 2018 +0800 Enhance: move moz-parser/meta-parser/usercss compiler to worker commit42e97ef153
Author: eight <eight04@gmail.com> Date: Tue Sep 25 20:45:07 2018 +0800 Fix: display error on install page commit64aa9fcf53
Author: eight <eight04@gmail.com> Date: Tue Sep 25 17:34:54 2018 +0800 Add: background worker commitb0e407e98f
Author: eight <eight04@gmail.com> Date: Tue Sep 25 14:52:35 2018 +0800 Add: worker util commit7a24547e09
Author: eight <eight04@gmail.com> Date: Tue Sep 25 00:01:18 2018 +0800 Add: usercss-meta commit8a6011de8c
Author: Rob Garrison <wowmotty@gmail.com> Date: Sun Jul 22 09:15:09 2018 -0500 Attempt to update icon count commit4fcb1a88d7
Author: Rob Garrison <wowmotty@gmail.com> Date: Sun Jul 15 13:44:29 2018 -0500 Fix empty exclusion storage error commitbfe54ab4c4
Author: Rob Garrison <wowmotty@gmail.com> Date: Sun Jul 15 12:59:51 2018 -0500 Add tab communication commit983a7bc219
Author: Rob Garrison <wowmotty@gmail.com> Date: Sun Jul 15 10:51:11 2018 -0500 Fix escaped regex example commit3950482f34
Author: Rob Garrison <wowmotty@gmail.com> Date: Wed Apr 25 18:11:37 2018 -0500 Fix undefined error commite94c7edb38
Author: Rob Garrison <wowmotty@gmail.com> Date: Wed Apr 25 17:09:45 2018 -0500 Attempt to fix popup exclusion issues commit2b4a1a5635
Author: Rob Garrison <wowmotty@gmail.com> Date: Thu Apr 19 13:00:27 2018 -0500 Modify input method commit9f75b69cd8
Author: Rob Garrison <wowmotty@gmail.com> Date: Wed Mar 7 11:54:05 2018 -0600 Include iframe urls in exclusion popup commit68dfa0153c
Author: Rob Garrison <wowmotty@gmail.com> Date: Wed Jan 24 19:42:02 2018 -0600 Add style exclusions. Closes #113 * Revert: exclusions * Fix: pass eslint * Fix: the style is injected twice * Fix: don't load script async * Fix: styleCodeEmpty returns true for empty string * Fix: drop array selection * Fix: the config dialog is broken * Fix: popup doesn't use getStyle/getStylesByUrl correctly * Fix: keep disabled state in setStyleContent * Fix: allow live-preview to assign newest vars * Fix: transition fix is broken because setStyleContent becomes async * Fix: typo, TypeError in styleExists * Fix: use new API * Fix: pass linter * Fix: LICENCE -> LICENSE * Fix: remove unused distroy function
This commit is contained in:
parent
79c6506c5c
commit
e3d3604afc
61
.eslintrc
61
.eslintrc
|
@ -8,64 +8,6 @@ env:
|
||||||
es6: true
|
es6: true
|
||||||
webextensions: true
|
webextensions: true
|
||||||
|
|
||||||
globals:
|
|
||||||
# messaging.js
|
|
||||||
KEEP_CHANNEL_OPEN: false
|
|
||||||
CHROME: false
|
|
||||||
FIREFOX: false
|
|
||||||
VIVALDI: false
|
|
||||||
OPERA: false
|
|
||||||
URLS: false
|
|
||||||
BG: false
|
|
||||||
API: false
|
|
||||||
notifyAllTabs: false
|
|
||||||
sendMessage: false
|
|
||||||
queryTabs: false
|
|
||||||
getTab: false
|
|
||||||
getOwnTab: false
|
|
||||||
getActiveTab: false
|
|
||||||
getActiveTabRealURL: false
|
|
||||||
getTabRealURL: false
|
|
||||||
openURL: false
|
|
||||||
activateTab: false
|
|
||||||
stringAsRegExp: false
|
|
||||||
ignoreChromeError: false
|
|
||||||
tryCatch: false
|
|
||||||
tryRegExp: false
|
|
||||||
tryJSONparse: false
|
|
||||||
debounce: false
|
|
||||||
deepCopy: false
|
|
||||||
sessionStorageHash: false
|
|
||||||
download: false
|
|
||||||
invokeOrPostpone: false
|
|
||||||
# localization.js
|
|
||||||
template: false
|
|
||||||
t: false
|
|
||||||
o: false
|
|
||||||
tE: false
|
|
||||||
tHTML: false
|
|
||||||
tNodeList: false
|
|
||||||
tDocLoader: false
|
|
||||||
tWordBreak: false
|
|
||||||
formatDate: false
|
|
||||||
# dom.js
|
|
||||||
onDOMready: false
|
|
||||||
onDOMscriptReady: false
|
|
||||||
scrollElementIntoView: false
|
|
||||||
enforceInputRange: false
|
|
||||||
animateElement: false
|
|
||||||
$: false
|
|
||||||
$$: false
|
|
||||||
$create: false
|
|
||||||
$createLink: false
|
|
||||||
# prefs.js
|
|
||||||
prefs: false
|
|
||||||
setupLivePrefs: false
|
|
||||||
# storage-util.js
|
|
||||||
chromeLocal: false
|
|
||||||
chromeSync: false
|
|
||||||
LZString: false
|
|
||||||
|
|
||||||
rules:
|
rules:
|
||||||
accessor-pairs: [2]
|
accessor-pairs: [2]
|
||||||
array-bracket-spacing: [2, never]
|
array-bracket-spacing: [2, never]
|
||||||
|
@ -214,7 +156,6 @@ rules:
|
||||||
no-trailing-spaces: [2]
|
no-trailing-spaces: [2]
|
||||||
no-undef-init: [2]
|
no-undef-init: [2]
|
||||||
no-undef: [2]
|
no-undef: [2]
|
||||||
no-undefined: [0]
|
|
||||||
no-underscore-dangle: [0]
|
no-underscore-dangle: [0]
|
||||||
no-unexpected-multiline: [2]
|
no-unexpected-multiline: [2]
|
||||||
no-unmodified-loop-condition: [0]
|
no-unmodified-loop-condition: [0]
|
||||||
|
@ -224,7 +165,7 @@ rules:
|
||||||
no-unsafe-negation: [2]
|
no-unsafe-negation: [2]
|
||||||
no-unused-expressions: [1]
|
no-unused-expressions: [1]
|
||||||
no-unused-labels: [0]
|
no-unused-labels: [0]
|
||||||
no-unused-vars: [1, {args: after-used, vars: local, argsIgnorePattern: ^_}]
|
no-unused-vars: [2, {args: after-used}]
|
||||||
no-use-before-define: [2, nofunc]
|
no-use-before-define: [2, nofunc]
|
||||||
no-useless-call: [2]
|
no-useless-call: [2]
|
||||||
no-useless-computed-key: [2]
|
no-useless-computed-key: [2]
|
||||||
|
|
|
@ -689,6 +689,194 @@
|
||||||
"message": "Show active style count",
|
"message": "Show active style count",
|
||||||
"description": "Label (must be very short) for the checkbox in the toolbar button context menu controlling toolbar badge text."
|
"description": "Label (must be very short) for the checkbox in the toolbar button context menu controlling toolbar badge text."
|
||||||
},
|
},
|
||||||
|
"meta_invalidCheckboxDefault": {
|
||||||
|
"message": "Invalid @var checkbox: value must be 0 or 1",
|
||||||
|
"description": "Error displayed when the value of @var checkbox is invalid"
|
||||||
|
},
|
||||||
|
"meta_invalidColor": {
|
||||||
|
"message": "Invalid @var color: $color$ is not a color",
|
||||||
|
"description": "Error displayed when the value of @var color is invalid",
|
||||||
|
"placeholders": {
|
||||||
|
"color": {
|
||||||
|
"content": "$1"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"meta_invalidRange": {
|
||||||
|
"message": "Invalid @var $type$: value must be a number or an array",
|
||||||
|
"description": "Error displayed when the value of @var range or @var number is invalid",
|
||||||
|
"placeholders": {
|
||||||
|
"type": {
|
||||||
|
"content": "$1"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"meta_invalidRangeMultipleUnits": {
|
||||||
|
"message": "Invalid @var $type$: multiple units are defined",
|
||||||
|
"description": "Error displayed when the value of @var range or @var number is invalid",
|
||||||
|
"placeholders": {
|
||||||
|
"type": {
|
||||||
|
"content": "$1"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"meta_invalidRangeTooManyValues": {
|
||||||
|
"message": "Invalid @var $type$: the array contains too many items",
|
||||||
|
"description": "Error displayed when the value of @var range or @var number is invalid",
|
||||||
|
"placeholders": {
|
||||||
|
"type": {
|
||||||
|
"content": "$1"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"meta_invalidRangeValue": {
|
||||||
|
"message": "Invalid @var $type$: items in the array must be number, string, or null",
|
||||||
|
"description": "Error displayed when the value of @var range or @var number is invalid",
|
||||||
|
"placeholders": {
|
||||||
|
"type": {
|
||||||
|
"content": "$1"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"meta_invalidRangeDefault": {
|
||||||
|
"message": "Invalid @var $type$: default value is null",
|
||||||
|
"description": "Error displayed when the value of @var range or @var number is invalid",
|
||||||
|
"placeholders": {
|
||||||
|
"type": {
|
||||||
|
"content": "$1"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"meta_invalidRangeMin": {
|
||||||
|
"message": "Invalid @var $type$: default value is lower than the minimum",
|
||||||
|
"description": "Error displayed when the value of @var range or @var number is invalid",
|
||||||
|
"placeholders": {
|
||||||
|
"type": {
|
||||||
|
"content": "$1"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"meta_invalidRangeMax": {
|
||||||
|
"message": "Invalid @var $type$: default value is larger than the maximum",
|
||||||
|
"description": "Error displayed when the value of @var range or @var number is invalid",
|
||||||
|
"placeholders": {
|
||||||
|
"type": {
|
||||||
|
"content": "$1"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"meta_invalidRangeStep": {
|
||||||
|
"message": "Invalid @var $type$: default value is not a mutiple of the step",
|
||||||
|
"description": "Error displayed when the value of @var range or @var number is invalid",
|
||||||
|
"placeholders": {
|
||||||
|
"type": {
|
||||||
|
"content": "$1"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"meta_invalidSelectEmptyOptions": {
|
||||||
|
"message": "Invalid @var select: options list is empty",
|
||||||
|
"description": "Error displayed when the value of @var select is invalid"
|
||||||
|
},
|
||||||
|
"meta_invalidSelectMultipleDefaults": {
|
||||||
|
"message": "Invalid @var select: multiple default options are defined",
|
||||||
|
"description": "Error displayed when the value of @var select is invalid"
|
||||||
|
},
|
||||||
|
"meta_invalidSelectValueMismatch": {
|
||||||
|
"message": "Invalid @var select: value doesn't exist in the option list",
|
||||||
|
"description": "Error displayed when the value of @var select is invalid"
|
||||||
|
},
|
||||||
|
"meta_invalidURLProtocol": {
|
||||||
|
"message": "Invalid URL protocol. Only http and https are allowed: $protocol$",
|
||||||
|
"description": "Error displayed when the protocol of the URL is invalid",
|
||||||
|
"placeholders": {
|
||||||
|
"protocol": {
|
||||||
|
"content": "$1"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"meta_invalidVersion": {
|
||||||
|
"message": "Invalid version number. The value doesn't match SemVer pattern: $version$",
|
||||||
|
"description": "Error displayed when @version is invalid",
|
||||||
|
"placeholders": {
|
||||||
|
"version": {
|
||||||
|
"content": "$1"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"meta_invalidNumber": {
|
||||||
|
"message": "Expect a number",
|
||||||
|
"description": "Error displayed when the value is expected to be a number"
|
||||||
|
},
|
||||||
|
"meta_invalidString": {
|
||||||
|
"message": "Expect a quoted string",
|
||||||
|
"description": "Error displayed when the value is expected to be a quoted string"
|
||||||
|
},
|
||||||
|
"meta_invalidWord": {
|
||||||
|
"message": "Expect a word",
|
||||||
|
"description": "Error displayed when the value is expected to be a word"
|
||||||
|
},
|
||||||
|
"meta_missingChar": {
|
||||||
|
"message": "Expect characters: $chars$",
|
||||||
|
"description": "Error displayed when the value is expected to be some characters",
|
||||||
|
"placeholders": {
|
||||||
|
"chars": {
|
||||||
|
"content": "$1"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"meta_missingEOT": {
|
||||||
|
"message": "Expect EOT data",
|
||||||
|
"description": "Error displayed when the value is expected to be an EOT list"
|
||||||
|
},
|
||||||
|
"meta_missingMandatory": {
|
||||||
|
"message": "Missing mandatory metadata: $keys$",
|
||||||
|
"description": "Error displayed when mandatory keys are missing",
|
||||||
|
"placeholders": {
|
||||||
|
"keys": {
|
||||||
|
"content": "$1"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"meta_unknownJSONLiteral": {
|
||||||
|
"message": "Invalid JSON: $literal$ is not a valid JSON literal",
|
||||||
|
"description": "Error displayed when JSON value is invalid",
|
||||||
|
"placeholders": {
|
||||||
|
"literal": {
|
||||||
|
"content": "$1"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"meta_unknownMeta": {
|
||||||
|
"message": "Unknown metadata: $key$",
|
||||||
|
"description": "Error displayed when unknown metadata is parsed",
|
||||||
|
"placeholders": {
|
||||||
|
"key": {
|
||||||
|
"content": "$1"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"meta_unknownVarType": {
|
||||||
|
"message": "Unknown @$varkey$ type: $vartype$",
|
||||||
|
"description": "Error displayed when unknown variable type is parsed",
|
||||||
|
"placeholders": {
|
||||||
|
"varkey": {
|
||||||
|
"content": "$1"
|
||||||
|
},
|
||||||
|
"vartype": {
|
||||||
|
"content": "$2"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"meta_unknownPreprocessor": {
|
||||||
|
"message": "Unknown @preprocessor: $preprocessor$",
|
||||||
|
"description": "Error displayed when unknown @preprocessor is parsed",
|
||||||
|
"placeholders": {
|
||||||
|
"preprocessor": {
|
||||||
|
"content": "$1"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"noStylesForSite": {
|
"noStylesForSite": {
|
||||||
"message": "No styles installed for this site.",
|
"message": "No styles installed for this site.",
|
||||||
"description": "Text displayed when no styles are installed for the current site"
|
"description": "Text displayed when no styles are installed for the current site"
|
||||||
|
@ -922,10 +1110,6 @@
|
||||||
"message": "Code",
|
"message": "Code",
|
||||||
"description": "Label for the code for a section"
|
"description": "Label for the code for a section"
|
||||||
},
|
},
|
||||||
"sectionHelp": {
|
|
||||||
"message": "Sections let you define different pieces of code to apply to different sets of URLs in the same style. For example, a single style could change the homepage of a site one way, while changing the rest of a site another way.",
|
|
||||||
"description": "Help text for sections"
|
|
||||||
},
|
|
||||||
"sectionRemove": {
|
"sectionRemove": {
|
||||||
"message": "Remove section",
|
"message": "Remove section",
|
||||||
"description": "Label for the button to remove a section"
|
"description": "Label for the button to remove a section"
|
||||||
|
@ -1038,50 +1222,6 @@
|
||||||
},
|
},
|
||||||
"description": "Confirmation when re-installing a style"
|
"description": "Confirmation when re-installing a style"
|
||||||
},
|
},
|
||||||
"styleMetaErrorCheckbox": {
|
|
||||||
"message": "Invalid @var checkbox: value must be 0 or 1",
|
|
||||||
"description": "Error displayed when the value of @var checkbox is invalid"
|
|
||||||
},
|
|
||||||
"styleMetaErrorColor": {
|
|
||||||
"message": "$color$ is not a valid color",
|
|
||||||
"placeholders": {
|
|
||||||
"color": {
|
|
||||||
"content": "$1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"description": "Error displayed when the value of @var color is invalid"
|
|
||||||
},
|
|
||||||
"styleMetaErrorRangeOrNumber": {
|
|
||||||
"message": "Invalid @var $type$: value must be an array containing at least one number at index zero",
|
|
||||||
"description": "Error displayed when the value of @var number or @var range is invalid",
|
|
||||||
"placeholders": {
|
|
||||||
"type": {
|
|
||||||
"content": "$1"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"styleMetaErrorPreprocessor": {
|
|
||||||
"message": "Unsupported @preprocessor: $preprocessor$",
|
|
||||||
"placeholders": {
|
|
||||||
"preprocessor": {
|
|
||||||
"content": "$1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"description": "Error displayed when the value of @preprocessor is not supported"
|
|
||||||
},
|
|
||||||
"styleMetaErrorSelectValueMismatch": {
|
|
||||||
"message": "Invalid @select: value doesn't exist in the list",
|
|
||||||
"description": "Error displayed when the value of @select is invalid"
|
|
||||||
},
|
|
||||||
"styleMissingMeta": {
|
|
||||||
"message": "Missing metadata @$key$",
|
|
||||||
"placeholders": {
|
|
||||||
"key": {
|
|
||||||
"content": "$1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"description": "Error displayed when a mandatory metadata is missing"
|
|
||||||
},
|
|
||||||
"styleMissingName": {
|
"styleMissingName": {
|
||||||
"message": "Enter a name",
|
"message": "Enter a name",
|
||||||
"description": "Error displayed when user saves without providing a name"
|
"description": "Error displayed when user saves without providing a name"
|
||||||
|
@ -1136,10 +1276,6 @@
|
||||||
"message": "Save",
|
"message": "Save",
|
||||||
"description": "Label for save button for style editing"
|
"description": "Label for save button for style editing"
|
||||||
},
|
},
|
||||||
"styleSectionsTitle": {
|
|
||||||
"message": "Sections",
|
|
||||||
"description": "Title for the style sections section"
|
|
||||||
},
|
|
||||||
"styleToMozillaFormatHelp": {
|
"styleToMozillaFormatHelp": {
|
||||||
"message": "The Mozilla format of the code can be submitted to userstyles.org and used with the classic Stylish for Firefox",
|
"message": "The Mozilla format of the code can be submitted to userstyles.org and used with the classic Stylish for Firefox",
|
||||||
"description": "Help info for the Mozilla format header section that converts the code to/from Mozilla format"
|
"description": "Help info for the Mozilla format header section that converts the code to/from Mozilla format"
|
||||||
|
|
167
background/background-worker.js
Normal file
167
background/background-worker.js
Normal file
|
@ -0,0 +1,167 @@
|
||||||
|
/* global workerUtil importScripts parseMozFormat metaParser styleCodeEmpty colorConverter */
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
importScripts('/js/worker-util.js');
|
||||||
|
const {loadScript, createAPI} = workerUtil;
|
||||||
|
|
||||||
|
createAPI({
|
||||||
|
parseMozFormat(arg) {
|
||||||
|
loadScript('/vendor-overwrites/csslint/parserlib.js', '/js/moz-parser.js');
|
||||||
|
return parseMozFormat(arg);
|
||||||
|
},
|
||||||
|
compileUsercss,
|
||||||
|
parseUsercssMeta(text, indexOffset = 0) {
|
||||||
|
loadScript(
|
||||||
|
'/vendor/usercss-meta/usercss-meta.min.js',
|
||||||
|
'/vendor-overwrites/colorpicker/colorconverter.js',
|
||||||
|
'/js/meta-parser.js'
|
||||||
|
);
|
||||||
|
return metaParser.parse(text, indexOffset);
|
||||||
|
},
|
||||||
|
nullifyInvalidVars(vars) {
|
||||||
|
loadScript(
|
||||||
|
'/vendor/usercss-meta/usercss-meta.min.js',
|
||||||
|
'/vendor-overwrites/colorpicker/colorconverter.js',
|
||||||
|
'/js/meta-parser.js'
|
||||||
|
);
|
||||||
|
return metaParser.nullifyInvalidVars(vars);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function compileUsercss(preprocessor, code, vars) {
|
||||||
|
loadScript('/vendor-overwrites/csslint/parserlib.js', '/js/moz-parser.js');
|
||||||
|
const builder = getUsercssCompiler(preprocessor);
|
||||||
|
vars = simpleVars(vars);
|
||||||
|
return Promise.resolve(builder.preprocess ? builder.preprocess(code, vars) : code)
|
||||||
|
.then(code => parseMozFormat({code}))
|
||||||
|
.then(({sections, errors}) => {
|
||||||
|
if (builder.postprocess) {
|
||||||
|
builder.postprocess(sections, vars);
|
||||||
|
}
|
||||||
|
return {sections, errors};
|
||||||
|
});
|
||||||
|
|
||||||
|
function simpleVars(vars) {
|
||||||
|
if (!vars) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
// simplify vars by merging `va.default` to `va.value`, so BUILDER don't
|
||||||
|
// need to test each va's default value.
|
||||||
|
return Object.keys(vars).reduce((output, key) => {
|
||||||
|
const va = vars[key];
|
||||||
|
output[key] = Object.assign({}, va, {
|
||||||
|
value: va.value === null || va.value === undefined ?
|
||||||
|
getVarValue(va, 'default') : getVarValue(va, 'value')
|
||||||
|
});
|
||||||
|
return output;
|
||||||
|
}, {});
|
||||||
|
}
|
||||||
|
|
||||||
|
function getVarValue(va, prop) {
|
||||||
|
if (va.type === 'select' || va.type === 'dropdown' || va.type === 'image') {
|
||||||
|
// TODO: handle customized image
|
||||||
|
return va.options.find(o => o.name === va[prop]).value;
|
||||||
|
}
|
||||||
|
if ((va.type === 'number' || va.type === 'range') && va.units) {
|
||||||
|
return va[prop] + va.units;
|
||||||
|
}
|
||||||
|
return va[prop];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getUsercssCompiler(preprocessor) {
|
||||||
|
const BUILDER = {
|
||||||
|
default: {
|
||||||
|
postprocess(sections, vars) {
|
||||||
|
loadScript('/js/sections-util.js');
|
||||||
|
let varDef = Object.keys(vars).map(k => ` --${k}: ${vars[k].value};\n`).join('');
|
||||||
|
if (!varDef) return;
|
||||||
|
varDef = ':root {\n' + varDef + '}\n';
|
||||||
|
for (const section of sections) {
|
||||||
|
if (!styleCodeEmpty(section.code)) {
|
||||||
|
section.code = varDef + section.code;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
stylus: {
|
||||||
|
preprocess(source, vars) {
|
||||||
|
loadScript('/vendor/stylus-lang-bundle/stylus.min.js');
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const varDef = Object.keys(vars).map(key => `${key} = ${vars[key].value};\n`).join('');
|
||||||
|
if (!Error.captureStackTrace) Error.captureStackTrace = () => {};
|
||||||
|
self.stylus(varDef + source).render((err, output) => {
|
||||||
|
if (err) {
|
||||||
|
reject(err);
|
||||||
|
} else {
|
||||||
|
resolve(output);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
less: {
|
||||||
|
preprocess(source, vars) {
|
||||||
|
if (!self.less) {
|
||||||
|
self.less = {
|
||||||
|
logLevel: 0,
|
||||||
|
useFileCache: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
loadScript('/vendor/less/less.min.js');
|
||||||
|
const varDefs = Object.keys(vars).map(key => `@${key}:${vars[key].value};\n`).join('');
|
||||||
|
return self.less.render(varDefs + source)
|
||||||
|
.then(({css}) => css);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
uso: {
|
||||||
|
preprocess(source, vars) {
|
||||||
|
loadScript('/vendor-overwrites/colorpicker/colorconverter.js');
|
||||||
|
const pool = new Map();
|
||||||
|
return Promise.resolve(doReplace(source));
|
||||||
|
|
||||||
|
function getValue(name, rgb) {
|
||||||
|
if (!vars.hasOwnProperty(name)) {
|
||||||
|
if (name.endsWith('-rgb')) {
|
||||||
|
return getValue(name.slice(0, -4), true);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (rgb) {
|
||||||
|
if (vars[name].type === 'color') {
|
||||||
|
const color = colorConverter.parse(vars[name].value);
|
||||||
|
if (!color) return null;
|
||||||
|
const {r, g, b} = color;
|
||||||
|
return `${r}, ${g}, ${b}`;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (vars[name].type === 'dropdown' || vars[name].type === 'select') {
|
||||||
|
// prevent infinite recursion
|
||||||
|
pool.set(name, '');
|
||||||
|
return doReplace(vars[name].value);
|
||||||
|
}
|
||||||
|
return vars[name].value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function doReplace(text) {
|
||||||
|
return text.replace(/\/\*\[\[([\w-]+)\]\]\*\//g, (match, name) => {
|
||||||
|
if (!pool.has(name)) {
|
||||||
|
const value = getValue(name);
|
||||||
|
pool.set(name, value === null ? match : value);
|
||||||
|
}
|
||||||
|
return pool.get(name);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (preprocessor) {
|
||||||
|
if (!BUILDER[preprocessor]) {
|
||||||
|
throw new Error('unknwon preprocessor');
|
||||||
|
}
|
||||||
|
return BUILDER[preprocessor];
|
||||||
|
}
|
||||||
|
return BUILDER.default;
|
||||||
|
}
|
|
@ -1,54 +1,54 @@
|
||||||
/*
|
/* global download prefs openURL FIREFOX CHROME VIVALDI
|
||||||
global dbExec getStyles saveStyle deleteStyle
|
openEditor debounce URLS ignoreChromeError queryTabs getTab
|
||||||
global handleCssTransitionBug detectSloppyRegexps
|
styleManager msg navigatorUtil iconUtil workerUtil */
|
||||||
global openEditor
|
|
||||||
global styleViaAPI
|
|
||||||
global loadScript
|
|
||||||
global usercss
|
|
||||||
*/
|
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-var
|
||||||
|
var backgroundWorker = workerUtil.createWorker({
|
||||||
|
url: '/background/background-worker.js'
|
||||||
|
});
|
||||||
|
|
||||||
window.API_METHODS = Object.assign(window.API_METHODS || {}, {
|
window.API_METHODS = Object.assign(window.API_METHODS || {}, {
|
||||||
|
deleteStyle: styleManager.deleteStyle,
|
||||||
|
editSave: styleManager.editSave,
|
||||||
|
findStyle: styleManager.findStyle,
|
||||||
|
getAllStyles: styleManager.getAllStyles, // used by importer
|
||||||
|
getSectionsByUrl: styleManager.getSectionsByUrl,
|
||||||
|
getStyle: styleManager.get,
|
||||||
|
getStylesByUrl: styleManager.getStylesByUrl,
|
||||||
|
importStyle: styleManager.importStyle,
|
||||||
|
installStyle: styleManager.installStyle,
|
||||||
|
styleExists: styleManager.styleExists,
|
||||||
|
toggleStyle: styleManager.toggleStyle,
|
||||||
|
|
||||||
getStyles,
|
getTabUrlPrefix() {
|
||||||
saveStyle,
|
return this.sender.tab.url.match(/^([\w-]+:\/+[^/#]+)/)[1];
|
||||||
deleteStyle,
|
},
|
||||||
|
|
||||||
getStyleFromDB: id =>
|
|
||||||
dbExec('get', id).then(event => event.target.result),
|
|
||||||
|
|
||||||
download(msg) {
|
download(msg) {
|
||||||
delete msg.method;
|
delete msg.method;
|
||||||
return download(msg.url, msg);
|
return download(msg.url, msg);
|
||||||
},
|
},
|
||||||
parseCss({code}) {
|
parseCss({code}) {
|
||||||
return usercss.invokeWorker({action: 'parse', code});
|
return backgroundWorker.parseMozFormat({code});
|
||||||
},
|
},
|
||||||
getPrefs: prefs.getAll,
|
getPrefs: prefs.getAll,
|
||||||
healthCheck: () => dbExec().then(() => true),
|
|
||||||
|
|
||||||
detectSloppyRegexps,
|
|
||||||
openEditor,
|
openEditor,
|
||||||
updateIcon,
|
|
||||||
|
updateIconBadge(count) {
|
||||||
|
return updateIconBadge(this.sender.tab.id, count);
|
||||||
|
},
|
||||||
|
|
||||||
// exposed for stuff that requires followup sendMessage() like popup::openSettings
|
// exposed for stuff that requires followup sendMessage() like popup::openSettings
|
||||||
// that would fail otherwise if another extension forced the tab to open
|
// that would fail otherwise if another extension forced the tab to open
|
||||||
// in the foreground thus auto-closing the popup (in Chrome)
|
// in the foreground thus auto-closing the popup (in Chrome)
|
||||||
openURL,
|
openURL,
|
||||||
|
|
||||||
closeTab: (msg, sender, respond) => {
|
|
||||||
chrome.tabs.remove(msg.tabId || sender.tab.id, () => {
|
|
||||||
if (chrome.runtime.lastError && msg.tabId !== sender.tab.id) {
|
|
||||||
respond(new Error(chrome.runtime.lastError.message));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return KEEP_CHANNEL_OPEN;
|
|
||||||
},
|
|
||||||
|
|
||||||
optionsCustomizeHotkeys() {
|
optionsCustomizeHotkeys() {
|
||||||
return browser.runtime.openOptionsPage()
|
return browser.runtime.openOptionsPage()
|
||||||
.then(() => new Promise(resolve => setTimeout(resolve, 100)))
|
.then(() => new Promise(resolve => setTimeout(resolve, 100)))
|
||||||
.then(() => sendMessage({method: 'optionsCustomizeHotkeys'}));
|
.then(() => msg.broadcastExtension({method: 'optionsCustomizeHotkeys'}));
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -57,67 +57,31 @@ var browserCommands, contextMenus;
|
||||||
|
|
||||||
// *************************************************************************
|
// *************************************************************************
|
||||||
// register all listeners
|
// register all listeners
|
||||||
chrome.runtime.onMessage.addListener(onRuntimeMessage);
|
msg.on(onRuntimeMessage);
|
||||||
|
|
||||||
|
navigatorUtil.onUrlChange(({tabId, frameId}, type) => {
|
||||||
|
if (type === 'committed') {
|
||||||
|
// styles would be updated when content script is injected.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
msg.sendTab(tabId, {method: 'urlChanged'}, {frameId})
|
||||||
|
.catch(msg.ignoreError);
|
||||||
|
});
|
||||||
|
|
||||||
if (FIREFOX) {
|
if (FIREFOX) {
|
||||||
// see notes in apply.js for getStylesFallback
|
// FF applies page CSP even to content scripts, https://bugzil.la/1267027
|
||||||
const MSG_GET_STYLES = 'getStyles:';
|
navigatorUtil.onCommitted(webNavUsercssInstallerFF, {
|
||||||
const MSG_GET_STYLES_LEN = MSG_GET_STYLES.length;
|
url: [
|
||||||
chrome.runtime.onConnect.addListener(port => {
|
{hostSuffix: '.githubusercontent.com', urlSuffix: '.user.css'},
|
||||||
if (!port.name.startsWith(MSG_GET_STYLES)) return;
|
{hostSuffix: '.githubusercontent.com', urlSuffix: '.user.styl'},
|
||||||
const tabId = port.sender.tab.id;
|
]
|
||||||
const frameId = port.sender.frameId;
|
});
|
||||||
const options = tryJSONparse(port.name.slice(MSG_GET_STYLES_LEN));
|
// FF misses some about:blank iframes so we inject our content script explicitly
|
||||||
port.disconnect();
|
navigatorUtil.onDOMContentLoaded(webNavIframeHelperFF, {
|
||||||
getStyles(options).then(styles => {
|
url: [
|
||||||
if (!styles.length) return;
|
{urlEquals: 'about:blank'},
|
||||||
chrome.tabs.executeScript(tabId, {
|
]
|
||||||
code: `
|
|
||||||
applyOnMessage({
|
|
||||||
method: 'styleApply',
|
|
||||||
styles: ${JSON.stringify(styles)},
|
|
||||||
})
|
|
||||||
`,
|
|
||||||
runAt: 'document_start',
|
|
||||||
frameId,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
const listener =
|
|
||||||
URLS.chromeProtectsNTP
|
|
||||||
? webNavigationListenerChrome
|
|
||||||
: webNavigationListener;
|
|
||||||
|
|
||||||
chrome.webNavigation.onBeforeNavigate.addListener(data =>
|
|
||||||
listener(null, data));
|
|
||||||
|
|
||||||
chrome.webNavigation.onCommitted.addListener(data =>
|
|
||||||
listener('styleApply', data));
|
|
||||||
|
|
||||||
chrome.webNavigation.onHistoryStateUpdated.addListener(data =>
|
|
||||||
listener('styleReplaceAll', data));
|
|
||||||
|
|
||||||
chrome.webNavigation.onReferenceFragmentUpdated.addListener(data =>
|
|
||||||
listener('styleReplaceAll', data));
|
|
||||||
|
|
||||||
if (FIREFOX) {
|
|
||||||
// FF applies page CSP even to content scripts, https://bugzil.la/1267027
|
|
||||||
chrome.webNavigation.onCommitted.addListener(webNavUsercssInstallerFF, {
|
|
||||||
url: [
|
|
||||||
{hostSuffix: '.githubusercontent.com', urlSuffix: '.user.css'},
|
|
||||||
{hostSuffix: '.githubusercontent.com', urlSuffix: '.user.styl'},
|
|
||||||
]
|
|
||||||
});
|
|
||||||
// FF misses some about:blank iframes so we inject our content script explicitly
|
|
||||||
chrome.webNavigation.onDOMContentLoaded.addListener(webNavIframeHelperFF, {
|
|
||||||
url: [
|
|
||||||
{urlEquals: 'about:blank'},
|
|
||||||
]
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (chrome.contextMenus) {
|
if (chrome.contextMenus) {
|
||||||
|
@ -130,22 +94,45 @@ if (chrome.commands) {
|
||||||
chrome.commands.onCommand.addListener(command => browserCommands[command]());
|
chrome.commands.onCommand.addListener(command => browserCommands[command]());
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!chrome.browserAction ||
|
|
||||||
!['setIcon', 'setBadgeBackgroundColor', 'setBadgeText'].every(name => chrome.browserAction[name])) {
|
|
||||||
window.updateIcon = () => {};
|
|
||||||
}
|
|
||||||
|
|
||||||
const tabIcons = new Map();
|
const tabIcons = new Map();
|
||||||
chrome.tabs.onRemoved.addListener(tabId => tabIcons.delete(tabId));
|
chrome.tabs.onRemoved.addListener(tabId => tabIcons.delete(tabId));
|
||||||
chrome.tabs.onReplaced.addListener((added, removed) => tabIcons.delete(removed));
|
chrome.tabs.onReplaced.addListener((added, removed) => tabIcons.delete(removed));
|
||||||
|
|
||||||
// *************************************************************************
|
prefs.subscribe([
|
||||||
// set the default icon displayed after a tab is created until webNavigation kicks in
|
'disableAll',
|
||||||
prefs.subscribe(['iconset'], () =>
|
'badgeDisabled',
|
||||||
updateIcon({
|
'badgeNormal',
|
||||||
tab: {id: undefined},
|
], () => debounce(refreshIconBadgeColor));
|
||||||
styles: {},
|
|
||||||
}));
|
prefs.subscribe([
|
||||||
|
'show-badge'
|
||||||
|
], () => debounce(refreshIconBadgeText));
|
||||||
|
|
||||||
|
prefs.subscribe([
|
||||||
|
'disableAll',
|
||||||
|
'iconset',
|
||||||
|
], () => debounce(refreshAllIcons));
|
||||||
|
|
||||||
|
prefs.initializing.then(() => {
|
||||||
|
refreshIconBadgeColor();
|
||||||
|
refreshAllIconsBadgeText();
|
||||||
|
refreshAllIcons();
|
||||||
|
});
|
||||||
|
|
||||||
|
navigatorUtil.onUrlChange(({tabId, frameId, transitionQualifiers}, type) => {
|
||||||
|
if (type === 'committed' && !frameId) {
|
||||||
|
// it seems that the tab icon would be reset by navigation. We
|
||||||
|
// invalidate the cache here so it would be refreshed by `apply.js`.
|
||||||
|
tabIcons.delete(tabId);
|
||||||
|
|
||||||
|
// however, if the tab was swapped in by forward/backward buttons,
|
||||||
|
// `apply.js` doesn't notify the background to update the icon,
|
||||||
|
// so we have to refresh it manually.
|
||||||
|
if (transitionQualifiers.includes('forward_back')) {
|
||||||
|
msg.sendTab(tabId, {method: 'updateCount'}).catch(msg.ignoreError);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// *************************************************************************
|
// *************************************************************************
|
||||||
chrome.runtime.onInstalled.addListener(({reason}) => {
|
chrome.runtime.onInstalled.addListener(({reason}) => {
|
||||||
|
@ -191,7 +178,7 @@ contextMenus = {
|
||||||
contexts: ['editable'],
|
contexts: ['editable'],
|
||||||
documentUrlPatterns: [URLS.ownOrigin + 'edit*'],
|
documentUrlPatterns: [URLS.ownOrigin + 'edit*'],
|
||||||
click: (info, tab) => {
|
click: (info, tab) => {
|
||||||
sendMessage({tabId: tab.id, method: 'editDeleteText'});
|
msg.sendTab(tab.id, {method: 'editDeleteText'});
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -205,11 +192,10 @@ if (chrome.contextMenus) {
|
||||||
}
|
}
|
||||||
item = Object.assign({id}, item);
|
item = Object.assign({id}, item);
|
||||||
delete item.presentIf;
|
delete item.presentIf;
|
||||||
const prefValue = prefs.readOnlyValues[id];
|
|
||||||
item.title = chrome.i18n.getMessage(item.title);
|
item.title = chrome.i18n.getMessage(item.title);
|
||||||
if (!item.type && typeof prefValue === 'boolean') {
|
if (!item.type && typeof prefs.defaults[id] === 'boolean') {
|
||||||
item.type = 'checkbox';
|
item.type = 'checkbox';
|
||||||
item.checked = prefValue;
|
item.checked = prefs.get(id);
|
||||||
}
|
}
|
||||||
if (!item.contexts) {
|
if (!item.contexts) {
|
||||||
item.contexts = ['browser_action'];
|
item.contexts = ['browser_action'];
|
||||||
|
@ -233,24 +219,35 @@ if (chrome.contextMenus) {
|
||||||
};
|
};
|
||||||
|
|
||||||
const keys = Object.keys(contextMenus);
|
const keys = Object.keys(contextMenus);
|
||||||
prefs.subscribe(keys.filter(id => typeof prefs.readOnlyValues[id] === 'boolean'), toggleCheckmark);
|
prefs.subscribe(keys.filter(id => typeof prefs.defaults[id] === 'boolean'), toggleCheckmark);
|
||||||
prefs.subscribe(keys.filter(id => contextMenus[id].presentIf), togglePresence);
|
prefs.subscribe(keys.filter(id => contextMenus[id].presentIf), togglePresence);
|
||||||
createContextMenus(keys);
|
createContextMenus(keys);
|
||||||
}
|
}
|
||||||
|
|
||||||
// *************************************************************************
|
// reinject content scripts when the extension is reloaded/updated. Firefox
|
||||||
// [re]inject content scripts
|
// would handle this automatically.
|
||||||
window.addEventListener('storageReady', function _() {
|
if (!FIREFOX) {
|
||||||
window.removeEventListener('storageReady', _);
|
reinjectContentScripts();
|
||||||
|
}
|
||||||
|
|
||||||
updateIcon({
|
// register hotkeys
|
||||||
tab: {id: undefined},
|
if (FIREFOX && browser.commands && browser.commands.update) {
|
||||||
styles: {},
|
const hotkeyPrefs = Object.keys(prefs.defaults).filter(k => k.startsWith('hotkey.'));
|
||||||
|
prefs.subscribe(hotkeyPrefs, (name, value) => {
|
||||||
|
try {
|
||||||
|
name = name.split('.')[1];
|
||||||
|
if (value.trim()) {
|
||||||
|
browser.commands.update({name, shortcut: value});
|
||||||
|
} else {
|
||||||
|
browser.commands.reset(name);
|
||||||
|
}
|
||||||
|
} catch (e) {}
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Firefox injects content script automatically
|
msg.broadcastTab({method: 'backgroundReady'});
|
||||||
if (FIREFOX) return;
|
|
||||||
|
|
||||||
|
function reinjectContentScripts() {
|
||||||
const NTP = 'chrome://newtab/';
|
const NTP = 'chrome://newtab/';
|
||||||
const ALL_URLS = '<all_urls>';
|
const ALL_URLS = '<all_urls>';
|
||||||
const contentScripts = chrome.runtime.getManifest().content_scripts;
|
const contentScripts = chrome.runtime.getManifest().content_scripts;
|
||||||
|
@ -266,20 +263,23 @@ window.addEventListener('storageReady', function _() {
|
||||||
|
|
||||||
const injectCS = (cs, tabId) => {
|
const injectCS = (cs, tabId) => {
|
||||||
ignoreChromeError();
|
ignoreChromeError();
|
||||||
chrome.tabs.executeScript(tabId, {
|
for (const file of cs.js) {
|
||||||
file: cs.js[0],
|
chrome.tabs.executeScript(tabId, {
|
||||||
runAt: cs.run_at,
|
file,
|
||||||
allFrames: cs.all_frames,
|
runAt: cs.run_at,
|
||||||
matchAboutBlank: cs.match_about_blank,
|
allFrames: cs.all_frames,
|
||||||
}, ignoreChromeError);
|
matchAboutBlank: cs.match_about_blank,
|
||||||
|
}, ignoreChromeError);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const pingCS = (cs, {id, url}) => {
|
const pingCS = (cs, {id, url}) => {
|
||||||
const maybeInject = pong => !pong && injectCS(cs, id);
|
|
||||||
cs.matches.some(match => {
|
cs.matches.some(match => {
|
||||||
if ((match === ALL_URLS || url.match(match)) &&
|
if ((match === ALL_URLS || url.match(match)) &&
|
||||||
(!url.startsWith('chrome') || url === NTP)) {
|
(!url.startsWith('chrome') || url === NTP)) {
|
||||||
sendMessage({method: 'ping', tabId: id}, maybeInject);
|
msg.sendTab(id, {method: 'ping'})
|
||||||
|
.catch(() => false)
|
||||||
|
.then(pong => !pong && injectCS(cs, id));
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -293,85 +293,19 @@ window.addEventListener('storageReady', function _() {
|
||||||
setTimeout(pingCS, 0, cs, tab));
|
setTimeout(pingCS, 0, cs, tab));
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
});
|
|
||||||
|
|
||||||
// *************************************************************************
|
|
||||||
{
|
|
||||||
const getStylesForFrame = (msg, sender) => {
|
|
||||||
const stylesTask = getStyles(msg);
|
|
||||||
if (!sender || !sender.frameId) return stylesTask;
|
|
||||||
return Promise.all([
|
|
||||||
stylesTask,
|
|
||||||
getTab(sender.tab.id),
|
|
||||||
]).then(([styles, tab]) => {
|
|
||||||
if (tab) styles.exposeIframes = tab.url.replace(/(\/\/[^/]*).*/, '$1');
|
|
||||||
return styles;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
const updateAPI = (_, enabled) => {
|
|
||||||
window.API_METHODS.getStylesForFrame = enabled ? getStylesForFrame : getStyles;
|
|
||||||
};
|
|
||||||
prefs.subscribe(['exposeIframes'], updateAPI);
|
|
||||||
updateAPI(null, prefs.readOnlyValues.exposeIframes);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// *************************************************************************
|
|
||||||
|
|
||||||
function webNavigationListener(method, {url, tabId, frameId}) {
|
|
||||||
Promise.all([
|
|
||||||
getStyles({matchUrl: url, asHash: true}),
|
|
||||||
frameId && prefs.readOnlyValues.exposeIframes && getTab(tabId),
|
|
||||||
]).then(([styles, tab]) => {
|
|
||||||
if (method && URLS.supported(url) && tabId >= 0) {
|
|
||||||
if (method === 'styleApply') {
|
|
||||||
handleCssTransitionBug({tabId, frameId, url, styles});
|
|
||||||
}
|
|
||||||
if (tab) styles.exposeIframes = tab.url.replace(/(\/\/[^/]*).*/, '$1');
|
|
||||||
sendMessage({
|
|
||||||
tabId,
|
|
||||||
frameId,
|
|
||||||
method,
|
|
||||||
// ping own page so it retrieves the styles directly
|
|
||||||
styles: url.startsWith(URLS.ownOrigin) ? 'DIY' : styles,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
// main page frame id is 0
|
|
||||||
if (frameId === 0) {
|
|
||||||
tabIcons.delete(tabId);
|
|
||||||
updateIcon({tab: {id: tabId, url}, styles});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function webNavigationListenerChrome(method, data) {
|
|
||||||
// Chrome 61.0.3161+ doesn't run content scripts on NTP
|
|
||||||
if (
|
|
||||||
!data.url.startsWith('https://www.google.') ||
|
|
||||||
!data.url.includes('/_/chrome/newtab?')
|
|
||||||
) {
|
|
||||||
webNavigationListener(method, data);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
getTab(data.tabId).then(tab => {
|
|
||||||
if (tab.url === 'chrome://newtab/') {
|
|
||||||
data.url = tab.url;
|
|
||||||
}
|
|
||||||
webNavigationListener(method, data);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function webNavUsercssInstallerFF(data) {
|
function webNavUsercssInstallerFF(data) {
|
||||||
const {tabId} = data;
|
const {tabId} = data;
|
||||||
Promise.all([
|
Promise.all([
|
||||||
sendMessage({tabId, method: 'ping'}),
|
msg.sendTab(tabId, {method: 'ping'})
|
||||||
|
.catch(() => false),
|
||||||
// we need tab index to open the installer next to the original one
|
// we need tab index to open the installer next to the original one
|
||||||
// and also to skip the double-invocation in FF which assigns tab url later
|
// and also to skip the double-invocation in FF which assigns tab url later
|
||||||
getTab(tabId),
|
getTab(tabId),
|
||||||
]).then(([pong, tab]) => {
|
]).then(([pong, tab]) => {
|
||||||
if (pong !== true && tab.url !== 'about:blank') {
|
if (pong !== true && tab.url !== 'about:blank') {
|
||||||
window.API_METHODS.installUsercss({direct: true}, {tab});
|
window.API_METHODS.openUsercssInstallPage({direct: true}, {tab});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -379,135 +313,107 @@ function webNavUsercssInstallerFF(data) {
|
||||||
|
|
||||||
function webNavIframeHelperFF({tabId, frameId}) {
|
function webNavIframeHelperFF({tabId, frameId}) {
|
||||||
if (!frameId) return;
|
if (!frameId) return;
|
||||||
sendMessage({method: 'ping', tabId, frameId}, pong => {
|
msg.sendTab(tabId, {method: 'ping'}, {frameId})
|
||||||
ignoreChromeError();
|
.catch(() => false)
|
||||||
if (pong) return;
|
.then(pong => {
|
||||||
chrome.tabs.executeScript(tabId, {
|
if (pong) return;
|
||||||
frameId,
|
// insert apply.js to iframe
|
||||||
file: '/content/apply.js',
|
const files = chrome.runtime.getManifest().content_scripts[0].js;
|
||||||
matchAboutBlank: true,
|
for (const file of files) {
|
||||||
}, ignoreChromeError);
|
chrome.tabs.executeScript(tabId, {
|
||||||
|
frameId,
|
||||||
|
file,
|
||||||
|
matchAboutBlank: true,
|
||||||
|
}, ignoreChromeError);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateIconBadge(tabId, count) {
|
||||||
|
let tabIcon = tabIcons.get(tabId);
|
||||||
|
if (!tabIcon) tabIcons.set(tabId, (tabIcon = {}));
|
||||||
|
if (tabIcon.count === count) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const oldCount = tabIcon.count;
|
||||||
|
tabIcon.count = count;
|
||||||
|
refreshIconBadgeText(tabId, tabIcon);
|
||||||
|
if (Boolean(oldCount) !== Boolean(count)) {
|
||||||
|
refreshIcon(tabId, tabIcon);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function refreshIconBadgeText(tabId, icon) {
|
||||||
|
iconUtil.setBadgeText({
|
||||||
|
text: prefs.get('show-badge') && icon.count ? String(icon.count) : '',
|
||||||
|
tabId
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function refreshIcon(tabId, icon) {
|
||||||
|
const disableAll = prefs.get('disableAll');
|
||||||
|
const iconset = prefs.get('iconset') === 1 ? 'light/' : '';
|
||||||
|
const postfix = disableAll ? 'x' : !icon.count ? 'w' : '';
|
||||||
|
const iconType = iconset + postfix;
|
||||||
|
|
||||||
function updateIcon({tab, styles}) {
|
if (icon.iconType === iconType) {
|
||||||
if (tab.id < 0) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (URLS.chromeProtectsNTP && tab.url === 'chrome://newtab/') {
|
icon.iconType = iconset + postfix;
|
||||||
styles = {};
|
const sizes = FIREFOX || CHROME >= 2883 && !VIVALDI ? [16, 32] : [19, 38];
|
||||||
|
iconUtil.setIcon({
|
||||||
|
path: sizes.reduce(
|
||||||
|
(obj, size) => {
|
||||||
|
obj[size] = `/images/icon/${iconset}${size}${postfix}.png`;
|
||||||
|
return obj;
|
||||||
|
},
|
||||||
|
{}
|
||||||
|
),
|
||||||
|
tabId
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function refreshIconBadgeColor() {
|
||||||
|
const color = prefs.get(prefs.get('disableAll') ? 'badgeDisabled' : 'badgeNormal');
|
||||||
|
iconUtil.setBadgeBackgroundColor({
|
||||||
|
color
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function refreshAllIcons() {
|
||||||
|
for (const [tabId, icon] of tabIcons) {
|
||||||
|
refreshIcon(tabId, icon);
|
||||||
}
|
}
|
||||||
if (styles) {
|
refreshIcon(null, {}); // default icon
|
||||||
stylesReceived(styles);
|
}
|
||||||
return;
|
|
||||||
}
|
|
||||||
getTabRealURL(tab)
|
|
||||||
.then(url => getStyles({matchUrl: url, asHash: true}))
|
|
||||||
.then(stylesReceived);
|
|
||||||
|
|
||||||
function stylesReceived(styles) {
|
function refreshAllIconsBadgeText() {
|
||||||
const disableAll = 'disableAll' in styles ? styles.disableAll : prefs.get('disableAll');
|
for (const [tabId, icon] of tabIcons) {
|
||||||
const postfix = disableAll ? 'x' : !styles.length ? 'w' : '';
|
refreshIconBadgeText(tabId, icon);
|
||||||
const color = prefs.get(disableAll ? 'badgeDisabled' : 'badgeNormal');
|
|
||||||
const text = prefs.get('show-badge') && styles.length ? String(styles.length) : '';
|
|
||||||
const iconset = ['', 'light/'][prefs.get('iconset')] || '';
|
|
||||||
|
|
||||||
let tabIcon = tabIcons.get(tab.id);
|
|
||||||
if (!tabIcon) tabIcons.set(tab.id, (tabIcon = {}));
|
|
||||||
|
|
||||||
if (tabIcon.iconType !== iconset + postfix) {
|
|
||||||
tabIcon.iconType = iconset + postfix;
|
|
||||||
const sizes = FIREFOX || CHROME >= 2883 && !VIVALDI ? [16, 32] : [19, 38];
|
|
||||||
const usePath = tabIcons.get('usePath');
|
|
||||||
Promise.all(sizes.map(size => {
|
|
||||||
const src = `/images/icon/${iconset}${size}${postfix}.png`;
|
|
||||||
return usePath ? src : tabIcons.get(src) || loadIcon(src);
|
|
||||||
})).then(data => {
|
|
||||||
const imageKey = typeof data[0] === 'string' ? 'path' : 'imageData';
|
|
||||||
const imageData = {};
|
|
||||||
sizes.forEach((size, i) => (imageData[size] = data[i]));
|
|
||||||
chrome.browserAction.setIcon({
|
|
||||||
tabId: tab.id,
|
|
||||||
[imageKey]: imageData,
|
|
||||||
}, ignoreChromeError);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (tab.id === undefined) return;
|
|
||||||
|
|
||||||
let defaultIcon = tabIcons.get(undefined);
|
|
||||||
if (!defaultIcon) tabIcons.set(undefined, (defaultIcon = {}));
|
|
||||||
if (defaultIcon.color !== color) {
|
|
||||||
defaultIcon.color = color;
|
|
||||||
chrome.browserAction.setBadgeBackgroundColor({color});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (tabIcon.text === text) return;
|
|
||||||
tabIcon.text = text;
|
|
||||||
try {
|
|
||||||
// Chrome supports the callback since 67.0.3381.0, see https://crbug.com/451320
|
|
||||||
chrome.browserAction.setBadgeText({text, tabId: tab.id}, ignoreChromeError);
|
|
||||||
} catch (e) {
|
|
||||||
setTimeout(() => {
|
|
||||||
getTab(tab.id).then(realTab => {
|
|
||||||
// skip pre-rendered tabs
|
|
||||||
if (realTab.index >= 0) {
|
|
||||||
chrome.browserAction.setBadgeText({text, tabId: tab.id});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function loadIcon(src, resolve) {
|
|
||||||
if (!resolve) return new Promise(resolve => loadIcon(src, resolve));
|
|
||||||
const canvas = document.createElement('canvas');
|
|
||||||
const ctx = canvas.getContext('2d');
|
|
||||||
const img = new Image();
|
|
||||||
img.src = src;
|
|
||||||
img.onload = () => {
|
|
||||||
const w = canvas.width = img.width;
|
|
||||||
const h = canvas.height = img.height;
|
|
||||||
ctx.clearRect(0, 0, w, h);
|
|
||||||
ctx.drawImage(img, 0, 0, w, h);
|
|
||||||
const data = ctx.getImageData(0, 0, w, h);
|
|
||||||
// Firefox breaks Canvas when privacy.resistFingerprinting=true, https://bugzil.la/1412961
|
|
||||||
let usePath = tabIcons.get('usePath');
|
|
||||||
if (usePath === undefined) {
|
|
||||||
usePath = data.data.every(b => b === 255);
|
|
||||||
tabIcons.set('usePath', usePath);
|
|
||||||
}
|
|
||||||
if (usePath) {
|
|
||||||
resolve(src);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
tabIcons.set(src, data);
|
|
||||||
resolve(data);
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function onRuntimeMessage(msg, sender) {
|
||||||
|
if (msg.method !== 'invokeAPI') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const fn = window.API_METHODS[msg.name];
|
||||||
|
if (!fn) {
|
||||||
|
throw new Error(`unknown API: ${msg.name}`);
|
||||||
|
}
|
||||||
|
const context = {msg, sender};
|
||||||
|
return fn.apply(context, msg.args);
|
||||||
|
}
|
||||||
|
|
||||||
function onRuntimeMessage(msg, sender, sendResponse) {
|
// FIXME: popup.js also open editor but it doesn't use this API.
|
||||||
const fn = window.API_METHODS[msg.method];
|
function openEditor({id}) {
|
||||||
if (!fn) return;
|
let url = '/edit.html';
|
||||||
|
if (id) {
|
||||||
// wrap 'Error' object instance as {__ERROR__: message},
|
url += `?id=${id}`;
|
||||||
// which will be unwrapped by sendMessage,
|
}
|
||||||
// and prevent exceptions on sending to a closed tab
|
if (chrome.windows && prefs.get('openEditInWindow')) {
|
||||||
const respond = data =>
|
chrome.windows.create(Object.assign({url}, prefs.get('windowPosition')));
|
||||||
tryCatch(sendResponse,
|
} else {
|
||||||
data instanceof Error ? {__ERROR__: data.message} : data);
|
openURL({url});
|
||||||
|
|
||||||
const result = fn(msg, sender, respond);
|
|
||||||
if (result instanceof Promise) {
|
|
||||||
result
|
|
||||||
.catch(e => ({__ERROR__: e instanceof Error ? e.message : e}))
|
|
||||||
.then(respond);
|
|
||||||
return KEEP_CHANNEL_OPEN;
|
|
||||||
} else if (result === KEEP_CHANNEL_OPEN) {
|
|
||||||
return KEEP_CHANNEL_OPEN;
|
|
||||||
} else if (result !== undefined) {
|
|
||||||
respond(result);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
154
background/db.js
Normal file
154
background/db.js
Normal file
|
@ -0,0 +1,154 @@
|
||||||
|
/* global tryCatch chromeLocal ignoreChromeError */
|
||||||
|
/* exported db */
|
||||||
|
/*
|
||||||
|
Initialize a database. There are some problems using IndexedDB in Firefox:
|
||||||
|
https://www.reddit.com/r/firefox/comments/74wttb/note_to_firefox_webextension_developers_who_use/
|
||||||
|
|
||||||
|
Some of them are fixed in FF59:
|
||||||
|
https://www.reddit.com/r/firefox/comments/7ijuaq/firefox_59_webextensions_can_use_indexeddb_when/
|
||||||
|
*/
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const db = (() => {
|
||||||
|
let exec;
|
||||||
|
const preparing = prepare();
|
||||||
|
return {
|
||||||
|
exec: (...args) =>
|
||||||
|
preparing.then(() => exec(...args))
|
||||||
|
};
|
||||||
|
|
||||||
|
function prepare() {
|
||||||
|
// we use chrome.storage.local fallback if IndexedDB doesn't save data,
|
||||||
|
// which, once detected on the first run, is remembered in chrome.storage.local
|
||||||
|
// for reliablility and in localStorage for fast synchronous access
|
||||||
|
// (FF may block localStorage depending on its privacy options)
|
||||||
|
|
||||||
|
// test localStorage
|
||||||
|
const fallbackSet = localStorage.dbInChromeStorage;
|
||||||
|
if (fallbackSet === 'true' || !tryCatch(() => indexedDB)) {
|
||||||
|
useChromeStorage();
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
if (fallbackSet === 'false') {
|
||||||
|
useIndexedDB();
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
// test storage.local
|
||||||
|
return chromeLocal.get('dbInChromeStorage')
|
||||||
|
.then(data =>
|
||||||
|
data && data.dbInChromeStorage && Promise.reject())
|
||||||
|
.then(() =>
|
||||||
|
tryCatch(dbExecIndexedDB, 'getAllKeys', IDBKeyRange.lowerBound(1), 1) ||
|
||||||
|
Promise.reject())
|
||||||
|
.then(({target}) => (
|
||||||
|
(target.result || [])[0] ?
|
||||||
|
Promise.reject('ok') :
|
||||||
|
dbExecIndexedDB('put', {id: -1})))
|
||||||
|
.then(() =>
|
||||||
|
dbExecIndexedDB('get', -1))
|
||||||
|
.then(({target}) => (
|
||||||
|
(target.result || {}).id === -1 ?
|
||||||
|
dbExecIndexedDB('delete', -1) :
|
||||||
|
Promise.reject()))
|
||||||
|
.then(() =>
|
||||||
|
Promise.reject('ok'))
|
||||||
|
.catch(result => {
|
||||||
|
if (result === 'ok') {
|
||||||
|
useIndexedDB();
|
||||||
|
} else {
|
||||||
|
useChromeStorage();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function useChromeStorage() {
|
||||||
|
exec = dbExecChromeStorage;
|
||||||
|
chromeLocal.set({dbInChromeStorage: true}, ignoreChromeError);
|
||||||
|
localStorage.dbInChromeStorage = 'true';
|
||||||
|
}
|
||||||
|
|
||||||
|
function useIndexedDB() {
|
||||||
|
exec = dbExecIndexedDB;
|
||||||
|
chromeLocal.set({dbInChromeStorage: false}, ignoreChromeError);
|
||||||
|
localStorage.dbInChromeStorage = 'false';
|
||||||
|
}
|
||||||
|
|
||||||
|
function dbExecIndexedDB(method, ...args) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
Object.assign(indexedDB.open('stylish', 2), {
|
||||||
|
onsuccess(event) {
|
||||||
|
const database = event.target.result;
|
||||||
|
if (!method) {
|
||||||
|
resolve(database);
|
||||||
|
} else {
|
||||||
|
const transaction = database.transaction(['styles'], 'readwrite');
|
||||||
|
const store = transaction.objectStore('styles');
|
||||||
|
try {
|
||||||
|
Object.assign(store[method](...args), {
|
||||||
|
onsuccess: event => resolve(event, store, transaction, database),
|
||||||
|
onerror: reject,
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
reject(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onerror(event) {
|
||||||
|
console.warn(event.target.error || event.target.errorCode);
|
||||||
|
reject(event);
|
||||||
|
},
|
||||||
|
onupgradeneeded(event) {
|
||||||
|
if (event.oldVersion === 0) {
|
||||||
|
event.target.result.createObjectStore('styles', {
|
||||||
|
keyPath: 'id',
|
||||||
|
autoIncrement: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function dbExecChromeStorage(method, data) {
|
||||||
|
const STYLE_KEY_PREFIX = 'style-';
|
||||||
|
switch (method) {
|
||||||
|
case 'get':
|
||||||
|
return chromeLocal.getValue(STYLE_KEY_PREFIX + data)
|
||||||
|
.then(result => ({target: {result}}));
|
||||||
|
|
||||||
|
case 'put':
|
||||||
|
if (!data.id) {
|
||||||
|
return getAllStyles().then(styles => {
|
||||||
|
data.id = 1;
|
||||||
|
for (const style of styles) {
|
||||||
|
data.id = Math.max(data.id, style.id + 1);
|
||||||
|
}
|
||||||
|
return dbExecChromeStorage('put', data);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return chromeLocal.setValue(STYLE_KEY_PREFIX + data.id, data)
|
||||||
|
.then(() => (chrome.runtime.lastError ? Promise.reject() : data.id));
|
||||||
|
|
||||||
|
case 'delete':
|
||||||
|
return chromeLocal.remove(STYLE_KEY_PREFIX + data);
|
||||||
|
|
||||||
|
case 'getAll':
|
||||||
|
return getAllStyles()
|
||||||
|
.then(styles => ({target: {result: styles}}));
|
||||||
|
}
|
||||||
|
return Promise.reject();
|
||||||
|
|
||||||
|
function getAllStyles() {
|
||||||
|
return chromeLocal.get(null).then(storage => {
|
||||||
|
const styles = [];
|
||||||
|
for (const key in storage) {
|
||||||
|
if (key.startsWith(STYLE_KEY_PREFIX) &&
|
||||||
|
Number(key.substr(STYLE_KEY_PREFIX.length))) {
|
||||||
|
styles.push(storage[key]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return styles;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})();
|
91
background/icon-util.js
Normal file
91
background/icon-util.js
Normal file
|
@ -0,0 +1,91 @@
|
||||||
|
/* global ignoreChromeError */
|
||||||
|
/* exported iconUtil */
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const iconUtil = (() => {
|
||||||
|
const canvas = document.createElement('canvas');
|
||||||
|
const ctx = canvas.getContext('2d');
|
||||||
|
// https://github.com/openstyles/stylus/issues/335
|
||||||
|
let noCanvas;
|
||||||
|
const imageDataCache = new Map();
|
||||||
|
// test if canvas is usable
|
||||||
|
const canvasReady = loadImage('/images/icon/16.png')
|
||||||
|
.then(imageData => {
|
||||||
|
noCanvas = imageData.data.every(b => b === 255);
|
||||||
|
});
|
||||||
|
|
||||||
|
return extendNative({
|
||||||
|
/*
|
||||||
|
Cache imageData for paths
|
||||||
|
*/
|
||||||
|
setIcon,
|
||||||
|
setBadgeText
|
||||||
|
});
|
||||||
|
|
||||||
|
function loadImage(url) {
|
||||||
|
let result = imageDataCache.get(url);
|
||||||
|
if (!result) {
|
||||||
|
result = new Promise((resolve, reject) => {
|
||||||
|
const img = new Image();
|
||||||
|
img.src = url;
|
||||||
|
img.onload = () => {
|
||||||
|
const w = canvas.width = img.width;
|
||||||
|
const h = canvas.height = img.height;
|
||||||
|
ctx.clearRect(0, 0, w, h);
|
||||||
|
ctx.drawImage(img, 0, 0, w, h);
|
||||||
|
resolve(ctx.getImageData(0, 0, w, h));
|
||||||
|
};
|
||||||
|
img.onerror = reject;
|
||||||
|
});
|
||||||
|
imageDataCache.set(url, result);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setIcon(data) {
|
||||||
|
canvasReady.then(() => {
|
||||||
|
if (noCanvas) {
|
||||||
|
chrome.browserAction.setIcon(data, ignoreChromeError);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const pending = [];
|
||||||
|
data.imageData = {};
|
||||||
|
for (const [key, url] of Object.entries(data.path)) {
|
||||||
|
pending.push(loadImage(url)
|
||||||
|
.then(imageData => {
|
||||||
|
data.imageData[key] = imageData;
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
Promise.all(pending).then(() => {
|
||||||
|
delete data.path;
|
||||||
|
chrome.browserAction.setIcon(data, ignoreChromeError);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function setBadgeText(data) {
|
||||||
|
try {
|
||||||
|
// Chrome supports the callback since 67.0.3381.0, see https://crbug.com/451320
|
||||||
|
chrome.browserAction.setBadgeText(data, ignoreChromeError);
|
||||||
|
} catch (e) {
|
||||||
|
// FIXME: skip pre-rendered tabs?
|
||||||
|
chrome.browserAction.setBadgeText(data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function extendNative(target) {
|
||||||
|
return new Proxy(target, {
|
||||||
|
get: (target, prop) => {
|
||||||
|
// FIXME: do we really need this?
|
||||||
|
if (!chrome.browserAction ||
|
||||||
|
!['setIcon', 'setBadgeBackgroundColor', 'setBadgeText'].every(name => chrome.browserAction[name])) {
|
||||||
|
return () => {};
|
||||||
|
}
|
||||||
|
if (target[prop]) {
|
||||||
|
return target[prop];
|
||||||
|
}
|
||||||
|
return chrome.browserAction[prop].bind(chrome.browserAction);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})();
|
75
background/navigator-util.js
Normal file
75
background/navigator-util.js
Normal file
|
@ -0,0 +1,75 @@
|
||||||
|
/* global promisify CHROME URLS */
|
||||||
|
/* exported navigatorUtil */
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const navigatorUtil = (() => {
|
||||||
|
const handler = {
|
||||||
|
urlChange: null
|
||||||
|
};
|
||||||
|
const tabGet = promisify(chrome.tabs.get.bind(chrome.tabs));
|
||||||
|
return extendNative({onUrlChange});
|
||||||
|
|
||||||
|
function onUrlChange(fn) {
|
||||||
|
initUrlChange();
|
||||||
|
handler.urlChange.push(fn);
|
||||||
|
}
|
||||||
|
|
||||||
|
function initUrlChange() {
|
||||||
|
if (handler.urlChange) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
handler.urlChange = [];
|
||||||
|
|
||||||
|
chrome.webNavigation.onCommitted.addListener(data =>
|
||||||
|
fixNTPUrl(data)
|
||||||
|
.then(() => executeCallbacks(handler.urlChange, data, 'committed'))
|
||||||
|
.catch(console.error)
|
||||||
|
);
|
||||||
|
|
||||||
|
chrome.webNavigation.onHistoryStateUpdated.addListener(data =>
|
||||||
|
fixNTPUrl(data)
|
||||||
|
.then(() => executeCallbacks(handler.urlChange, data, 'historyStateUpdated'))
|
||||||
|
.catch(console.error)
|
||||||
|
);
|
||||||
|
|
||||||
|
chrome.webNavigation.onReferenceFragmentUpdated.addListener(data =>
|
||||||
|
fixNTPUrl(data)
|
||||||
|
.then(() => executeCallbacks(handler.urlChange, data, 'referenceFragmentUpdated'))
|
||||||
|
.catch(console.error)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function fixNTPUrl(data) {
|
||||||
|
if (
|
||||||
|
!CHROME ||
|
||||||
|
!URLS.chromeProtectsNTP ||
|
||||||
|
!data.url.startsWith('https://www.google.') ||
|
||||||
|
!data.url.includes('/_/chrome/newtab?')
|
||||||
|
) {
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
return tabGet(data.tabId)
|
||||||
|
.then(tab => {
|
||||||
|
if (tab.url === 'chrome://newtab/') {
|
||||||
|
data.url = tab.url;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function executeCallbacks(callbacks, data, type) {
|
||||||
|
for (const cb of callbacks) {
|
||||||
|
cb(data, type);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function extendNative(target) {
|
||||||
|
return new Proxy(target, {
|
||||||
|
get: (target, prop) => {
|
||||||
|
if (target[prop]) {
|
||||||
|
return target[prop];
|
||||||
|
}
|
||||||
|
return chrome.webNavigation[prop].addListener.bind(chrome.webNavigation[prop]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})();
|
|
@ -1,9 +0,0 @@
|
||||||
/* global importScripts parserlib CSSLint parseMozFormat */
|
|
||||||
'use strict';
|
|
||||||
|
|
||||||
importScripts('/vendor-overwrites/csslint/parserlib.js', '/js/moz-parser.js');
|
|
||||||
parserlib.css.Tokens[parserlib.css.Tokens.COMMENT].hide = false;
|
|
||||||
|
|
||||||
self.onmessage = ({data}) => {
|
|
||||||
self.postMessage(parseMozFormat(data));
|
|
||||||
};
|
|
|
@ -1,226 +0,0 @@
|
||||||
/*
|
|
||||||
global API_METHODS cachedStyles
|
|
||||||
global getStyles filterStyles invalidateCache normalizeStyleSections
|
|
||||||
global updateIcon
|
|
||||||
*/
|
|
||||||
'use strict';
|
|
||||||
|
|
||||||
(() => {
|
|
||||||
const previewFromTabs = new Map();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* When style id and state is provided, only that style is propagated.
|
|
||||||
* Otherwise all styles are replaced and the toolbar icon is updated.
|
|
||||||
* @param {Object} [msg]
|
|
||||||
* @param {{id:Number, enabled?:Boolean, sections?: (Array|String)}} [msg.style] -
|
|
||||||
* style to propagate
|
|
||||||
* @param {Boolean} [msg.codeIsUpdated]
|
|
||||||
* @returns {Promise<void>}
|
|
||||||
*/
|
|
||||||
API_METHODS.refreshAllTabs = (msg = {}) =>
|
|
||||||
Promise.all([
|
|
||||||
queryTabs(),
|
|
||||||
maybeParseUsercss(msg),
|
|
||||||
getStyles(),
|
|
||||||
]).then(([tabs, style]) =>
|
|
||||||
new Promise(resolve => {
|
|
||||||
if (style) msg.style.sections = normalizeStyleSections(style);
|
|
||||||
run(tabs, msg, resolve);
|
|
||||||
}));
|
|
||||||
|
|
||||||
|
|
||||||
function run(tabs, msg, resolve) {
|
|
||||||
const {style, codeIsUpdated, refreshOwnTabs} = msg;
|
|
||||||
|
|
||||||
// the style was updated/saved so we need to remove the old copy of the original style
|
|
||||||
if (msg.method === 'styleUpdated' && msg.reason !== 'editPreview') {
|
|
||||||
for (const [tabId, original] of previewFromTabs.entries()) {
|
|
||||||
if (style.id === original.id) {
|
|
||||||
previewFromTabs.delete(tabId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!previewFromTabs.size) {
|
|
||||||
unregisterTabListeners();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!style) {
|
|
||||||
msg = {method: 'styleReplaceAll'};
|
|
||||||
|
|
||||||
// live preview puts the code in cachedStyles, saves the original in previewFromTabs,
|
|
||||||
// and if preview is being disabled, but the style is already deleted, we bail out
|
|
||||||
} else if (msg.reason === 'editPreview' && !updateCache(msg)) {
|
|
||||||
return;
|
|
||||||
|
|
||||||
// simple style update:
|
|
||||||
// * if disabled, apply.js will remove the element
|
|
||||||
// * if toggled and code is unchanged, apply.js will toggle the element
|
|
||||||
} else if (!style.enabled || codeIsUpdated === false) {
|
|
||||||
msg = {
|
|
||||||
method: 'styleUpdated',
|
|
||||||
reason: msg.reason,
|
|
||||||
style: {
|
|
||||||
id: style.id,
|
|
||||||
enabled: style.enabled,
|
|
||||||
},
|
|
||||||
codeIsUpdated,
|
|
||||||
};
|
|
||||||
|
|
||||||
// live preview normal operation, the new code is already in cachedStyles
|
|
||||||
} else {
|
|
||||||
msg.method = 'styleApply';
|
|
||||||
msg.style = {id: msg.style.id};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!tabs || !tabs.length) {
|
|
||||||
resolve();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const last = tabs[tabs.length - 1];
|
|
||||||
for (const tab of tabs) {
|
|
||||||
if (FIREFOX && !tab.width) continue;
|
|
||||||
if (refreshOwnTabs === false && tab.url.startsWith(URLS.ownOrigin)) continue;
|
|
||||||
chrome.webNavigation.getAllFrames({tabId: tab.id}, frames =>
|
|
||||||
refreshFrame(tab, frames, msg, tab === last && resolve));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function refreshFrame(tab, frames, msg, resolve) {
|
|
||||||
ignoreChromeError();
|
|
||||||
if (!frames || !frames.length) {
|
|
||||||
frames = [{
|
|
||||||
frameId: 0,
|
|
||||||
url: tab.url,
|
|
||||||
}];
|
|
||||||
}
|
|
||||||
msg.tabId = tab.id;
|
|
||||||
const styleId = msg.style && msg.style.id;
|
|
||||||
|
|
||||||
for (const frame of frames) {
|
|
||||||
|
|
||||||
const styles = filterStyles({
|
|
||||||
matchUrl: getFrameUrl(frame, frames),
|
|
||||||
asHash: true,
|
|
||||||
id: styleId,
|
|
||||||
});
|
|
||||||
|
|
||||||
msg = Object.assign({}, msg);
|
|
||||||
msg.frameId = frame.frameId;
|
|
||||||
|
|
||||||
if (msg.method !== 'styleUpdated') {
|
|
||||||
msg.styles = styles;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (msg.method === 'styleApply' && !styles.length) {
|
|
||||||
// remove the style from a previously matching frame
|
|
||||||
invokeOrPostpone(tab.active, sendMessage, {
|
|
||||||
method: 'styleUpdated',
|
|
||||||
reason: 'editPreview',
|
|
||||||
style: {
|
|
||||||
id: styleId,
|
|
||||||
enabled: false,
|
|
||||||
},
|
|
||||||
tabId: tab.id,
|
|
||||||
frameId: frame.frameId,
|
|
||||||
}, ignoreChromeError);
|
|
||||||
} else {
|
|
||||||
invokeOrPostpone(tab.active, sendMessage, msg, ignoreChromeError);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!frame.frameId) {
|
|
||||||
setTimeout(updateIcon, 0, {
|
|
||||||
tab,
|
|
||||||
styles: msg.method === 'styleReplaceAll' ? styles : undefined,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (resolve) resolve();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function getFrameUrl(frame, frames) {
|
|
||||||
while (frame.url === 'about:blank' && frame.frameId > 0) {
|
|
||||||
const parent = frames.find(f => f.frameId === frame.parentFrameId);
|
|
||||||
if (!parent) break;
|
|
||||||
frame.url = parent.url;
|
|
||||||
frame = parent;
|
|
||||||
}
|
|
||||||
return (frame || frames[0]).url;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function maybeParseUsercss({style}) {
|
|
||||||
if (style && typeof style.sections === 'string') {
|
|
||||||
return API_METHODS.parseUsercss({sourceCode: style.sections});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function updateCache(msg) {
|
|
||||||
const {style, tabId, restoring} = msg;
|
|
||||||
const spoofed = !restoring && previewFromTabs.get(tabId);
|
|
||||||
const original = cachedStyles.byId.get(style.id);
|
|
||||||
|
|
||||||
if (style.sections && !restoring) {
|
|
||||||
if (!previewFromTabs.size) {
|
|
||||||
registerTabListeners();
|
|
||||||
}
|
|
||||||
if (!spoofed) {
|
|
||||||
previewFromTabs.set(tabId, Object.assign({}, original));
|
|
||||||
}
|
|
||||||
|
|
||||||
} else {
|
|
||||||
previewFromTabs.delete(tabId);
|
|
||||||
if (!previewFromTabs.size) {
|
|
||||||
unregisterTabListeners();
|
|
||||||
}
|
|
||||||
if (!original) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!restoring) {
|
|
||||||
msg.style = spoofed || original;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
invalidateCache({updated: msg.style});
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function registerTabListeners() {
|
|
||||||
chrome.tabs.onRemoved.addListener(onTabRemoved);
|
|
||||||
chrome.tabs.onReplaced.addListener(onTabReplaced);
|
|
||||||
chrome.webNavigation.onCommitted.addListener(onTabNavigated);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function unregisterTabListeners() {
|
|
||||||
chrome.tabs.onRemoved.removeListener(onTabRemoved);
|
|
||||||
chrome.tabs.onReplaced.removeListener(onTabReplaced);
|
|
||||||
chrome.webNavigation.onCommitted.removeListener(onTabNavigated);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function onTabRemoved(tabId) {
|
|
||||||
const style = previewFromTabs.get(tabId);
|
|
||||||
if (style) {
|
|
||||||
API_METHODS.refreshAllTabs({
|
|
||||||
style,
|
|
||||||
tabId,
|
|
||||||
reason: 'editPreview',
|
|
||||||
restoring: true,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function onTabReplaced(addedTabId, removedTabId) {
|
|
||||||
onTabRemoved(removedTabId);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function onTabNavigated({tabId}) {
|
|
||||||
onTabRemoved(tabId);
|
|
||||||
}
|
|
||||||
})();
|
|
|
@ -1,4 +1,4 @@
|
||||||
/* global API_METHODS filterStyles cachedStyles */
|
/* global API_METHODS styleManager tryRegExp debounce */
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
(() => {
|
(() => {
|
||||||
|
@ -25,7 +25,8 @@
|
||||||
if (/^url:/i.test(query)) {
|
if (/^url:/i.test(query)) {
|
||||||
matchUrl = query.slice(query.indexOf(':') + 1).trim();
|
matchUrl = query.slice(query.indexOf(':') + 1).trim();
|
||||||
if (matchUrl) {
|
if (matchUrl) {
|
||||||
return filterStyles({matchUrl}).map(style => style.id);
|
return styleManager.getStylesByUrl(matchUrl)
|
||||||
|
.then(results => results.map(r => r.data.id));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (query.startsWith('/') && /^\/(.+?)\/([gimsuy]*)$/.test(query)) {
|
if (query.startsWith('/') && /^\/(.+?)\/([gimsuy]*)$/.test(query)) {
|
||||||
|
@ -43,26 +44,29 @@
|
||||||
icase = words.some(w => w === lower(w));
|
icase = words.some(w => w === lower(w));
|
||||||
}
|
}
|
||||||
|
|
||||||
const results = [];
|
return styleManager.getAllStyles().then(styles => {
|
||||||
for (const item of ids || cachedStyles.list) {
|
if (ids) {
|
||||||
const id = isNaN(item) ? item.id : item;
|
const idSet = new Set(ids);
|
||||||
if (!query || words && !words.length) {
|
styles = styles.filter(s => idSet.has(s.id));
|
||||||
results.push(id);
|
|
||||||
continue;
|
|
||||||
}
|
}
|
||||||
const style = isNaN(item) ? item : cachedStyles.byId.get(item);
|
const results = [];
|
||||||
if (!style) continue;
|
for (const style of styles) {
|
||||||
for (const part in PARTS) {
|
const id = style.id;
|
||||||
const text = style[part];
|
if (!query || words && !words.length) {
|
||||||
if (text && PARTS[part](text, rx, words, icase)) {
|
|
||||||
results.push(id);
|
results.push(id);
|
||||||
break;
|
continue;
|
||||||
|
}
|
||||||
|
for (const part in PARTS) {
|
||||||
|
const text = style[part];
|
||||||
|
if (text && PARTS[part](text, rx, words, icase)) {
|
||||||
|
results.push(id);
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
if (cache.size) debounce(clearCache, 60e3);
|
||||||
|
return results;
|
||||||
if (cache.size) debounce(clearCache, 60e3);
|
});
|
||||||
return results;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
function searchText(text, rx, words, icase) {
|
function searchText(text, rx, words, icase) {
|
||||||
|
|
|
@ -1,78 +0,0 @@
|
||||||
'use strict';
|
|
||||||
|
|
||||||
// eslint-disable-next-line no-unused-expressions
|
|
||||||
(chrome.runtime.id.includes('@temporary') || !('sync' in chrome.storage)) && (() => {
|
|
||||||
|
|
||||||
const listeners = new Set();
|
|
||||||
Object.assign(chrome.storage.onChanged, {
|
|
||||||
addListener: fn => listeners.add(fn),
|
|
||||||
hasListener: fn => listeners.has(fn),
|
|
||||||
removeListener: fn => listeners.delete(fn),
|
|
||||||
});
|
|
||||||
|
|
||||||
for (const name of ['local', 'sync']) {
|
|
||||||
const dummy = tryJSONparse(localStorage['dummyStorage.' + name]) || {};
|
|
||||||
chrome.storage[name] = {
|
|
||||||
get(data, cb) {
|
|
||||||
let result = {};
|
|
||||||
if (data === null) {
|
|
||||||
result = deepCopy(dummy);
|
|
||||||
} else if (Array.isArray(data)) {
|
|
||||||
for (const key of data) {
|
|
||||||
result[key] = dummy[key];
|
|
||||||
}
|
|
||||||
} else if (typeof data === 'object') {
|
|
||||||
const hasOwnProperty = Object.prototype.hasOwnProperty;
|
|
||||||
for (const key in data) {
|
|
||||||
if (hasOwnProperty.call(data, key)) {
|
|
||||||
const value = dummy[key];
|
|
||||||
result[key] = value === undefined ? data[key] : value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
result[data] = dummy[data];
|
|
||||||
}
|
|
||||||
if (typeof cb === 'function') cb(result);
|
|
||||||
},
|
|
||||||
set(data, cb) {
|
|
||||||
const hasOwnProperty = Object.prototype.hasOwnProperty;
|
|
||||||
const changes = {};
|
|
||||||
for (const key in data) {
|
|
||||||
if (!hasOwnProperty.call(data, key)) continue;
|
|
||||||
const newValue = data[key];
|
|
||||||
changes[key] = {newValue, oldValue: dummy[key]};
|
|
||||||
dummy[key] = newValue;
|
|
||||||
}
|
|
||||||
localStorage['dummyStorage.' + name] = JSON.stringify(dummy);
|
|
||||||
if (typeof cb === 'function') cb();
|
|
||||||
notify(changes);
|
|
||||||
},
|
|
||||||
remove(keyOrKeys, cb) {
|
|
||||||
const changes = {};
|
|
||||||
for (const key of Array.isArray(keyOrKeys) ? keyOrKeys : [keyOrKeys]) {
|
|
||||||
changes[key] = {oldValue: dummy[key]};
|
|
||||||
delete dummy[key];
|
|
||||||
}
|
|
||||||
localStorage['dummyStorage.' + name] = JSON.stringify(dummy);
|
|
||||||
if (typeof cb === 'function') cb();
|
|
||||||
notify(changes);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
window.API_METHODS = Object.assign(window.API_METHODS || {}, {
|
|
||||||
dummyStorageGet: ({data, name}) => new Promise(resolve => chrome.storage[name].get(data, resolve)),
|
|
||||||
dummyStorageSet: ({data, name}) => new Promise(resolve => chrome.storage[name].set(data, resolve)),
|
|
||||||
dummyStorageRemove: ({data, name}) => new Promise(resolve => chrome.storage[name].remove(data, resolve)),
|
|
||||||
});
|
|
||||||
|
|
||||||
function notify(changes, name) {
|
|
||||||
for (const fn of listeners.values()) {
|
|
||||||
fn(changes, name);
|
|
||||||
}
|
|
||||||
sendMessage({
|
|
||||||
dummyStorageChanges: changes,
|
|
||||||
dummyStorageName: name,
|
|
||||||
}, ignoreChromeError);
|
|
||||||
}
|
|
||||||
})();
|
|
|
@ -1,847 +0,0 @@
|
||||||
/* global getStyleWithNoCode styleSectionsEqual */
|
|
||||||
'use strict';
|
|
||||||
|
|
||||||
const RX_NAMESPACE = /\s*(@namespace\s+(?:\S+\s+)?url\(http:\/\/.*?\);)\s*/g;
|
|
||||||
const RX_CHARSET = /\s*@charset\s+(['"]).*?\1\s*;\s*/g;
|
|
||||||
const RX_CSS_COMMENTS = /\/\*[\s\S]*?(?:\*\/|$)/g;
|
|
||||||
// eslint-disable-next-line no-var
|
|
||||||
var SLOPPY_REGEXP_PREFIX = '\0';
|
|
||||||
|
|
||||||
// CSS transition bug workaround: since we insert styles asynchronously,
|
|
||||||
// the browsers, especially Firefox, may apply all transitions on page load
|
|
||||||
const CSS_TRANSITION_SUPPRESSOR = '* { transition: none !important; }';
|
|
||||||
const RX_CSS_TRANSITION_DETECTOR = /([\s\n;/{]|-webkit-|-moz-)transition[\s\n]*:[\s\n]*(?!none)/;
|
|
||||||
|
|
||||||
// Note, only 'var'-declared variables are visible from another extension page
|
|
||||||
// eslint-disable-next-line no-var
|
|
||||||
var cachedStyles = {
|
|
||||||
list: null, // array of all styles
|
|
||||||
byId: new Map(), // all styles indexed by id
|
|
||||||
filters: new Map(), // filterStyles() parameters mapped to the returned results, 10k max
|
|
||||||
regexps: new Map(), // compiled style regexps
|
|
||||||
urlDomains: new Map(), // getDomain() results for 100 last checked urls
|
|
||||||
needTransitionPatch: new Map(), // FF bug workaround
|
|
||||||
mutex: {
|
|
||||||
inProgress: true, // while getStyles() is reading IndexedDB all subsequent calls
|
|
||||||
// (initially 'true' to prevent rogue getStyles before dbExec.initialized)
|
|
||||||
onDone: [], // to getStyles() are queued and resolved when the first one finishes
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
// eslint-disable-next-line no-var
|
|
||||||
var dbExec = dbExecIndexedDB;
|
|
||||||
dbExec.initialized = false;
|
|
||||||
|
|
||||||
// we use chrome.storage.local fallback if IndexedDB doesn't save data,
|
|
||||||
// which, once detected on the first run, is remembered in chrome.storage.local
|
|
||||||
// for reliablility and in localStorage for fast synchronous access
|
|
||||||
// (FF may block localStorage depending on its privacy options)
|
|
||||||
do {
|
|
||||||
const done = () => {
|
|
||||||
cachedStyles.mutex.inProgress = false;
|
|
||||||
getStyles().then(() => {
|
|
||||||
dbExec.initialized = true;
|
|
||||||
window.dispatchEvent(new Event('storageReady'));
|
|
||||||
});
|
|
||||||
};
|
|
||||||
const fallback = () => {
|
|
||||||
dbExec = dbExecChromeStorage;
|
|
||||||
chromeLocal.set({dbInChromeStorage: true});
|
|
||||||
localStorage.dbInChromeStorage = 'true';
|
|
||||||
ignoreChromeError();
|
|
||||||
done();
|
|
||||||
};
|
|
||||||
const fallbackSet = localStorage.dbInChromeStorage;
|
|
||||||
if (fallbackSet === 'true' || !tryCatch(() => indexedDB)) {
|
|
||||||
fallback();
|
|
||||||
break;
|
|
||||||
} else if (fallbackSet === 'false') {
|
|
||||||
done();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
chromeLocal.get('dbInChromeStorage')
|
|
||||||
.then(data =>
|
|
||||||
data && data.dbInChromeStorage && Promise.reject())
|
|
||||||
.then(() =>
|
|
||||||
tryCatch(dbExecIndexedDB, 'getAllKeys', IDBKeyRange.lowerBound(1), 1) ||
|
|
||||||
Promise.reject())
|
|
||||||
.then(({target}) => (
|
|
||||||
(target.result || [])[0] ?
|
|
||||||
Promise.reject('ok') :
|
|
||||||
dbExecIndexedDB('put', {id: -1})))
|
|
||||||
.then(() =>
|
|
||||||
dbExecIndexedDB('get', -1))
|
|
||||||
.then(({target}) => (
|
|
||||||
(target.result || {}).id === -1 ?
|
|
||||||
dbExecIndexedDB('delete', -1) :
|
|
||||||
Promise.reject()))
|
|
||||||
.then(() =>
|
|
||||||
Promise.reject('ok'))
|
|
||||||
.catch(result => {
|
|
||||||
if (result === 'ok') {
|
|
||||||
chromeLocal.set({dbInChromeStorage: false});
|
|
||||||
localStorage.dbInChromeStorage = 'false';
|
|
||||||
done();
|
|
||||||
} else {
|
|
||||||
fallback();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} while (0);
|
|
||||||
|
|
||||||
|
|
||||||
function dbExecIndexedDB(method, ...args) {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
Object.assign(indexedDB.open('stylish', 2), {
|
|
||||||
onsuccess(event) {
|
|
||||||
const database = event.target.result;
|
|
||||||
if (!method) {
|
|
||||||
resolve(database);
|
|
||||||
} else {
|
|
||||||
const transaction = database.transaction(['styles'], 'readwrite');
|
|
||||||
const store = transaction.objectStore('styles');
|
|
||||||
Object.assign(store[method](...args), {
|
|
||||||
onsuccess: event => resolve(event, store, transaction, database),
|
|
||||||
onerror: reject,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onerror(event) {
|
|
||||||
console.warn(event.target.error || event.target.errorCode);
|
|
||||||
reject(event);
|
|
||||||
},
|
|
||||||
onupgradeneeded(event) {
|
|
||||||
if (event.oldVersion === 0) {
|
|
||||||
event.target.result.createObjectStore('styles', {
|
|
||||||
keyPath: 'id',
|
|
||||||
autoIncrement: true,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function dbExecChromeStorage(method, data) {
|
|
||||||
const STYLE_KEY_PREFIX = 'style-';
|
|
||||||
switch (method) {
|
|
||||||
case 'get':
|
|
||||||
return chromeLocal.getValue(STYLE_KEY_PREFIX + data)
|
|
||||||
.then(result => ({target: {result}}));
|
|
||||||
|
|
||||||
case 'put':
|
|
||||||
if (!data.id) {
|
|
||||||
return getStyles().then(() => {
|
|
||||||
data.id = 1;
|
|
||||||
for (const style of cachedStyles.list) {
|
|
||||||
data.id = Math.max(data.id, style.id + 1);
|
|
||||||
}
|
|
||||||
return dbExecChromeStorage('put', data);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return chromeLocal.setValue(STYLE_KEY_PREFIX + data.id, data)
|
|
||||||
.then(() => (chrome.runtime.lastError ? Promise.reject() : data.id));
|
|
||||||
|
|
||||||
case 'delete':
|
|
||||||
return chromeLocal.remove(STYLE_KEY_PREFIX + data);
|
|
||||||
|
|
||||||
case 'getAll':
|
|
||||||
return chromeLocal.get(null).then(storage => {
|
|
||||||
const styles = [];
|
|
||||||
for (const key in storage) {
|
|
||||||
if (key.startsWith(STYLE_KEY_PREFIX) &&
|
|
||||||
Number(key.substr(STYLE_KEY_PREFIX.length))) {
|
|
||||||
styles.push(storage[key]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return {target: {result: styles}};
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return Promise.reject();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function getStyles(options) {
|
|
||||||
if (cachedStyles.list) {
|
|
||||||
return Promise.resolve(filterStyles(options));
|
|
||||||
}
|
|
||||||
if (cachedStyles.mutex.inProgress) {
|
|
||||||
return new Promise(resolve => {
|
|
||||||
cachedStyles.mutex.onDone.push({options, resolve});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
cachedStyles.mutex.inProgress = true;
|
|
||||||
|
|
||||||
return dbExec('getAll').then(event => {
|
|
||||||
cachedStyles.list = event.target.result || [];
|
|
||||||
cachedStyles.list.forEach(fixUsoMd5Issue);
|
|
||||||
cachedStyles.byId.clear();
|
|
||||||
for (const style of cachedStyles.list) {
|
|
||||||
cachedStyles.byId.set(style.id, style);
|
|
||||||
if (!style.name) {
|
|
||||||
style.name = 'ID: ' + style.id;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
cachedStyles.mutex.inProgress = false;
|
|
||||||
for (const {options, resolve} of cachedStyles.mutex.onDone) {
|
|
||||||
resolve(filterStyles(options));
|
|
||||||
}
|
|
||||||
cachedStyles.mutex.onDone = [];
|
|
||||||
return filterStyles(options);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function filterStyles({
|
|
||||||
enabled = null,
|
|
||||||
id = null,
|
|
||||||
matchUrl = null,
|
|
||||||
md5Url = null,
|
|
||||||
asHash = null,
|
|
||||||
omitCode,
|
|
||||||
strictRegexp = true, // used by the popup to detect bad regexps
|
|
||||||
} = {}) {
|
|
||||||
if (id) id = Number(id);
|
|
||||||
if (asHash) enabled = true;
|
|
||||||
|
|
||||||
if (
|
|
||||||
enabled === null &&
|
|
||||||
id === null &&
|
|
||||||
matchUrl === null &&
|
|
||||||
md5Url === null &&
|
|
||||||
asHash !== true
|
|
||||||
) {
|
|
||||||
return cachedStyles.list;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (matchUrl && !URLS.supported(matchUrl)) {
|
|
||||||
return asHash ? {length: 0} : [];
|
|
||||||
}
|
|
||||||
|
|
||||||
const blankHash = asHash && {
|
|
||||||
length: 0,
|
|
||||||
disableAll: prefs.get('disableAll'),
|
|
||||||
exposeIframes: prefs.get('exposeIframes'),
|
|
||||||
};
|
|
||||||
|
|
||||||
// make sure to use the same order in updateFiltersCache()
|
|
||||||
const cacheKey =
|
|
||||||
enabled + '\t' +
|
|
||||||
id + '\t' +
|
|
||||||
matchUrl + '\t' +
|
|
||||||
md5Url + '\t' +
|
|
||||||
asHash + '\t' +
|
|
||||||
strictRegexp;
|
|
||||||
const cached = cachedStyles.filters.get(cacheKey);
|
|
||||||
let styles;
|
|
||||||
if (cached) {
|
|
||||||
cached.hits++;
|
|
||||||
cached.lastHit = Date.now();
|
|
||||||
styles = asHash
|
|
||||||
? Object.assign(blankHash, cached.styles)
|
|
||||||
: cached.styles.slice();
|
|
||||||
} else {
|
|
||||||
styles = filterStylesInternal({
|
|
||||||
enabled,
|
|
||||||
id,
|
|
||||||
matchUrl,
|
|
||||||
md5Url,
|
|
||||||
asHash,
|
|
||||||
strictRegexp,
|
|
||||||
blankHash,
|
|
||||||
cacheKey,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (!omitCode) return styles;
|
|
||||||
if (!asHash) return styles.map(getStyleWithNoCode);
|
|
||||||
for (const id in styles) {
|
|
||||||
const sections = styles[id];
|
|
||||||
if (Array.isArray(sections)) {
|
|
||||||
styles[id] = getStyleWithNoCode({sections}).sections;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return styles;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// The md5Url provided by USO includes a duplicate "update" subdomain (see #523),
|
|
||||||
// This fixes any already installed styles containing this error
|
|
||||||
function fixUsoMd5Issue(style) {
|
|
||||||
if (style && style.md5Url && style.md5Url.includes('update.update.userstyles')) {
|
|
||||||
style.md5Url = style.md5Url.replace('update.update.userstyles', 'update.userstyles');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function filterStylesInternal({
|
|
||||||
// js engines don't like big functions (V8 often deoptimized the original filterStyles)
|
|
||||||
// it also makes sense to extract the less frequently executed code
|
|
||||||
enabled,
|
|
||||||
id,
|
|
||||||
matchUrl,
|
|
||||||
md5Url,
|
|
||||||
asHash,
|
|
||||||
strictRegexp,
|
|
||||||
blankHash,
|
|
||||||
cacheKey,
|
|
||||||
}) {
|
|
||||||
if (matchUrl && !cachedStyles.urlDomains.has(matchUrl)) {
|
|
||||||
cachedStyles.urlDomains.set(matchUrl, getDomains(matchUrl));
|
|
||||||
for (let i = cachedStyles.urlDomains.size - 100; i > 0; i--) {
|
|
||||||
const firstKey = cachedStyles.urlDomains.keys().next().value;
|
|
||||||
cachedStyles.urlDomains.delete(firstKey);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const styles = id === null
|
|
||||||
? cachedStyles.list
|
|
||||||
: [cachedStyles.byId.get(id)];
|
|
||||||
if (!styles[0]) {
|
|
||||||
// may happen when users [accidentally] reopen an old URL
|
|
||||||
// of edit.html with a non-existent style id parameter
|
|
||||||
return asHash ? blankHash : [];
|
|
||||||
}
|
|
||||||
const filtered = asHash ? {length: 0} : [];
|
|
||||||
const needSections = asHash || matchUrl !== null;
|
|
||||||
const matchUrlBase = matchUrl && matchUrl.includes('#') && matchUrl.split('#', 1)[0];
|
|
||||||
|
|
||||||
let style;
|
|
||||||
for (let i = 0; (style = styles[i]); i++) {
|
|
||||||
if ((enabled === null || style.enabled === enabled)
|
|
||||||
&& (md5Url === null || style.md5Url === md5Url)
|
|
||||||
&& (id === null || style.id === id)) {
|
|
||||||
const sections = needSections &&
|
|
||||||
getApplicableSections({
|
|
||||||
style,
|
|
||||||
matchUrl,
|
|
||||||
strictRegexp,
|
|
||||||
stopOnFirst: !asHash,
|
|
||||||
skipUrlCheck: true,
|
|
||||||
matchUrlBase,
|
|
||||||
});
|
|
||||||
if (asHash) {
|
|
||||||
if (sections.length) {
|
|
||||||
filtered[style.id] = sections;
|
|
||||||
filtered.length++;
|
|
||||||
}
|
|
||||||
} else if (matchUrl === null || sections.length) {
|
|
||||||
filtered.push(style);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
cachedStyles.filters.set(cacheKey, {
|
|
||||||
styles: filtered,
|
|
||||||
lastHit: Date.now(),
|
|
||||||
hits: 1,
|
|
||||||
});
|
|
||||||
if (cachedStyles.filters.size > 10000) {
|
|
||||||
cleanupCachedFilters();
|
|
||||||
}
|
|
||||||
|
|
||||||
// a shallow copy is needed because the cache doesn't store options like disableAll
|
|
||||||
return asHash
|
|
||||||
? Object.assign(blankHash, filtered)
|
|
||||||
: filtered;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function saveStyle(style) {
|
|
||||||
const id = Number(style.id) || null;
|
|
||||||
const reason = style.reason;
|
|
||||||
const notify = style.notify !== false;
|
|
||||||
delete style.method;
|
|
||||||
delete style.reason;
|
|
||||||
delete style.notify;
|
|
||||||
if (!style.name) {
|
|
||||||
delete style.name;
|
|
||||||
}
|
|
||||||
let existed;
|
|
||||||
let codeIsUpdated;
|
|
||||||
|
|
||||||
fixUsoMd5Issue(style);
|
|
||||||
|
|
||||||
return maybeCalcDigest()
|
|
||||||
.then(maybeImportFix)
|
|
||||||
.then(decide);
|
|
||||||
|
|
||||||
function maybeCalcDigest() {
|
|
||||||
if (['install', 'update', 'update-digest'].includes(reason)) {
|
|
||||||
return calcStyleDigest(style).then(digest => {
|
|
||||||
style.originalDigest = digest;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return Promise.resolve();
|
|
||||||
}
|
|
||||||
|
|
||||||
function maybeImportFix() {
|
|
||||||
if (reason === 'import') {
|
|
||||||
style.originalDigest = style.originalDigest || style.styleDigest; // TODO: remove in the future
|
|
||||||
delete style.styleDigest; // TODO: remove in the future
|
|
||||||
if (typeof style.originalDigest !== 'string' || style.originalDigest.length !== 40) {
|
|
||||||
delete style.originalDigest;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function decide() {
|
|
||||||
if (id !== null) {
|
|
||||||
// Update or create
|
|
||||||
style.id = id;
|
|
||||||
return dbExec('get', id).then((event, store) => {
|
|
||||||
const oldStyle = event.target.result;
|
|
||||||
existed = Boolean(oldStyle);
|
|
||||||
if (reason === 'update-digest' && oldStyle.originalDigest === style.originalDigest) {
|
|
||||||
return style;
|
|
||||||
}
|
|
||||||
codeIsUpdated = !existed || 'sections' in style && !styleSectionsEqual(style, oldStyle);
|
|
||||||
style = Object.assign({installDate: Date.now()}, oldStyle, style);
|
|
||||||
return write(style, store);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
// Create
|
|
||||||
delete style.id;
|
|
||||||
style = Object.assign({
|
|
||||||
// Set optional things if they're undefined
|
|
||||||
enabled: true,
|
|
||||||
updateUrl: null,
|
|
||||||
md5Url: null,
|
|
||||||
url: null,
|
|
||||||
originalMd5: null,
|
|
||||||
installDate: Date.now(),
|
|
||||||
}, style);
|
|
||||||
return write(style);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function write(style, store) {
|
|
||||||
style.sections = normalizeStyleSections(style);
|
|
||||||
if (store) {
|
|
||||||
return new Promise(resolve => {
|
|
||||||
store.put(style).onsuccess = event => resolve(done(event));
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
return dbExec('put', style).then(done);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function done(event) {
|
|
||||||
if (reason === 'update-digest') {
|
|
||||||
return style;
|
|
||||||
}
|
|
||||||
style.id = style.id || event.target.result;
|
|
||||||
invalidateCache(existed ? {updated: style} : {added: style});
|
|
||||||
if (notify) {
|
|
||||||
notifyAllTabs({
|
|
||||||
method: existed ? 'styleUpdated' : 'styleAdded',
|
|
||||||
style, codeIsUpdated, reason,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return style;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function deleteStyle({id, notify = true}) {
|
|
||||||
id = Number(id);
|
|
||||||
return dbExec('delete', id).then(() => {
|
|
||||||
invalidateCache({deletedId: id});
|
|
||||||
if (notify) {
|
|
||||||
notifyAllTabs({method: 'styleDeleted', id});
|
|
||||||
}
|
|
||||||
return id;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function getApplicableSections({
|
|
||||||
style,
|
|
||||||
matchUrl,
|
|
||||||
strictRegexp = true,
|
|
||||||
// filterStylesInternal() sets the following to avoid recalc on each style:
|
|
||||||
stopOnFirst,
|
|
||||||
skipUrlCheck,
|
|
||||||
matchUrlBase = matchUrl.includes('#') && matchUrl.split('#', 1)[0],
|
|
||||||
// as per spec the fragment portion is ignored in @-moz-document:
|
|
||||||
// https://www.w3.org/TR/2012/WD-css3-conditional-20120911/#url-of-doc
|
|
||||||
// but the spec is outdated and doesn't account for SPA sites
|
|
||||||
// so we only respect it in case of url("http://exact.url/without/hash")
|
|
||||||
}) {
|
|
||||||
if (!skipUrlCheck && !URLS.supported(matchUrl)) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
const sections = [];
|
|
||||||
for (const section of style.sections) {
|
|
||||||
const {urls, domains, urlPrefixes, regexps, code} = section;
|
|
||||||
const isGlobal = !urls.length && !urlPrefixes.length && !domains.length && !regexps.length;
|
|
||||||
const isMatching = !isGlobal && (
|
|
||||||
urls.length
|
|
||||||
&& (urls.includes(matchUrl) || matchUrlBase && urls.includes(matchUrlBase))
|
|
||||||
|| urlPrefixes.length
|
|
||||||
&& arraySomeIsPrefix(urlPrefixes, matchUrl)
|
|
||||||
|| domains.length
|
|
||||||
&& arraySomeIn(cachedStyles.urlDomains.get(matchUrl) || getDomains(matchUrl), domains)
|
|
||||||
|| regexps.length
|
|
||||||
&& arraySomeMatches(regexps, matchUrl, strictRegexp));
|
|
||||||
if (isGlobal && !styleCodeEmpty(code) || isMatching) {
|
|
||||||
sections.push(section);
|
|
||||||
if (stopOnFirst) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return sections;
|
|
||||||
|
|
||||||
function arraySomeIsPrefix(array, string) {
|
|
||||||
for (const prefix of array) {
|
|
||||||
if (string.startsWith(prefix)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
function arraySomeIn(array, haystack) {
|
|
||||||
for (const el of array) {
|
|
||||||
if (haystack.indexOf(el) >= 0) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
function arraySomeMatches(array, matchUrl, strictRegexp) {
|
|
||||||
for (const regexp of array) {
|
|
||||||
for (let pass = 1; pass <= (strictRegexp ? 1 : 2); pass++) {
|
|
||||||
const cacheKey = pass === 1 ? regexp : SLOPPY_REGEXP_PREFIX + regexp;
|
|
||||||
let rx = cachedStyles.regexps.get(cacheKey);
|
|
||||||
if (rx === false) {
|
|
||||||
// invalid regexp
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
if (!rx) {
|
|
||||||
const anchored = pass === 1 ? '^(?:' + regexp + ')$' : '^' + regexp + '$';
|
|
||||||
rx = tryRegExp(anchored);
|
|
||||||
cachedStyles.regexps.set(cacheKey, rx || false);
|
|
||||||
if (!rx) {
|
|
||||||
// invalid regexp
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (rx.test(matchUrl)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function styleCodeEmpty(code) {
|
|
||||||
// Collect the global section if it's not empty, not comment-only, not namespace-only.
|
|
||||||
const cmtOpen = code && code.indexOf('/*');
|
|
||||||
if (cmtOpen >= 0) {
|
|
||||||
const cmtCloseLast = code.lastIndexOf('*/');
|
|
||||||
if (cmtCloseLast < 0) {
|
|
||||||
code = code.substr(0, cmtOpen);
|
|
||||||
} else {
|
|
||||||
code = code.substr(0, cmtOpen) +
|
|
||||||
code.substring(cmtOpen, cmtCloseLast + 2).replace(RX_CSS_COMMENTS, '') +
|
|
||||||
code.substr(cmtCloseLast + 2);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!code || !code.trim()) return true;
|
|
||||||
if (code.includes('@namespace')) code = code.replace(RX_NAMESPACE, '').trim();
|
|
||||||
if (code.includes('@charset')) code = code.replace(RX_CHARSET, '').trim();
|
|
||||||
return !code;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function invalidateCache({added, updated, deletedId} = {}) {
|
|
||||||
if (!cachedStyles.list) return;
|
|
||||||
const id = added ? added.id : updated ? updated.id : deletedId;
|
|
||||||
const cached = cachedStyles.byId.get(id);
|
|
||||||
|
|
||||||
if (updated) {
|
|
||||||
if (cached) {
|
|
||||||
const isSectionGlobal = section =>
|
|
||||||
!section.urls.length &&
|
|
||||||
!section.urlPrefixes.length &&
|
|
||||||
!section.domains.length &&
|
|
||||||
!section.regexps.length;
|
|
||||||
const hadOrHasGlobals = cached.sections.some(isSectionGlobal) ||
|
|
||||||
updated.sections.some(isSectionGlobal);
|
|
||||||
const reenabled = !cached.enabled && updated.enabled;
|
|
||||||
const equal = !hadOrHasGlobals &&
|
|
||||||
!reenabled &&
|
|
||||||
styleSectionsEqual(updated, cached, {ignoreCode: true});
|
|
||||||
Object.assign(cached, updated);
|
|
||||||
if (equal) {
|
|
||||||
updateFiltersCache(cached);
|
|
||||||
} else {
|
|
||||||
cachedStyles.filters.clear();
|
|
||||||
}
|
|
||||||
cachedStyles.needTransitionPatch.delete(id);
|
|
||||||
return;
|
|
||||||
} else {
|
|
||||||
added = updated;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (added) {
|
|
||||||
if (!cached) {
|
|
||||||
cachedStyles.list.push(added);
|
|
||||||
cachedStyles.byId.set(added.id, added);
|
|
||||||
cachedStyles.filters.clear();
|
|
||||||
cachedStyles.needTransitionPatch.delete(id);
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (deletedId !== undefined) {
|
|
||||||
if (cached) {
|
|
||||||
const cachedIndex = cachedStyles.list.indexOf(cached);
|
|
||||||
cachedStyles.list.splice(cachedIndex, 1);
|
|
||||||
cachedStyles.byId.delete(deletedId);
|
|
||||||
for (const {styles} of cachedStyles.filters.values()) {
|
|
||||||
if (Array.isArray(styles)) {
|
|
||||||
const index = styles.findIndex(({id}) => id === deletedId);
|
|
||||||
if (index >= 0) styles.splice(index, 1);
|
|
||||||
} else if (deletedId in styles) {
|
|
||||||
delete styles[deletedId];
|
|
||||||
styles.length--;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
cachedStyles.needTransitionPatch.delete(id);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
cachedStyles.list = null;
|
|
||||||
cachedStyles.filters.clear();
|
|
||||||
cachedStyles.needTransitionPatch.clear(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function updateFiltersCache(style) {
|
|
||||||
const {id} = style;
|
|
||||||
for (const [key, {styles}] of cachedStyles.filters.entries()) {
|
|
||||||
if (Array.isArray(styles)) {
|
|
||||||
const index = styles.findIndex(style => style.id === id);
|
|
||||||
if (index >= 0) styles[index] = Object.assign({}, style);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (id in styles) {
|
|
||||||
const [, , matchUrl, , , strictRegexp] = key.split('\t');
|
|
||||||
if (!style.enabled) {
|
|
||||||
delete styles[id];
|
|
||||||
styles.length--;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const matchUrlBase = matchUrl && matchUrl.includes('#') && matchUrl.split('#', 1)[0];
|
|
||||||
const sections = getApplicableSections({
|
|
||||||
style,
|
|
||||||
matchUrl,
|
|
||||||
matchUrlBase,
|
|
||||||
strictRegexp,
|
|
||||||
skipUrlCheck: true,
|
|
||||||
});
|
|
||||||
if (sections.length) {
|
|
||||||
styles[id] = sections;
|
|
||||||
} else {
|
|
||||||
delete styles[id];
|
|
||||||
styles.length--;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function cleanupCachedFilters({force = false} = {}) {
|
|
||||||
if (!force) {
|
|
||||||
debounce(cleanupCachedFilters, 1000, {force: true});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const size = cachedStyles.filters.size;
|
|
||||||
const oldestHit = cachedStyles.filters.values().next().value.lastHit;
|
|
||||||
const now = Date.now();
|
|
||||||
const timeSpan = now - oldestHit;
|
|
||||||
const recencyWeight = 5 / size;
|
|
||||||
const hitWeight = 1 / 4; // we make ~4 hits per URL
|
|
||||||
const lastHitWeight = 10;
|
|
||||||
// delete the oldest 10%
|
|
||||||
[...cachedStyles.filters.entries()]
|
|
||||||
.map(([id, v], index) => ({
|
|
||||||
id,
|
|
||||||
weight:
|
|
||||||
index * recencyWeight +
|
|
||||||
v.hits * hitWeight +
|
|
||||||
(v.lastHit - oldestHit) / timeSpan * lastHitWeight,
|
|
||||||
}))
|
|
||||||
.sort((a, b) => a.weight - b.weight)
|
|
||||||
.slice(0, size / 10 + 1)
|
|
||||||
.forEach(({id}) => cachedStyles.filters.delete(id));
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function getDomains(url) {
|
|
||||||
let d = /.*?:\/*([^/:]+)|$/.exec(url)[1];
|
|
||||||
if (!d || url.startsWith('file:')) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
const domains = [d];
|
|
||||||
while (d.indexOf('.') !== -1) {
|
|
||||||
d = d.substring(d.indexOf('.') + 1);
|
|
||||||
domains.push(d);
|
|
||||||
}
|
|
||||||
return domains;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function normalizeStyleSections({sections}) {
|
|
||||||
// retain known properties in an arbitrarily predefined order
|
|
||||||
return (sections || []).map(section => ({
|
|
||||||
code: section.code || '',
|
|
||||||
urls: section.urls || [],
|
|
||||||
urlPrefixes: section.urlPrefixes || [],
|
|
||||||
domains: section.domains || [],
|
|
||||||
regexps: section.regexps || [],
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function calcStyleDigest(style) {
|
|
||||||
const jsonString = style.usercssData ?
|
|
||||||
style.sourceCode : JSON.stringify(normalizeStyleSections(style));
|
|
||||||
const text = new TextEncoder('utf-8').encode(jsonString);
|
|
||||||
return crypto.subtle.digest('SHA-1', text).then(hex);
|
|
||||||
|
|
||||||
function hex(buffer) {
|
|
||||||
const parts = [];
|
|
||||||
const PAD8 = '00000000';
|
|
||||||
const view = new DataView(buffer);
|
|
||||||
for (let i = 0; i < view.byteLength; i += 4) {
|
|
||||||
parts.push((PAD8 + view.getUint32(i).toString(16)).slice(-8));
|
|
||||||
}
|
|
||||||
return parts.join('');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function handleCssTransitionBug({tabId, frameId, url, styles}) {
|
|
||||||
for (let id in styles) {
|
|
||||||
id |= 0;
|
|
||||||
if (!id) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
let need = cachedStyles.needTransitionPatch.get(id);
|
|
||||||
if (need === false) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (need !== true) {
|
|
||||||
need = styles[id].some(sectionContainsTransitions);
|
|
||||||
cachedStyles.needTransitionPatch.set(id, need);
|
|
||||||
if (!need) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (FIREFOX && !url.startsWith(URLS.ownOrigin)) {
|
|
||||||
patchFirefox();
|
|
||||||
} else {
|
|
||||||
styles.needTransitionPatch = true;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
function patchFirefox() {
|
|
||||||
const options = {
|
|
||||||
frameId,
|
|
||||||
code: CSS_TRANSITION_SUPPRESSOR,
|
|
||||||
matchAboutBlank: true,
|
|
||||||
};
|
|
||||||
if (FIREFOX >= 53) {
|
|
||||||
options.cssOrigin = 'user';
|
|
||||||
}
|
|
||||||
browser.tabs.insertCSS(tabId, Object.assign(options, {
|
|
||||||
runAt: 'document_start',
|
|
||||||
})).then(() => setTimeout(() => {
|
|
||||||
browser.tabs.removeCSS(tabId, options).catch(ignoreChromeError);
|
|
||||||
})).catch(ignoreChromeError);
|
|
||||||
}
|
|
||||||
|
|
||||||
function sectionContainsTransitions(section) {
|
|
||||||
let code = section.code;
|
|
||||||
const firstTransition = code.indexOf('transition');
|
|
||||||
if (firstTransition < 0) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
const firstCmt = code.indexOf('/*');
|
|
||||||
// check the part before the first comment
|
|
||||||
if (firstCmt < 0 || firstTransition < firstCmt) {
|
|
||||||
if (quickCheckAround(code, firstTransition)) {
|
|
||||||
return true;
|
|
||||||
} else if (firstCmt < 0) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// check the rest
|
|
||||||
const lastCmt = code.lastIndexOf('*/');
|
|
||||||
if (lastCmt < firstCmt) {
|
|
||||||
// the comment is unclosed and we already checked the preceding part
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
let mid = code.slice(firstCmt, lastCmt + 2);
|
|
||||||
mid = mid.indexOf('*/') === mid.length - 2 ? '' : mid.replace(RX_CSS_COMMENTS, '');
|
|
||||||
code = mid + code.slice(lastCmt + 2);
|
|
||||||
return quickCheckAround(code) || RX_CSS_TRANSITION_DETECTOR.test(code);
|
|
||||||
}
|
|
||||||
|
|
||||||
function quickCheckAround(code, pos = code.indexOf('transition')) {
|
|
||||||
return RX_CSS_TRANSITION_DETECTOR.test(code.substr(Math.max(0, pos - 10), 50));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/*
|
|
||||||
According to CSS4 @document specification the entire URL must match.
|
|
||||||
Stylish-for-Chrome implemented it incorrectly since the very beginning.
|
|
||||||
We'll detect styles that abuse the bug by finding the sections that
|
|
||||||
would have been applied by Stylish but not by us as we follow the spec.
|
|
||||||
Additionally we'll check for invalid regexps.
|
|
||||||
*/
|
|
||||||
function detectSloppyRegexps({matchUrl, ids}) {
|
|
||||||
const results = [];
|
|
||||||
for (const id of ids) {
|
|
||||||
const style = cachedStyles.byId.get(id);
|
|
||||||
if (!style) continue;
|
|
||||||
// make sure all regexps are compiled
|
|
||||||
const rxCache = cachedStyles.regexps;
|
|
||||||
let hasRegExp = false;
|
|
||||||
for (const section of style.sections) {
|
|
||||||
for (const regexp of section.regexps) {
|
|
||||||
hasRegExp = true;
|
|
||||||
for (let pass = 1; pass <= 2; pass++) {
|
|
||||||
const cacheKey = pass === 1 ? regexp : SLOPPY_REGEXP_PREFIX + regexp;
|
|
||||||
if (!rxCache.has(cacheKey)) {
|
|
||||||
// according to CSS4 @document specification the entire URL must match
|
|
||||||
const anchored = pass === 1 ? '^(?:' + regexp + ')$' : '^' + regexp + '$';
|
|
||||||
// create in the bg context to avoid leaking of "dead objects"
|
|
||||||
const rx = tryRegExp(anchored);
|
|
||||||
rxCache.set(cacheKey, rx || false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!hasRegExp) continue;
|
|
||||||
const applied = getApplicableSections({style, matchUrl});
|
|
||||||
const wannabe = getApplicableSections({style, matchUrl, strictRegexp: false});
|
|
||||||
results.push({
|
|
||||||
id,
|
|
||||||
applied,
|
|
||||||
skipped: wannabe.length - applied.length,
|
|
||||||
hasInvalidRegexps: wannabe.some(({regexps}) => regexps.some(rx => !rxCache.has(rx))),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return results;
|
|
||||||
}
|
|
492
background/style-manager.js
Normal file
492
background/style-manager.js
Normal file
|
@ -0,0 +1,492 @@
|
||||||
|
/* eslint no-eq-null: 0, eqeqeq: [2, "smart"] */
|
||||||
|
/* global createCache db calcStyleDigest db tryRegExp styleCodeEmpty
|
||||||
|
getStyleWithNoCode msg */
|
||||||
|
/* exported styleManager */
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
/*
|
||||||
|
This style manager is a layer between content script and the DB. When a style
|
||||||
|
is added/updated, it broadcast a message to content script and the content
|
||||||
|
script would try to fetch the new code.
|
||||||
|
|
||||||
|
The live preview feature relies on `runtime.connect` and `port.onDisconnect`
|
||||||
|
to cleanup the temporary code. See /edit/live-preview.js.
|
||||||
|
*/
|
||||||
|
const styleManager = (() => {
|
||||||
|
const preparing = prepare();
|
||||||
|
|
||||||
|
/* styleId => {
|
||||||
|
data: styleData,
|
||||||
|
preview: styleData,
|
||||||
|
appliesTo: Set<url>
|
||||||
|
} */
|
||||||
|
const styles = new Map();
|
||||||
|
|
||||||
|
/* url => {
|
||||||
|
maybeMatch: Set<styleId>,
|
||||||
|
sections: Object<styleId => {
|
||||||
|
id: styleId,
|
||||||
|
code: Array<String>
|
||||||
|
}>
|
||||||
|
} */
|
||||||
|
const cachedStyleForUrl = createCache({
|
||||||
|
onDeleted: (url, cache) => {
|
||||||
|
for (const section of Object.values(cache.sections)) {
|
||||||
|
const style = styles.get(section.id);
|
||||||
|
if (style) {
|
||||||
|
style.appliesTo.delete(url);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const BAD_MATCHER = {test: () => false};
|
||||||
|
const compileRe = createCompiler(text => `^(${text})$`);
|
||||||
|
const compileSloppyRe = createCompiler(text => `^${text}$`);
|
||||||
|
const compileExclusion = createCompiler(buildGlob);
|
||||||
|
|
||||||
|
handleLivePreviewConnections();
|
||||||
|
|
||||||
|
return ensurePrepared({
|
||||||
|
get,
|
||||||
|
getSectionsByUrl,
|
||||||
|
installStyle,
|
||||||
|
deleteStyle,
|
||||||
|
editSave,
|
||||||
|
findStyle,
|
||||||
|
importStyle,
|
||||||
|
toggleStyle,
|
||||||
|
setStyleExclusions,
|
||||||
|
getAllStyles, // used by import-export
|
||||||
|
getStylesByUrl, // used by popup
|
||||||
|
styleExists,
|
||||||
|
});
|
||||||
|
|
||||||
|
function handleLivePreviewConnections() {
|
||||||
|
chrome.runtime.onConnect.addListener(port => {
|
||||||
|
if (port.name !== 'livePreview') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let id;
|
||||||
|
port.onMessage.addListener(data => {
|
||||||
|
if (!id) {
|
||||||
|
id = data.id;
|
||||||
|
}
|
||||||
|
const style = styles.get(id);
|
||||||
|
style.preview = data;
|
||||||
|
broadcastStyleUpdated(style.preview, 'editPreview');
|
||||||
|
});
|
||||||
|
port.onDisconnect.addListener(() => {
|
||||||
|
port = null;
|
||||||
|
if (id) {
|
||||||
|
const style = styles.get(id);
|
||||||
|
if (!style) {
|
||||||
|
// maybe deleted
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
style.preview = null;
|
||||||
|
broadcastStyleUpdated(style.data, 'editPreviewEnd');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function get(id, noCode = false) {
|
||||||
|
const data = styles.get(id).data;
|
||||||
|
return noCode ? getStyleWithNoCode(data) : data;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getAllStyles(noCode = false) {
|
||||||
|
const datas = [...styles.values()].map(s => s.data);
|
||||||
|
return noCode ? datas.map(getStyleWithNoCode) : datas;
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleStyle(id, enabled) {
|
||||||
|
const style = styles.get(id);
|
||||||
|
const data = Object.assign({}, style.data, {enabled});
|
||||||
|
return saveStyle(data)
|
||||||
|
.then(newData => handleSave(newData, 'toggle', false))
|
||||||
|
.then(() => id);
|
||||||
|
}
|
||||||
|
|
||||||
|
// used by install-hook-userstyles.js
|
||||||
|
function findStyle(filter, noCode = false) {
|
||||||
|
for (const style of styles.values()) {
|
||||||
|
if (filterMatch(filter, style.data)) {
|
||||||
|
return noCode ? getStyleWithNoCode(style.data) : style.data;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function styleExists(filter) {
|
||||||
|
return [...styles.values()].some(s => filterMatch(filter, s.data));
|
||||||
|
}
|
||||||
|
|
||||||
|
function filterMatch(filter, target) {
|
||||||
|
for (const key of Object.keys(filter)) {
|
||||||
|
if (filter[key] !== target[key]) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function importStyle(data) {
|
||||||
|
// FIXME: is it a good idea to save the data directly?
|
||||||
|
return saveStyle(data)
|
||||||
|
.then(newData => handleSave(newData, 'import'));
|
||||||
|
}
|
||||||
|
|
||||||
|
function installStyle(data, reason = null) {
|
||||||
|
const style = styles.get(data.id);
|
||||||
|
if (!style) {
|
||||||
|
data = Object.assign(createNewStyle(), data);
|
||||||
|
} else {
|
||||||
|
data = Object.assign({}, style.data, data);
|
||||||
|
}
|
||||||
|
if (!reason) {
|
||||||
|
reason = style ? 'update' : 'install';
|
||||||
|
}
|
||||||
|
// FIXME: update updateDate? what about usercss config?
|
||||||
|
return calcStyleDigest(data)
|
||||||
|
.then(digest => {
|
||||||
|
data.originalDigest = digest;
|
||||||
|
return saveStyle(data);
|
||||||
|
})
|
||||||
|
.then(newData => handleSave(newData, reason));
|
||||||
|
}
|
||||||
|
|
||||||
|
function editSave(data) {
|
||||||
|
const style = styles.get(data.id);
|
||||||
|
if (style) {
|
||||||
|
data = Object.assign({}, style.data, data);
|
||||||
|
} else {
|
||||||
|
data = Object.assign(createNewStyle(), data);
|
||||||
|
}
|
||||||
|
return saveStyle(data)
|
||||||
|
.then(newData => handleSave(newData, 'editSave'));
|
||||||
|
}
|
||||||
|
|
||||||
|
function setStyleExclusions(id, exclusions) {
|
||||||
|
const data = Object.assign({}, styles.get(id).data, {exclusions});
|
||||||
|
return saveStyle(data)
|
||||||
|
.then(newData => handleSave(newData, 'exclusions'));
|
||||||
|
}
|
||||||
|
|
||||||
|
function deleteStyle(id) {
|
||||||
|
const style = styles.get(id);
|
||||||
|
return db.exec('delete', id)
|
||||||
|
.then(() => {
|
||||||
|
for (const url of style.appliesTo) {
|
||||||
|
const cache = cachedStyleForUrl.get(url);
|
||||||
|
if (cache) {
|
||||||
|
delete cache.sections[id];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
styles.delete(id);
|
||||||
|
return msg.broadcast({
|
||||||
|
method: 'styleDeleted',
|
||||||
|
style: {id}
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.then(() => id);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ensurePrepared(methods) {
|
||||||
|
const prepared = {};
|
||||||
|
for (const [name, fn] of Object.entries(methods)) {
|
||||||
|
prepared[name] = (...args) =>
|
||||||
|
preparing.then(() => fn(...args));
|
||||||
|
}
|
||||||
|
return prepared;
|
||||||
|
}
|
||||||
|
|
||||||
|
function createNewStyle() {
|
||||||
|
return {
|
||||||
|
enabled: true,
|
||||||
|
updateUrl: null,
|
||||||
|
md5Url: null,
|
||||||
|
url: null,
|
||||||
|
originalMd5: null,
|
||||||
|
installDate: Date.now()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function broadcastStyleUpdated(data, reason, method = 'styleUpdated', codeIsUpdated = true) {
|
||||||
|
const style = styles.get(data.id);
|
||||||
|
const excluded = new Set();
|
||||||
|
const updated = new Set();
|
||||||
|
for (const [url, cache] of cachedStyleForUrl.entries()) {
|
||||||
|
if (!style.appliesTo.has(url)) {
|
||||||
|
cache.maybeMatch.add(data.id);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const code = getAppliedCode(url, data);
|
||||||
|
if (!code) {
|
||||||
|
excluded.add(url);
|
||||||
|
delete cache.sections[data.id];
|
||||||
|
} else {
|
||||||
|
updated.add(url);
|
||||||
|
cache.sections[data.id] = {
|
||||||
|
id: data.id,
|
||||||
|
code
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
style.appliesTo = updated;
|
||||||
|
return msg.broadcast({
|
||||||
|
method,
|
||||||
|
style: {
|
||||||
|
id: data.id,
|
||||||
|
enabled: data.enabled
|
||||||
|
},
|
||||||
|
reason,
|
||||||
|
codeIsUpdated
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveStyle(style) {
|
||||||
|
if (!style.name) {
|
||||||
|
throw new Error('style name is empty');
|
||||||
|
}
|
||||||
|
if (style.id == null) {
|
||||||
|
delete style.id;
|
||||||
|
}
|
||||||
|
fixUsoMd5Issue(style);
|
||||||
|
return db.exec('put', style)
|
||||||
|
.then(event => {
|
||||||
|
if (style.id == null) {
|
||||||
|
style.id = event.target.result;
|
||||||
|
}
|
||||||
|
return style;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleSave(data, reason, codeIsUpdated) {
|
||||||
|
const style = styles.get(data.id);
|
||||||
|
let method;
|
||||||
|
if (!style) {
|
||||||
|
styles.set(data.id, {
|
||||||
|
appliesTo: new Set(),
|
||||||
|
data
|
||||||
|
});
|
||||||
|
method = 'styleAdded';
|
||||||
|
} else {
|
||||||
|
style.data = data;
|
||||||
|
method = 'styleUpdated';
|
||||||
|
}
|
||||||
|
return broadcastStyleUpdated(data, reason, method, codeIsUpdated)
|
||||||
|
.then(() => data);
|
||||||
|
}
|
||||||
|
|
||||||
|
// get styles matching a URL, including sloppy regexps and excluded items.
|
||||||
|
function getStylesByUrl(url, id = null) {
|
||||||
|
// FIXME: do we want to cache this? Who would like to open popup rapidly
|
||||||
|
// or search the DB with the same URL?
|
||||||
|
const result = [];
|
||||||
|
const datas = !id ? [...styles.values()].map(s => s.data) :
|
||||||
|
styles.has(id) ? [styles.get(id).data] : [];
|
||||||
|
for (const data of datas) {
|
||||||
|
let excluded = false;
|
||||||
|
let sloppy = false;
|
||||||
|
let sectionMatched = false;
|
||||||
|
const match = urlMatchStyle(url, data);
|
||||||
|
// TODO: enable this when the function starts returning false
|
||||||
|
// if (match === false) {
|
||||||
|
// continue;
|
||||||
|
// }
|
||||||
|
if (match === 'excluded') {
|
||||||
|
excluded = true;
|
||||||
|
}
|
||||||
|
for (const section of data.sections) {
|
||||||
|
if (styleCodeEmpty(section.code)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const match = urlMatchSection(url, section);
|
||||||
|
if (match) {
|
||||||
|
if (match === 'sloppy') {
|
||||||
|
sloppy = true;
|
||||||
|
}
|
||||||
|
sectionMatched = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (sectionMatched) {
|
||||||
|
result.push({
|
||||||
|
data: getStyleWithNoCode(data),
|
||||||
|
excluded,
|
||||||
|
sloppy
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSectionsByUrl(url, id) {
|
||||||
|
let cache = cachedStyleForUrl.get(url);
|
||||||
|
if (!cache) {
|
||||||
|
cache = {
|
||||||
|
sections: {},
|
||||||
|
maybeMatch: new Set()
|
||||||
|
};
|
||||||
|
buildCache(styles.values());
|
||||||
|
cachedStyleForUrl.set(url, cache);
|
||||||
|
} else if (cache.maybeMatch.size) {
|
||||||
|
buildCache(
|
||||||
|
[...cache.maybeMatch]
|
||||||
|
.filter(i => styles.has(i))
|
||||||
|
.map(i => styles.get(i))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (id) {
|
||||||
|
if (cache.sections[id]) {
|
||||||
|
return {[id]: cache.sections[id]};
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
return cache.sections;
|
||||||
|
|
||||||
|
function buildCache(styleList) {
|
||||||
|
for (const {appliesTo, data, preview} of styleList) {
|
||||||
|
const code = getAppliedCode(url, preview || data);
|
||||||
|
if (code) {
|
||||||
|
cache.sections[data.id] = {
|
||||||
|
id: data.id,
|
||||||
|
code
|
||||||
|
};
|
||||||
|
appliesTo.add(url);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getAppliedCode(url, data) {
|
||||||
|
if (urlMatchStyle(url, data) !== true) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const code = [];
|
||||||
|
for (const section of data.sections) {
|
||||||
|
if (urlMatchSection(url, section) === true && !styleCodeEmpty(section.code)) {
|
||||||
|
code.push(section.code);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return code.length && code;
|
||||||
|
}
|
||||||
|
|
||||||
|
function prepare() {
|
||||||
|
return db.exec('getAll').then(event => {
|
||||||
|
const styleList = event.target.result;
|
||||||
|
if (!styleList) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (const style of styleList) {
|
||||||
|
fixUsoMd5Issue(style);
|
||||||
|
styles.set(style.id, {
|
||||||
|
appliesTo: new Set(),
|
||||||
|
data: style
|
||||||
|
});
|
||||||
|
if (!style.name) {
|
||||||
|
style.name = 'ID: ' + style.id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function urlMatchStyle(url, style) {
|
||||||
|
if (style.exclusions && style.exclusions.some(e => compileExclusion(e).test(url))) {
|
||||||
|
return 'excluded';
|
||||||
|
}
|
||||||
|
if (!style.enabled) {
|
||||||
|
return 'disabled';
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function urlMatchSection(url, section) {
|
||||||
|
const domain = getDomain(url);
|
||||||
|
if (section.domains && section.domains.some(d => d === domain || domain.endsWith(`.${d}`))) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (section.urlPrefixes && section.urlPrefixes.some(p => url.startsWith(p))) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
// as per spec the fragment portion is ignored in @-moz-document:
|
||||||
|
// https://www.w3.org/TR/2012/WD-css3-conditional-20120911/#url-of-doc
|
||||||
|
// but the spec is outdated and doesn't account for SPA sites
|
||||||
|
// so we only respect it for `url()` function
|
||||||
|
if (section.urls && (
|
||||||
|
section.urls.includes(url) ||
|
||||||
|
section.urls.includes(getUrlNoHash(url))
|
||||||
|
)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (section.regexps && section.regexps.some(r => compileRe(r).test(url))) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
/*
|
||||||
|
According to CSS4 @document specification the entire URL must match.
|
||||||
|
Stylish-for-Chrome implemented it incorrectly since the very beginning.
|
||||||
|
We'll detect styles that abuse the bug by finding the sections that
|
||||||
|
would have been applied by Stylish but not by us as we follow the spec.
|
||||||
|
*/
|
||||||
|
if (section.regexps && section.regexps.some(r => compileSloppyRe(r).test(url))) {
|
||||||
|
return 'sloppy';
|
||||||
|
}
|
||||||
|
// TODO: check for invalid regexps?
|
||||||
|
if (
|
||||||
|
(!section.regexps || !section.regexps.length) &&
|
||||||
|
(!section.urlPrefixes || !section.urlPrefixes.length) &&
|
||||||
|
(!section.urls || !section.urls.length) &&
|
||||||
|
(!section.domains || !section.domains.length)
|
||||||
|
) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function createCompiler(compile) {
|
||||||
|
// FIXME: FIFO cache doesn't work well here, if we want to match many
|
||||||
|
// regexps more than the cache size, we will never hit the cache because
|
||||||
|
// the first cache is deleted. So we use a simple map but it leaks memory.
|
||||||
|
const cache = new Map();
|
||||||
|
return text => {
|
||||||
|
let re = cache.get(text);
|
||||||
|
if (!re) {
|
||||||
|
re = tryRegExp(compile(text));
|
||||||
|
if (!re) {
|
||||||
|
re = BAD_MATCHER;
|
||||||
|
}
|
||||||
|
cache.set(text, re);
|
||||||
|
}
|
||||||
|
return re;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildGlob(text) {
|
||||||
|
const prefix = text[0] === '^' ? '' : '\\b';
|
||||||
|
const suffix = text[text.length - 1] === '$' ? '' : '\\b';
|
||||||
|
return `${prefix}${escape(text)}${suffix}`;
|
||||||
|
|
||||||
|
function escape(text) {
|
||||||
|
// FIXME: using .* everywhere is slow
|
||||||
|
return text.replace(/[.*]/g, m => m === '.' ? '\\.' : '.*');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDomain(url) {
|
||||||
|
return url.match(/^[\w-]+:\/+(?:[\w:-]+@)?([^:/#]+)/)[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
function getUrlNoHash(url) {
|
||||||
|
return url.split('#')[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
// The md5Url provided by USO includes a duplicate "update" subdomain (see #523),
|
||||||
|
// This fixes any already installed styles containing this error
|
||||||
|
function fixUsoMd5Issue(style) {
|
||||||
|
if (style && style.md5Url && style.md5Url.includes('update.update.userstyles')) {
|
||||||
|
style.md5Url = style.md5Url.replace('update.update.userstyles', 'update.userstyles');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})();
|
|
@ -1,4 +1,4 @@
|
||||||
/* global getStyles API_METHODS */
|
/* global API_METHODS styleManager CHROME prefs updateIconBadge */
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
API_METHODS.styleViaAPI = !CHROME && (() => {
|
API_METHODS.styleViaAPI = !CHROME && (() => {
|
||||||
|
@ -9,6 +9,7 @@ API_METHODS.styleViaAPI = !CHROME && (() => {
|
||||||
styleAdded,
|
styleAdded,
|
||||||
styleReplaceAll,
|
styleReplaceAll,
|
||||||
prefChanged,
|
prefChanged,
|
||||||
|
updateCount,
|
||||||
};
|
};
|
||||||
const NOP = Promise.resolve(new Error('NOP'));
|
const NOP = Promise.resolve(new Error('NOP'));
|
||||||
const onError = () => {};
|
const onError = () => {};
|
||||||
|
@ -22,15 +23,23 @@ API_METHODS.styleViaAPI = !CHROME && (() => {
|
||||||
|
|
||||||
let observingTabs = false;
|
let observingTabs = false;
|
||||||
|
|
||||||
return (request, sender) => {
|
return function (request) {
|
||||||
const action = ACTIONS[request.action];
|
const action = ACTIONS[request.method];
|
||||||
return !action ? NOP :
|
return !action ? NOP :
|
||||||
action(request, sender)
|
action(request, this.sender)
|
||||||
.catch(onError)
|
.catch(onError)
|
||||||
.then(maybeToggleObserver);
|
.then(maybeToggleObserver);
|
||||||
};
|
};
|
||||||
|
|
||||||
function styleApply({id = null, ignoreUrlCheck}, {tab, frameId, url}) {
|
function updateCount(request, {tab, frameId}) {
|
||||||
|
if (frameId) {
|
||||||
|
throw new Error('we do not count styles for frames');
|
||||||
|
}
|
||||||
|
const {frameStyles} = getCachedData(tab.id, frameId);
|
||||||
|
updateIconBadge(tab.id, Object.keys(frameStyles).length);
|
||||||
|
}
|
||||||
|
|
||||||
|
function styleApply({id = null, ignoreUrlCheck = false}, {tab, frameId, url}) {
|
||||||
if (prefs.get('disableAll')) {
|
if (prefs.get('disableAll')) {
|
||||||
return NOP;
|
return NOP;
|
||||||
}
|
}
|
||||||
|
@ -38,24 +47,15 @@ API_METHODS.styleViaAPI = !CHROME && (() => {
|
||||||
if (id === null && !ignoreUrlCheck && frameStyles.url === url) {
|
if (id === null && !ignoreUrlCheck && frameStyles.url === url) {
|
||||||
return NOP;
|
return NOP;
|
||||||
}
|
}
|
||||||
return getStyles({id, matchUrl: url, asHash: true}).then(styles => {
|
return styleManager.getSectionsByUrl(url, id).then(sections => {
|
||||||
const tasks = [];
|
const tasks = [];
|
||||||
for (const styleId in styles) {
|
for (const section of Object.values(sections)) {
|
||||||
if (isNaN(parseInt(styleId))) {
|
const styleId = section.id;
|
||||||
continue;
|
const code = section.code.join('\n');
|
||||||
}
|
|
||||||
// shallow-extract code from the sections array in order to reuse references
|
|
||||||
// in other places whereas the combined string gets garbage-collected
|
|
||||||
const styleSections = styles[styleId].map(section => section.code);
|
|
||||||
const code = styleSections.join('\n');
|
|
||||||
if (!code) {
|
|
||||||
delete frameStyles[styleId];
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (code === (frameStyles[styleId] || []).join('\n')) {
|
if (code === (frameStyles[styleId] || []).join('\n')) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
frameStyles[styleId] = styleSections;
|
frameStyles[styleId] = section.code;
|
||||||
tasks.push(
|
tasks.push(
|
||||||
browser.tabs.insertCSS(tab.id, {
|
browser.tabs.insertCSS(tab.id, {
|
||||||
code,
|
code,
|
||||||
|
@ -70,16 +70,18 @@ API_METHODS.styleViaAPI = !CHROME && (() => {
|
||||||
cache.set(tab.id, tabFrames);
|
cache.set(tab.id, tabFrames);
|
||||||
}
|
}
|
||||||
return Promise.all(tasks);
|
return Promise.all(tasks);
|
||||||
});
|
})
|
||||||
|
.then(() => updateCount(null, {tab, frameId}));
|
||||||
}
|
}
|
||||||
|
|
||||||
function styleDeleted({id}, {tab, frameId}) {
|
function styleDeleted({style: {id}}, {tab, frameId}) {
|
||||||
const {tabFrames, frameStyles, styleSections} = getCachedData(tab.id, frameId, id);
|
const {tabFrames, frameStyles, styleSections} = getCachedData(tab.id, frameId, id);
|
||||||
const code = styleSections.join('\n');
|
const code = styleSections.join('\n');
|
||||||
if (code && !duplicateCodeExists({frameStyles, id, code})) {
|
if (code && !duplicateCodeExists({frameStyles, id, code})) {
|
||||||
delete frameStyles[id];
|
delete frameStyles[id];
|
||||||
removeFrameIfEmpty(tab.id, frameId, tabFrames, frameStyles);
|
removeFrameIfEmpty(tab.id, frameId, tabFrames, frameStyles);
|
||||||
return removeCSS(tab.id, frameId, code);
|
return removeCSS(tab.id, frameId, code)
|
||||||
|
.then(() => updateCount(null, {tab, frameId}));
|
||||||
} else {
|
} else {
|
||||||
return NOP;
|
return NOP;
|
||||||
}
|
}
|
||||||
|
@ -87,7 +89,7 @@ API_METHODS.styleViaAPI = !CHROME && (() => {
|
||||||
|
|
||||||
function styleUpdated({style}, sender) {
|
function styleUpdated({style}, sender) {
|
||||||
if (!style.enabled) {
|
if (!style.enabled) {
|
||||||
return styleDeleted(style, sender);
|
return styleDeleted({style}, sender);
|
||||||
}
|
}
|
||||||
const {tab, frameId} = sender;
|
const {tab, frameId} = sender;
|
||||||
const {frameStyles, styleSections} = getCachedData(tab.id, frameId, style.id);
|
const {frameStyles, styleSections} = getCachedData(tab.id, frameId, style.id);
|
||||||
|
|
|
@ -1,9 +1,7 @@
|
||||||
/*
|
/* global styleSectionsEqual prefs download tryJSONparse ignoreChromeError
|
||||||
global getStyles saveStyle styleSectionsEqual
|
calcStyleDigest getStyleWithNoCode debounce chromeLocal
|
||||||
global calcStyleDigest cachedStyles getStyleWithNoCode
|
usercss semverCompare
|
||||||
global usercss semverCompare
|
API_METHODS styleManager */
|
||||||
global API_METHODS
|
|
||||||
*/
|
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
(() => {
|
(() => {
|
||||||
|
@ -51,7 +49,7 @@ global API_METHODS
|
||||||
checkingAll = true;
|
checkingAll = true;
|
||||||
retrying.clear();
|
retrying.clear();
|
||||||
const port = observe && chrome.runtime.connect({name: 'updater'});
|
const port = observe && chrome.runtime.connect({name: 'updater'});
|
||||||
return getStyles({}).then(styles => {
|
return styleManager.getAllStyles().then(styles => {
|
||||||
styles = styles.filter(style => style.updateUrl);
|
styles = styles.filter(style => style.updateUrl);
|
||||||
if (port) port.postMessage({count: styles.length});
|
if (port) port.postMessage({count: styles.length});
|
||||||
log('');
|
log('');
|
||||||
|
@ -70,7 +68,7 @@ global API_METHODS
|
||||||
|
|
||||||
function checkStyle({
|
function checkStyle({
|
||||||
id,
|
id,
|
||||||
style = cachedStyles.byId.get(id),
|
style,
|
||||||
port,
|
port,
|
||||||
save = true,
|
save = true,
|
||||||
ignoreDigest,
|
ignoreDigest,
|
||||||
|
@ -89,14 +87,33 @@ global API_METHODS
|
||||||
|
|
||||||
'ignoreDigest' option is set on the second manual individual update check on the manage page.
|
'ignoreDigest' option is set on the second manual individual update check on the manage page.
|
||||||
*/
|
*/
|
||||||
return Promise.resolve(style)
|
return fetchStyle()
|
||||||
.then([calcStyleDigest][!ignoreDigest ? 0 : 'skip'])
|
.then(() => {
|
||||||
.then([checkIfEdited][!ignoreDigest ? 0 : 'skip'])
|
if (!ignoreDigest) {
|
||||||
.then([maybeUpdateUSO, maybeUpdateUsercss][style.usercssData ? 1 : 0])
|
return calcStyleDigest(style)
|
||||||
|
.then(checkIfEdited);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
if (style.usercssData) {
|
||||||
|
return maybeUpdateUsercss();
|
||||||
|
}
|
||||||
|
return maybeUpdateUSO();
|
||||||
|
})
|
||||||
.then(maybeSave)
|
.then(maybeSave)
|
||||||
.then(reportSuccess)
|
.then(reportSuccess)
|
||||||
.catch(reportFailure);
|
.catch(reportFailure);
|
||||||
|
|
||||||
|
function fetchStyle() {
|
||||||
|
if (style) {
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
return styleManager.get(id)
|
||||||
|
.then(style_ => {
|
||||||
|
style = style_;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function reportSuccess(saved) {
|
function reportSuccess(saved) {
|
||||||
log(STATES.UPDATED + ` #${style.id} ${style.name}`);
|
log(STATES.UPDATED + ` #${style.id} ${style.name}`);
|
||||||
const info = {updated: true, style: saved};
|
const info = {updated: true, style: saved};
|
||||||
|
@ -145,24 +162,25 @@ global API_METHODS
|
||||||
|
|
||||||
function maybeUpdateUsercss() {
|
function maybeUpdateUsercss() {
|
||||||
// TODO: when sourceCode is > 100kB use http range request(s) for version check
|
// TODO: when sourceCode is > 100kB use http range request(s) for version check
|
||||||
return download(style.updateUrl).then(text => {
|
return download(style.updateUrl).then(text =>
|
||||||
const json = usercss.buildMeta(text);
|
usercss.buildMeta(text).then(json => {
|
||||||
const {usercssData: {version}} = style;
|
const {usercssData: {version}} = style;
|
||||||
const {usercssData: {version: newVersion}} = json;
|
const {usercssData: {version: newVersion}} = json;
|
||||||
switch (Math.sign(semverCompare(version, newVersion))) {
|
switch (Math.sign(semverCompare(version, newVersion))) {
|
||||||
case 0:
|
case 0:
|
||||||
// re-install is invalid in a soft upgrade
|
// re-install is invalid in a soft upgrade
|
||||||
if (!ignoreDigest) {
|
if (!ignoreDigest) {
|
||||||
const sameCode = text === style.sourceCode;
|
const sameCode = text === style.sourceCode;
|
||||||
return Promise.reject(sameCode ? STATES.SAME_CODE : STATES.SAME_VERSION);
|
return Promise.reject(sameCode ? STATES.SAME_CODE : STATES.SAME_VERSION);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 1:
|
case 1:
|
||||||
// downgrade is always invalid
|
// downgrade is always invalid
|
||||||
return Promise.reject(STATES.ERROR_VERSION);
|
return Promise.reject(STATES.ERROR_VERSION);
|
||||||
}
|
}
|
||||||
return usercss.buildCode(json);
|
return usercss.buildCode(json);
|
||||||
});
|
})
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function maybeSave(json = {}) {
|
function maybeSave(json = {}) {
|
||||||
|
@ -173,7 +191,6 @@ global API_METHODS
|
||||||
|
|
||||||
json.id = style.id;
|
json.id = style.id;
|
||||||
json.updateDate = Date.now();
|
json.updateDate = Date.now();
|
||||||
json.reason = 'update';
|
|
||||||
|
|
||||||
// keep current state
|
// keep current state
|
||||||
delete json.enabled;
|
delete json.enabled;
|
||||||
|
@ -185,10 +202,10 @@ global API_METHODS
|
||||||
json.originalName = json.name;
|
json.originalName = json.name;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const newStyle = Object.assign({}, style, json);
|
||||||
if (styleSectionsEqual(json, style, {checkSource: true})) {
|
if (styleSectionsEqual(json, style, {checkSource: true})) {
|
||||||
// update digest even if save === false as there might be just a space added etc.
|
// update digest even if save === false as there might be just a space added etc.
|
||||||
json.reason = 'update-digest';
|
return styleManager.installStyle(newStyle)
|
||||||
return saveStyle(json)
|
|
||||||
.then(saved => {
|
.then(saved => {
|
||||||
style.originalDigest = saved.originalDigest;
|
style.originalDigest = saved.originalDigest;
|
||||||
return Promise.reject(STATES.SAME_CODE);
|
return Promise.reject(STATES.SAME_CODE);
|
||||||
|
@ -200,8 +217,8 @@ global API_METHODS
|
||||||
}
|
}
|
||||||
|
|
||||||
return save ?
|
return save ?
|
||||||
API_METHODS[json.usercssData ? 'saveUsercss' : 'saveStyle'](json) :
|
API_METHODS[json.usercssData ? 'installUsercss' : 'installStyle'](newStyle) :
|
||||||
json;
|
newStyle;
|
||||||
}
|
}
|
||||||
|
|
||||||
function styleJSONseemsValid(json) {
|
function styleJSONseemsValid(json) {
|
||||||
|
|
|
@ -1,13 +1,15 @@
|
||||||
/* global API_METHODS usercss saveStyle getStyles chromeLocal cachedStyles */
|
/* global API_METHODS usercss chromeLocal styleManager FIREFOX deepCopy openURL
|
||||||
|
download */
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
(() => {
|
(() => {
|
||||||
|
API_METHODS.installUsercss = installUsercss;
|
||||||
|
API_METHODS.editSaveUsercss = editSaveUsercss;
|
||||||
|
API_METHODS.configUsercssVars = configUsercssVars;
|
||||||
|
|
||||||
API_METHODS.saveUsercss = style => save(style, false);
|
|
||||||
API_METHODS.saveUsercssUnsafe = style => save(style, true);
|
|
||||||
API_METHODS.buildUsercss = build;
|
API_METHODS.buildUsercss = build;
|
||||||
API_METHODS.installUsercss = install;
|
API_METHODS.openUsercssInstallPage = install;
|
||||||
API_METHODS.parseUsercss = parse;
|
|
||||||
API_METHODS.findUsercss = find;
|
API_METHODS.findUsercss = find;
|
||||||
|
|
||||||
const TEMP_CODE_PREFIX = 'tempUsercssCode';
|
const TEMP_CODE_PREFIX = 'tempUsercssCode';
|
||||||
|
@ -40,69 +42,96 @@
|
||||||
if (style.usercssData) {
|
if (style.usercssData) {
|
||||||
return Promise.resolve(style);
|
return Promise.resolve(style);
|
||||||
}
|
}
|
||||||
try {
|
|
||||||
const {sourceCode} = style;
|
// allow sourceCode to be normalized
|
||||||
// allow sourceCode to be normalized
|
const {sourceCode} = style;
|
||||||
delete style.sourceCode;
|
delete style.sourceCode;
|
||||||
return Promise.resolve(Object.assign(usercss.buildMeta(sourceCode), style));
|
|
||||||
} catch (e) {
|
return usercss.buildMeta(sourceCode)
|
||||||
return Promise.reject(e);
|
.then(newStyle => Object.assign(newStyle, style));
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function assignVars(style) {
|
function assignVars(style) {
|
||||||
if (style.reason === 'config' && style.id) {
|
return find(style)
|
||||||
return style;
|
.then(dup => {
|
||||||
}
|
if (dup) {
|
||||||
const dup = find(style);
|
style.id = dup.id;
|
||||||
if (dup) {
|
// preserve style.vars during update
|
||||||
style.id = dup.id;
|
return usercss.assignVars(style, dup)
|
||||||
if (style.reason !== 'config') {
|
.then(() => style);
|
||||||
// preserve style.vars during update
|
}
|
||||||
usercss.assignVars(style, dup);
|
return style;
|
||||||
}
|
});
|
||||||
}
|
|
||||||
return style;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parse the source and find the duplication
|
* Parse the source, find the duplication, and build sections with variables
|
||||||
* @param _
|
* @param _
|
||||||
* @param {String} _.sourceCode
|
* @param {String} _.sourceCode
|
||||||
* @param {Boolean=} _.checkDup
|
* @param {Boolean=} _.checkDup
|
||||||
* @param {Boolean=} _.metaOnly
|
* @param {Boolean=} _.metaOnly
|
||||||
|
* @param {Object} _.vars
|
||||||
|
* @param {Boolean=} _.assignVars
|
||||||
* @returns {Promise<{style, dup:Boolean?}>}
|
* @returns {Promise<{style, dup:Boolean?}>}
|
||||||
*/
|
*/
|
||||||
function build({
|
function build({
|
||||||
sourceCode,
|
sourceCode,
|
||||||
checkDup,
|
checkDup,
|
||||||
metaOnly,
|
metaOnly,
|
||||||
|
vars,
|
||||||
|
assignVars = false,
|
||||||
}) {
|
}) {
|
||||||
const task = buildMeta({sourceCode});
|
return usercss.buildMeta(sourceCode)
|
||||||
return (metaOnly ? task : task.then(usercss.buildCode))
|
.then(style => {
|
||||||
.then(style => ({
|
const findDup = checkDup || assignVars ? find(style) : null;
|
||||||
style,
|
return Promise.all([
|
||||||
dup: checkDup && find(style),
|
metaOnly ? style : doBuild(style, findDup),
|
||||||
}));
|
findDup
|
||||||
}
|
]);
|
||||||
|
})
|
||||||
|
.then(([style, dup]) => ({style, dup}));
|
||||||
|
|
||||||
// Parse the source, apply customizations, report fatal/syntax errors
|
function doBuild(style, findDup) {
|
||||||
function parse(style, allowErrors = false) {
|
if (vars || assignVars) {
|
||||||
// restore if stripped by getStyleWithNoCode
|
const getOld = vars ? Promise.resolve({usercssData: {vars}}) : findDup;
|
||||||
if (typeof style.sourceCode !== 'string') {
|
return getOld
|
||||||
style.sourceCode = cachedStyles.byId.get(style.id).sourceCode;
|
.then(oldStyle => usercss.assignVars(style, oldStyle))
|
||||||
|
.then(() => usercss.buildCode(style));
|
||||||
|
}
|
||||||
|
return usercss.buildCode(style);
|
||||||
}
|
}
|
||||||
return buildMeta(style)
|
|
||||||
.then(assignVars)
|
|
||||||
.then(style => usercss.buildCode(style, allowErrors));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function save(style, allowErrors = false) {
|
// Build the style within aditional properties then inherit variable values
|
||||||
return parse(style, allowErrors)
|
// from the old style.
|
||||||
.then(result =>
|
function parse(style) {
|
||||||
allowErrors ?
|
return buildMeta(style)
|
||||||
saveStyle(result.style).then(style => ({style, errors: result.errors})) :
|
.then(buildMeta)
|
||||||
saveStyle(result));
|
.then(assignVars)
|
||||||
|
.then(usercss.buildCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
// FIXME: simplify this to `installUsercss(sourceCode)`?
|
||||||
|
function installUsercss(style) {
|
||||||
|
return parse(style)
|
||||||
|
.then(styleManager.installStyle);
|
||||||
|
}
|
||||||
|
|
||||||
|
// FIXME: simplify this to `editSaveUsercss({sourceCode, exclusions})`?
|
||||||
|
function editSaveUsercss(style) {
|
||||||
|
return parse(style)
|
||||||
|
.then(styleManager.editSave);
|
||||||
|
}
|
||||||
|
|
||||||
|
function configUsercssVars(id, vars) {
|
||||||
|
return styleManager.get(id)
|
||||||
|
.then(style => {
|
||||||
|
const newStyle = deepCopy(style);
|
||||||
|
newStyle.usercssData.vars = vars;
|
||||||
|
return usercss.buildCode(newStyle);
|
||||||
|
})
|
||||||
|
.then(style => styleManager.installStyle(style, 'config'))
|
||||||
|
.then(style => style.usercssData.vars);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -110,19 +139,23 @@
|
||||||
* @returns {Style}
|
* @returns {Style}
|
||||||
*/
|
*/
|
||||||
function find(styleOrData) {
|
function find(styleOrData) {
|
||||||
if (styleOrData.id) return cachedStyles.byId.get(styleOrData.id);
|
if (styleOrData.id) {
|
||||||
const {name, namespace} = styleOrData.usercssData || styleOrData;
|
return styleManager.get(styleOrData.id);
|
||||||
for (const dup of cachedStyles.list) {
|
|
||||||
const data = dup.usercssData;
|
|
||||||
if (!data) continue;
|
|
||||||
if (data.name === name &&
|
|
||||||
data.namespace === namespace) {
|
|
||||||
return dup;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
const {name, namespace} = styleOrData.usercssData || styleOrData;
|
||||||
|
return styleManager.getAllStyles().then(styleList => {
|
||||||
|
for (const dup of styleList) {
|
||||||
|
const data = dup.usercssData;
|
||||||
|
if (!data) continue;
|
||||||
|
if (data.name === name &&
|
||||||
|
data.namespace === namespace) {
|
||||||
|
return dup;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function install({url, direct, downloaded, tab}, sender) {
|
function install({url, direct, downloaded, tab}, sender = this.sender) {
|
||||||
tab = tab !== undefined ? tab : sender.tab;
|
tab = tab !== undefined ? tab : sender.tab;
|
||||||
url = url || tab.url;
|
url = url || tab.url;
|
||||||
if (direct && !downloaded) {
|
if (direct && !downloaded) {
|
||||||
|
|
510
content/apply.js
510
content/apply.js
|
@ -1,44 +1,128 @@
|
||||||
/* eslint no-var: 0 */
|
/* eslint no-var: 0 */
|
||||||
|
/* global msg API prefs */
|
||||||
|
/* exported APPLY */
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
(() => {
|
// some weird bug in new Chrome: the content script gets injected multiple times
|
||||||
if (typeof window.applyOnMessage === 'function') {
|
// define a constant so it throws when redefined
|
||||||
// some weird bug in new Chrome: the content script gets injected multiple times
|
const APPLY = (() => {
|
||||||
return;
|
|
||||||
}
|
|
||||||
const CHROME = chrome.app ? parseInt(navigator.userAgent.match(/Chrom\w+\/(?:\d+\.){2}(\d+)|$/)[1]) : NaN;
|
const CHROME = chrome.app ? parseInt(navigator.userAgent.match(/Chrom\w+\/(?:\d+\.){2}(\d+)|$/)[1]) : NaN;
|
||||||
|
const STYLE_VIA_API = !chrome.app && document instanceof XMLDocument;
|
||||||
var ID_PREFIX = 'stylus-';
|
var ID_PREFIX = 'stylus-';
|
||||||
var ROOT = document.documentElement;
|
var ROOT;
|
||||||
var isOwnPage = location.protocol.endsWith('-extension:');
|
var isOwnPage = location.protocol.endsWith('-extension:');
|
||||||
var disableAll = false;
|
var disableAll = false;
|
||||||
var exposeIframes = false;
|
|
||||||
var styleElements = new Map();
|
var styleElements = new Map();
|
||||||
var disabledElements = new Map();
|
var disabledElements = new Map();
|
||||||
var retiredStyleTimers = new Map();
|
|
||||||
var docRewriteObserver;
|
var docRewriteObserver;
|
||||||
var docRootObserver;
|
var docRootObserver;
|
||||||
|
const setStyleContent = createSetStyleContent();
|
||||||
|
const initializing = init();
|
||||||
|
|
||||||
// FF59+ bug workaround
|
msg.onTab(applyOnMessage);
|
||||||
// See https://github.com/openstyles/stylus/issues/461
|
|
||||||
// Since it's easy to spoof the browser version in pre-Quantum FF we're checking
|
|
||||||
// for getPreventDefault which got removed in FF59 https://bugzil.la/691151
|
|
||||||
const FF_BUG461 = !CHROME && !isOwnPage && !Event.prototype.getPreventDefault;
|
|
||||||
const pageContextQueue = [];
|
|
||||||
|
|
||||||
requestStyles();
|
|
||||||
chrome.runtime.onMessage.addListener(applyOnMessage);
|
|
||||||
window.applyOnMessage = applyOnMessage;
|
|
||||||
|
|
||||||
if (!isOwnPage) {
|
if (!isOwnPage) {
|
||||||
window.dispatchEvent(new CustomEvent(chrome.runtime.id));
|
window.dispatchEvent(new CustomEvent(chrome.runtime.id, {
|
||||||
|
detail: pageObject({method: 'orphan'})
|
||||||
|
}));
|
||||||
window.addEventListener(chrome.runtime.id, orphanCheck, true);
|
window.addEventListener(chrome.runtime.id, orphanCheck, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
function requestStyles(options, callback = applyStyles) {
|
let parentDomain;
|
||||||
if (!chrome.app && document instanceof XMLDocument) {
|
|
||||||
chrome.runtime.sendMessage({method: 'styleViaAPI', action: 'styleApply'});
|
prefs.subscribe(['disableAll'], (key, value) => doDisableAll(value));
|
||||||
return;
|
if (window !== parent) {
|
||||||
|
prefs.subscribe(['exposeIframes'], updateExposeIframes);
|
||||||
|
}
|
||||||
|
|
||||||
|
function init() {
|
||||||
|
if (STYLE_VIA_API) {
|
||||||
|
return API.styleViaAPI({method: 'styleApply'});
|
||||||
}
|
}
|
||||||
|
return API.getSectionsByUrl(getMatchUrl())
|
||||||
|
.then(result => {
|
||||||
|
ROOT = document.documentElement;
|
||||||
|
applyStyles(result, () => {
|
||||||
|
// CSS transition bug workaround: since we insert styles asynchronously,
|
||||||
|
// the browsers, especially Firefox, may apply all transitions on page load
|
||||||
|
if ([...styleElements.values()].some(n => n.textContent.includes('transition'))) {
|
||||||
|
applyTransitionPatch();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function pageObject(target) {
|
||||||
|
// https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/Sharing_objects_with_page_scripts
|
||||||
|
const obj = new window.Object();
|
||||||
|
Object.assign(obj, target);
|
||||||
|
return obj;
|
||||||
|
}
|
||||||
|
|
||||||
|
function createSetStyleContent() {
|
||||||
|
// FF59+ bug workaround
|
||||||
|
// See https://github.com/openstyles/stylus/issues/461
|
||||||
|
// Since it's easy to spoof the browser version in pre-Quantum FF we're checking
|
||||||
|
// for getPreventDefault which got removed in FF59 https://bugzil.la/691151
|
||||||
|
const EVENT_NAME = chrome.runtime.id;
|
||||||
|
const usePageScript = CHROME || isOwnPage || Event.prototype.getPreventDefault ?
|
||||||
|
Promise.resolve(false) : injectPageScript();
|
||||||
|
return (el, content) =>
|
||||||
|
usePageScript.then(ok => {
|
||||||
|
if (!ok) {
|
||||||
|
const disabled = el.disabled;
|
||||||
|
el.textContent = content;
|
||||||
|
el.disabled = disabled;
|
||||||
|
} else {
|
||||||
|
const detail = pageObject({
|
||||||
|
method: 'setStyleContent',
|
||||||
|
id: el.id,
|
||||||
|
content
|
||||||
|
});
|
||||||
|
window.dispatchEvent(new CustomEvent(EVENT_NAME, {detail}));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function injectPageScript() {
|
||||||
|
const scriptContent = EVENT_NAME => {
|
||||||
|
document.currentScript.remove();
|
||||||
|
window.addEventListener(EVENT_NAME, function handler(e) {
|
||||||
|
const {method, id, content} = e.detail;
|
||||||
|
if (method === 'setStyleContent') {
|
||||||
|
const el = document.getElementById(id);
|
||||||
|
if (!el) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const disabled = el.disabled;
|
||||||
|
el.textContent = content;
|
||||||
|
el.disabled = disabled;
|
||||||
|
} else if (method === 'orphan') {
|
||||||
|
window.removeEventListener(EVENT_NAME, handler);
|
||||||
|
}
|
||||||
|
}, true);
|
||||||
|
};
|
||||||
|
const code = `(${scriptContent})(${JSON.stringify(EVENT_NAME)})`;
|
||||||
|
const src = `data:application/javascript;base64,${btoa(code)}`;
|
||||||
|
const script = document.createElement('script');
|
||||||
|
const {resolve, promise} = deferred();
|
||||||
|
script.src = src;
|
||||||
|
script.onload = () => resolve(true);
|
||||||
|
script.onerror = () => resolve(false);
|
||||||
|
document.documentElement.appendChild(script);
|
||||||
|
return promise;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function deferred() {
|
||||||
|
const o = {};
|
||||||
|
o.promise = new Promise((resolve, reject) => {
|
||||||
|
o.resolve = resolve;
|
||||||
|
o.reject = reject;
|
||||||
|
});
|
||||||
|
return o;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getMatchUrl() {
|
||||||
var matchUrl = location.href;
|
var matchUrl = location.href;
|
||||||
if (!matchUrl.match(/^(http|file|chrome|ftp)/)) {
|
if (!matchUrl.match(/^(http|file|chrome|ftp)/)) {
|
||||||
// dynamic about: and javascript: iframes don't have an URL yet
|
// dynamic about: and javascript: iframes don't have an URL yet
|
||||||
|
@ -49,78 +133,38 @@
|
||||||
}
|
}
|
||||||
} catch (e) {}
|
} catch (e) {}
|
||||||
}
|
}
|
||||||
const request = Object.assign({
|
return matchUrl;
|
||||||
method: 'getStylesForFrame',
|
|
||||||
asHash: true,
|
|
||||||
matchUrl,
|
|
||||||
}, options);
|
|
||||||
// On own pages we request the styles directly to minimize delay and flicker
|
|
||||||
if (typeof API === 'function') {
|
|
||||||
API.getStyles(request).then(callback);
|
|
||||||
} else if (!CHROME && getStylesFallback(request)) {
|
|
||||||
// NOP
|
|
||||||
} else {
|
|
||||||
chrome.runtime.sendMessage(request, callback);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
function applyOnMessage(request) {
|
||||||
* TODO: remove when FF fixes the bug.
|
if (request.method === 'ping') {
|
||||||
* Firefox borks sendMessage in same-origin iframes that have 'src' with a real path on the site.
|
return true;
|
||||||
* We implement a workaround for the initial styleApply case only.
|
|
||||||
* Everything else (like toggling of styles) is still buggy.
|
|
||||||
* @param {Object} msg
|
|
||||||
* @param {Function} callback
|
|
||||||
* @returns {Boolean|undefined}
|
|
||||||
*/
|
|
||||||
function getStylesFallback(msg) {
|
|
||||||
if (window !== parent &&
|
|
||||||
location.href !== 'about:blank') {
|
|
||||||
try {
|
|
||||||
if (parent.location.origin === location.origin &&
|
|
||||||
parent.location.href !== location.href) {
|
|
||||||
chrome.runtime.connect({name: 'getStyles:' + JSON.stringify(msg)});
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
} catch (e) {}
|
|
||||||
}
|
}
|
||||||
}
|
if (STYLE_VIA_API) {
|
||||||
|
if (request.method === 'urlChanged') {
|
||||||
function applyOnMessage(request, sender, sendResponse) {
|
request.method = 'styleReplaceAll';
|
||||||
if (request.styles === 'DIY') {
|
|
||||||
// Do-It-Yourself tells our built-in pages to fetch the styles directly
|
|
||||||
// which is faster because IPC messaging JSON-ifies everything internally
|
|
||||||
requestStyles({}, styles => {
|
|
||||||
request.styles = styles;
|
|
||||||
applyOnMessage(request);
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!chrome.app && document instanceof XMLDocument && request.method !== 'ping') {
|
|
||||||
request.action = request.method;
|
|
||||||
request.method = 'styleViaAPI';
|
|
||||||
request.styles = null;
|
|
||||||
if (request.style) {
|
|
||||||
request.style.sections = null;
|
|
||||||
}
|
}
|
||||||
chrome.runtime.sendMessage(request);
|
API.styleViaAPI(request);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (request.method) {
|
switch (request.method) {
|
||||||
case 'styleDeleted':
|
case 'styleDeleted':
|
||||||
removeStyle(request);
|
removeStyle(request.style);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'styleUpdated':
|
case 'styleUpdated':
|
||||||
if (request.codeIsUpdated === false) {
|
if (request.codeIsUpdated === false) {
|
||||||
applyStyleState(request.style);
|
applyStyleState(request.style);
|
||||||
break;
|
} else if (request.style.enabled) {
|
||||||
}
|
API.getSectionsByUrl(getMatchUrl(), request.style.id)
|
||||||
if (request.style.enabled) {
|
.then(sections => {
|
||||||
removeStyle({id: request.style.id, retire: true});
|
if (!sections[request.style.id]) {
|
||||||
requestStyles({id: request.style.id});
|
removeStyle(request.style);
|
||||||
|
} else {
|
||||||
|
applyStyles(sections);
|
||||||
|
}
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
removeStyle(request.style);
|
removeStyle(request.style);
|
||||||
}
|
}
|
||||||
|
@ -128,29 +172,28 @@
|
||||||
|
|
||||||
case 'styleAdded':
|
case 'styleAdded':
|
||||||
if (request.style.enabled) {
|
if (request.style.enabled) {
|
||||||
requestStyles({id: request.style.id});
|
API.getSectionsByUrl(getMatchUrl(), request.style.id)
|
||||||
|
.then(applyStyles);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'styleApply':
|
case 'urlChanged':
|
||||||
applyStyles(request.styles);
|
API.getSectionsByUrl(getMatchUrl())
|
||||||
|
.then(replaceAll);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'styleReplaceAll':
|
case 'backgroundReady':
|
||||||
replaceAll(request.styles);
|
initializing
|
||||||
|
.catch(err => {
|
||||||
|
if (msg.RX_NO_RECEIVER.test(err.message)) {
|
||||||
|
return init();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(console.error);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'prefChanged':
|
case 'updateCount':
|
||||||
if ('disableAll' in request.prefs) {
|
updateCount();
|
||||||
doDisableAll(request.prefs.disableAll);
|
|
||||||
}
|
|
||||||
if ('exposeIframes' in request.prefs) {
|
|
||||||
doExposeIframes(request.prefs.exposeIframes);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'ping':
|
|
||||||
sendResponse(true);
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -160,27 +203,63 @@
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
disableAll = disable;
|
disableAll = disable;
|
||||||
Array.prototype.forEach.call(document.styleSheets, stylesheet => {
|
if (STYLE_VIA_API) {
|
||||||
if (stylesheet.ownerNode.matches(`style.stylus[id^="${ID_PREFIX}"]`)
|
API.styleViaAPI({method: 'prefChanged', prefs: {disableAll}});
|
||||||
&& stylesheet.disabled !== disable) {
|
} else {
|
||||||
stylesheet.disabled = disable;
|
Array.prototype.forEach.call(document.styleSheets, stylesheet => {
|
||||||
}
|
if (stylesheet.ownerNode.matches(`style.stylus[id^="${ID_PREFIX}"]`)
|
||||||
});
|
&& stylesheet.disabled !== disable) {
|
||||||
|
stylesheet.disabled = disable;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function doExposeIframes(state = exposeIframes) {
|
function fetchParentDomain() {
|
||||||
if (state === exposeIframes ||
|
if (parentDomain) {
|
||||||
state === true && typeof exposeIframes === 'string' ||
|
return Promise.resolve();
|
||||||
window === parent) {
|
}
|
||||||
|
return API.getTabUrlPrefix()
|
||||||
|
.then(newDomain => {
|
||||||
|
parentDomain = newDomain;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateExposeIframes() {
|
||||||
|
if (!prefs.get('exposeIframes') || window === parent || !styleElements.size) {
|
||||||
|
document.documentElement.removeAttribute('stylus-iframe');
|
||||||
|
} else {
|
||||||
|
fetchParentDomain().then(() => {
|
||||||
|
document.documentElement.setAttribute('stylus-iframe', parentDomain);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateCount() {
|
||||||
|
if (window !== parent) {
|
||||||
|
// we don't care about iframes
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
exposeIframes = state;
|
if (/^\w+?-extension:\/\/.+(popup|options)\.html$/.test(location.href)) {
|
||||||
const attr = document.documentElement.getAttribute('stylus-iframe');
|
// popup and the option page are not tabs
|
||||||
if (state && state !== attr) {
|
return;
|
||||||
document.documentElement.setAttribute('stylus-iframe', state);
|
|
||||||
} else if (!state && attr !== undefined) {
|
|
||||||
document.documentElement.removeAttribute('stylus-iframe');
|
|
||||||
}
|
}
|
||||||
|
if (STYLE_VIA_API) {
|
||||||
|
API.styleViaAPI({method: 'updateCount'}).catch(msg.ignoreError);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let count = 0;
|
||||||
|
for (const id of styleElements.keys()) {
|
||||||
|
if (!disabledElements.has(id)) {
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// we have to send the tabId so we can't use `sendBg` that is used by `API`
|
||||||
|
msg.send({
|
||||||
|
method: 'invokeAPI',
|
||||||
|
name: 'updateIconBadge',
|
||||||
|
args: [count]
|
||||||
|
}).catch(msg.ignoreError);
|
||||||
}
|
}
|
||||||
|
|
||||||
function applyStyleState({id, enabled}) {
|
function applyStyleState({id, enabled}) {
|
||||||
|
@ -193,7 +272,8 @@
|
||||||
addStyleElement(inCache);
|
addStyleElement(inCache);
|
||||||
disabledElements.delete(id);
|
disabledElements.delete(id);
|
||||||
} else {
|
} else {
|
||||||
requestStyles({id});
|
return API.getSectionsByUrl(getMatchUrl(), id)
|
||||||
|
.then(applyStyles);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (inDoc) {
|
if (inDoc) {
|
||||||
|
@ -201,32 +281,25 @@
|
||||||
docRootObserver.evade(() => inDoc.remove());
|
docRootObserver.evade(() => inDoc.remove());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
updateCount();
|
||||||
}
|
}
|
||||||
|
|
||||||
function removeStyle({id, retire = false}) {
|
function removeStyle({id}) {
|
||||||
const el = document.getElementById(ID_PREFIX + id);
|
const el = document.getElementById(ID_PREFIX + id);
|
||||||
if (el) {
|
if (el) {
|
||||||
if (retire) {
|
docRootObserver.evade(() => el.remove());
|
||||||
// to avoid page flicker when the style is updated
|
|
||||||
// instead of removing it immediately we rename its ID and queue it
|
|
||||||
// to be deleted in applyStyles after a new version is fetched and applied
|
|
||||||
const deadID = id + '-ghost';
|
|
||||||
el.id = ID_PREFIX + deadID;
|
|
||||||
// in case something went wrong and new style was never applied
|
|
||||||
retiredStyleTimers.set(deadID, setTimeout(removeStyle, 1000, {id: deadID}));
|
|
||||||
} else {
|
|
||||||
docRootObserver.evade(() => el.remove());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
styleElements.delete(ID_PREFIX + id);
|
|
||||||
disabledElements.delete(id);
|
disabledElements.delete(id);
|
||||||
retiredStyleTimers.delete(id);
|
if (styleElements.delete(id)) {
|
||||||
|
updateCount();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function applyStyles(styles) {
|
function applyStyles(sections, done) {
|
||||||
if (!styles) {
|
if (!Object.keys(sections).length) {
|
||||||
// Chrome is starting up
|
if (done) {
|
||||||
requestStyles();
|
done();
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -234,72 +307,40 @@
|
||||||
new MutationObserver((mutations, observer) => {
|
new MutationObserver((mutations, observer) => {
|
||||||
if (document.documentElement) {
|
if (document.documentElement) {
|
||||||
observer.disconnect();
|
observer.disconnect();
|
||||||
applyStyles(styles);
|
applyStyles(sections, done);
|
||||||
}
|
}
|
||||||
}).observe(document, {childList: true});
|
}).observe(document, {childList: true});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ('disableAll' in styles) {
|
if (docRootObserver) {
|
||||||
doDisableAll(styles.disableAll);
|
docRootObserver.stop();
|
||||||
|
} else {
|
||||||
|
initDocRootObserver();
|
||||||
}
|
}
|
||||||
if ('exposeIframes' in styles) {
|
const pending = [];
|
||||||
doExposeIframes(styles.exposeIframes);
|
for (const section of Object.values(sections)) {
|
||||||
}
|
pending.push(applySections(section.id, section.code.join('')));
|
||||||
|
|
||||||
const gotNewStyles = styles.length || styles.needTransitionPatch;
|
|
||||||
if (gotNewStyles) {
|
|
||||||
if (docRootObserver) {
|
|
||||||
docRootObserver.stop();
|
|
||||||
} else {
|
|
||||||
initDocRootObserver();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (styles.needTransitionPatch) {
|
|
||||||
applyTransitionPatch();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (gotNewStyles) {
|
|
||||||
for (const id in styles) {
|
|
||||||
const sections = styles[id];
|
|
||||||
if (!Array.isArray(sections)) continue;
|
|
||||||
applySections(id, sections.map(({code}) => code).join('\n'));
|
|
||||||
}
|
|
||||||
docRootObserver.firstStart();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (FF_BUG461 && (gotNewStyles || styles.needTransitionPatch)) {
|
|
||||||
setContentsInPageContext();
|
|
||||||
}
|
}
|
||||||
|
docRootObserver.firstStart();
|
||||||
|
|
||||||
if (!isOwnPage && !docRewriteObserver && styleElements.size) {
|
if (!isOwnPage && !docRewriteObserver && styleElements.size) {
|
||||||
initDocRewriteObserver();
|
initDocRewriteObserver();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (retiredStyleTimers.size) {
|
updateExposeIframes();
|
||||||
setTimeout(() => {
|
updateCount();
|
||||||
for (const [id, timer] of retiredStyleTimers.entries()) {
|
if (done) {
|
||||||
removeStyle({id});
|
Promise.all(pending).then(done);
|
||||||
clearTimeout(timer);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function applySections(styleId, code) {
|
function applySections(id, code) {
|
||||||
const id = ID_PREFIX + styleId;
|
let el = styleElements.get(id) || document.getElementById(ID_PREFIX + id);
|
||||||
let el = styleElements.get(id) || document.getElementById(id);
|
if (el && CHROME < 3321) {
|
||||||
if (el && el.textContent !== code) {
|
// workaround for Chrome devtools bug fixed in v65
|
||||||
if (CHROME < 3321) {
|
el.remove();
|
||||||
// workaround for Chrome devtools bug fixed in v65
|
el = null;
|
||||||
el.remove();
|
|
||||||
el = null;
|
|
||||||
} else if (FF_BUG461) {
|
|
||||||
pageContextQueue.push({id: el.id, el, code});
|
|
||||||
} else {
|
|
||||||
el.textContent = code;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if (!el) {
|
if (!el) {
|
||||||
if (document.documentElement instanceof SVGSVGElement) {
|
if (document.documentElement instanceof SVGSVGElement) {
|
||||||
|
@ -312,48 +353,19 @@
|
||||||
// HTML document style; also works on HTML-embedded SVG
|
// HTML document style; also works on HTML-embedded SVG
|
||||||
el = document.createElement('style');
|
el = document.createElement('style');
|
||||||
}
|
}
|
||||||
el.id = id;
|
el.id = ID_PREFIX + id;
|
||||||
el.type = 'text/css';
|
el.type = 'text/css';
|
||||||
// SVG className is not a string, but an instance of SVGAnimatedString
|
// SVG className is not a string, but an instance of SVGAnimatedString
|
||||||
el.classList.add('stylus');
|
el.classList.add('stylus');
|
||||||
if (FF_BUG461) {
|
|
||||||
pageContextQueue.push({id: el.id, el, code});
|
|
||||||
} else {
|
|
||||||
el.textContent = code;
|
|
||||||
}
|
|
||||||
addStyleElement(el);
|
addStyleElement(el);
|
||||||
}
|
}
|
||||||
|
let settingStyle;
|
||||||
|
if (el.textContent !== code) {
|
||||||
|
settingStyle = setStyleContent(el, code);
|
||||||
|
}
|
||||||
styleElements.set(id, el);
|
styleElements.set(id, el);
|
||||||
disabledElements.delete(Number(styleId));
|
disabledElements.delete(id);
|
||||||
return el;
|
return Promise.resolve(settingStyle);
|
||||||
}
|
|
||||||
|
|
||||||
function setContentsInPageContext() {
|
|
||||||
try {
|
|
||||||
(document.head || ROOT).appendChild(document.createElement('script')).text = `(${queue => {
|
|
||||||
document.currentScript.remove();
|
|
||||||
for (const {id, code} of queue) {
|
|
||||||
const el = document.getElementById(id) ||
|
|
||||||
document.querySelector('style.stylus[id="' + id + '"]');
|
|
||||||
if (!el) continue;
|
|
||||||
const {disabled} = el.sheet;
|
|
||||||
el.textContent = code;
|
|
||||||
el.sheet.disabled = disabled;
|
|
||||||
}
|
|
||||||
}})(${JSON.stringify(pageContextQueue)})`;
|
|
||||||
} catch (e) {}
|
|
||||||
let failedSome;
|
|
||||||
for (const {el, code} of pageContextQueue) {
|
|
||||||
if (el.textContent !== code) {
|
|
||||||
el.textContent = code;
|
|
||||||
failedSome = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (failedSome) {
|
|
||||||
console.debug('Could not set code of some styles in page context, ' +
|
|
||||||
'see https://github.com/openstyles/stylus/issues/461');
|
|
||||||
}
|
|
||||||
pageContextQueue.length = 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function addStyleElement(newElement) {
|
function addStyleElement(newElement) {
|
||||||
|
@ -371,34 +383,32 @@
|
||||||
if (next === newElement.nextElementSibling) {
|
if (next === newElement.nextElementSibling) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
docRootObserver.evade(() => {
|
const insert = () => {
|
||||||
ROOT.insertBefore(newElement, next || null);
|
ROOT.insertBefore(newElement, next || null);
|
||||||
if (disableAll) {
|
if (disableAll) {
|
||||||
newElement.disabled = true;
|
newElement.disabled = true;
|
||||||
}
|
}
|
||||||
});
|
};
|
||||||
|
if (docRootObserver) {
|
||||||
|
docRootObserver.evade(insert);
|
||||||
|
} else {
|
||||||
|
insert();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function replaceAll(newStyles) {
|
function replaceAll(newStyles) {
|
||||||
if ('disableAll' in newStyles &&
|
|
||||||
disableAll === newStyles.disableAll &&
|
|
||||||
styleElements.size === countStylesInHash(newStyles) &&
|
|
||||||
[...styleElements.values()].every(el =>
|
|
||||||
el.disabled === disableAll &&
|
|
||||||
el.parentNode === ROOT &&
|
|
||||||
el.textContent === (newStyles[getStyleId(el)] || []).map(({code}) => code).join('\n'))) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const oldStyles = Array.prototype.slice.call(
|
const oldStyles = Array.prototype.slice.call(
|
||||||
document.querySelectorAll(`style.stylus[id^="${ID_PREFIX}"]`));
|
document.querySelectorAll(`style.stylus[id^="${ID_PREFIX}"]`));
|
||||||
oldStyles.forEach(el => (el.id += '-ghost'));
|
oldStyles.forEach(el => (el.id += '-ghost'));
|
||||||
styleElements.clear();
|
styleElements.clear();
|
||||||
disabledElements.clear();
|
disabledElements.clear();
|
||||||
[...retiredStyleTimers.values()].forEach(clearTimeout);
|
|
||||||
retiredStyleTimers.clear();
|
|
||||||
applyStyles(newStyles);
|
applyStyles(newStyles);
|
||||||
docRootObserver.evade(() =>
|
const removeOld = () => oldStyles.forEach(el => el.remove());
|
||||||
oldStyles.forEach(el => el.remove()));
|
if (docRewriteObserver) {
|
||||||
|
docRootObserver.evade(removeOld);
|
||||||
|
} else {
|
||||||
|
removeOld();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function applyTransitionPatch() {
|
function applyTransitionPatch() {
|
||||||
|
@ -408,29 +418,27 @@
|
||||||
const docId = document.documentElement.id ? '#' + document.documentElement.id : '';
|
const docId = document.documentElement.id ? '#' + document.documentElement.id : '';
|
||||||
document.documentElement.classList.add(className);
|
document.documentElement.classList.add(className);
|
||||||
applySections(0, `
|
applySections(0, `
|
||||||
${docId}.${className}:root * {
|
${docId}.${CSS.escape(className)}:root * {
|
||||||
transition: none !important;
|
transition: none !important;
|
||||||
}
|
}
|
||||||
`);
|
`)
|
||||||
setTimeout(() => {
|
.then(() => {
|
||||||
removeStyle({id: 0});
|
// repaint
|
||||||
document.documentElement.classList.remove(className);
|
// eslint-disable-next-line no-unused-expressions
|
||||||
});
|
document.documentElement.offsetWidth;
|
||||||
|
removeStyle({id: 0});
|
||||||
|
document.documentElement.classList.remove(className);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function getStyleId(el) {
|
function getStyleId(el) {
|
||||||
return parseInt(el.id.substr(ID_PREFIX.length));
|
return parseInt(el.id.substr(ID_PREFIX.length));
|
||||||
}
|
}
|
||||||
|
|
||||||
function countStylesInHash(styleHash) {
|
function orphanCheck(e) {
|
||||||
let num = 0;
|
if (e && e.detail.method !== 'orphan') {
|
||||||
for (const k in styleHash) {
|
return;
|
||||||
num += !isNaN(parseInt(k)) ? 1 : 0;
|
|
||||||
}
|
}
|
||||||
return num;
|
|
||||||
}
|
|
||||||
|
|
||||||
function orphanCheck() {
|
|
||||||
if (chrome.i18n && chrome.i18n.getUILanguage()) {
|
if (chrome.i18n && chrome.i18n.getUILanguage()) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
@ -439,7 +447,7 @@
|
||||||
[docRewriteObserver, docRootObserver].forEach(ob => ob && ob.disconnect());
|
[docRewriteObserver, docRootObserver].forEach(ob => ob && ob.disconnect());
|
||||||
window.removeEventListener(chrome.runtime.id, orphanCheck, true);
|
window.removeEventListener(chrome.runtime.id, orphanCheck, true);
|
||||||
try {
|
try {
|
||||||
chrome.runtime.onMessage.removeListener(applyOnMessage);
|
msg.off(applyOnMessage);
|
||||||
} catch (e) {}
|
} catch (e) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
/* global API */
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
(() => {
|
(() => {
|
||||||
|
@ -33,11 +34,10 @@
|
||||||
&& event.data.type === 'ouc-is-installed'
|
&& event.data.type === 'ouc-is-installed'
|
||||||
&& allowedOrigins.includes(event.origin)
|
&& allowedOrigins.includes(event.origin)
|
||||||
) {
|
) {
|
||||||
chrome.runtime.sendMessage({
|
API.findUsercss({
|
||||||
method: 'findUsercss',
|
|
||||||
name: event.data.name,
|
name: event.data.name,
|
||||||
namespace: event.data.namespace
|
namespace: event.data.namespace
|
||||||
}, style => {
|
}).then(style => {
|
||||||
const data = {event};
|
const data = {event};
|
||||||
const callbackObject = {
|
const callbackObject = {
|
||||||
installed: Boolean(style),
|
installed: Boolean(style),
|
||||||
|
@ -129,12 +129,10 @@
|
||||||
&& event.data.type === 'ouc-install-usercss'
|
&& event.data.type === 'ouc-install-usercss'
|
||||||
&& allowedOrigins.includes(event.origin)
|
&& allowedOrigins.includes(event.origin)
|
||||||
) {
|
) {
|
||||||
chrome.runtime.sendMessage({
|
API.installUsercss({
|
||||||
method: 'saveUsercss',
|
|
||||||
reason: 'install',
|
|
||||||
name: event.data.title,
|
name: event.data.title,
|
||||||
sourceCode: event.data.code,
|
sourceCode: event.data.code,
|
||||||
}, style => {
|
}).then(style => {
|
||||||
sendInstallCallback({
|
sendInstallCallback({
|
||||||
enabled: style.enabled,
|
enabled: style.enabled,
|
||||||
key: event.data.key
|
key: event.data.key
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
/* global API */
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
(() => {
|
(() => {
|
||||||
|
@ -16,8 +17,8 @@
|
||||||
let sourceCode, port, timer;
|
let sourceCode, port, timer;
|
||||||
|
|
||||||
chrome.runtime.onConnect.addListener(onConnected);
|
chrome.runtime.onConnect.addListener(onConnected);
|
||||||
chrome.runtime.sendMessage({method: 'installUsercss', url}, r =>
|
API.openUsercssInstallPage({url})
|
||||||
r && r.__ERROR__ && alert(r.__ERROR__));
|
.catch(err => alert(err));
|
||||||
|
|
||||||
function onConnected(newPort) {
|
function onConnected(newPort) {
|
||||||
port = newPort;
|
port = newPort;
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
/* global cloneInto */
|
/* global cloneInto msg API */
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
(() => {
|
(() => {
|
||||||
|
@ -8,7 +8,7 @@
|
||||||
document.addEventListener('stylishInstallChrome', onClick);
|
document.addEventListener('stylishInstallChrome', onClick);
|
||||||
document.addEventListener('stylishUpdateChrome', onClick);
|
document.addEventListener('stylishUpdateChrome', onClick);
|
||||||
|
|
||||||
chrome.runtime.onMessage.addListener(onMessage);
|
msg.on(onMessage);
|
||||||
|
|
||||||
onDOMready().then(() => {
|
onDOMready().then(() => {
|
||||||
window.postMessage({
|
window.postMessage({
|
||||||
|
@ -30,10 +30,9 @@
|
||||||
gotBody = true;
|
gotBody = true;
|
||||||
// TODO: remove the following statement when USO pagination title is fixed
|
// TODO: remove the following statement when USO pagination title is fixed
|
||||||
document.title = document.title.replace(/^(\d+)&\w+=/, '#$1: ');
|
document.title = document.title.replace(/^(\d+)&\w+=/, '#$1: ');
|
||||||
chrome.runtime.sendMessage({
|
API.findStyle({
|
||||||
method: 'getStyles',
|
|
||||||
md5Url: getMeta('stylish-md5-url') || location.href
|
md5Url: getMeta('stylish-md5-url') || location.href
|
||||||
}, checkUpdatability);
|
}).then(checkUpdatability);
|
||||||
}
|
}
|
||||||
if (document.getElementById('install_button')) {
|
if (document.getElementById('install_button')) {
|
||||||
onDOMready().then(() => {
|
onDOMready().then(() => {
|
||||||
|
@ -44,16 +43,14 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function onMessage(msg, sender, sendResponse) {
|
function onMessage(msg) {
|
||||||
switch (msg.method) {
|
switch (msg.method) {
|
||||||
case 'ping':
|
case 'ping':
|
||||||
// orphaned content script check
|
// orphaned content script check
|
||||||
sendResponse(true);
|
return true;
|
||||||
break;
|
|
||||||
case 'openSettings':
|
case 'openSettings':
|
||||||
openSettings();
|
openSettings();
|
||||||
sendResponse(true);
|
return true;
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -69,7 +66,7 @@
|
||||||
return jsonUrl + (paramsMissing ? textUrl.replace(/^[^?]+/, '') : '');
|
return jsonUrl + (paramsMissing ? textUrl.replace(/^[^?]+/, '') : '');
|
||||||
}
|
}
|
||||||
|
|
||||||
function checkUpdatability([installedStyle]) {
|
function checkUpdatability(installedStyle) {
|
||||||
// TODO: remove the following statement when USO is fixed
|
// TODO: remove the following statement when USO is fixed
|
||||||
document.dispatchEvent(new CustomEvent('stylusFixBuggyUSOsettings', {
|
document.dispatchEvent(new CustomEvent('stylusFixBuggyUSOsettings', {
|
||||||
detail: installedStyle && installedStyle.updateUrl,
|
detail: installedStyle && installedStyle.updateUrl,
|
||||||
|
@ -148,10 +145,9 @@
|
||||||
|
|
||||||
function onUpdate() {
|
function onUpdate() {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
chrome.runtime.sendMessage({
|
API.findStyle({
|
||||||
method: 'getStyles',
|
md5Url: getMeta('stylish-md5-url') || location.href
|
||||||
md5Url: getMeta('stylish-md5-url') || location.href,
|
}, true).then(style => {
|
||||||
}, ([style]) => {
|
|
||||||
saveStyleCode('styleUpdate', style.name, {id: style.id})
|
saveStyleCode('styleUpdate', style.name, {id: style.id})
|
||||||
.then(resolve, reject);
|
.then(resolve, reject);
|
||||||
});
|
});
|
||||||
|
@ -160,36 +156,27 @@
|
||||||
|
|
||||||
|
|
||||||
function saveStyleCode(message, name, addProps) {
|
function saveStyleCode(message, name, addProps) {
|
||||||
return new Promise((resolve, reject) => {
|
const isNew = message === 'styleInstall';
|
||||||
const isNew = message === 'styleInstall';
|
const needsConfirmation = isNew || !saveStyleCode.confirmed;
|
||||||
const needsConfirmation = isNew || !saveStyleCode.confirmed;
|
if (needsConfirmation && !confirm(chrome.i18n.getMessage(message, [name]))) {
|
||||||
if (needsConfirmation && !confirm(chrome.i18n.getMessage(message, [name]))) {
|
return Promise.reject();
|
||||||
reject();
|
}
|
||||||
|
saveStyleCode.confirmed = true;
|
||||||
|
enableUpdateButton(false);
|
||||||
|
return getStyleJson().then(json => {
|
||||||
|
if (!json) {
|
||||||
|
prompt(chrome.i18n.getMessage('styleInstallFailed', ''),
|
||||||
|
'https://github.com/openstyles/stylus/issues/195');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
saveStyleCode.confirmed = true;
|
return API.installStyle(Object.assign(json, addProps))
|
||||||
enableUpdateButton(false);
|
.then(style => {
|
||||||
getStyleJson().then(json => {
|
if (!isNew && style.updateUrl.includes('?')) {
|
||||||
if (!json) {
|
enableUpdateButton(true);
|
||||||
prompt(chrome.i18n.getMessage('styleInstallFailed', ''),
|
} else {
|
||||||
'https://github.com/openstyles/stylus/issues/195');
|
sendEvent({type: 'styleInstalledChrome'});
|
||||||
return;
|
|
||||||
}
|
|
||||||
chrome.runtime.sendMessage(
|
|
||||||
Object.assign(json, addProps, {
|
|
||||||
method: 'saveStyle',
|
|
||||||
reason: isNew ? 'install' : 'update',
|
|
||||||
}),
|
|
||||||
style => {
|
|
||||||
if (!isNew && style.updateUrl.includes('?')) {
|
|
||||||
enableUpdateButton(true);
|
|
||||||
} else {
|
|
||||||
sendEvent({type: 'styleInstalledChrome'});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
);
|
});
|
||||||
resolve();
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
function enableUpdateButton(state) {
|
function enableUpdateButton(state) {
|
||||||
|
@ -216,26 +203,19 @@
|
||||||
|
|
||||||
|
|
||||||
function getResource(url, options) {
|
function getResource(url, options) {
|
||||||
return new Promise(resolve => {
|
if (url.startsWith('#')) {
|
||||||
if (url.startsWith('#')) {
|
return Promise.resolve(document.getElementById(url.slice(1)).textContent);
|
||||||
resolve(document.getElementById(url.slice(1)).textContent);
|
}
|
||||||
} else {
|
return API.download(Object.assign({
|
||||||
chrome.runtime.sendMessage(Object.assign({
|
url,
|
||||||
url,
|
timeout: 60e3,
|
||||||
method: 'download',
|
// USO can't handle POST requests for style json
|
||||||
timeout: 60e3,
|
body: null,
|
||||||
// USO can't handle POST requests for style json
|
}, options))
|
||||||
body: null,
|
.catch(error => {
|
||||||
}, options), result => {
|
alert('Error' + (error ? '\n' + error : ''));
|
||||||
const error = result && result.__ERROR__;
|
throw error;
|
||||||
if (error) {
|
});
|
||||||
alert('Error' + (error ? '\n' + error : ''));
|
|
||||||
} else {
|
|
||||||
resolve(result);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// USO providing md5Url as "https://update.update.userstyles.org/#####.md5"
|
// USO providing md5Url as "https://update.update.userstyles.org/#####.md5"
|
||||||
|
@ -257,12 +237,12 @@
|
||||||
if (codeElement && !codeElement.textContent.trim()) {
|
if (codeElement && !codeElement.textContent.trim()) {
|
||||||
return style;
|
return style;
|
||||||
}
|
}
|
||||||
return getResource(getMeta('stylish-update-url')).then(code => new Promise(resolve => {
|
return getResource(getMeta('stylish-update-url'))
|
||||||
chrome.runtime.sendMessage({method: 'parseCss', code}, ({sections}) => {
|
.then(code => API.parseCss({code}))
|
||||||
style.sections = sections;
|
.then(result => {
|
||||||
resolve(style);
|
style.sections = result.sections;
|
||||||
|
return style;
|
||||||
});
|
});
|
||||||
}));
|
|
||||||
})
|
})
|
||||||
.then(tryFixMd5)
|
.then(tryFixMd5)
|
||||||
.catch(() => null);
|
.catch(() => null);
|
||||||
|
@ -349,7 +329,7 @@
|
||||||
document.removeEventListener('stylishInstallChrome', onClick);
|
document.removeEventListener('stylishInstallChrome', onClick);
|
||||||
document.removeEventListener('stylishUpdateChrome', onClick);
|
document.removeEventListener('stylishUpdateChrome', onClick);
|
||||||
try {
|
try {
|
||||||
chrome.runtime.onMessage.removeListener(onMessage);
|
msg.off(onMessage);
|
||||||
} catch (e) {}
|
} catch (e) {}
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
|
|
77
edit.html
77
edit.html
|
@ -18,26 +18,6 @@
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<script src="js/dom.js"></script>
|
|
||||||
<script src="js/messaging.js"></script>
|
|
||||||
<script src="js/prefs.js"></script>
|
|
||||||
<script src="js/localization.js"></script>
|
|
||||||
<script src="js/script-loader.js"></script>
|
|
||||||
<script src="js/storage-util.js"></script>
|
|
||||||
<script src="content/apply.js"></script>
|
|
||||||
<script src="edit/util.js"></script>
|
|
||||||
<script src="edit/regexp-tester.js"></script>
|
|
||||||
<script src="edit/applies-to-line-widget.js"></script>
|
|
||||||
<script src="edit/source-editor.js"></script>
|
|
||||||
<script src="edit/colorpicker-helper.js"></script>
|
|
||||||
<script src="edit/beautify.js"></script>
|
|
||||||
<script src="edit/sections.js"></script>
|
|
||||||
<script src="edit/show-keymap-help.js"></script>
|
|
||||||
<script src="edit/codemirror-editing-hooks.js"></script>
|
|
||||||
<script src="edit/edit.js"></script>
|
|
||||||
|
|
||||||
<script src="msgbox/msgbox.js" async></script>
|
|
||||||
|
|
||||||
<link href="vendor/codemirror/lib/codemirror.css" rel="stylesheet">
|
<link href="vendor/codemirror/lib/codemirror.css" rel="stylesheet">
|
||||||
<script src="vendor/codemirror/lib/codemirror.js"></script>
|
<script src="vendor/codemirror/lib/codemirror.js"></script>
|
||||||
|
|
||||||
|
@ -46,6 +26,8 @@
|
||||||
<link href="vendor/codemirror/addon/dialog/dialog.css" rel="stylesheet">
|
<link href="vendor/codemirror/addon/dialog/dialog.css" rel="stylesheet">
|
||||||
<script src="vendor/codemirror/addon/dialog/dialog.js"></script>
|
<script src="vendor/codemirror/addon/dialog/dialog.js"></script>
|
||||||
|
|
||||||
|
<script src="vendor/codemirror/addon/edit/closebrackets.js"></script>
|
||||||
|
|
||||||
<link href="vendor/codemirror/addon/search/matchesonscrollbar.css" rel="stylesheet">
|
<link href="vendor/codemirror/addon/search/matchesonscrollbar.css" rel="stylesheet">
|
||||||
<script src="vendor/codemirror/addon/search/matchesonscrollbar.js"></script>
|
<script src="vendor/codemirror/addon/search/matchesonscrollbar.js"></script>
|
||||||
<script src="vendor/codemirror/addon/scroll/annotatescrollbar.js"></script>
|
<script src="vendor/codemirror/addon/scroll/annotatescrollbar.js"></script>
|
||||||
|
@ -80,6 +62,18 @@
|
||||||
<script src="vendor-overwrites/colorpicker/colorpicker.js"></script>
|
<script src="vendor-overwrites/colorpicker/colorpicker.js"></script>
|
||||||
<script src="vendor-overwrites/colorpicker/colorview.js"></script>
|
<script src="vendor-overwrites/colorpicker/colorview.js"></script>
|
||||||
|
|
||||||
|
<script src="js/promisify.js"></script>
|
||||||
|
<script src="js/dom.js"></script>
|
||||||
|
<script src="js/messaging.js"></script>
|
||||||
|
<script src="js/prefs.js"></script>
|
||||||
|
<script src="js/localization.js"></script>
|
||||||
|
<script src="js/script-loader.js"></script>
|
||||||
|
<script src="js/storage-util.js"></script>
|
||||||
|
<script src="js/msg.js"></script>
|
||||||
|
<script src="js/worker-util.js"></script>
|
||||||
|
|
||||||
|
<script src="content/apply.js"></script>
|
||||||
|
|
||||||
<link href="edit/global-search.css" rel="stylesheet">
|
<link href="edit/global-search.css" rel="stylesheet">
|
||||||
<script src="edit/global-search.js"></script>
|
<script src="edit/global-search.js"></script>
|
||||||
|
|
||||||
|
@ -88,6 +82,25 @@
|
||||||
<link href="edit/codemirror-default.css" rel="stylesheet">
|
<link href="edit/codemirror-default.css" rel="stylesheet">
|
||||||
<script src="edit/codemirror-default.js"></script>
|
<script src="edit/codemirror-default.js"></script>
|
||||||
|
|
||||||
|
<script src="edit/util.js"></script>
|
||||||
|
<script src="edit/regexp-tester.js"></script>
|
||||||
|
<script src="edit/live-preview.js"></script>
|
||||||
|
<script src="edit/applies-to-line-widget.js"></script>
|
||||||
|
<script src="edit/reroute-hotkeys.js"></script>
|
||||||
|
<script src="edit/codemirror-factory.js"></script>
|
||||||
|
<script src="edit/colorpicker-helper.js"></script>
|
||||||
|
<script src="edit/beautify.js"></script>
|
||||||
|
<script src="edit/show-keymap-help.js"></script>
|
||||||
|
<script src="edit/refresh-on-view.js"></script>
|
||||||
|
|
||||||
|
<script src="edit/source-editor.js"></script>
|
||||||
|
<script src="edit/sections-editor-section.js"></script>
|
||||||
|
<script src="edit/sections-editor.js"></script>
|
||||||
|
|
||||||
|
<script src="edit/edit.js"></script>
|
||||||
|
|
||||||
|
<script src="msgbox/msgbox.js" async></script>
|
||||||
|
|
||||||
<script src="edit/linter.js"></script>
|
<script src="edit/linter.js"></script>
|
||||||
<script src="edit/linter-defaults.js"></script>
|
<script src="edit/linter-defaults.js"></script>
|
||||||
<script src="edit/linter-engines.js"></script>
|
<script src="edit/linter-engines.js"></script>
|
||||||
|
@ -96,8 +109,6 @@
|
||||||
<script src="edit/linter-report.js"></script>
|
<script src="edit/linter-report.js"></script>
|
||||||
<script src="edit/linter-config-dialog.js"></script>
|
<script src="edit/linter-config-dialog.js"></script>
|
||||||
|
|
||||||
<script src="edit/editor-worker.js"></script>
|
|
||||||
|
|
||||||
<link id="cm-theme" rel="stylesheet">
|
<link id="cm-theme" rel="stylesheet">
|
||||||
|
|
||||||
<template data-id="appliesTo">
|
<template data-id="appliesTo">
|
||||||
|
@ -133,8 +144,11 @@
|
||||||
|
|
||||||
<template data-id="section">
|
<template data-id="section">
|
||||||
<div class="section">
|
<div class="section">
|
||||||
|
<!-- not using DIV to make our CSS work for #sections > div:only-of-type .remove-section -->
|
||||||
|
<p class="deleted-section">
|
||||||
|
<button class="restore-section" i18n-text="sectionRestore"></button>
|
||||||
|
</p>
|
||||||
<label i18n-text="sectionCode" class="code-label"></label>
|
<label i18n-text="sectionCode" class="code-label"></label>
|
||||||
<br>
|
|
||||||
<div class="applies-to">
|
<div class="applies-to">
|
||||||
<label i18n-text="appliesLabel">
|
<label i18n-text="appliesLabel">
|
||||||
<a href="#" class="svg-inline-wrapper applies-to-help" tabindex="0">
|
<a href="#" class="svg-inline-wrapper applies-to-help" tabindex="0">
|
||||||
|
@ -155,13 +169,6 @@
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<!-- not using DIV to make our CSS work for #sections > div:only-of-type .remove-section -->
|
|
||||||
<template data-id="deletedSection">
|
|
||||||
<p class="deleted-section">
|
|
||||||
<button class="restore-section" i18n-text="sectionRestore"></button>
|
|
||||||
</p>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<template data-id="searchReplaceDialog">
|
<template data-id="searchReplaceDialog">
|
||||||
<div id="search-replace-dialog">
|
<div id="search-replace-dialog">
|
||||||
<div data-type="main">
|
<div data-type="main">
|
||||||
|
@ -277,7 +284,7 @@
|
||||||
<h1 id="heading"> </h1> <!-- nbsp allocates the actual height which prevents page shift -->
|
<h1 id="heading"> </h1> <!-- nbsp allocates the actual height which prevents page shift -->
|
||||||
<section id="basic-info">
|
<section id="basic-info">
|
||||||
<div id="basic-info-name">
|
<div id="basic-info-name">
|
||||||
<input id="name" class="style-contributor" spellcheck="false">
|
<input id="name" class="style-contributor" spellcheck="false" required>
|
||||||
<a id="url" target="_blank"><svg class="svg-icon"><use xlink:href="#svg-icon-external-link"/></svg></a>
|
<a id="url" target="_blank"><svg class="svg-icon"><use xlink:href="#svg-icon-external-link"/></svg></a>
|
||||||
</div>
|
</div>
|
||||||
<div id="basic-info-enabled">
|
<div id="basic-info-enabled">
|
||||||
|
@ -437,11 +444,15 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<section id="sections">
|
<section id="sections">
|
||||||
<h2><span id="sections-heading" i18n-text="styleSectionsTitle"></span>
|
<!--
|
||||||
|
It seems that we don't use these anymore
|
||||||
|
https://github.com/openstyles/stylus/blob/5cbe8a8d780a6eb9fce11d5846e92bf244c3a3f3/edit/sections.js#L18
|
||||||
|
-->
|
||||||
|
<!-- <h2><span id="sections-heading" i18n-text="styleSectionsTitle"></span>
|
||||||
<a id="sections-help" href="#" class="svg-inline-wrapper" tabindex="0">
|
<a id="sections-help" href="#" class="svg-inline-wrapper" tabindex="0">
|
||||||
<svg class="svg-icon info"><use xlink:href="#svg-icon-help"/></svg>
|
<svg class="svg-icon info"><use xlink:href="#svg-icon-help"/></svg>
|
||||||
</a>
|
</a>
|
||||||
</h2>
|
</h2> -->
|
||||||
</section>
|
</section>
|
||||||
<div id="help-popup">
|
<div id="help-popup">
|
||||||
<div class="title"></div><svg id="sections-help" class="svg-icon dismiss"><use xlink:href="#svg-icon-close"/></svg>
|
<div class="title"></div><svg id="sections-help" class="svg-icon dismiss"><use xlink:href="#svg-icon-close"/></svg>
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
/* global regExpTester debounce messageBox CodeMirror template colorMimicry */
|
/* global regExpTester debounce messageBox CodeMirror template colorMimicry msg
|
||||||
|
$ $create t prefs tryCatch */
|
||||||
|
/* exported createAppliesToLineWidget */
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
function createAppliesToLineWidget(cm) {
|
function createAppliesToLineWidget(cm) {
|
||||||
|
@ -131,7 +133,7 @@ function createAppliesToLineWidget(cm) {
|
||||||
cm.on('change', onChange);
|
cm.on('change', onChange);
|
||||||
cm.on('optionChange', onOptionChange);
|
cm.on('optionChange', onOptionChange);
|
||||||
|
|
||||||
chrome.runtime.onMessage.addListener(onRuntimeMessage);
|
msg.onExtension(onRuntimeMessage);
|
||||||
|
|
||||||
requestAnimationFrame(updateWidgetStyle);
|
requestAnimationFrame(updateWidgetStyle);
|
||||||
update();
|
update();
|
||||||
|
@ -144,7 +146,7 @@ function createAppliesToLineWidget(cm) {
|
||||||
widgets.length = 0;
|
widgets.length = 0;
|
||||||
cm.off('change', onChange);
|
cm.off('change', onChange);
|
||||||
cm.off('optionChange', onOptionChange);
|
cm.off('optionChange', onOptionChange);
|
||||||
chrome.runtime.onMessage.removeListener(onRuntimeMessage);
|
msg.off(onRuntimeMessage);
|
||||||
actualStyle.remove();
|
actualStyle.remove();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,10 +1,8 @@
|
||||||
/*
|
/* global loadScript css_beautify showHelp prefs t $ $create */
|
||||||
global CodeMirror loadScript css_beautify
|
/* exported beautify */
|
||||||
global editors getSectionForChild showHelp
|
|
||||||
*/
|
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
function beautify(event) {
|
function beautify(scope) {
|
||||||
loadScript('/vendor-overwrites/beautify/beautify-css-mod.js')
|
loadScript('/vendor-overwrites/beautify/beautify-css-mod.js')
|
||||||
.then(() => {
|
.then(() => {
|
||||||
if (!window.css_beautify && window.exports) {
|
if (!window.css_beautify && window.exports) {
|
||||||
|
@ -22,9 +20,6 @@ function beautify(event) {
|
||||||
options.indent_size = tabs ? 1 : prefs.get('editor.tabSize');
|
options.indent_size = tabs ? 1 : prefs.get('editor.tabSize');
|
||||||
options.indent_char = tabs ? '\t' : ' ';
|
options.indent_char = tabs ? '\t' : ' ';
|
||||||
|
|
||||||
const section = getSectionForChild(event.target);
|
|
||||||
const scope = section ? [section.CodeMirror] : editors;
|
|
||||||
|
|
||||||
showHelp(t('styleBeautify'),
|
showHelp(t('styleBeautify'),
|
||||||
$create([
|
$create([
|
||||||
$create('.beautify-options', [
|
$create('.beautify-options', [
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
/* global CodeMirror prefs loadScript editor editors */
|
/* global CodeMirror prefs loadScript editor $ template */
|
||||||
|
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
|
@ -9,6 +9,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
const defaults = {
|
const defaults = {
|
||||||
|
autoCloseBrackets: prefs.get('editor.autoCloseBrackets'),
|
||||||
mode: 'css',
|
mode: 'css',
|
||||||
lineNumbers: true,
|
lineNumbers: true,
|
||||||
lineWrapping: prefs.get('editor.lineWrapping'),
|
lineWrapping: prefs.get('editor.lineWrapping'),
|
||||||
|
@ -19,11 +20,10 @@
|
||||||
...(prefs.get('editor.linter') ? ['CodeMirror-lint-markers'] : []),
|
...(prefs.get('editor.linter') ? ['CodeMirror-lint-markers'] : []),
|
||||||
],
|
],
|
||||||
matchBrackets: true,
|
matchBrackets: true,
|
||||||
highlightSelectionMatches: {showToken: /[#.\-\w]/, annotateScrollbar: true},
|
|
||||||
hintOptions: {},
|
hintOptions: {},
|
||||||
lintReportDelay: prefs.get('editor.lintReportDelay'),
|
lintReportDelay: prefs.get('editor.lintReportDelay'),
|
||||||
styleActiveLine: true,
|
styleActiveLine: true,
|
||||||
theme: 'default',
|
theme: prefs.get('editor.theme'),
|
||||||
keyMap: prefs.get('editor.keyMap'),
|
keyMap: prefs.get('editor.keyMap'),
|
||||||
extraKeys: Object.assign(CodeMirror.defaults.extraKeys || {}, {
|
extraKeys: Object.assign(CodeMirror.defaults.extraKeys || {}, {
|
||||||
// independent of current keyMap
|
// independent of current keyMap
|
||||||
|
@ -228,69 +228,48 @@
|
||||||
return isBlank;
|
return isBlank;
|
||||||
});
|
});
|
||||||
|
|
||||||
// doubleclick option
|
// editor commands
|
||||||
if (typeof editors !== 'undefined') {
|
for (const name of ['save', 'toggleStyle', 'nextEditor', 'prevEditor']) {
|
||||||
const fn = (cm, repeat) =>
|
CodeMirror.commands[name] = () => editor[name]();
|
||||||
repeat === 'double' ?
|
|
||||||
{unit: selectTokenOnDoubleclick} :
|
|
||||||
{};
|
|
||||||
const configure = (_, enabled) => {
|
|
||||||
editors.forEach(cm => cm.setOption('configureMouse', enabled ? fn : null));
|
|
||||||
CodeMirror.defaults.configureMouse = enabled ? fn : null;
|
|
||||||
};
|
|
||||||
configure(null, prefs.get('editor.selectByTokens'));
|
|
||||||
prefs.subscribe(['editor.selectByTokens'], configure);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function selectTokenOnDoubleclick(cm, pos) {
|
// CodeMirror convenience commands
|
||||||
let {ch} = pos;
|
Object.assign(CodeMirror.commands, {
|
||||||
const {line, sticky} = pos;
|
toggleEditorFocus,
|
||||||
const {text, styles} = cm.getLineHandle(line);
|
jumpToLine,
|
||||||
|
commentSelection,
|
||||||
|
});
|
||||||
|
|
||||||
const execAt = (rx, i) => (rx.lastIndex = i) && null || rx.exec(text);
|
function jumpToLine(cm) {
|
||||||
const at = (rx, i) => (rx.lastIndex = i) && null || rx.test(text);
|
const cur = cm.getCursor();
|
||||||
const atWord = ch => at(/\w/y, ch);
|
const oldDialog = $('.CodeMirror-dialog', cm.display.wrapper);
|
||||||
const atSpace = ch => at(/\s/y, ch);
|
if (oldDialog) {
|
||||||
|
// close the currently opened minidialog
|
||||||
const atTokenEnd = styles.indexOf(ch, 1);
|
cm.focus();
|
||||||
ch += atTokenEnd < 0 ? 0 : sticky === 'before' && atWord(ch - 1) ? 0 : atSpace(ch + 1) ? 0 : 1;
|
}
|
||||||
ch = Math.min(text.length, ch);
|
// make sure to focus the input in newly opened minidialog
|
||||||
const type = cm.getTokenTypeAt({line, ch: ch + (sticky === 'after' ? 1 : 0)});
|
// setTimeout(() => {
|
||||||
if (atTokenEnd > 0) ch--;
|
// $('.CodeMirror-dialog', section).focus();
|
||||||
|
// });
|
||||||
const isCss = type && !/^(comment|string)/.test(type);
|
cm.openDialog(template.jumpToLine.cloneNode(true), str => {
|
||||||
const isNumber = type === 'number';
|
const m = str.match(/^\s*(\d+)(?:\s*:\s*(\d+))?\s*$/);
|
||||||
const isSpace = atSpace(ch);
|
if (m) {
|
||||||
let wordChars =
|
cm.setCursor(m[1] - 1, m[2] ? m[2] - 1 : cur.ch);
|
||||||
isNumber ? /[-+\w.%]/y :
|
|
||||||
isCss ? /[-\w@]/y :
|
|
||||||
isSpace ? /\s/y :
|
|
||||||
atWord(ch) ? /\w/y : /[^\w\s]/y;
|
|
||||||
|
|
||||||
let a = ch;
|
|
||||||
while (a && at(wordChars, a)) a--;
|
|
||||||
a += !a && at(wordChars, a) || isCss && at(/[.!#@]/y, a) ? 0 : at(wordChars, a + 1);
|
|
||||||
|
|
||||||
let b, found;
|
|
||||||
|
|
||||||
if (isNumber) {
|
|
||||||
b = a + execAt(/[+-]?[\d.]+(e\d+)?|$/yi, a)[0].length;
|
|
||||||
found = b >= ch;
|
|
||||||
if (!found) {
|
|
||||||
a = b;
|
|
||||||
ch = a;
|
|
||||||
}
|
}
|
||||||
}
|
}, {value: cur.line + 1});
|
||||||
|
}
|
||||||
|
|
||||||
if (!found) {
|
function commentSelection(cm) {
|
||||||
wordChars = isCss ? /[-\w]*/y : new RegExp(wordChars.source + '*', 'uy');
|
cm.blockComment(cm.getCursor('from'), cm.getCursor('to'), {fullLines: false});
|
||||||
b = ch + execAt(wordChars, ch)[0].length;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
function toggleEditorFocus(cm) {
|
||||||
from: {line, ch: a},
|
if (!cm) return;
|
||||||
to: {line, ch: b},
|
if (cm.hasFocus()) {
|
||||||
};
|
setTimeout(() => cm.display.input.blur());
|
||||||
|
} else {
|
||||||
|
cm.focus();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
|
|
||||||
|
@ -371,8 +350,9 @@ CodeMirror.hint && (() => {
|
||||||
}
|
}
|
||||||
|
|
||||||
// USO vars in usercss mode editor
|
// USO vars in usercss mode editor
|
||||||
const list = Object.keys(editor.getStyle().usercssData.vars)
|
const vars = editor.getStyle().usercssData.vars;
|
||||||
.filter(name => name.startsWith(leftPart));
|
const list = vars ?
|
||||||
|
Object.keys(vars).filter(name => name.startsWith(leftPart)) : [];
|
||||||
return {
|
return {
|
||||||
list,
|
list,
|
||||||
from: {line, ch: prev},
|
from: {line, ch: prev},
|
||||||
|
|
|
@ -1,698 +0,0 @@
|
||||||
/*
|
|
||||||
global CodeMirror loadScript
|
|
||||||
global editors editor styleId ownTabId
|
|
||||||
global save toggleStyle setupAutocomplete makeSectionVisible getSectionForChild
|
|
||||||
global getSectionsHashes
|
|
||||||
global messageBox
|
|
||||||
*/
|
|
||||||
'use strict';
|
|
||||||
|
|
||||||
onDOMscriptReady('/codemirror.js').then(() => {
|
|
||||||
const COMMANDS = {
|
|
||||||
save,
|
|
||||||
toggleStyle,
|
|
||||||
toggleEditorFocus,
|
|
||||||
jumpToLine,
|
|
||||||
nextEditor, prevEditor,
|
|
||||||
commentSelection,
|
|
||||||
};
|
|
||||||
const ORIGINAL_COMMANDS = {
|
|
||||||
insertTab: CodeMirror.commands.insertTab,
|
|
||||||
};
|
|
||||||
// reroute handling to nearest editor when keypress resolves to one of these commands
|
|
||||||
const REROUTED = new Set([
|
|
||||||
'save',
|
|
||||||
'toggleStyle',
|
|
||||||
'jumpToLine',
|
|
||||||
'nextEditor', 'prevEditor',
|
|
||||||
'toggleEditorFocus',
|
|
||||||
'find', 'findNext', 'findPrev', 'replace', 'replaceAll',
|
|
||||||
'colorpicker',
|
|
||||||
]);
|
|
||||||
Object.assign(CodeMirror, {
|
|
||||||
getOption,
|
|
||||||
setOption,
|
|
||||||
closestVisible,
|
|
||||||
});
|
|
||||||
Object.assign(CodeMirror.prototype, {
|
|
||||||
getSection,
|
|
||||||
rerouteHotkeys,
|
|
||||||
});
|
|
||||||
|
|
||||||
CodeMirror.defineInitHook(cm => {
|
|
||||||
if (!cm.display.wrapper.closest('#sections')) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (prefs.get('editor.livePreview') && styleId) {
|
|
||||||
cm.on('changes', updatePreview);
|
|
||||||
}
|
|
||||||
if (prefs.get('editor.autocompleteOnTyping')) {
|
|
||||||
setupAutocomplete(cm);
|
|
||||||
}
|
|
||||||
const wrapper = cm.display.wrapper;
|
|
||||||
cm.on('blur', () => {
|
|
||||||
editors.lastActive = cm;
|
|
||||||
cm.rerouteHotkeys(true);
|
|
||||||
setTimeout(() => {
|
|
||||||
wrapper.classList.toggle('CodeMirror-active', wrapper.contains(document.activeElement));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
cm.on('focus', () => {
|
|
||||||
cm.rerouteHotkeys(false);
|
|
||||||
wrapper.classList.add('CodeMirror-active');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
new MutationObserver((mutations, observer) => {
|
|
||||||
if (!$('#sections')) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
observer.disconnect();
|
|
||||||
|
|
||||||
prefs.subscribe(['editor.keyMap'], showHotkeyInTooltip);
|
|
||||||
addEventListener('showHotkeyInTooltip', showHotkeyInTooltip);
|
|
||||||
showHotkeyInTooltip();
|
|
||||||
|
|
||||||
// N.B. the onchange event listeners should be registered before setupLivePrefs()
|
|
||||||
$('#options').addEventListener('change', onOptionElementChanged);
|
|
||||||
setupLivePreview();
|
|
||||||
buildThemeElement();
|
|
||||||
buildKeymapElement();
|
|
||||||
setupLivePrefs();
|
|
||||||
|
|
||||||
Object.assign(CodeMirror.commands, COMMANDS);
|
|
||||||
rerouteHotkeys(true);
|
|
||||||
}).observe(document, {childList: true, subtree: true});
|
|
||||||
|
|
||||||
return;
|
|
||||||
|
|
||||||
////////////////////////////////////////////////
|
|
||||||
|
|
||||||
function getOption(o) {
|
|
||||||
return CodeMirror.defaults[o];
|
|
||||||
}
|
|
||||||
|
|
||||||
function setOption(o, v) {
|
|
||||||
CodeMirror.defaults[o] = v;
|
|
||||||
if (editors.length > 4 && (o === 'theme' || o === 'lineWrapping')) {
|
|
||||||
throttleSetOption({key: o, value: v, index: 0});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
editors.forEach(editor => {
|
|
||||||
editor.setOption(o, v);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function throttleSetOption({
|
|
||||||
key,
|
|
||||||
value,
|
|
||||||
index,
|
|
||||||
timeStart = performance.now(),
|
|
||||||
cmStart = editors.lastActive || editors[0],
|
|
||||||
editorsCopy = editors.slice(),
|
|
||||||
progress,
|
|
||||||
}) {
|
|
||||||
if (index === 0) {
|
|
||||||
if (!cmStart) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
cmStart.setOption(key, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
const THROTTLE_AFTER_MS = 100;
|
|
||||||
const THROTTLE_SHOW_PROGRESS_AFTER_MS = 100;
|
|
||||||
|
|
||||||
const t0 = performance.now();
|
|
||||||
const total = editorsCopy.length;
|
|
||||||
while (index < total) {
|
|
||||||
const cm = editorsCopy[index++];
|
|
||||||
if (cm === cmStart ||
|
|
||||||
cm !== editors[index] && !editors.includes(cm)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
cm.setOption(key, value);
|
|
||||||
if (performance.now() - t0 > THROTTLE_AFTER_MS) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (index >= total) {
|
|
||||||
$.remove(progress);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!progress &&
|
|
||||||
index < total / 2 &&
|
|
||||||
t0 - timeStart > THROTTLE_SHOW_PROGRESS_AFTER_MS) {
|
|
||||||
let option = $('#editor.' + key);
|
|
||||||
if (option) {
|
|
||||||
if (option.type === 'checkbox') {
|
|
||||||
option = (option.labels || [])[0] || option.nextElementSibling || option;
|
|
||||||
}
|
|
||||||
progress = document.body.appendChild(
|
|
||||||
$create('.set-option-progress', {targetElement: option}));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (progress) {
|
|
||||||
const optionBounds = progress.targetElement.getBoundingClientRect();
|
|
||||||
const bounds = {
|
|
||||||
top: optionBounds.top + window.scrollY + 1,
|
|
||||||
left: optionBounds.left + window.scrollX + 1,
|
|
||||||
width: (optionBounds.width - 2) * index / total | 0,
|
|
||||||
height: optionBounds.height - 2,
|
|
||||||
};
|
|
||||||
const style = progress.style;
|
|
||||||
for (const prop in bounds) {
|
|
||||||
if (bounds[prop] !== parseFloat(style[prop])) {
|
|
||||||
style[prop] = bounds[prop] + 'px';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
setTimeout(throttleSetOption, 0, {
|
|
||||||
key,
|
|
||||||
value,
|
|
||||||
index,
|
|
||||||
timeStart,
|
|
||||||
cmStart,
|
|
||||||
editorsCopy,
|
|
||||||
progress,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function getSection() {
|
|
||||||
return this.display.wrapper.parentNode;
|
|
||||||
}
|
|
||||||
|
|
||||||
function nextEditor(cm) {
|
|
||||||
return nextPrevEditor(cm, 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
function prevEditor(cm) {
|
|
||||||
return nextPrevEditor(cm, -1);
|
|
||||||
}
|
|
||||||
|
|
||||||
function nextPrevEditor(cm, direction) {
|
|
||||||
cm = editors[(editors.indexOf(cm) + direction + editors.length) % editors.length];
|
|
||||||
makeSectionVisible(cm);
|
|
||||||
cm.focus();
|
|
||||||
return cm;
|
|
||||||
}
|
|
||||||
|
|
||||||
function jumpToLine(cm) {
|
|
||||||
const cur = cm.getCursor();
|
|
||||||
refocusMinidialog(cm);
|
|
||||||
cm.openDialog(template.jumpToLine.cloneNode(true), str => {
|
|
||||||
const m = str.match(/^\s*(\d+)(?:\s*:\s*(\d+))?\s*$/);
|
|
||||||
if (m) {
|
|
||||||
cm.setCursor(m[1] - 1, m[2] ? m[2] - 1 : cur.ch);
|
|
||||||
}
|
|
||||||
}, {value: cur.line + 1});
|
|
||||||
}
|
|
||||||
|
|
||||||
function commentSelection(cm) {
|
|
||||||
cm.blockComment(cm.getCursor('from'), cm.getCursor('to'), {fullLines: false});
|
|
||||||
}
|
|
||||||
|
|
||||||
function toggleEditorFocus(cm) {
|
|
||||||
if (!cm) return;
|
|
||||||
if (cm.hasFocus()) {
|
|
||||||
setTimeout(() => cm.display.input.blur());
|
|
||||||
} else {
|
|
||||||
cm.focus();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function refocusMinidialog(cm) {
|
|
||||||
const section = cm.getSection();
|
|
||||||
if (!$('.CodeMirror-dialog', section)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// close the currently opened minidialog
|
|
||||||
cm.focus();
|
|
||||||
// make sure to focus the input in newly opened minidialog
|
|
||||||
setTimeout(() => {
|
|
||||||
$('.CodeMirror-dialog', section).focus();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function onOptionElementChanged(event) {
|
|
||||||
const el = event.target;
|
|
||||||
let option = el.id.replace(/^editor\./, '');
|
|
||||||
if (!option) {
|
|
||||||
console.error('no "cm_option"', el);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let value = el.type === 'checkbox' ? el.checked : el.value;
|
|
||||||
switch (option) {
|
|
||||||
case 'tabSize':
|
|
||||||
value = Number(value);
|
|
||||||
CodeMirror.setOption('indentUnit', value);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'indentWithTabs':
|
|
||||||
CodeMirror.commands.insertTab = value ?
|
|
||||||
ORIGINAL_COMMANDS.insertTab :
|
|
||||||
CodeMirror.commands.insertSoftTab;
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'theme': {
|
|
||||||
const themeLink = $('#cm-theme');
|
|
||||||
// use non-localized 'default' internally
|
|
||||||
if (!value || value === 'default' || value === t('defaultTheme')) {
|
|
||||||
value = 'default';
|
|
||||||
if (prefs.get(el.id) !== value) {
|
|
||||||
prefs.set(el.id, value);
|
|
||||||
}
|
|
||||||
themeLink.href = '';
|
|
||||||
el.selectedIndex = 0;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
const url = chrome.runtime.getURL('vendor/codemirror/theme/' + value + '.css');
|
|
||||||
if (themeLink.href === url) {
|
|
||||||
// preloaded in initCodeMirror()
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
// avoid flicker: wait for the second stylesheet to load, then apply the theme
|
|
||||||
document.head.appendChild($create('link#cm-theme2', {rel: 'stylesheet', href: url}));
|
|
||||||
setTimeout(() => {
|
|
||||||
CodeMirror.setOption(option, value);
|
|
||||||
themeLink.remove();
|
|
||||||
$('#cm-theme2').id = 'cm-theme';
|
|
||||||
}, 100);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
case 'autocompleteOnTyping':
|
|
||||||
editors.forEach(cm => setupAutocomplete(cm, el.checked));
|
|
||||||
return;
|
|
||||||
|
|
||||||
case 'autoCloseBrackets':
|
|
||||||
Promise.resolve(value && loadScript('/vendor/codemirror/addon/edit/closebrackets.js')).then(() => {
|
|
||||||
CodeMirror.setOption(option, value);
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
|
|
||||||
case 'matchHighlight':
|
|
||||||
switch (value) {
|
|
||||||
case 'token':
|
|
||||||
case 'selection':
|
|
||||||
document.body.dataset[option] = value;
|
|
||||||
value = {showToken: value === 'token' && /[#.\-\w]/, annotateScrollbar: true};
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
value = null;
|
|
||||||
}
|
|
||||||
option = 'highlightSelectionMatches';
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'colorpicker':
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
CodeMirror.setOption(option, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
function buildThemeElement() {
|
|
||||||
const themeElement = $('#editor.theme');
|
|
||||||
const themeList = localStorage.codeMirrorThemes;
|
|
||||||
|
|
||||||
const optionsFromArray = options => {
|
|
||||||
const fragment = document.createDocumentFragment();
|
|
||||||
options.forEach(opt => fragment.appendChild($create('option', opt)));
|
|
||||||
themeElement.appendChild(fragment);
|
|
||||||
};
|
|
||||||
|
|
||||||
if (themeList) {
|
|
||||||
optionsFromArray(themeList.split(/\s+/));
|
|
||||||
} else {
|
|
||||||
// Chrome is starting up and shows our edit.html, but the background page isn't loaded yet
|
|
||||||
const theme = prefs.get('editor.theme');
|
|
||||||
optionsFromArray([theme === 'default' ? t('defaultTheme') : theme]);
|
|
||||||
getCodeMirrorThemes().then(() => {
|
|
||||||
const themes = (localStorage.codeMirrorThemes || '').split(/\s+/);
|
|
||||||
optionsFromArray(themes);
|
|
||||||
themeElement.selectedIndex = Math.max(0, themes.indexOf(theme));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function buildKeymapElement() {
|
|
||||||
// move 'pc' or 'mac' prefix to the end of the displayed label
|
|
||||||
const maps = Object.keys(CodeMirror.keyMap)
|
|
||||||
.map(name => ({
|
|
||||||
value: name,
|
|
||||||
name: name.replace(/^(pc|mac)(.+)/, (s, arch, baseName) =>
|
|
||||||
baseName.toLowerCase() + '-' + (arch === 'mac' ? 'Mac' : 'PC')),
|
|
||||||
}))
|
|
||||||
.sort((a, b) => a.name < b.name && -1 || a.name > b.name && 1);
|
|
||||||
|
|
||||||
const fragment = document.createDocumentFragment();
|
|
||||||
let bin = fragment;
|
|
||||||
let groupName;
|
|
||||||
// group suffixed maps in <optgroup>
|
|
||||||
maps.forEach(({value, name}, i) => {
|
|
||||||
groupName = !name.includes('-') ? name : groupName;
|
|
||||||
const groupWithNext = maps[i + 1] && maps[i + 1].name.startsWith(groupName);
|
|
||||||
if (groupWithNext) {
|
|
||||||
if (bin === fragment) {
|
|
||||||
bin = fragment.appendChild($create('optgroup', {label: name.split('-')[0]}));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const el = bin.appendChild($create('option', {value}, name));
|
|
||||||
if (value === prefs.defaults['editor.keyMap']) {
|
|
||||||
el.dataset.default = '';
|
|
||||||
el.title = t('defaultTheme');
|
|
||||||
}
|
|
||||||
if (!groupWithNext) bin = fragment;
|
|
||||||
});
|
|
||||||
$('#editor.keyMap').appendChild(fragment);
|
|
||||||
}
|
|
||||||
|
|
||||||
////////////////////////////////////////////////
|
|
||||||
|
|
||||||
function rerouteHotkeys(enable, immediately) {
|
|
||||||
if (!immediately) {
|
|
||||||
debounce(rerouteHotkeys, 0, enable, true);
|
|
||||||
} else if (enable) {
|
|
||||||
document.addEventListener('keydown', rerouteHandler);
|
|
||||||
} else {
|
|
||||||
document.removeEventListener('keydown', rerouteHandler);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function rerouteHandler(event) {
|
|
||||||
const keyName = CodeMirror.keyName(event);
|
|
||||||
if (!keyName) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const rerouteCommand = name => {
|
|
||||||
if (REROUTED.has(name)) {
|
|
||||||
CodeMirror.commands[name](closestVisible(event.target));
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
if (CodeMirror.lookupKey(keyName, CodeMirror.defaults.keyMap, rerouteCommand) === 'handled' ||
|
|
||||||
CodeMirror.lookupKey(keyName, CodeMirror.defaults.extraKeys, rerouteCommand) === 'handled') {
|
|
||||||
event.preventDefault();
|
|
||||||
event.stopPropagation();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
////////////////////////////////////////////////
|
|
||||||
|
|
||||||
// priority:
|
|
||||||
// 1. associated CM for applies-to element
|
|
||||||
// 2. last active if visible
|
|
||||||
// 3. first visible
|
|
||||||
function closestVisible(nearbyElement) {
|
|
||||||
const cm =
|
|
||||||
nearbyElement instanceof CodeMirror ? nearbyElement :
|
|
||||||
nearbyElement instanceof Node && (getSectionForChild(nearbyElement) || {}).CodeMirror ||
|
|
||||||
editors.lastActive;
|
|
||||||
if (nearbyElement instanceof Node && cm) {
|
|
||||||
const {left, top} = nearbyElement.getBoundingClientRect();
|
|
||||||
const bounds = cm.display.wrapper.getBoundingClientRect();
|
|
||||||
if (top >= 0 && top >= bounds.top &&
|
|
||||||
left >= 0 && left >= bounds.left) {
|
|
||||||
return cm;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// closest editor should have at least 2 lines visible
|
|
||||||
const lineHeight = editors[0].defaultTextHeight();
|
|
||||||
const scrollY = window.scrollY;
|
|
||||||
const windowBottom = scrollY + window.innerHeight - 2 * lineHeight;
|
|
||||||
const allSectionsContainerTop = scrollY + $('#sections').getBoundingClientRect().top;
|
|
||||||
const distances = [];
|
|
||||||
const alreadyInView = cm && offscreenDistance(null, cm) === 0;
|
|
||||||
return alreadyInView ? cm : findClosest();
|
|
||||||
|
|
||||||
function offscreenDistance(index, cm) {
|
|
||||||
if (index >= 0 && distances[index] !== undefined) {
|
|
||||||
return distances[index];
|
|
||||||
}
|
|
||||||
const section = (cm || editors[index]).getSection();
|
|
||||||
if (!section) {
|
|
||||||
return 1e9;
|
|
||||||
}
|
|
||||||
const top = allSectionsContainerTop + section.offsetTop;
|
|
||||||
if (top < scrollY + lineHeight) {
|
|
||||||
return Math.max(0, scrollY - top - lineHeight);
|
|
||||||
}
|
|
||||||
if (top < windowBottom) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
const distance = top - windowBottom + section.offsetHeight;
|
|
||||||
if (index >= 0) {
|
|
||||||
distances[index] = distance;
|
|
||||||
}
|
|
||||||
return distance;
|
|
||||||
}
|
|
||||||
|
|
||||||
function findClosest() {
|
|
||||||
const last = editors.length - 1;
|
|
||||||
let a = 0;
|
|
||||||
let b = last;
|
|
||||||
let c;
|
|
||||||
let distance;
|
|
||||||
while (a < b - 1) {
|
|
||||||
c = (a + b) / 2 | 0;
|
|
||||||
distance = offscreenDistance(c);
|
|
||||||
if (!distance || !c) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
const distancePrev = offscreenDistance(c - 1);
|
|
||||||
const distanceNext = c < last ? offscreenDistance(c + 1) : 1e20;
|
|
||||||
if (distancePrev <= distance && distance <= distanceNext) {
|
|
||||||
b = c;
|
|
||||||
} else {
|
|
||||||
a = c;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
while (b && offscreenDistance(b - 1) <= offscreenDistance(b)) {
|
|
||||||
b--;
|
|
||||||
}
|
|
||||||
const cm = editors[b];
|
|
||||||
if (distances[b] > 0) {
|
|
||||||
makeSectionVisible(cm);
|
|
||||||
}
|
|
||||||
return cm;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
////////////////////////////////////////////////
|
|
||||||
|
|
||||||
function getCodeMirrorThemes() {
|
|
||||||
if (!chrome.runtime.getPackageDirectoryEntry) {
|
|
||||||
const themes = [
|
|
||||||
chrome.i18n.getMessage('defaultTheme'),
|
|
||||||
/* populate-theme-start */
|
|
||||||
'3024-day',
|
|
||||||
'3024-night',
|
|
||||||
'abcdef',
|
|
||||||
'ambiance',
|
|
||||||
'ambiance-mobile',
|
|
||||||
'base16-dark',
|
|
||||||
'base16-light',
|
|
||||||
'bespin',
|
|
||||||
'blackboard',
|
|
||||||
'cobalt',
|
|
||||||
'colorforth',
|
|
||||||
'darcula',
|
|
||||||
'dracula',
|
|
||||||
'duotone-dark',
|
|
||||||
'duotone-light',
|
|
||||||
'eclipse',
|
|
||||||
'elegant',
|
|
||||||
'erlang-dark',
|
|
||||||
'gruvbox-dark',
|
|
||||||
'hopscotch',
|
|
||||||
'icecoder',
|
|
||||||
'idea',
|
|
||||||
'isotope',
|
|
||||||
'lesser-dark',
|
|
||||||
'liquibyte',
|
|
||||||
'lucario',
|
|
||||||
'material',
|
|
||||||
'mbo',
|
|
||||||
'mdn-like',
|
|
||||||
'midnight',
|
|
||||||
'monokai',
|
|
||||||
'neat',
|
|
||||||
'neo',
|
|
||||||
'night',
|
|
||||||
'oceanic-next',
|
|
||||||
'panda-syntax',
|
|
||||||
'paraiso-dark',
|
|
||||||
'paraiso-light',
|
|
||||||
'pastel-on-dark',
|
|
||||||
'railscasts',
|
|
||||||
'rubyblue',
|
|
||||||
'seti',
|
|
||||||
'shadowfox',
|
|
||||||
'solarized',
|
|
||||||
'ssms',
|
|
||||||
'the-matrix',
|
|
||||||
'tomorrow-night-bright',
|
|
||||||
'tomorrow-night-eighties',
|
|
||||||
'ttcn',
|
|
||||||
'twilight',
|
|
||||||
'vibrant-ink',
|
|
||||||
'xq-dark',
|
|
||||||
'xq-light',
|
|
||||||
'yeti',
|
|
||||||
'zenburn',
|
|
||||||
/* populate-theme-end */
|
|
||||||
];
|
|
||||||
localStorage.codeMirrorThemes = themes.join(' ');
|
|
||||||
return Promise.resolve(themes);
|
|
||||||
}
|
|
||||||
return new Promise(resolve => {
|
|
||||||
chrome.runtime.getPackageDirectoryEntry(rootDir => {
|
|
||||||
rootDir.getDirectory('vendor/codemirror/theme', {create: false}, themeDir => {
|
|
||||||
themeDir.createReader().readEntries(entries => {
|
|
||||||
const themes = [
|
|
||||||
chrome.i18n.getMessage('defaultTheme')
|
|
||||||
].concat(
|
|
||||||
entries.filter(entry => entry.isFile)
|
|
||||||
.sort((a, b) => (a.name < b.name ? -1 : 1))
|
|
||||||
.map(entry => entry.name.replace(/\.css$/, ''))
|
|
||||||
);
|
|
||||||
localStorage.codeMirrorThemes = themes.join(' ');
|
|
||||||
resolve(themes);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function showHotkeyInTooltip(_, mapName = prefs.get('editor.keyMap')) {
|
|
||||||
const extraKeys = CodeMirror.defaults.extraKeys;
|
|
||||||
for (const el of $$('[data-hotkey-tooltip]')) {
|
|
||||||
if (el._hotkeyTooltipKeyMap !== mapName) {
|
|
||||||
el._hotkeyTooltipKeyMap = mapName;
|
|
||||||
const title = el._hotkeyTooltipTitle = el._hotkeyTooltipTitle || el.title;
|
|
||||||
const cmd = el.dataset.hotkeyTooltip;
|
|
||||||
const key = cmd[0] === '=' ? cmd.slice(1) :
|
|
||||||
findKeyForCommand(cmd, mapName) ||
|
|
||||||
extraKeys && findKeyForCommand(cmd, extraKeys);
|
|
||||||
const newTitle = title + (title && key ? '\n' : '') + (key || '');
|
|
||||||
if (el.title !== newTitle) el.title = newTitle;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function findKeyForCommand(command, map) {
|
|
||||||
if (typeof map === 'string') map = CodeMirror.keyMap[map];
|
|
||||||
let key = Object.keys(map).find(k => map[k] === command);
|
|
||||||
if (key) {
|
|
||||||
return key;
|
|
||||||
}
|
|
||||||
for (const ft of Array.isArray(map.fallthrough) ? map.fallthrough : [map.fallthrough]) {
|
|
||||||
key = ft && findKeyForCommand(command, ft);
|
|
||||||
if (key) {
|
|
||||||
return key;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
|
|
||||||
function setupLivePreview() {
|
|
||||||
if (!prefs.get('editor.livePreview') && !editors.length) {
|
|
||||||
setTimeout(setupLivePreview);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (styleId) {
|
|
||||||
$('#editor.livePreview').onchange = livePreviewToggled;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// wait for #preview-label's class to lose 'hidden' after the first save
|
|
||||||
new MutationObserver((_, observer) => {
|
|
||||||
if (!styleId) return;
|
|
||||||
observer.disconnect();
|
|
||||||
setupLivePreview();
|
|
||||||
livePreviewToggled();
|
|
||||||
}).observe($('#preview-label'), {
|
|
||||||
attributes: true,
|
|
||||||
attributeFilter: ['class'],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function livePreviewToggled() {
|
|
||||||
const me = this instanceof Node ? this : $('#editor.livePreview');
|
|
||||||
const previewing = me.checked;
|
|
||||||
editors.forEach(cm => cm[previewing ? 'on' : 'off']('changes', updatePreview));
|
|
||||||
const addRemove = EventTarget.prototype[previewing ? 'addEventListener' : 'removeEventListener'];
|
|
||||||
addRemove.call($('#enabled'), 'change', updatePreview);
|
|
||||||
if (!editor) {
|
|
||||||
for (const el of $$('#sections .applies-to')) {
|
|
||||||
addRemove.call(el, 'input', updatePreview);
|
|
||||||
}
|
|
||||||
toggleLivePreviewSectionsObserver(previewing);
|
|
||||||
}
|
|
||||||
if (!previewing || document.body.classList.contains('dirty')) {
|
|
||||||
updatePreview(null, previewing);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Observes newly added section elements, and sets these event listeners:
|
|
||||||
* 1. 'changes' on CodeMirror inside
|
|
||||||
* 2. 'input' on .applies-to inside
|
|
||||||
* The goal is to avoid listening to 'input' on the entire #sections tree,
|
|
||||||
* which would trigger updatePreview() twice on any keystroke -
|
|
||||||
* both for the synthetic event from CodeMirror and the original event.
|
|
||||||
* Side effects:
|
|
||||||
* two expando properties on #sections
|
|
||||||
* 1. __livePreviewObserver
|
|
||||||
* 2. __livePreviewObserverEnabled
|
|
||||||
* @param {Boolean} enable
|
|
||||||
*/
|
|
||||||
function toggleLivePreviewSectionsObserver(enable) {
|
|
||||||
const sections = $('#sections');
|
|
||||||
const observing = sections.__livePreviewObserverEnabled;
|
|
||||||
let mo = sections.__livePreviewObserver;
|
|
||||||
if (enable && !mo) {
|
|
||||||
sections.__livePreviewObserver = mo = new MutationObserver(mutations => {
|
|
||||||
for (const {addedNodes} of mutations) {
|
|
||||||
for (const node of addedNodes) {
|
|
||||||
const el = node.children && $('.applies-to', node);
|
|
||||||
if (el) el.addEventListener('input', updatePreview);
|
|
||||||
if (node.CodeMirror) node.CodeMirror.on('changes', updatePreview);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (enable && !observing) {
|
|
||||||
mo.observe(sections, {childList: true});
|
|
||||||
sections.__livePreviewObserverEnabled = true;
|
|
||||||
} else if (!enable && observing) {
|
|
||||||
mo.disconnect();
|
|
||||||
sections.__livePreviewObserverEnabled = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function updatePreview(data, previewing) {
|
|
||||||
if (previewing !== true && previewing !== false) {
|
|
||||||
if (data instanceof Event && !data.target.matches('.style-contributor')) return;
|
|
||||||
debounce(updatePreview, data && data.id === 'enabled' ? 0 : 400, null, true);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const errors = $('#preview-errors');
|
|
||||||
API.refreshAllTabs({
|
|
||||||
reason: 'editPreview',
|
|
||||||
tabId: ownTabId,
|
|
||||||
style: {
|
|
||||||
id: styleId,
|
|
||||||
enabled: $('#enabled').checked,
|
|
||||||
sections: previewing && (editor ? editors[0].getValue() : getSectionsHashes()),
|
|
||||||
},
|
|
||||||
}).then(() => {
|
|
||||||
errors.classList.add('hidden');
|
|
||||||
}).catch(err => {
|
|
||||||
if (Array.isArray(err)) err = err.join('\n');
|
|
||||||
if (err && editor && !isNaN(err.index)) {
|
|
||||||
const pos = editors[0].posFromIndex(err.index);
|
|
||||||
err = `${pos.line}:${pos.ch} ${err}`;
|
|
||||||
}
|
|
||||||
errors.classList.remove('hidden');
|
|
||||||
errors.onclick = () => messageBox.alert(String(err), 'pre');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
287
edit/codemirror-factory.js
Normal file
287
edit/codemirror-factory.js
Normal file
|
@ -0,0 +1,287 @@
|
||||||
|
/* global CodeMirror loadScript rerouteHotkeys prefs $ debounce $create */
|
||||||
|
/* exported cmFactory */
|
||||||
|
'use strict';
|
||||||
|
/*
|
||||||
|
All cm instances created by this module are collected so we can broadcast prefs
|
||||||
|
settings to them. You should `cmFactory.destroy(cm)` to unregister the listener
|
||||||
|
when the instance is not used anymore.
|
||||||
|
*/
|
||||||
|
const cmFactory = (() => {
|
||||||
|
const editors = new Set();
|
||||||
|
// used by `indentWithTabs` option
|
||||||
|
const INSERT_TAB_COMMAND = CodeMirror.commands.insertTab;
|
||||||
|
const INSERT_SOFT_TAB_COMMAND = CodeMirror.commands.insertSoftTab;
|
||||||
|
|
||||||
|
CodeMirror.defineOption('tabSize', prefs.get('editor.tabSize'), (cm, value) => {
|
||||||
|
cm.setOption('indentUnit', Number(value));
|
||||||
|
});
|
||||||
|
|
||||||
|
CodeMirror.defineOption('indentWithTabs', prefs.get('editor.indentWithTabs'), (cm, value) => {
|
||||||
|
CodeMirror.commands.insertTab = value ?
|
||||||
|
INSERT_TAB_COMMAND :
|
||||||
|
INSERT_SOFT_TAB_COMMAND;
|
||||||
|
});
|
||||||
|
|
||||||
|
CodeMirror.defineOption('autocompleteOnTyping', prefs.get('editor.autocompleteOnTyping'), (cm, value) => {
|
||||||
|
const onOff = value ? 'on' : 'off';
|
||||||
|
cm[onOff]('changes', autocompleteOnTyping);
|
||||||
|
cm[onOff]('pick', autocompletePicked);
|
||||||
|
});
|
||||||
|
|
||||||
|
CodeMirror.defineOption('matchHighlight', prefs.get('editor.matchHighlight'), (cm, value) => {
|
||||||
|
if (value === 'token') {
|
||||||
|
cm.setOption('highlightSelectionMatches', {
|
||||||
|
showToken: /[#.\-\w]/,
|
||||||
|
annotateScrollbar: true
|
||||||
|
});
|
||||||
|
} else if (value === 'selection') {
|
||||||
|
cm.setOption('highlightSelectionMatches', {
|
||||||
|
showToken: false,
|
||||||
|
annotateScrollbar: true
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
cm.setOption('highlightSelectionMatches', null);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
CodeMirror.defineOption('selectByTokens', prefs.get('editor.selectByTokens'), (cm, value) => {
|
||||||
|
cm.setOption('configureMouse', value ? configureMouseFn : null);
|
||||||
|
});
|
||||||
|
|
||||||
|
prefs.subscribe(null, (key, value) => {
|
||||||
|
const option = key.replace(/^editor\./, '');
|
||||||
|
if (!option) {
|
||||||
|
console.error('no "cm_option"', key);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// FIXME: this is implemented in `colorpicker-helper.js`.
|
||||||
|
if (option === 'colorpicker') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (option === 'theme') {
|
||||||
|
const themeLink = $('#cm-theme');
|
||||||
|
// use non-localized 'default' internally
|
||||||
|
if (value === 'default') {
|
||||||
|
themeLink.href = '';
|
||||||
|
} else {
|
||||||
|
const url = chrome.runtime.getURL('vendor/codemirror/theme/' + value + '.css');
|
||||||
|
if (themeLink.href !== url) {
|
||||||
|
// avoid flicker: wait for the second stylesheet to load, then apply the theme
|
||||||
|
return loadScript(url, true).then(([newThemeLink]) => {
|
||||||
|
setOption(option, value);
|
||||||
|
themeLink.remove();
|
||||||
|
newThemeLink.id = 'cm-theme';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// broadcast option
|
||||||
|
setOption(option, value);
|
||||||
|
});
|
||||||
|
return {create, destroy, setOption};
|
||||||
|
|
||||||
|
function configureMouseFn(cm, repeat) {
|
||||||
|
return repeat === 'double' ?
|
||||||
|
{unit: selectTokenOnDoubleclick} :
|
||||||
|
{};
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectTokenOnDoubleclick(cm, pos) {
|
||||||
|
let {ch} = pos;
|
||||||
|
const {line, sticky} = pos;
|
||||||
|
const {text, styles} = cm.getLineHandle(line);
|
||||||
|
|
||||||
|
const execAt = (rx, i) => (rx.lastIndex = i) && null || rx.exec(text);
|
||||||
|
const at = (rx, i) => (rx.lastIndex = i) && null || rx.test(text);
|
||||||
|
const atWord = ch => at(/\w/y, ch);
|
||||||
|
const atSpace = ch => at(/\s/y, ch);
|
||||||
|
|
||||||
|
const atTokenEnd = styles.indexOf(ch, 1);
|
||||||
|
ch += atTokenEnd < 0 ? 0 : sticky === 'before' && atWord(ch - 1) ? 0 : atSpace(ch + 1) ? 0 : 1;
|
||||||
|
ch = Math.min(text.length, ch);
|
||||||
|
const type = cm.getTokenTypeAt({line, ch: ch + (sticky === 'after' ? 1 : 0)});
|
||||||
|
if (atTokenEnd > 0) ch--;
|
||||||
|
|
||||||
|
const isCss = type && !/^(comment|string)/.test(type);
|
||||||
|
const isNumber = type === 'number';
|
||||||
|
const isSpace = atSpace(ch);
|
||||||
|
let wordChars =
|
||||||
|
isNumber ? /[-+\w.%]/y :
|
||||||
|
isCss ? /[-\w@]/y :
|
||||||
|
isSpace ? /\s/y :
|
||||||
|
atWord(ch) ? /\w/y : /[^\w\s]/y;
|
||||||
|
|
||||||
|
let a = ch;
|
||||||
|
while (a && at(wordChars, a)) a--;
|
||||||
|
a += !a && at(wordChars, a) || isCss && at(/[.!#@]/y, a) ? 0 : at(wordChars, a + 1);
|
||||||
|
|
||||||
|
let b, found;
|
||||||
|
|
||||||
|
if (isNumber) {
|
||||||
|
b = a + execAt(/[+-]?[\d.]+(e\d+)?|$/yi, a)[0].length;
|
||||||
|
found = b >= ch;
|
||||||
|
if (!found) {
|
||||||
|
a = b;
|
||||||
|
ch = a;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!found) {
|
||||||
|
wordChars = isCss ? /[-\w]*/y : new RegExp(wordChars.source + '*', 'uy');
|
||||||
|
b = ch + execAt(wordChars, ch)[0].length;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
from: {line, ch: a},
|
||||||
|
to: {line, ch: b},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function autocompleteOnTyping(cm, [info], debounced) {
|
||||||
|
const lastLine = info.text[info.text.length - 1];
|
||||||
|
if (
|
||||||
|
cm.state.completionActive ||
|
||||||
|
info.origin && !info.origin.includes('input') ||
|
||||||
|
!lastLine
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (cm.state.autocompletePicked) {
|
||||||
|
cm.state.autocompletePicked = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!debounced) {
|
||||||
|
debounce(autocompleteOnTyping, 100, cm, [info], true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (lastLine.match(/[-a-z!]+$/i)) {
|
||||||
|
cm.state.autocompletePicked = false;
|
||||||
|
cm.options.hintOptions.completeSingle = false;
|
||||||
|
cm.execCommand('autocomplete');
|
||||||
|
setTimeout(() => {
|
||||||
|
cm.options.hintOptions.completeSingle = true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function autocompletePicked(cm) {
|
||||||
|
cm.state.autocompletePicked = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function destroy(cm) {
|
||||||
|
editors.delete(cm);
|
||||||
|
}
|
||||||
|
|
||||||
|
function create(init, options) {
|
||||||
|
const cm = CodeMirror(init, options);
|
||||||
|
cm.lastActive = 0;
|
||||||
|
const wrapper = cm.display.wrapper;
|
||||||
|
cm.on('blur', () => {
|
||||||
|
rerouteHotkeys(true);
|
||||||
|
setTimeout(() => {
|
||||||
|
wrapper.classList.toggle('CodeMirror-active', wrapper.contains(document.activeElement));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
cm.on('focus', () => {
|
||||||
|
rerouteHotkeys(false);
|
||||||
|
wrapper.classList.add('CodeMirror-active');
|
||||||
|
cm.lastActive = Date.now();
|
||||||
|
});
|
||||||
|
editors.add(cm);
|
||||||
|
return cm;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getLastActivated() {
|
||||||
|
let result;
|
||||||
|
for (const cm of editors) {
|
||||||
|
if (!result || result.lastActive < cm.lastActive) {
|
||||||
|
result = cm;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setOption(key, value) {
|
||||||
|
CodeMirror.defaults[key] = value;
|
||||||
|
if (editors.size > 4 && (key === 'theme' || key === 'lineWrapping')) {
|
||||||
|
throttleSetOption({key, value, index: 0});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (const cm of editors) {
|
||||||
|
cm.setOption(key, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function throttleSetOption({
|
||||||
|
key,
|
||||||
|
value,
|
||||||
|
index,
|
||||||
|
timeStart = performance.now(),
|
||||||
|
editorsCopy = [...editors],
|
||||||
|
cmStart = getLastActivated(),
|
||||||
|
progress,
|
||||||
|
}) {
|
||||||
|
if (index === 0) {
|
||||||
|
if (!cmStart) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
cmStart.setOption(key, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
const THROTTLE_AFTER_MS = 100;
|
||||||
|
const THROTTLE_SHOW_PROGRESS_AFTER_MS = 100;
|
||||||
|
|
||||||
|
const t0 = performance.now();
|
||||||
|
const total = editorsCopy.length;
|
||||||
|
while (index < total) {
|
||||||
|
const cm = editorsCopy[index++];
|
||||||
|
if (cm === cmStart || !editors.has(cm)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
cm.setOption(key, value);
|
||||||
|
if (performance.now() - t0 > THROTTLE_AFTER_MS) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (index >= total) {
|
||||||
|
$.remove(progress);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!progress &&
|
||||||
|
index < total / 2 &&
|
||||||
|
t0 - timeStart > THROTTLE_SHOW_PROGRESS_AFTER_MS) {
|
||||||
|
let option = $('#editor.' + key);
|
||||||
|
if (option) {
|
||||||
|
if (option.type === 'checkbox') {
|
||||||
|
option = (option.labels || [])[0] || option.nextElementSibling || option;
|
||||||
|
}
|
||||||
|
progress = document.body.appendChild(
|
||||||
|
$create('.set-option-progress', {targetElement: option}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (progress) {
|
||||||
|
const optionBounds = progress.targetElement.getBoundingClientRect();
|
||||||
|
const bounds = {
|
||||||
|
top: optionBounds.top + window.scrollY + 1,
|
||||||
|
left: optionBounds.left + window.scrollX + 1,
|
||||||
|
width: (optionBounds.width - 2) * index / total | 0,
|
||||||
|
height: optionBounds.height - 2,
|
||||||
|
};
|
||||||
|
const style = progress.style;
|
||||||
|
for (const prop in bounds) {
|
||||||
|
if (bounds[prop] !== parseFloat(style[prop])) {
|
||||||
|
style[prop] = bounds[prop] + 'px';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setTimeout(throttleSetOption, 0, {
|
||||||
|
key,
|
||||||
|
value,
|
||||||
|
index,
|
||||||
|
timeStart,
|
||||||
|
cmStart,
|
||||||
|
editorsCopy,
|
||||||
|
progress,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})();
|
|
@ -1,7 +1,7 @@
|
||||||
/* global CodeMirror loadScript editors showHelp */
|
/* global CodeMirror showHelp cmFactory onDOMready $ $create prefs t */
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
onDOMscriptReady('/colorview.js').then(() => {
|
(() => {
|
||||||
onDOMready().then(() => {
|
onDOMready().then(() => {
|
||||||
$('#colorpicker-settings').onclick = configureColorpicker;
|
$('#colorpicker-settings').onclick = configureColorpicker;
|
||||||
});
|
});
|
||||||
|
@ -20,7 +20,8 @@ onDOMscriptReady('/colorview.js').then(() => {
|
||||||
defaults.extraKeys[keyName] = 'colorpicker';
|
defaults.extraKeys[keyName] = 'colorpicker';
|
||||||
}
|
}
|
||||||
defaults.colorpicker = {
|
defaults.colorpicker = {
|
||||||
forceUpdate: editors.length > 0,
|
// FIXME: who uses this?
|
||||||
|
// forceUpdate: editor.getEditors().length > 0,
|
||||||
tooltip: t('colorpickerTooltip'),
|
tooltip: t('colorpickerTooltip'),
|
||||||
popup: {
|
popup: {
|
||||||
tooltipForSwitcher: t('colorpickerSwitchFormatTooltip'),
|
tooltipForSwitcher: t('colorpickerSwitchFormatTooltip'),
|
||||||
|
@ -38,8 +39,7 @@ onDOMscriptReady('/colorview.js').then(() => {
|
||||||
delete defaults.extraKeys[keyName];
|
delete defaults.extraKeys[keyName];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// on page load runs before CodeMirror.setOption is defined
|
cmFactory.setOption('colorpicker', defaults.colorpicker);
|
||||||
editors.forEach(cm => cm.setOption('colorpicker', defaults.colorpicker));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function registerHotkey(id, hotkey) {
|
function registerHotkey(id, hotkey) {
|
||||||
|
@ -112,4 +112,4 @@ onDOMscriptReady('/colorview.js').then(() => {
|
||||||
}
|
}
|
||||||
input.focus();
|
input.focus();
|
||||||
}
|
}
|
||||||
});
|
})();
|
||||||
|
|
|
@ -265,13 +265,10 @@ input:invalid {
|
||||||
}
|
}
|
||||||
/************ content ***********/
|
/************ content ***********/
|
||||||
#sections > * {
|
#sections > * {
|
||||||
margin: 0.7rem;
|
margin: 0 0.7rem;
|
||||||
padding: 1rem 1rem .3rem;
|
padding: 1rem;
|
||||||
}
|
}
|
||||||
#sections > *:first-child {
|
#sections > :not(:first-child) {
|
||||||
padding: 0 1rem .3rem;
|
|
||||||
}
|
|
||||||
#sections > *:not(:first-child) {
|
|
||||||
border-top: 2px solid hsl(0, 0%, 80%);
|
border-top: 2px solid hsl(0, 0%, 80%);
|
||||||
}
|
}
|
||||||
.add-section:after {
|
.add-section:after {
|
||||||
|
@ -288,7 +285,7 @@ input:invalid {
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
.edit-actions button {
|
.edit-actions button {
|
||||||
margin: 0 .2rem .5rem 0;
|
margin-right: .2rem;
|
||||||
}
|
}
|
||||||
.dirty > label::before {
|
.dirty > label::before {
|
||||||
content: "*";
|
content: "*";
|
||||||
|
@ -312,6 +309,25 @@ input:invalid {
|
||||||
.section:only-of-type .move-section-down {
|
.section:only-of-type .move-section-down {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
.section .CodeMirror {
|
||||||
|
margin-bottom: .875rem;
|
||||||
|
}
|
||||||
|
/* deleted section */
|
||||||
|
.deleted-section {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
.section .deleted-section {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.section.removed .deleted-section {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
.section.removed .code-label,
|
||||||
|
.section.removed .applies-to,
|
||||||
|
.section.removed .edit-actions,
|
||||||
|
.section.removed .CodeMirror {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
.move-section-up:after {
|
.move-section-up:after {
|
||||||
content: "";
|
content: "";
|
||||||
display: block;
|
display: block;
|
||||||
|
|
619
edit/edit.js
619
edit/edit.js
|
@ -1,82 +1,244 @@
|
||||||
/*
|
/* global CodeMirror onDOMready prefs setupLivePrefs $ $$ $create t tHTML
|
||||||
global CodeMirror loadScript
|
createSourceEditor queryTabs sessionStorageHash getOwnTab FIREFOX API tryCatch
|
||||||
global createSourceEditor
|
closeCurrentTab messageBox debounce workerUtil
|
||||||
global closeCurrentTab regExpTester messageBox
|
beautify ignoreChromeError
|
||||||
global setupCodeMirror
|
moveFocus msg createSectionsEditor rerouteHotkeys */
|
||||||
global beautify
|
/* exported showCodeMirrorPopup editorWorker toggleContextMenuDelete */
|
||||||
global initWithSectionStyle addSections removeSection getSectionsHashes
|
|
||||||
global sectionsToMozFormat
|
|
||||||
global moveFocus editorWorker
|
|
||||||
*/
|
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
let styleId = null;
|
const editorWorker = workerUtil.createWorker({
|
||||||
// only the actually dirty items here
|
url: '/edit/editor-worker.js'
|
||||||
let dirty = {};
|
});
|
||||||
// array of all CodeMirror instances
|
|
||||||
const editors = [];
|
|
||||||
let saveSizeOnClose;
|
let saveSizeOnClose;
|
||||||
let ownTabId;
|
|
||||||
|
|
||||||
// direct & reverse mapping of @-moz-document keywords and internal property names
|
// direct & reverse mapping of @-moz-document keywords and internal property names
|
||||||
const propertyToCss = {urls: 'url', urlPrefixes: 'url-prefix', domains: 'domain', regexps: 'regexp'};
|
const propertyToCss = {urls: 'url', urlPrefixes: 'url-prefix', domains: 'domain', regexps: 'regexp'};
|
||||||
const CssToProperty = {'url': 'urls', 'url-prefix': 'urlPrefixes', 'domain': 'domains', 'regexp': 'regexps'};
|
const CssToProperty = Object.entries(propertyToCss)
|
||||||
|
.reduce((o, v) => {
|
||||||
|
o[v[1]] = v[0];
|
||||||
|
return o;
|
||||||
|
}, {});
|
||||||
|
|
||||||
let editor;
|
let editor;
|
||||||
|
|
||||||
|
|
||||||
document.addEventListener('visibilitychange', beforeUnload);
|
document.addEventListener('visibilitychange', beforeUnload);
|
||||||
chrome.runtime.onMessage.addListener(onRuntimeMessage);
|
window.addEventListener('beforeunload', beforeUnload);
|
||||||
|
msg.onExtension(onRuntimeMessage);
|
||||||
|
|
||||||
preinit();
|
preinit();
|
||||||
|
|
||||||
Promise.all([
|
(() => {
|
||||||
initStyleData(),
|
onDOMready().then(() => {
|
||||||
onDOMready(),
|
prefs.subscribe(['editor.keyMap'], showHotkeyInTooltip);
|
||||||
])
|
addEventListener('showHotkeyInTooltip', showHotkeyInTooltip);
|
||||||
.then(([style]) => {
|
showHotkeyInTooltip();
|
||||||
const usercss = isUsercss(style);
|
|
||||||
$('#heading').textContent = t(styleId ? 'editStyleHeading' : 'addStyleTitle');
|
|
||||||
$('#name').placeholder = t(usercss ? 'usercssEditorNamePlaceholder' : 'styleMissingName');
|
|
||||||
$('#name').title = usercss ? t('usercssReplaceTemplateName') : '';
|
|
||||||
|
|
||||||
$('#preview-label').classList.toggle('hidden', !styleId);
|
buildThemeElement();
|
||||||
|
buildKeymapElement();
|
||||||
|
|
||||||
$('#beautify').onclick = beautify;
|
setupLivePrefs();
|
||||||
$('#lint').addEventListener('scroll', hideLintHeaderOnScroll, {passive: true});
|
});
|
||||||
window.addEventListener('resize', () => debounce(rememberWindowSize, 100));
|
|
||||||
|
|
||||||
if (usercss) {
|
initEditor();
|
||||||
editor = createSourceEditor(style);
|
|
||||||
} else {
|
function getCodeMirrorThemes() {
|
||||||
initWithSectionStyle(style);
|
if (!chrome.runtime.getPackageDirectoryEntry) {
|
||||||
document.addEventListener('wheel', scrollEntirePageOnCtrlShift);
|
const themes = [
|
||||||
|
chrome.i18n.getMessage('defaultTheme'),
|
||||||
|
/* populate-theme-start */
|
||||||
|
'3024-day',
|
||||||
|
'3024-night',
|
||||||
|
'abcdef',
|
||||||
|
'ambiance',
|
||||||
|
'ambiance-mobile',
|
||||||
|
'base16-dark',
|
||||||
|
'base16-light',
|
||||||
|
'bespin',
|
||||||
|
'blackboard',
|
||||||
|
'cobalt',
|
||||||
|
'colorforth',
|
||||||
|
'darcula',
|
||||||
|
'dracula',
|
||||||
|
'duotone-dark',
|
||||||
|
'duotone-light',
|
||||||
|
'eclipse',
|
||||||
|
'elegant',
|
||||||
|
'erlang-dark',
|
||||||
|
'gruvbox-dark',
|
||||||
|
'hopscotch',
|
||||||
|
'icecoder',
|
||||||
|
'idea',
|
||||||
|
'isotope',
|
||||||
|
'lesser-dark',
|
||||||
|
'liquibyte',
|
||||||
|
'lucario',
|
||||||
|
'material',
|
||||||
|
'mbo',
|
||||||
|
'mdn-like',
|
||||||
|
'midnight',
|
||||||
|
'monokai',
|
||||||
|
'neat',
|
||||||
|
'neo',
|
||||||
|
'night',
|
||||||
|
'oceanic-next',
|
||||||
|
'panda-syntax',
|
||||||
|
'paraiso-dark',
|
||||||
|
'paraiso-light',
|
||||||
|
'pastel-on-dark',
|
||||||
|
'railscasts',
|
||||||
|
'rubyblue',
|
||||||
|
'seti',
|
||||||
|
'shadowfox',
|
||||||
|
'solarized',
|
||||||
|
'ssms',
|
||||||
|
'the-matrix',
|
||||||
|
'tomorrow-night-bright',
|
||||||
|
'tomorrow-night-eighties',
|
||||||
|
'ttcn',
|
||||||
|
'twilight',
|
||||||
|
'vibrant-ink',
|
||||||
|
'xq-dark',
|
||||||
|
'xq-light',
|
||||||
|
'yeti',
|
||||||
|
'zenburn',
|
||||||
|
/* populate-theme-end */
|
||||||
|
];
|
||||||
|
localStorage.codeMirrorThemes = themes.join(' ');
|
||||||
|
return Promise.resolve(themes);
|
||||||
|
}
|
||||||
|
return new Promise(resolve => {
|
||||||
|
chrome.runtime.getPackageDirectoryEntry(rootDir => {
|
||||||
|
rootDir.getDirectory('vendor/codemirror/theme', {create: false}, themeDir => {
|
||||||
|
themeDir.createReader().readEntries(entries => {
|
||||||
|
const themes = [
|
||||||
|
chrome.i18n.getMessage('defaultTheme')
|
||||||
|
].concat(
|
||||||
|
entries.filter(entry => entry.isFile)
|
||||||
|
.sort((a, b) => (a.name < b.name ? -1 : 1))
|
||||||
|
.map(entry => entry.name.replace(/\.css$/, ''))
|
||||||
|
);
|
||||||
|
localStorage.codeMirrorThemes = themes.join(' ');
|
||||||
|
resolve(themes);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
function findKeyForCommand(command, map) {
|
||||||
|
if (typeof map === 'string') map = CodeMirror.keyMap[map];
|
||||||
|
let key = Object.keys(map).find(k => map[k] === command);
|
||||||
|
if (key) {
|
||||||
|
return key;
|
||||||
|
}
|
||||||
|
for (const ft of Array.isArray(map.fallthrough) ? map.fallthrough : [map.fallthrough]) {
|
||||||
|
key = ft && findKeyForCommand(command, ft);
|
||||||
|
if (key) {
|
||||||
|
return key;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildThemeElement() {
|
||||||
|
const themeElement = $('#editor.theme');
|
||||||
|
const themeList = localStorage.codeMirrorThemes;
|
||||||
|
|
||||||
|
const optionsFromArray = options => {
|
||||||
|
const fragment = document.createDocumentFragment();
|
||||||
|
options.forEach(opt => fragment.appendChild($create('option', opt)));
|
||||||
|
themeElement.appendChild(fragment);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (themeList) {
|
||||||
|
optionsFromArray(themeList.split(/\s+/));
|
||||||
|
} else {
|
||||||
|
// Chrome is starting up and shows our edit.html, but the background page isn't loaded yet
|
||||||
|
const theme = prefs.get('editor.theme');
|
||||||
|
optionsFromArray([theme === 'default' ? t('defaultTheme') : theme]);
|
||||||
|
getCodeMirrorThemes().then(() => {
|
||||||
|
const themes = (localStorage.codeMirrorThemes || '').split(/\s+/);
|
||||||
|
optionsFromArray(themes);
|
||||||
|
themeElement.selectedIndex = Math.max(0, themes.indexOf(theme));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildKeymapElement() {
|
||||||
|
// move 'pc' or 'mac' prefix to the end of the displayed label
|
||||||
|
const maps = Object.keys(CodeMirror.keyMap)
|
||||||
|
.map(name => ({
|
||||||
|
value: name,
|
||||||
|
name: name.replace(/^(pc|mac)(.+)/, (s, arch, baseName) =>
|
||||||
|
baseName.toLowerCase() + '-' + (arch === 'mac' ? 'Mac' : 'PC')),
|
||||||
|
}))
|
||||||
|
.sort((a, b) => a.name < b.name && -1 || a.name > b.name && 1);
|
||||||
|
|
||||||
|
const fragment = document.createDocumentFragment();
|
||||||
|
let bin = fragment;
|
||||||
|
let groupName;
|
||||||
|
// group suffixed maps in <optgroup>
|
||||||
|
maps.forEach(({value, name}, i) => {
|
||||||
|
groupName = !name.includes('-') ? name : groupName;
|
||||||
|
const groupWithNext = maps[i + 1] && maps[i + 1].name.startsWith(groupName);
|
||||||
|
if (groupWithNext) {
|
||||||
|
if (bin === fragment) {
|
||||||
|
bin = fragment.appendChild($create('optgroup', {label: name.split('-')[0]}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const el = bin.appendChild($create('option', {value}, name));
|
||||||
|
if (value === prefs.defaults['editor.keyMap']) {
|
||||||
|
el.dataset.default = '';
|
||||||
|
el.title = t('defaultTheme');
|
||||||
|
}
|
||||||
|
if (!groupWithNext) bin = fragment;
|
||||||
|
});
|
||||||
|
$('#editor.keyMap').appendChild(fragment);
|
||||||
|
}
|
||||||
|
|
||||||
|
function showHotkeyInTooltip(_, mapName = prefs.get('editor.keyMap')) {
|
||||||
|
const extraKeys = CodeMirror.defaults.extraKeys;
|
||||||
|
for (const el of $$('[data-hotkey-tooltip]')) {
|
||||||
|
if (el._hotkeyTooltipKeyMap !== mapName) {
|
||||||
|
el._hotkeyTooltipKeyMap = mapName;
|
||||||
|
const title = el._hotkeyTooltipTitle = el._hotkeyTooltipTitle || el.title;
|
||||||
|
const cmd = el.dataset.hotkeyTooltip;
|
||||||
|
const key = cmd[0] === '=' ? cmd.slice(1) :
|
||||||
|
findKeyForCommand(cmd, mapName) ||
|
||||||
|
extraKeys && findKeyForCommand(cmd, extraKeys);
|
||||||
|
const newTitle = title + (title && key ? '\n' : '') + (key || '');
|
||||||
|
if (el.title !== newTitle) el.title = newTitle;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function initEditor() {
|
||||||
|
return Promise.all([
|
||||||
|
initStyleData(),
|
||||||
|
onDOMready(),
|
||||||
|
prefs.initializing,
|
||||||
|
])
|
||||||
|
.then(([style]) => {
|
||||||
|
const usercss = isUsercss(style);
|
||||||
|
$('#heading').textContent = t(style.id ? 'editStyleHeading' : 'addStyleTitle');
|
||||||
|
$('#name').placeholder = t(usercss ? 'usercssEditorNamePlaceholder' : 'styleMissingName');
|
||||||
|
$('#name').title = usercss ? t('usercssReplaceTemplateName') : '';
|
||||||
|
|
||||||
|
$('#preview-label').classList.toggle('hidden', !style.id);
|
||||||
|
|
||||||
|
$('#beautify').onclick = () => beautify(editor.getEditors());
|
||||||
|
$('#lint').addEventListener('scroll', hideLintHeaderOnScroll, {passive: true});
|
||||||
|
window.addEventListener('resize', () => debounce(rememberWindowSize, 100));
|
||||||
|
editor = usercss ? createSourceEditor(style) : createSectionsEditor(style);
|
||||||
|
if (editor.ready) {
|
||||||
|
return editor.ready();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
function preinit() {
|
function preinit() {
|
||||||
// make querySelectorAll enumeration code readable
|
|
||||||
['forEach', 'some', 'indexOf', 'map'].forEach(method => {
|
|
||||||
NodeList.prototype[method] = Array.prototype[method];
|
|
||||||
});
|
|
||||||
|
|
||||||
// eslint-disable-next-line no-extend-native
|
|
||||||
Object.defineProperties(Array.prototype, {
|
|
||||||
last: {
|
|
||||||
get() {
|
|
||||||
return this[this.length - 1];
|
|
||||||
},
|
|
||||||
},
|
|
||||||
rotate: {
|
|
||||||
value: function (amount) {
|
|
||||||
// negative amount == rotate left
|
|
||||||
const r = this.slice(-amount, this.length);
|
|
||||||
Array.prototype.push.apply(r, this.slice(0, this.length - r.length));
|
|
||||||
return r;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// preload the theme so that CodeMirror can calculate its metrics in DOMContentLoaded->setupLivePrefs()
|
// preload the theme so that CodeMirror can calculate its metrics in DOMContentLoaded->setupLivePrefs()
|
||||||
new MutationObserver((mutations, observer) => {
|
new MutationObserver((mutations, observer) => {
|
||||||
const themeElement = $('#cm-theme');
|
const themeElement = $('#cm-theme');
|
||||||
|
@ -114,7 +276,7 @@ function preinit() {
|
||||||
}
|
}
|
||||||
|
|
||||||
getOwnTab().then(tab => {
|
getOwnTab().then(tab => {
|
||||||
ownTabId = tab.id;
|
const ownTabId = tab.id;
|
||||||
|
|
||||||
// use browser history back when 'back to manage' is clicked
|
// use browser history back when 'back to manage' is clicked
|
||||||
if (sessionStorageHash('manageStylesHistory').value[ownTabId] === location.href) {
|
if (sessionStorageHash('manageStylesHistory').value[ownTabId] === location.href) {
|
||||||
|
@ -156,37 +318,28 @@ function preinit() {
|
||||||
function onRuntimeMessage(request) {
|
function onRuntimeMessage(request) {
|
||||||
switch (request.method) {
|
switch (request.method) {
|
||||||
case 'styleUpdated':
|
case 'styleUpdated':
|
||||||
if (styleId && styleId === request.style.id &&
|
if (
|
||||||
request.reason !== 'editPreview' &&
|
editor.getStyleId() === request.style.id &&
|
||||||
request.reason !== 'editSave' &&
|
!['editPreview', 'editPreviewEnd', 'editSave', 'config']
|
||||||
request.reason !== 'config') {
|
.includes(request.reason)
|
||||||
// code-less style from notifyAllTabs
|
) {
|
||||||
const {sections, id} = request.style;
|
Promise.resolve(
|
||||||
((sections[0] || {}).code === null
|
request.codeIsUpdated === false ?
|
||||||
? API.getStyles({id})
|
request.style : API.getStyle(request.style.id)
|
||||||
: Promise.resolve([request.style])
|
)
|
||||||
).then(([style]) => {
|
.then(newStyle => {
|
||||||
if (isUsercss(style)) {
|
editor.replaceStyle(newStyle, request.codeIsUpdated);
|
||||||
editor.replaceStyle(style, request.codeIsUpdated);
|
});
|
||||||
} else {
|
|
||||||
initWithSectionStyle(style, request.codeIsUpdated);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 'styleDeleted':
|
case 'styleDeleted':
|
||||||
if (styleId === request.id || editor && editor.getStyle().id === request.id) {
|
if (editor.getStyleId() === request.style.id) {
|
||||||
document.removeEventListener('visibilitychange', beforeUnload);
|
document.removeEventListener('visibilitychange', beforeUnload);
|
||||||
window.onbeforeunload = null;
|
document.removeEventListener('beforeunload', beforeUnload);
|
||||||
closeCurrentTab();
|
closeCurrentTab();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 'prefChanged':
|
|
||||||
if ('editor.smartIndent' in request.prefs) {
|
|
||||||
CodeMirror.setOption('smartIndent', request.prefs['editor.smartIndent']);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case 'editDeleteText':
|
case 'editDeleteText':
|
||||||
document.execCommand('delete');
|
document.execCommand('delete');
|
||||||
break;
|
break;
|
||||||
|
@ -200,7 +353,7 @@ function onRuntimeMessage(request) {
|
||||||
* > Never add a beforeunload listener unconditionally or use it as an end-of-session signal.
|
* > Never add a beforeunload listener unconditionally or use it as an end-of-session signal.
|
||||||
* > Only add it when a user has unsaved work, and remove it as soon as that work has been saved.
|
* > Only add it when a user has unsaved work, and remove it as soon as that work has been saved.
|
||||||
*/
|
*/
|
||||||
function beforeUnload() {
|
function beforeUnload(e) {
|
||||||
if (saveSizeOnClose) rememberWindowSize();
|
if (saveSizeOnClose) rememberWindowSize();
|
||||||
const activeElement = document.activeElement;
|
const activeElement = document.activeElement;
|
||||||
if (activeElement) {
|
if (activeElement) {
|
||||||
|
@ -209,10 +362,9 @@ function beforeUnload() {
|
||||||
// refocus if unloading was canceled
|
// refocus if unloading was canceled
|
||||||
setTimeout(() => activeElement.focus());
|
setTimeout(() => activeElement.focus());
|
||||||
}
|
}
|
||||||
const isDirty = editor ? editor.isDirty() : !isCleanGlobal();
|
if (editor && editor.isDirty()) {
|
||||||
if (isDirty) {
|
|
||||||
// neither confirm() nor custom messages work in modern browsers but just in case
|
// neither confirm() nor custom messages work in modern browsers but just in case
|
||||||
return t('styleChangesNotSaved');
|
e.returnValue = t('styleChangesNotSaved');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -228,7 +380,6 @@ function initStyleData() {
|
||||||
const params = new URLSearchParams(location.search.replace(/^\?/, ''));
|
const params = new URLSearchParams(location.search.replace(/^\?/, ''));
|
||||||
const id = Number(params.get('id'));
|
const id = Number(params.get('id'));
|
||||||
const createEmptyStyle = () => ({
|
const createEmptyStyle = () => ({
|
||||||
id: null,
|
|
||||||
name: params.get('domain') ||
|
name: params.get('domain') ||
|
||||||
tryCatch(() => new URL(params.get('url-prefix')).hostname) ||
|
tryCatch(() => new URL(params.get('url-prefix')).hostname) ||
|
||||||
'',
|
'',
|
||||||
|
@ -244,15 +395,14 @@ function initStyleData() {
|
||||||
});
|
});
|
||||||
return fetchStyle()
|
return fetchStyle()
|
||||||
.then(style => {
|
.then(style => {
|
||||||
styleId = style.id;
|
if (style.id) sessionStorage.justEditedStyleId = style.id;
|
||||||
if (styleId) sessionStorage.justEditedStyleId = styleId;
|
|
||||||
// we set "usercss" class on <html> when <body> is empty
|
// we set "usercss" class on <html> when <body> is empty
|
||||||
// so there'll be no flickering of the elements that depend on it
|
// so there'll be no flickering of the elements that depend on it
|
||||||
if (isUsercss(style)) {
|
if (isUsercss(style)) {
|
||||||
document.documentElement.classList.add('usercss');
|
document.documentElement.classList.add('usercss');
|
||||||
}
|
}
|
||||||
// strip URL parameters when invoked for a non-existent id
|
// strip URL parameters when invoked for a non-existent id
|
||||||
if (!styleId) {
|
if (!style.id) {
|
||||||
history.replaceState({}, document.title, location.pathname);
|
history.replaceState({}, document.title, location.pathname);
|
||||||
}
|
}
|
||||||
return style;
|
return style;
|
||||||
|
@ -260,268 +410,12 @@ function initStyleData() {
|
||||||
|
|
||||||
function fetchStyle() {
|
function fetchStyle() {
|
||||||
if (id) {
|
if (id) {
|
||||||
return API.getStyleFromDB(id);
|
return API.getStyle(id);
|
||||||
}
|
}
|
||||||
return Promise.resolve(createEmptyStyle());
|
return Promise.resolve(createEmptyStyle());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function initHooks() {
|
|
||||||
if (initHooks.alreadyDone) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
initHooks.alreadyDone = true;
|
|
||||||
$$('#header .style-contributor').forEach(node => {
|
|
||||||
node.addEventListener('change', onChange);
|
|
||||||
node.addEventListener('input', onChange);
|
|
||||||
});
|
|
||||||
$('#to-mozilla').addEventListener('click', showMozillaFormat, false);
|
|
||||||
$('#to-mozilla-help').addEventListener('click', showToMozillaHelp, false);
|
|
||||||
$('#from-mozilla').addEventListener('click', fromMozillaFormat);
|
|
||||||
$('#save-button').addEventListener('click', save, false);
|
|
||||||
$('#sections-help').addEventListener('click', showSectionHelp, false);
|
|
||||||
|
|
||||||
if (!FIREFOX) {
|
|
||||||
$$([
|
|
||||||
'input:not([type])',
|
|
||||||
'input[type="text"]',
|
|
||||||
'input[type="search"]',
|
|
||||||
'input[type="number"]',
|
|
||||||
].join(','))
|
|
||||||
.forEach(e => e.addEventListener('mousedown', toggleContextMenuDelete));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function onChange(event) {
|
|
||||||
const node = event.target;
|
|
||||||
if ('savedValue' in node) {
|
|
||||||
const currentValue = node.type === 'checkbox' ? node.checked : node.value;
|
|
||||||
setCleanItem(node, node.savedValue === currentValue);
|
|
||||||
} else {
|
|
||||||
// the manually added section's applies-to is dirty only when the value is non-empty
|
|
||||||
setCleanItem(node, node.localName !== 'input' || !node.value.trim());
|
|
||||||
// only valid when actually saved
|
|
||||||
delete node.savedValue;
|
|
||||||
}
|
|
||||||
updateTitle();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set .dirty on stylesheet contributors that have changed
|
|
||||||
function setDirtyClass(node, isDirty) {
|
|
||||||
node.classList.toggle('dirty', isDirty);
|
|
||||||
}
|
|
||||||
|
|
||||||
function setCleanItem(node, isClean) {
|
|
||||||
if (!node.id) {
|
|
||||||
node.id = Date.now().toString(32).substr(-6);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isClean) {
|
|
||||||
delete dirty[node.id];
|
|
||||||
// code sections have .CodeMirror property
|
|
||||||
if (node.CodeMirror) {
|
|
||||||
node.savedValue = node.CodeMirror.changeGeneration();
|
|
||||||
} else {
|
|
||||||
node.savedValue = node.type === 'checkbox' ? node.checked : node.value;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
dirty[node.id] = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
setDirtyClass(node, !isClean);
|
|
||||||
}
|
|
||||||
|
|
||||||
function isCleanGlobal() {
|
|
||||||
const clean = Object.keys(dirty).length === 0;
|
|
||||||
setDirtyClass(document.body, !clean);
|
|
||||||
return clean;
|
|
||||||
}
|
|
||||||
|
|
||||||
function setCleanGlobal() {
|
|
||||||
setCleanItem($('#sections'), true);
|
|
||||||
$$('#header, #sections > .section').forEach(setCleanSection);
|
|
||||||
// forget the dirty applies-to ids from a deleted section after the style was saved
|
|
||||||
dirty = {};
|
|
||||||
}
|
|
||||||
|
|
||||||
function setCleanSection(section) {
|
|
||||||
$$('.style-contributor', section).forEach(node => setCleanItem(node, true));
|
|
||||||
setCleanItem(section, true);
|
|
||||||
updateTitle();
|
|
||||||
}
|
|
||||||
|
|
||||||
function toggleStyle() {
|
|
||||||
$('#enabled').dispatchEvent(new MouseEvent('click', {bubbles: true}));
|
|
||||||
}
|
|
||||||
|
|
||||||
function save() {
|
|
||||||
if (!validate()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
API.saveStyle({
|
|
||||||
id: styleId,
|
|
||||||
name: $('#name').value.trim(),
|
|
||||||
enabled: $('#enabled').checked,
|
|
||||||
reason: 'editSave',
|
|
||||||
sections: getSectionsHashes()
|
|
||||||
})
|
|
||||||
.then(style => {
|
|
||||||
styleId = style.id;
|
|
||||||
sessionStorage.justEditedStyleId = styleId;
|
|
||||||
setCleanGlobal();
|
|
||||||
// Go from new style URL to edit style URL
|
|
||||||
if (location.href.indexOf('id=') === -1) {
|
|
||||||
history.replaceState({}, document.title, 'edit.html?id=' + style.id);
|
|
||||||
$('#heading').textContent = t('editStyleHeading');
|
|
||||||
}
|
|
||||||
updateTitle();
|
|
||||||
$('#preview-label').classList.remove('hidden');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function validate() {
|
|
||||||
const name = $('#name').value.trim();
|
|
||||||
if (!name) {
|
|
||||||
$('#name').focus();
|
|
||||||
messageBox.alert(t('styleMissingName'));
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($$('.applies-to-list li:not(.applies-to-everything)')
|
|
||||||
.some(li => {
|
|
||||||
const type = $('[name=applies-type]', li).value;
|
|
||||||
const value = $('[name=applies-value]', li);
|
|
||||||
const rx = value.value.trim();
|
|
||||||
if (type === 'regexp' && rx && !tryRegExp(rx)) {
|
|
||||||
value.focus();
|
|
||||||
value.select();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
})) {
|
|
||||||
messageBox.alert(t('styleBadRegexp'));
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateTitle() {
|
|
||||||
const name = $('#name').savedValue;
|
|
||||||
const clean = isCleanGlobal();
|
|
||||||
const title = styleId === null ? t('addStyleTitle') : name;
|
|
||||||
document.title = (clean ? '' : '* ') + title;
|
|
||||||
window.onbeforeunload = clean ? null : beforeUnload;
|
|
||||||
$('#save-button').disabled = clean;
|
|
||||||
}
|
|
||||||
|
|
||||||
function showMozillaFormat() {
|
|
||||||
const popup = showCodeMirrorPopup(t('styleToMozillaFormatTitle'), '', {readOnly: true});
|
|
||||||
popup.codebox.setValue(toMozillaFormat());
|
|
||||||
popup.codebox.execCommand('selectAll');
|
|
||||||
}
|
|
||||||
|
|
||||||
function toMozillaFormat() {
|
|
||||||
return sectionsToMozFormat({sections: getSectionsHashes()});
|
|
||||||
}
|
|
||||||
|
|
||||||
function fromMozillaFormat() {
|
|
||||||
const popup = showCodeMirrorPopup(t('styleFromMozillaFormatPrompt'),
|
|
||||||
$create('.buttons', [
|
|
||||||
$create('button', {
|
|
||||||
name: 'import-replace',
|
|
||||||
textContent: t('importReplaceLabel'),
|
|
||||||
title: 'Ctrl-Shift-Enter:\n' + t('importReplaceTooltip'),
|
|
||||||
onclick: () => doImport({replaceOldStyle: true}),
|
|
||||||
}),
|
|
||||||
$create('button', {
|
|
||||||
name: 'import-append',
|
|
||||||
textContent: t('importAppendLabel'),
|
|
||||||
title: 'Ctrl-Enter:\n' + t('importAppendTooltip'),
|
|
||||||
onclick: doImport,
|
|
||||||
}),
|
|
||||||
]));
|
|
||||||
const contents = $('.contents', popup);
|
|
||||||
contents.insertBefore(popup.codebox.display.wrapper, contents.firstElementChild);
|
|
||||||
popup.codebox.focus();
|
|
||||||
popup.codebox.on('changes', cm => {
|
|
||||||
popup.classList.toggle('ready', !cm.isBlank());
|
|
||||||
cm.markClean();
|
|
||||||
});
|
|
||||||
// overwrite default extraKeys as those are inapplicable in popup context
|
|
||||||
popup.codebox.options.extraKeys = {
|
|
||||||
'Ctrl-Enter': doImport,
|
|
||||||
'Shift-Ctrl-Enter': () => doImport({replaceOldStyle: true}),
|
|
||||||
};
|
|
||||||
|
|
||||||
function doImport({replaceOldStyle = false}) {
|
|
||||||
lockPageUI(true);
|
|
||||||
editorWorker.parseMozFormat({code: popup.codebox.getValue().trim()})
|
|
||||||
.then(({sections, errors}) => {
|
|
||||||
// shouldn't happen but just in case
|
|
||||||
if (!sections.length && errors.length) {
|
|
||||||
return Promise.reject(errors);
|
|
||||||
}
|
|
||||||
// show the errors in case linting is disabled or stylelint misses what csslint has found
|
|
||||||
if (errors.length && prefs.get('editor.linter') !== 'csslint') {
|
|
||||||
showError(errors);
|
|
||||||
}
|
|
||||||
removeOldSections(replaceOldStyle);
|
|
||||||
return addSections(sections, div => setCleanItem(div, false));
|
|
||||||
})
|
|
||||||
.then(() => {
|
|
||||||
$('.dismiss').dispatchEvent(new Event('click'));
|
|
||||||
})
|
|
||||||
.catch(showError)
|
|
||||||
.then(() => lockPageUI(false));
|
|
||||||
}
|
|
||||||
|
|
||||||
function removeOldSections(removeAll) {
|
|
||||||
let toRemove;
|
|
||||||
if (removeAll) {
|
|
||||||
toRemove = editors.slice().reverse();
|
|
||||||
} else if (editors.last.isBlank() && $('.applies-to-everything', editors.last.getSection())) {
|
|
||||||
toRemove = [editors.last];
|
|
||||||
} else {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
toRemove.forEach(cm => removeSection({target: cm.getSection()}));
|
|
||||||
}
|
|
||||||
|
|
||||||
function lockPageUI(locked) {
|
|
||||||
document.documentElement.style.pointerEvents = locked ? 'none' : '';
|
|
||||||
if (popup.codebox) {
|
|
||||||
popup.classList.toggle('ready', locked ? false : !popup.codebox.isBlank());
|
|
||||||
popup.codebox.options.readOnly = locked;
|
|
||||||
popup.codebox.display.wrapper.style.opacity = locked ? '.5' : '';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function showError(errors) {
|
|
||||||
messageBox({
|
|
||||||
className: 'center danger',
|
|
||||||
title: t('styleFromMozillaFormatError'),
|
|
||||||
contents: $create('pre', Array.isArray(errors) ? errors.join('\n') : errors),
|
|
||||||
buttons: [t('confirmClose')],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function showSectionHelp(event) {
|
|
||||||
event.preventDefault();
|
|
||||||
showHelp(t('styleSectionsTitle'), t('sectionHelp'));
|
|
||||||
}
|
|
||||||
|
|
||||||
function showAppliesToHelp(event) {
|
|
||||||
event.preventDefault();
|
|
||||||
showHelp(t('appliesLabel'), t('appliesHelp'));
|
|
||||||
}
|
|
||||||
|
|
||||||
function showToMozillaHelp(event) {
|
|
||||||
event.preventDefault();
|
|
||||||
showHelp(t('styleMozillaFormatHeading'), t('styleToMozillaFormatHelp'));
|
|
||||||
}
|
|
||||||
|
|
||||||
function showHelp(title = '', body) {
|
function showHelp(title = '', body) {
|
||||||
const div = $('#help-popup');
|
const div = $('#help-popup');
|
||||||
div.className = '';
|
div.className = '';
|
||||||
|
@ -594,7 +488,7 @@ function showCodeMirrorPopup(title, html, options) {
|
||||||
keyMap: prefs.get('editor.keyMap')
|
keyMap: prefs.get('editor.keyMap')
|
||||||
}, options));
|
}, options));
|
||||||
cm.focus();
|
cm.focus();
|
||||||
cm.rerouteHotkeys(false);
|
rerouteHotkeys(false);
|
||||||
|
|
||||||
document.documentElement.style.pointerEvents = 'none';
|
document.documentElement.style.pointerEvents = 'none';
|
||||||
popup.style.pointerEvents = 'auto';
|
popup.style.pointerEvents = 'auto';
|
||||||
|
@ -613,36 +507,13 @@ function showCodeMirrorPopup(title, html, options) {
|
||||||
window.removeEventListener('closeHelp', _);
|
window.removeEventListener('closeHelp', _);
|
||||||
window.removeEventListener('keydown', onKeyDown, true);
|
window.removeEventListener('keydown', onKeyDown, true);
|
||||||
document.documentElement.style.removeProperty('pointer-events');
|
document.documentElement.style.removeProperty('pointer-events');
|
||||||
cm.rerouteHotkeys(true);
|
rerouteHotkeys(true);
|
||||||
cm = popup.codebox = null;
|
cm = popup.codebox = null;
|
||||||
});
|
});
|
||||||
|
|
||||||
return popup;
|
return popup;
|
||||||
}
|
}
|
||||||
|
|
||||||
function setGlobalProgress(done, total) {
|
|
||||||
const progressElement = $('#global-progress') ||
|
|
||||||
total && document.body.appendChild($create('#global-progress'));
|
|
||||||
if (total) {
|
|
||||||
const progress = (done / Math.max(done, total) * 100).toFixed(1);
|
|
||||||
progressElement.style.borderLeftWidth = progress + 'vw';
|
|
||||||
setTimeout(() => {
|
|
||||||
progressElement.title = progress + '%';
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
$.remove(progressElement);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function scrollEntirePageOnCtrlShift(event) {
|
|
||||||
// make Shift-Ctrl-Wheel scroll entire page even when mouse is over a code editor
|
|
||||||
if (event.shiftKey && event.ctrlKey && !event.altKey && !event.metaKey) {
|
|
||||||
// Chrome scrolls horizontally when Shift is pressed but on some PCs this might be different
|
|
||||||
window.scrollBy(0, event.deltaX || event.deltaY);
|
|
||||||
event.preventDefault();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function hideLintHeaderOnScroll() {
|
function hideLintHeaderOnScroll() {
|
||||||
// workaround part2 for the <details> not showing its toggle icon: hide <summary> on scroll
|
// workaround part2 for the <details> not showing its toggle icon: hide <summary> on scroll
|
||||||
const newOpacity = this.scrollTop === 0 ? '' : '0';
|
const newOpacity = this.scrollTop === 0 ? '' : '0';
|
||||||
|
|
|
@ -1,118 +0,0 @@
|
||||||
/* global importScripts parseMozFormat parserlib CSSLint require */
|
|
||||||
'use strict';
|
|
||||||
|
|
||||||
createAPI({
|
|
||||||
csslint: (code, config) => {
|
|
||||||
loadParserLib();
|
|
||||||
loadScript(['/vendor-overwrites/csslint/csslint.js']);
|
|
||||||
return CSSLint.verify(code, config).messages
|
|
||||||
.map(m => Object.assign(m, {rule: {id: m.rule.id}}));
|
|
||||||
},
|
|
||||||
stylelint: (code, config) => {
|
|
||||||
loadScript(['/vendor/stylelint-bundle/stylelint-bundle.min.js']);
|
|
||||||
return require('stylelint').lint({code, config});
|
|
||||||
},
|
|
||||||
parseMozFormat: data => {
|
|
||||||
loadParserLib();
|
|
||||||
loadScript(['/js/moz-parser.js']);
|
|
||||||
return parseMozFormat(data);
|
|
||||||
},
|
|
||||||
getStylelintRules,
|
|
||||||
getCsslintRules
|
|
||||||
});
|
|
||||||
|
|
||||||
function getCsslintRules() {
|
|
||||||
loadScript(['/vendor-overwrites/csslint/csslint.js']);
|
|
||||||
return CSSLint.getRules().map(rule => {
|
|
||||||
const output = {};
|
|
||||||
for (const [key, value] of Object.entries(rule)) {
|
|
||||||
if (typeof value !== 'function') {
|
|
||||||
output[key] = value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return output;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function getStylelintRules() {
|
|
||||||
loadScript(['/vendor/stylelint-bundle/stylelint-bundle.min.js']);
|
|
||||||
const stylelint = require('stylelint');
|
|
||||||
const options = {};
|
|
||||||
const rxPossible = /\bpossible:("(?:[^"]*?)"|\[(?:[^\]]*?)\]|\{(?:[^}]*?)\})/g;
|
|
||||||
const rxString = /"([-\w\s]{3,}?)"/g;
|
|
||||||
for (const id of Object.keys(stylelint.rules)) {
|
|
||||||
const ruleCode = String(stylelint.rules[id]);
|
|
||||||
const sets = [];
|
|
||||||
let m, mStr;
|
|
||||||
while ((m = rxPossible.exec(ruleCode))) {
|
|
||||||
const possible = m[1];
|
|
||||||
const set = [];
|
|
||||||
while ((mStr = rxString.exec(possible))) {
|
|
||||||
const s = mStr[1];
|
|
||||||
if (s.includes(' ')) {
|
|
||||||
set.push(...s.split(/\s+/));
|
|
||||||
} else {
|
|
||||||
set.push(s);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (possible.includes('ignoreAtRules')) {
|
|
||||||
set.push('ignoreAtRules');
|
|
||||||
}
|
|
||||||
if (possible.includes('ignoreShorthands')) {
|
|
||||||
set.push('ignoreShorthands');
|
|
||||||
}
|
|
||||||
if (set.length) {
|
|
||||||
sets.push(set);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (sets.length) {
|
|
||||||
options[id] = sets;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return options;
|
|
||||||
}
|
|
||||||
|
|
||||||
function loadParserLib() {
|
|
||||||
if (typeof parserlib !== 'undefined') {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
importScripts('/vendor-overwrites/csslint/parserlib.js');
|
|
||||||
parserlib.css.Tokens[parserlib.css.Tokens.COMMENT].hide = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const loadedUrls = new Set();
|
|
||||||
function loadScript(urls) {
|
|
||||||
urls = urls.filter(u => !loadedUrls.has(u));
|
|
||||||
importScripts(...urls);
|
|
||||||
urls.forEach(u => loadedUrls.add(u));
|
|
||||||
}
|
|
||||||
|
|
||||||
function createAPI(methods) {
|
|
||||||
self.onmessage = e => {
|
|
||||||
const message = e.data;
|
|
||||||
Promise.resolve()
|
|
||||||
.then(() => methods[message.action](...message.args))
|
|
||||||
.then(result => ({
|
|
||||||
id: message.id,
|
|
||||||
error: false,
|
|
||||||
data: result
|
|
||||||
}))
|
|
||||||
.catch(err => ({
|
|
||||||
id: message.id,
|
|
||||||
error: true,
|
|
||||||
data: cloneError(err)
|
|
||||||
}))
|
|
||||||
.then(data => self.postMessage(data));
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function cloneError(err) {
|
|
||||||
return Object.assign({
|
|
||||||
name: err.name,
|
|
||||||
stack: err.stack,
|
|
||||||
message: err.message,
|
|
||||||
lineNumber: err.lineNumber,
|
|
||||||
columnNumber: err.columnNumber,
|
|
||||||
fileName: err.fileName
|
|
||||||
}, err);
|
|
||||||
}
|
|
|
@ -1,39 +1,88 @@
|
||||||
|
/* global importScripts workerUtil CSSLint require metaParser */
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
// eslint-disable-next-line no-var
|
importScripts('/js/worker-util.js');
|
||||||
var editorWorker = (() => {
|
const {createAPI, loadScript} = workerUtil;
|
||||||
let worker;
|
|
||||||
return new Proxy({}, {
|
createAPI({
|
||||||
get: (target, prop) =>
|
csslint: (code, config) => {
|
||||||
(...args) => {
|
loadScript('/vendor-overwrites/csslint/parserlib.js', '/vendor-overwrites/csslint/csslint.js');
|
||||||
if (!worker) {
|
return CSSLint.verify(code, config).messages
|
||||||
worker = createWorker();
|
.map(m => Object.assign(m, {rule: {id: m.rule.id}}));
|
||||||
}
|
},
|
||||||
return worker.invoke(prop, args);
|
stylelint: (code, config) => {
|
||||||
|
loadScript('/vendor/stylelint-bundle/stylelint-bundle.min.js');
|
||||||
|
return require('stylelint').lint({code, config});
|
||||||
|
},
|
||||||
|
metalint: code => {
|
||||||
|
loadScript(
|
||||||
|
'/vendor/usercss-meta/usercss-meta.min.js',
|
||||||
|
'/vendor-overwrites/colorpicker/colorconverter.js',
|
||||||
|
'/js/meta-parser.js'
|
||||||
|
);
|
||||||
|
const result = metaParser.lint(code);
|
||||||
|
// extract needed info
|
||||||
|
result.errors = result.errors.map(err =>
|
||||||
|
({
|
||||||
|
code: err.code,
|
||||||
|
args: err.args,
|
||||||
|
message: err.message,
|
||||||
|
index: err.index
|
||||||
|
})
|
||||||
|
);
|
||||||
|
return result;
|
||||||
|
},
|
||||||
|
getStylelintRules,
|
||||||
|
getCsslintRules
|
||||||
|
});
|
||||||
|
|
||||||
|
function getCsslintRules() {
|
||||||
|
loadScript('/vendor-overwrites/csslint/csslint.js');
|
||||||
|
return CSSLint.getRules().map(rule => {
|
||||||
|
const output = {};
|
||||||
|
for (const [key, value] of Object.entries(rule)) {
|
||||||
|
if (typeof value !== 'function') {
|
||||||
|
output[key] = value;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
return output;
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function createWorker() {
|
function getStylelintRules() {
|
||||||
let id = 0;
|
loadScript('/vendor/stylelint-bundle/stylelint-bundle.min.js');
|
||||||
const pendingResponse = new Map();
|
const stylelint = require('stylelint');
|
||||||
const worker = new Worker('/edit/editor-worker-body.js');
|
const options = {};
|
||||||
worker.onmessage = e => {
|
const rxPossible = /\bpossible:("(?:[^"]*?)"|\[(?:[^\]]*?)\]|\{(?:[^}]*?)\})/g;
|
||||||
const message = e.data;
|
const rxString = /"([-\w\s]{3,}?)"/g;
|
||||||
pendingResponse.get(message.id)[message.error ? 'reject' : 'resolve'](message.data);
|
for (const id of Object.keys(stylelint.rules)) {
|
||||||
pendingResponse.delete(message.id);
|
const ruleCode = String(stylelint.rules[id]);
|
||||||
};
|
const sets = [];
|
||||||
return {invoke};
|
let m, mStr;
|
||||||
|
while ((m = rxPossible.exec(ruleCode))) {
|
||||||
function invoke(action, args) {
|
const possible = m[1];
|
||||||
return new Promise((resolve, reject) => {
|
const set = [];
|
||||||
pendingResponse.set(id, {resolve, reject});
|
while ((mStr = rxString.exec(possible))) {
|
||||||
worker.postMessage({
|
const s = mStr[1];
|
||||||
id,
|
if (s.includes(' ')) {
|
||||||
action,
|
set.push(...s.split(/\s+/));
|
||||||
args
|
} else {
|
||||||
});
|
set.push(s);
|
||||||
id++;
|
}
|
||||||
});
|
}
|
||||||
|
if (possible.includes('ignoreAtRules')) {
|
||||||
|
set.push('ignoreAtRules');
|
||||||
|
}
|
||||||
|
if (possible.includes('ignoreShorthands')) {
|
||||||
|
set.push('ignoreShorthands');
|
||||||
|
}
|
||||||
|
if (set.length) {
|
||||||
|
sets.push(set);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (sets.length) {
|
||||||
|
options[id] = sets;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})();
|
return options;
|
||||||
|
}
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
/* global CodeMirror editors makeSectionVisible */
|
/* global CodeMirror focusAccessibility colorMimicry editor
|
||||||
/* global focusAccessibility */
|
onDOMready $ $$ $create t debounce tryRegExp stringAsRegExp template */
|
||||||
/* global colorMimicry */
|
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
onDOMready().then(() => {
|
onDOMready().then(() => {
|
||||||
|
@ -207,12 +206,12 @@ onDOMready().then(() => {
|
||||||
}
|
}
|
||||||
const cmFocused = document.activeElement && document.activeElement.closest('.CodeMirror');
|
const cmFocused = document.activeElement && document.activeElement.closest('.CodeMirror');
|
||||||
state.activeAppliesTo = $(`.${APPLIES_VALUE_CLASS}:focus, .${APPLIES_VALUE_CLASS}.${TARGET_CLASS}`);
|
state.activeAppliesTo = $(`.${APPLIES_VALUE_CLASS}:focus, .${APPLIES_VALUE_CLASS}.${TARGET_CLASS}`);
|
||||||
state.cmStart = CodeMirror.closestVisible(
|
state.cmStart = editor.closestVisible(
|
||||||
cmFocused && document.activeElement ||
|
cmFocused && document.activeElement ||
|
||||||
state.activeAppliesTo ||
|
state.activeAppliesTo ||
|
||||||
state.cm);
|
state.cm);
|
||||||
const cmExtra = $('body > :not(#sections) .CodeMirror');
|
const cmExtra = $('body > :not(#sections) .CodeMirror');
|
||||||
state.editors = cmExtra ? [cmExtra.CodeMirror] : editors;
|
state.editors = cmExtra ? [cmExtra.CodeMirror] : editor.getEditors();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -291,7 +290,7 @@ onDOMready().then(() => {
|
||||||
|
|
||||||
function doSearchInApplies(cm, canAdvance) {
|
function doSearchInApplies(cm, canAdvance) {
|
||||||
if (!state.searchInApplies) return;
|
if (!state.searchInApplies) return;
|
||||||
const inputs = [...cm.getSection().getElementsByClassName(APPLIES_VALUE_CLASS)];
|
const inputs = editor.getSearchableInputs(cm);
|
||||||
if (state.reverse) inputs.reverse();
|
if (state.reverse) inputs.reverse();
|
||||||
inputs.splice(0, inputs.indexOf(state.activeAppliesTo));
|
inputs.splice(0, inputs.indexOf(state.activeAppliesTo));
|
||||||
for (const input of inputs) {
|
for (const input of inputs) {
|
||||||
|
@ -314,7 +313,7 @@ onDOMready().then(() => {
|
||||||
});
|
});
|
||||||
const canFocus = !state.dialog || !state.dialog.contains(document.activeElement);
|
const canFocus = !state.dialog || !state.dialog.contains(document.activeElement);
|
||||||
makeTargetVisible(!canFocus && input);
|
makeTargetVisible(!canFocus && input);
|
||||||
makeSectionVisible(cm);
|
editor.scrollToEditor(cm);
|
||||||
if (canFocus) input.focus();
|
if (canFocus) input.focus();
|
||||||
state.cm = cm;
|
state.cm = cm;
|
||||||
clearMarker();
|
clearMarker();
|
||||||
|
@ -778,7 +777,7 @@ onDOMready().then(() => {
|
||||||
cm.scrollIntoView(searchCursor.pos, SCROLL_REVEAL_MIN_PX);
|
cm.scrollIntoView(searchCursor.pos, SCROLL_REVEAL_MIN_PX);
|
||||||
|
|
||||||
// scroll to the editor itself
|
// scroll to the editor itself
|
||||||
makeSectionVisible(cm);
|
editor.scrollToEditor(cm);
|
||||||
|
|
||||||
// focus or expose as the current search target
|
// focus or expose as the current search target
|
||||||
clearMarker();
|
clearMarker();
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
/* global memoize editorWorker showCodeMirrorPopup loadScript messageBox LINTER_DEFAULTS*/
|
/* global memoize editorWorker showCodeMirrorPopup loadScript messageBox
|
||||||
|
LINTER_DEFAULTS rerouteHotkeys $ $create $createLink tryJSONparse t
|
||||||
|
chromeSync */
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
(() => {
|
(() => {
|
||||||
|
@ -50,10 +52,10 @@
|
||||||
});
|
});
|
||||||
cm.on('changes', updateButtonState);
|
cm.on('changes', updateButtonState);
|
||||||
|
|
||||||
cm.rerouteHotkeys(false);
|
rerouteHotkeys(false);
|
||||||
window.addEventListener('closeHelp', function _() {
|
window.addEventListener('closeHelp', function _() {
|
||||||
window.removeEventListener('closeHelp', _);
|
window.removeEventListener('closeHelp', _);
|
||||||
cm.rerouteHotkeys(true);
|
rerouteHotkeys(true);
|
||||||
cm = null;
|
cm = null;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
|
/* exported LINTER_DEFAULTS */
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
// eslint-disable-next-line no-var
|
const LINTER_DEFAULTS = (() => {
|
||||||
var LINTER_DEFAULTS = (() => {
|
|
||||||
const SEVERITY = {severity: 'warning'};
|
const SEVERITY = {severity: 'warning'};
|
||||||
const STYLELINT = {
|
const STYLELINT = {
|
||||||
// 'sugarss' is a indent-based syntax like Sass or Stylus
|
// 'sugarss' is a indent-based syntax like Sass or Stylus
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
/* global LINTER_DEFAULTS linter editorWorker */
|
/* global LINTER_DEFAULTS linter editorWorker prefs chromeSync */
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
(() => {
|
(() => {
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
/* global showHelp editorWorker memoize */
|
/* global showHelp editorWorker memoize $ $create $createLink t */
|
||||||
|
/* exported createLinterHelpDialog */
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
function createLinterHelpDialog(getIssues) {
|
function createLinterHelpDialog(getIssues) {
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
/* global linter */
|
/* global linter editorWorker */
|
||||||
|
/* exported createMetaCompiler */
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
function createMetaCompiler(cm) {
|
function createMetaCompiler(cm) {
|
||||||
|
@ -18,25 +19,23 @@ function createMetaCompiler(cm) {
|
||||||
if (match[0] === meta && match.index === metaIndex) {
|
if (match[0] === meta && match.index === metaIndex) {
|
||||||
return cache;
|
return cache;
|
||||||
}
|
}
|
||||||
return API.parseUsercss({sourceCode: match[0], metaOnly: true})
|
return editorWorker.metalint(match[0])
|
||||||
.then(result => result.usercssData)
|
.then(({metadata, errors}) => {
|
||||||
.then(result => {
|
if (errors.every(err => err.code === 'unknownMeta')) {
|
||||||
for (const cb of updateListeners) {
|
for (const cb of updateListeners) {
|
||||||
cb(result);
|
cb(metadata);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
cache = errors.map(err =>
|
||||||
|
({
|
||||||
|
from: cm.posFromIndex((err.index || 0) + match.index),
|
||||||
|
to: cm.posFromIndex((err.index || 0) + match.index),
|
||||||
|
message: err.code && chrome.i18n.getMessage(`meta_${err.code}`, err.args) || err.message,
|
||||||
|
severity: err.code === 'unknownMeta' ? 'warning' : 'error'
|
||||||
|
})
|
||||||
|
);
|
||||||
meta = match[0];
|
meta = match[0];
|
||||||
metaIndex = match.index;
|
metaIndex = match.index;
|
||||||
cache = [];
|
|
||||||
return cache;
|
|
||||||
}, err => {
|
|
||||||
meta = match[0];
|
|
||||||
metaIndex = match.index;
|
|
||||||
cache = [{
|
|
||||||
from: cm.posFromIndex((err.index || 0) + match.index),
|
|
||||||
to: cm.posFromIndex((err.index || 0) + match.index),
|
|
||||||
message: err.message,
|
|
||||||
severity: 'error'
|
|
||||||
}];
|
|
||||||
return cache;
|
return cache;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
/* global linter editors clipString createLinterHelpDialog makeSectionVisible */
|
/* global linter editor clipString createLinterHelpDialog $ $create */
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
// eslint-disable-next-line no-var
|
|
||||||
Object.assign(linter, (() => {
|
Object.assign(linter, (() => {
|
||||||
const tables = new Map();
|
const tables = new Map();
|
||||||
const helpDialog = createLinterHelpDialog(getIssues);
|
const helpDialog = createLinterHelpDialog(getIssues);
|
||||||
|
@ -16,12 +15,8 @@ Object.assign(linter, (() => {
|
||||||
table = createTable(cm);
|
table = createTable(cm);
|
||||||
tables.set(cm, table);
|
tables.set(cm, table);
|
||||||
const container = $('.lint-report-container');
|
const container = $('.lint-report-container');
|
||||||
if (typeof editor === 'object') {
|
const nextSibling = findNextSibling(tables, cm);
|
||||||
container.append(table.element);
|
container.insertBefore(table.element, nextSibling && tables.get(nextSibling).element);
|
||||||
} else {
|
|
||||||
const nextSibling = findNextSibling(tables, cm);
|
|
||||||
container.insertBefore(table.element, nextSibling && tables.get(nextSibling).element);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
table.updateCaption();
|
table.updateCaption();
|
||||||
table.updateAnnotations(annotations);
|
table.updateAnnotations(annotations);
|
||||||
|
@ -57,6 +52,7 @@ Object.assign(linter, (() => {
|
||||||
}
|
}
|
||||||
|
|
||||||
function findNextSibling(tables, cm) {
|
function findNextSibling(tables, cm) {
|
||||||
|
const editors = editor.getEditors();
|
||||||
let i = editors.indexOf(cm) + 1;
|
let i = editors.indexOf(cm) + 1;
|
||||||
while (i < editors.length) {
|
while (i < editors.length) {
|
||||||
if (tables.has(editors[i])) {
|
if (tables.has(editors[i])) {
|
||||||
|
@ -85,8 +81,7 @@ Object.assign(linter, (() => {
|
||||||
};
|
};
|
||||||
|
|
||||||
function updateCaption() {
|
function updateCaption() {
|
||||||
caption.textContent = typeof editor === 'object' ?
|
caption.textContent = editor.getEditorTitle(cm);
|
||||||
'' : `${t('sectionCode')} ${editors.indexOf(cm) + 1}`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateAnnotations(lines) {
|
function updateAnnotations(lines) {
|
||||||
|
@ -158,7 +153,7 @@ Object.assign(linter, (() => {
|
||||||
}
|
}
|
||||||
|
|
||||||
function gotoLintIssue(cm, anno) {
|
function gotoLintIssue(cm, anno) {
|
||||||
makeSectionVisible(cm);
|
editor.scrollToEditor(cm);
|
||||||
cm.focus();
|
cm.focus();
|
||||||
cm.setSelection(anno.from);
|
cm.setSelection(anno.from);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
|
/* global prefs */
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
// eslint-disable-next-line no-var
|
/* exported linter */
|
||||||
var linter = (() => {
|
const linter = (() => {
|
||||||
const lintingUpdatedListeners = [];
|
const lintingUpdatedListeners = [];
|
||||||
const unhookListeners = [];
|
const unhookListeners = [];
|
||||||
const linters = [];
|
const linters = [];
|
||||||
|
|
73
edit/live-preview.js
Normal file
73
edit/live-preview.js
Normal file
|
@ -0,0 +1,73 @@
|
||||||
|
/* global messageBox editor $ prefs */
|
||||||
|
/* exported createLivePreview */
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
function createLivePreview(preprocess) {
|
||||||
|
let data;
|
||||||
|
let previewer;
|
||||||
|
let enabled = prefs.get('editor.livePreview');
|
||||||
|
const label = $('#preview-label');
|
||||||
|
const errorContainer = $('#preview-errors');
|
||||||
|
|
||||||
|
prefs.subscribe(['editor.livePreview'], (key, value) => {
|
||||||
|
if (value && data && data.id && data.enabled) {
|
||||||
|
previewer = createPreviewer();
|
||||||
|
previewer.update(data);
|
||||||
|
}
|
||||||
|
if (!value && previewer) {
|
||||||
|
previewer.disconnect();
|
||||||
|
previewer = null;
|
||||||
|
}
|
||||||
|
enabled = value;
|
||||||
|
});
|
||||||
|
return {update, show};
|
||||||
|
|
||||||
|
function show(state) {
|
||||||
|
label.classList.toggle('hidden', !state);
|
||||||
|
}
|
||||||
|
|
||||||
|
function update(_data) {
|
||||||
|
data = _data;
|
||||||
|
if (!previewer) {
|
||||||
|
if (!data.id || !data.enabled || !enabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
previewer = createPreviewer();
|
||||||
|
}
|
||||||
|
previewer.update(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
function createPreviewer() {
|
||||||
|
const port = chrome.runtime.connect({
|
||||||
|
name: 'livePreview'
|
||||||
|
});
|
||||||
|
port.onDisconnect.addListener(err => {
|
||||||
|
throw err;
|
||||||
|
});
|
||||||
|
return {update, disconnect};
|
||||||
|
|
||||||
|
function update(data) {
|
||||||
|
Promise.resolve()
|
||||||
|
.then(() => preprocess ? preprocess(data) : data)
|
||||||
|
.then(data => port.postMessage(data))
|
||||||
|
.then(
|
||||||
|
() => errorContainer.classList.add('hidden'),
|
||||||
|
err => {
|
||||||
|
if (Array.isArray(err)) {
|
||||||
|
err = err.join('\n');
|
||||||
|
} else if (err && err.index !== undefined) {
|
||||||
|
// FIXME: this would fail if editors[0].getValue() !== data.sourceCode
|
||||||
|
const pos = editor.getEditors()[0].posFromIndex(err.index);
|
||||||
|
err.message = `${pos.line}:${pos.ch} ${err.message || String(err)}`;
|
||||||
|
}
|
||||||
|
errorContainer.classList.remove('hidden');
|
||||||
|
errorContainer.onclick = () => messageBox.alert(err.message || String(err), 'pre');
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function disconnect() {
|
||||||
|
port.disconnect();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
/* global CodeMirror */
|
/* global CodeMirror prefs */
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
(() => {
|
(() => {
|
||||||
|
|
27
edit/refresh-on-view.js
Normal file
27
edit/refresh-on-view.js
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
/* global CodeMirror */
|
||||||
|
/*
|
||||||
|
Initialization of the multi-sections editor is slow if there are many editors
|
||||||
|
e.g. https://github.com/openstyles/stylus/issues/178. So we only refresh the
|
||||||
|
editor when they were scroll into view.
|
||||||
|
*/
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
CodeMirror.defineExtension('refreshOnView', function () {
|
||||||
|
const cm = this;
|
||||||
|
if (typeof IntersectionObserver === 'undefined') {
|
||||||
|
// uh
|
||||||
|
cm.refresh();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const wrapper = cm.display.wrapper;
|
||||||
|
const observer = new IntersectionObserver(entries => {
|
||||||
|
for (const entry of entries) {
|
||||||
|
if (entry.isIntersecting) {
|
||||||
|
// wrapper.style.visibility = 'visible';
|
||||||
|
cm.refresh();
|
||||||
|
observer.disconnect();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
observer.observe(wrapper);
|
||||||
|
});
|
|
@ -1,8 +1,8 @@
|
||||||
/* global showHelp */
|
/* global showHelp $ $create tryRegExp queryTabs URLS t template openURL */
|
||||||
|
/* exported regExpTester */
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
// eslint-disable-next-line no-var
|
const regExpTester = (() => {
|
||||||
var regExpTester = (() => {
|
|
||||||
const GET_FAVICON_URL = 'https://www.google.com/s2/favicons?domain=';
|
const GET_FAVICON_URL = 'https://www.google.com/s2/favicons?domain=';
|
||||||
const OWN_ICON = chrome.runtime.getManifest().icons['16'];
|
const OWN_ICON = chrome.runtime.getManifest().icons['16'];
|
||||||
const cachedRegexps = new Map();
|
const cachedRegexps = new Map();
|
||||||
|
@ -58,7 +58,7 @@ var regExpTester = (() => {
|
||||||
const rxData = Object.assign({text}, cachedRegexps.get(text));
|
const rxData = Object.assign({text}, cachedRegexps.get(text));
|
||||||
if (!rxData.urls) {
|
if (!rxData.urls) {
|
||||||
cachedRegexps.set(text, Object.assign(rxData, {
|
cachedRegexps.set(text, Object.assign(rxData, {
|
||||||
// imitate buggy Stylish-for-chrome, see detectSloppyRegexps()
|
// imitate buggy Stylish-for-chrome
|
||||||
rx: tryRegExp('^' + text + '$'),
|
rx: tryRegExp('^' + text + '$'),
|
||||||
urls: new Map(),
|
urls: new Map(),
|
||||||
}));
|
}));
|
||||||
|
|
48
edit/reroute-hotkeys.js
Normal file
48
edit/reroute-hotkeys.js
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
/* global CodeMirror editor debounce */
|
||||||
|
/* exported rerouteHotkeys */
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const rerouteHotkeys = (() => {
|
||||||
|
// reroute handling to nearest editor when keypress resolves to one of these commands
|
||||||
|
const REROUTED = new Set([
|
||||||
|
'save',
|
||||||
|
'toggleStyle',
|
||||||
|
'jumpToLine',
|
||||||
|
'nextEditor', 'prevEditor',
|
||||||
|
'toggleEditorFocus',
|
||||||
|
'find', 'findNext', 'findPrev', 'replace', 'replaceAll',
|
||||||
|
'colorpicker',
|
||||||
|
]);
|
||||||
|
|
||||||
|
return rerouteHotkeys;
|
||||||
|
|
||||||
|
// note that this function relies on `editor`. Calling this function before
|
||||||
|
// the editor is initialized may throw an error.
|
||||||
|
function rerouteHotkeys(enable, immediately) {
|
||||||
|
if (!immediately) {
|
||||||
|
debounce(rerouteHotkeys, 0, enable, true);
|
||||||
|
} else if (enable) {
|
||||||
|
document.addEventListener('keydown', rerouteHandler);
|
||||||
|
} else {
|
||||||
|
document.removeEventListener('keydown', rerouteHandler);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function rerouteHandler(event) {
|
||||||
|
const keyName = CodeMirror.keyName(event);
|
||||||
|
if (!keyName) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const rerouteCommand = name => {
|
||||||
|
if (REROUTED.has(name)) {
|
||||||
|
CodeMirror.commands[name](editor.closestVisible(event.target));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if (CodeMirror.lookupKey(keyName, CodeMirror.defaults.keyMap, rerouteCommand) === 'handled' ||
|
||||||
|
CodeMirror.lookupKey(keyName, CodeMirror.defaults.extraKeys, rerouteCommand) === 'handled') {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})();
|
409
edit/sections-editor-section.js
Normal file
409
edit/sections-editor-section.js
Normal file
|
@ -0,0 +1,409 @@
|
||||||
|
/* global template cmFactory $ propertyToCss CssToProperty linter regExpTester
|
||||||
|
FIREFOX toggleContextMenuDelete beautify showHelp t tryRegExp */
|
||||||
|
/* exported createSection */
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
function createResizeGrip(cm) {
|
||||||
|
const wrapper = cm.display.wrapper;
|
||||||
|
wrapper.classList.add('resize-grip-enabled');
|
||||||
|
const resizeGrip = template.resizeGrip.cloneNode(true);
|
||||||
|
wrapper.appendChild(resizeGrip);
|
||||||
|
let lastClickTime = 0;
|
||||||
|
resizeGrip.onmousedown = event => {
|
||||||
|
if (event.button !== 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
event.preventDefault();
|
||||||
|
if (Date.now() - lastClickTime < 500) {
|
||||||
|
lastClickTime = 0;
|
||||||
|
toggleSectionHeight(cm);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
lastClickTime = Date.now();
|
||||||
|
const minHeight = cm.defaultTextHeight() +
|
||||||
|
/* .CodeMirror-lines padding */
|
||||||
|
cm.display.lineDiv.offsetParent.offsetTop +
|
||||||
|
/* borders */
|
||||||
|
wrapper.offsetHeight - wrapper.clientHeight;
|
||||||
|
wrapper.style.pointerEvents = 'none';
|
||||||
|
document.body.style.cursor = 's-resize';
|
||||||
|
document.addEventListener('mousemove', resize);
|
||||||
|
document.addEventListener('mouseup', resizeStop);
|
||||||
|
|
||||||
|
function resize(e) {
|
||||||
|
const cmPageY = wrapper.getBoundingClientRect().top + window.scrollY;
|
||||||
|
const height = Math.max(minHeight, e.pageY - cmPageY);
|
||||||
|
if (height !== wrapper.clientHeight) {
|
||||||
|
cm.setSize(null, height);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function resizeStop() {
|
||||||
|
document.removeEventListener('mouseup', resizeStop);
|
||||||
|
document.removeEventListener('mousemove', resize);
|
||||||
|
wrapper.style.pointerEvents = '';
|
||||||
|
document.body.style.cursor = '';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
function toggleSectionHeight(cm) {
|
||||||
|
if (cm.state.toggleHeightSaved) {
|
||||||
|
// restore previous size
|
||||||
|
cm.setSize(null, cm.state.toggleHeightSaved);
|
||||||
|
cm.state.toggleHeightSaved = 0;
|
||||||
|
} else {
|
||||||
|
// maximize
|
||||||
|
const wrapper = cm.display.wrapper;
|
||||||
|
const allBounds = $('#sections').getBoundingClientRect();
|
||||||
|
const pageExtrasHeight = allBounds.top + window.scrollY +
|
||||||
|
parseFloat(getComputedStyle($('#sections')).paddingBottom);
|
||||||
|
const sectionEl = wrapper.parentNode;
|
||||||
|
const sectionExtrasHeight = sectionEl.clientHeight - wrapper.offsetHeight;
|
||||||
|
cm.state.toggleHeightSaved = wrapper.clientHeight;
|
||||||
|
cm.setSize(null, window.innerHeight - sectionExtrasHeight - pageExtrasHeight);
|
||||||
|
const bounds = sectionEl.getBoundingClientRect();
|
||||||
|
if (bounds.top < 0 || bounds.bottom > window.innerHeight) {
|
||||||
|
window.scrollBy(0, bounds.top);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function createSection({
|
||||||
|
// data model
|
||||||
|
originalSection,
|
||||||
|
dirty,
|
||||||
|
// util
|
||||||
|
nextEditor,
|
||||||
|
prevEditor,
|
||||||
|
genId,
|
||||||
|
// emit events
|
||||||
|
// TODO: better names like `onRemoved`? Or make a real event emitter.
|
||||||
|
showMozillaFormatImport,
|
||||||
|
removeSection,
|
||||||
|
insertSectionAfter,
|
||||||
|
moveSectionUp,
|
||||||
|
moveSectionDown,
|
||||||
|
restoreSection,
|
||||||
|
}) {
|
||||||
|
const sectionId = genId();
|
||||||
|
const el = template.section.cloneNode(true);
|
||||||
|
const cm = cmFactory.create(wrapper => {
|
||||||
|
el.insertBefore(wrapper, $('.code-label', el).nextSibling);
|
||||||
|
}, {value: originalSection.code});
|
||||||
|
|
||||||
|
const changeListeners = new Set();
|
||||||
|
|
||||||
|
const appliesToContainer = $('.applies-to-list', el);
|
||||||
|
const appliesTo = [];
|
||||||
|
for (const [key, fnName] of Object.entries(propertyToCss)) {
|
||||||
|
if (originalSection[key]) {
|
||||||
|
originalSection[key].forEach(value =>
|
||||||
|
insertApplyAfter({type: fnName, value})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!appliesTo.length) {
|
||||||
|
insertApplyAfter({all: true});
|
||||||
|
}
|
||||||
|
|
||||||
|
let changeGeneration = cm.changeGeneration();
|
||||||
|
let removed = false;
|
||||||
|
|
||||||
|
registerEvents();
|
||||||
|
updateRegexpTester();
|
||||||
|
createResizeGrip(cm);
|
||||||
|
|
||||||
|
linter.enableForEditor(cm);
|
||||||
|
|
||||||
|
const section = {
|
||||||
|
id: sectionId,
|
||||||
|
el,
|
||||||
|
cm,
|
||||||
|
render,
|
||||||
|
getModel,
|
||||||
|
remove,
|
||||||
|
destroy,
|
||||||
|
restore,
|
||||||
|
isRemoved: () => removed,
|
||||||
|
onChange,
|
||||||
|
off,
|
||||||
|
appliesTo
|
||||||
|
};
|
||||||
|
return section;
|
||||||
|
|
||||||
|
function onChange(fn) {
|
||||||
|
changeListeners.add(fn);
|
||||||
|
}
|
||||||
|
|
||||||
|
function off(fn) {
|
||||||
|
changeListeners.delete(fn);
|
||||||
|
}
|
||||||
|
|
||||||
|
function emitSectionChange() {
|
||||||
|
for (const fn of changeListeners) {
|
||||||
|
fn();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getModel() {
|
||||||
|
const section = {
|
||||||
|
code: cm.getValue()
|
||||||
|
};
|
||||||
|
for (const apply of appliesTo) {
|
||||||
|
if (apply.all) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const key = CssToProperty[apply.getType()];
|
||||||
|
if (!section[key]) {
|
||||||
|
section[key] = [];
|
||||||
|
}
|
||||||
|
section[key].push(apply.getValue());
|
||||||
|
}
|
||||||
|
return section;
|
||||||
|
}
|
||||||
|
|
||||||
|
function registerEvents() {
|
||||||
|
cm.on('changes', () => {
|
||||||
|
const newGeneration = cm.changeGeneration();
|
||||||
|
dirty.modify(`section.${sectionId}.code`, changeGeneration, newGeneration);
|
||||||
|
changeGeneration = newGeneration;
|
||||||
|
emitSectionChange();
|
||||||
|
});
|
||||||
|
cm.on('paste', (cm, event) => {
|
||||||
|
const text = event.clipboardData.getData('text') || '';
|
||||||
|
if (
|
||||||
|
text.includes('@-moz-document') &&
|
||||||
|
text.replace(/\/\*[\s\S]*?(?:\*\/|$)/g, '')
|
||||||
|
.match(/@-moz-document[\s\r\n]+(url|url-prefix|domain|regexp)\(/)
|
||||||
|
) {
|
||||||
|
event.preventDefault();
|
||||||
|
showMozillaFormatImport(text);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (!FIREFOX) {
|
||||||
|
cm.on('mousedown', (cm, event) => toggleContextMenuDelete.call(cm, event));
|
||||||
|
}
|
||||||
|
cm.display.wrapper.addEventListener('keydown', event =>
|
||||||
|
handleKeydown(cm, event), true);
|
||||||
|
|
||||||
|
$('.applies-to-help', el).addEventListener('click', showAppliesToHelp);
|
||||||
|
$('.remove-section', el).addEventListener('click', () => removeSection(section));
|
||||||
|
$('.add-section', el).addEventListener('click', () => insertSectionAfter(undefined, section));
|
||||||
|
$('.clone-section', el).addEventListener('click', () => insertSectionAfter(getModel(), section));
|
||||||
|
$('.move-section-up', el).addEventListener('click', () => moveSectionUp(section));
|
||||||
|
$('.move-section-down', el).addEventListener('click', () => moveSectionDown(section));
|
||||||
|
$('.beautify-section', el).addEventListener('click', () => beautify([cm]));
|
||||||
|
$('.restore-section', el).addEventListener('click', () => restoreSection(section));
|
||||||
|
$('.test-regexp', el).addEventListener('click', () => {
|
||||||
|
regExpTester.toggle();
|
||||||
|
updateRegexpTester();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleKeydown(cm, event) {
|
||||||
|
const key = event.which;
|
||||||
|
if (key < 37 || key > 40 || event.shiftKey || event.altKey || event.metaKey) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const {line, ch} = cm.getCursor();
|
||||||
|
switch (key) {
|
||||||
|
case 37:
|
||||||
|
// arrow Left
|
||||||
|
if (line || ch) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// fallthrough to arrow Up
|
||||||
|
case 38:
|
||||||
|
// arrow Up
|
||||||
|
cm = line === 0 && prevEditor(cm, false);
|
||||||
|
if (!cm) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
cm.setCursor(cm.doc.size - 1, key === 37 ? 1e20 : ch);
|
||||||
|
break;
|
||||||
|
case 39:
|
||||||
|
// arrow Right
|
||||||
|
if (line < cm.doc.size - 1 || ch < cm.getLine(line).length - 1) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// fallthrough to arrow Down
|
||||||
|
case 40:
|
||||||
|
// arrow Down
|
||||||
|
cm = line === cm.doc.size - 1 && nextEditor(cm, false);
|
||||||
|
if (!cm) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
cm.setCursor(0, 0);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
// FIXME: what is this?
|
||||||
|
// const animation = (cm.getSection().firstElementChild.getAnimations() || [])[0];
|
||||||
|
// if (animation) {
|
||||||
|
// animation.playbackRate = -1;
|
||||||
|
// animation.currentTime = 2000;
|
||||||
|
// animation.play();
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
|
||||||
|
function showAppliesToHelp(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
showHelp(t('appliesLabel'), t('appliesHelp'));
|
||||||
|
}
|
||||||
|
|
||||||
|
function remove() {
|
||||||
|
linter.disableForEditor(cm);
|
||||||
|
el.classList.add('removed');
|
||||||
|
removed = true;
|
||||||
|
appliesTo.forEach(a => a.remove());
|
||||||
|
}
|
||||||
|
|
||||||
|
function destroy() {
|
||||||
|
cmFactory.destroy(cm);
|
||||||
|
}
|
||||||
|
|
||||||
|
function restore() {
|
||||||
|
linter.enableForEditor(cm);
|
||||||
|
el.classList.remove('removed');
|
||||||
|
removed = false;
|
||||||
|
appliesTo.forEach(a => a.restore());
|
||||||
|
render();
|
||||||
|
}
|
||||||
|
|
||||||
|
function render() {
|
||||||
|
cm.refresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateRegexpTester() {
|
||||||
|
const regexps = appliesTo.filter(a => a.getType() === 'regexp')
|
||||||
|
.map(a => a.getValue());
|
||||||
|
if (regexps.length) {
|
||||||
|
el.classList.add('has-regexp');
|
||||||
|
regExpTester.update(regexps);
|
||||||
|
} else {
|
||||||
|
el.classList.remove('has-regexp');
|
||||||
|
regExpTester.toggle(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function insertApplyAfter(init, base) {
|
||||||
|
const apply = createApply(init);
|
||||||
|
if (base) {
|
||||||
|
const index = appliesTo.indexOf(base);
|
||||||
|
appliesTo.splice(index + 1, 0, apply);
|
||||||
|
appliesToContainer.insertBefore(apply.el, base.el.nextSibling);
|
||||||
|
} else {
|
||||||
|
appliesTo.push(apply);
|
||||||
|
appliesToContainer.appendChild(apply.el);
|
||||||
|
}
|
||||||
|
dirty.add(apply, apply);
|
||||||
|
if (appliesTo.length > 1 && appliesTo[0].all) {
|
||||||
|
removeApply(appliesTo[0]);
|
||||||
|
}
|
||||||
|
emitSectionChange();
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeApply(apply) {
|
||||||
|
const index = appliesTo.indexOf(apply);
|
||||||
|
appliesTo.splice(index, 1);
|
||||||
|
apply.remove();
|
||||||
|
apply.el.remove();
|
||||||
|
dirty.remove(apply, apply);
|
||||||
|
if (!appliesTo.length) {
|
||||||
|
insertApplyAfter({all: true});
|
||||||
|
}
|
||||||
|
emitSectionChange();
|
||||||
|
}
|
||||||
|
|
||||||
|
function createApply({type = 'url', value, all = false}) {
|
||||||
|
const applyId = genId();
|
||||||
|
const dirtyPrefix = `section.${sectionId}.apply.${applyId}`;
|
||||||
|
const el = all ? template.appliesToEverything.cloneNode(true) :
|
||||||
|
template.appliesTo.cloneNode(true);
|
||||||
|
|
||||||
|
const selectEl = !all && $('.applies-type', el);
|
||||||
|
if (selectEl) {
|
||||||
|
selectEl.value = type;
|
||||||
|
selectEl.addEventListener('change', () => {
|
||||||
|
const oldType = type;
|
||||||
|
dirty.modify(`${dirtyPrefix}.type`, type, selectEl.value);
|
||||||
|
type = selectEl.value;
|
||||||
|
if (oldType === 'regexp' || type === 'regexp') {
|
||||||
|
updateRegexpTester();
|
||||||
|
}
|
||||||
|
emitSectionChange();
|
||||||
|
validate();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const valueEl = !all && $('.applies-value', el);
|
||||||
|
if (valueEl) {
|
||||||
|
valueEl.value = value;
|
||||||
|
valueEl.addEventListener('input', () => {
|
||||||
|
dirty.modify(`${dirtyPrefix}.value`, value, valueEl.value);
|
||||||
|
value = valueEl.value;
|
||||||
|
if (type === 'regexp') {
|
||||||
|
updateRegexpTester();
|
||||||
|
}
|
||||||
|
emitSectionChange();
|
||||||
|
});
|
||||||
|
valueEl.addEventListener('change', validate);
|
||||||
|
}
|
||||||
|
|
||||||
|
restore();
|
||||||
|
|
||||||
|
const apply = {
|
||||||
|
id: applyId,
|
||||||
|
all,
|
||||||
|
remove,
|
||||||
|
restore,
|
||||||
|
el,
|
||||||
|
getType: () => type,
|
||||||
|
getValue: () => value,
|
||||||
|
valueEl // used by validator
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeButton = $('.remove-applies-to', el);
|
||||||
|
if (removeButton) {
|
||||||
|
removeButton.addEventListener('click', e => {
|
||||||
|
e.preventDefault();
|
||||||
|
removeApply(apply);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
$('.add-applies-to', el).addEventListener('click', e => {
|
||||||
|
e.preventDefault();
|
||||||
|
insertApplyAfter({type, value: ''}, apply);
|
||||||
|
});
|
||||||
|
|
||||||
|
return apply;
|
||||||
|
|
||||||
|
function validate() {
|
||||||
|
if (type !== 'regexp' || tryRegExp(value)) {
|
||||||
|
valueEl.setCustomValidity('');
|
||||||
|
} else {
|
||||||
|
valueEl.setCustomValidity(t('styleBadRegexp'));
|
||||||
|
setTimeout(() => valueEl.reportValidity());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function remove() {
|
||||||
|
if (all) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
dirty.remove(`${dirtyPrefix}.type`, type);
|
||||||
|
dirty.remove(`${dirtyPrefix}.value`, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
function restore() {
|
||||||
|
if (all) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
dirty.add(`${dirtyPrefix}.type`, type);
|
||||||
|
dirty.add(`${dirtyPrefix}.value`, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
594
edit/sections-editor.js
Normal file
594
edit/sections-editor.js
Normal file
|
@ -0,0 +1,594 @@
|
||||||
|
/* global dirtyReporter showHelp toggleContextMenuDelete createSection
|
||||||
|
CodeMirror linter createLivePreview showCodeMirrorPopup
|
||||||
|
sectionsToMozFormat messageBox clipString
|
||||||
|
rerouteHotkeys $ $$ $create t FIREFOX API
|
||||||
|
debounce */
|
||||||
|
/* exported createSectionsEditor */
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
function createSectionsEditor(style) {
|
||||||
|
let INC_ID = 0; // an increment id that is used by various object to track the order
|
||||||
|
const dirty = dirtyReporter();
|
||||||
|
dirty.onChange(updateTitle);
|
||||||
|
|
||||||
|
const container = $('#sections');
|
||||||
|
const sections = [];
|
||||||
|
|
||||||
|
const nameEl = $('#name');
|
||||||
|
nameEl.addEventListener('input', () => {
|
||||||
|
dirty.modify('name', style.name, nameEl.value);
|
||||||
|
style.name = nameEl.value;
|
||||||
|
updateTitle();
|
||||||
|
});
|
||||||
|
|
||||||
|
const enabledEl = $('#enabled');
|
||||||
|
enabledEl.addEventListener('change', () => {
|
||||||
|
dirty.modify('enabled', style.enabled, enabledEl.checked);
|
||||||
|
style.enabled = enabledEl.checked;
|
||||||
|
updateLivePreview();
|
||||||
|
});
|
||||||
|
|
||||||
|
$('#to-mozilla').addEventListener('click', showMozillaFormat);
|
||||||
|
$('#to-mozilla-help').addEventListener('click', showToMozillaHelp);
|
||||||
|
$('#from-mozilla').addEventListener('click', () => showMozillaFormatImport());
|
||||||
|
$('#save-button').addEventListener('click', saveStyle);
|
||||||
|
|
||||||
|
document.addEventListener('wheel', scrollEntirePageOnCtrlShift);
|
||||||
|
|
||||||
|
if (!FIREFOX) {
|
||||||
|
$$([
|
||||||
|
'input:not([type])',
|
||||||
|
'input[type="text"]',
|
||||||
|
'input[type="search"]',
|
||||||
|
'input[type="number"]',
|
||||||
|
].join(','))
|
||||||
|
.forEach(e => e.addEventListener('mousedown', toggleContextMenuDelete));
|
||||||
|
}
|
||||||
|
|
||||||
|
let sectionOrder = '';
|
||||||
|
const initializing = new Promise(resolve => initSection({
|
||||||
|
sections: style.sections.slice(),
|
||||||
|
done:() => {
|
||||||
|
// FIXME: implement this with CSS?
|
||||||
|
// https://github.com/openstyles/stylus/commit/2895ce11e271788df0e4f7314b3b981fde086574
|
||||||
|
dirty.clear();
|
||||||
|
rerouteHotkeys(true);
|
||||||
|
resolve();
|
||||||
|
updateHeader();
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
const livePreview = createLivePreview();
|
||||||
|
livePreview.show(Boolean(style.id));
|
||||||
|
|
||||||
|
return {
|
||||||
|
ready: () => initializing,
|
||||||
|
replaceStyle,
|
||||||
|
isDirty: dirty.isDirty,
|
||||||
|
getStyle: () => style,
|
||||||
|
getEditors,
|
||||||
|
scrollToEditor,
|
||||||
|
getStyleId: () => style.id,
|
||||||
|
getEditorTitle: cm => {
|
||||||
|
const index = sections.filter(s => !s.isRemoved()).findIndex(s => s.cm === cm);
|
||||||
|
return `${t('sectionCode')} ${index + 1}`;
|
||||||
|
},
|
||||||
|
save: saveStyle,
|
||||||
|
toggleStyle,
|
||||||
|
nextEditor,
|
||||||
|
prevEditor,
|
||||||
|
closestVisible,
|
||||||
|
getSearchableInputs,
|
||||||
|
};
|
||||||
|
|
||||||
|
function genId() {
|
||||||
|
return INC_ID++;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setGlobalProgress(done, total) {
|
||||||
|
const progressElement = $('#global-progress') ||
|
||||||
|
total && document.body.appendChild($create('#global-progress'));
|
||||||
|
if (total) {
|
||||||
|
const progress = (done / Math.max(done, total) * 100).toFixed(1);
|
||||||
|
progressElement.style.borderLeftWidth = progress + 'vw';
|
||||||
|
setTimeout(() => {
|
||||||
|
progressElement.title = progress + '%';
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
$.remove(progressElement);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function showToMozillaHelp(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
showHelp(t('styleMozillaFormatHeading'), t('styleToMozillaFormatHelp'));
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSearchableInputs(cm) {
|
||||||
|
return sections.find(s => s.cm === cm).appliesTo.map(a => a.valueEl).filter(Boolean);
|
||||||
|
}
|
||||||
|
|
||||||
|
// priority:
|
||||||
|
// 1. associated CM for applies-to element
|
||||||
|
// 2. last active if visible
|
||||||
|
// 3. first visible
|
||||||
|
function closestVisible(nearbyElement) {
|
||||||
|
const cm =
|
||||||
|
nearbyElement instanceof CodeMirror ? nearbyElement :
|
||||||
|
nearbyElement instanceof Node &&
|
||||||
|
(nearbyElement.closest('#sections > .section') || {}).CodeMirror ||
|
||||||
|
getLastActivatedEditor();
|
||||||
|
if (nearbyElement instanceof Node && cm) {
|
||||||
|
const {left, top} = nearbyElement.getBoundingClientRect();
|
||||||
|
const bounds = cm.display.wrapper.getBoundingClientRect();
|
||||||
|
if (top >= 0 && top >= bounds.top &&
|
||||||
|
left >= 0 && left >= bounds.left) {
|
||||||
|
return cm;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// closest editor should have at least 2 lines visible
|
||||||
|
const lineHeight = sections[0].cm.defaultTextHeight();
|
||||||
|
const scrollY = window.scrollY;
|
||||||
|
const windowBottom = scrollY + window.innerHeight - 2 * lineHeight;
|
||||||
|
const allSectionsContainerTop = scrollY + $('#sections').getBoundingClientRect().top;
|
||||||
|
const distances = [];
|
||||||
|
const alreadyInView = cm && offscreenDistance(null, cm) === 0;
|
||||||
|
return alreadyInView ? cm : findClosest();
|
||||||
|
|
||||||
|
function offscreenDistance(index, cm) {
|
||||||
|
if (index >= 0 && distances[index] !== undefined) {
|
||||||
|
return distances[index];
|
||||||
|
}
|
||||||
|
const section = cm.display.wrapper.closest('.section');
|
||||||
|
if (!section) {
|
||||||
|
return 1e9;
|
||||||
|
}
|
||||||
|
const top = allSectionsContainerTop + section.offsetTop;
|
||||||
|
if (top < scrollY + lineHeight) {
|
||||||
|
return Math.max(0, scrollY - top - lineHeight);
|
||||||
|
}
|
||||||
|
if (top < windowBottom) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
const distance = top - windowBottom + section.offsetHeight;
|
||||||
|
if (index >= 0) {
|
||||||
|
distances[index] = distance;
|
||||||
|
}
|
||||||
|
return distance;
|
||||||
|
}
|
||||||
|
|
||||||
|
function findClosest() {
|
||||||
|
const editors = getEditors();
|
||||||
|
const last = editors.length - 1;
|
||||||
|
let a = 0;
|
||||||
|
let b = last;
|
||||||
|
let c;
|
||||||
|
let distance;
|
||||||
|
while (a < b - 1) {
|
||||||
|
c = (a + b) / 2 | 0;
|
||||||
|
distance = offscreenDistance(c);
|
||||||
|
if (!distance || !c) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
const distancePrev = offscreenDistance(c - 1);
|
||||||
|
const distanceNext = c < last ? offscreenDistance(c + 1) : 1e20;
|
||||||
|
if (distancePrev <= distance && distance <= distanceNext) {
|
||||||
|
b = c;
|
||||||
|
} else {
|
||||||
|
a = c;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
while (b && offscreenDistance(b - 1) <= offscreenDistance(b)) {
|
||||||
|
b--;
|
||||||
|
}
|
||||||
|
const cm = editors[b];
|
||||||
|
if (distances[b] > 0) {
|
||||||
|
scrollToEditor(cm);
|
||||||
|
}
|
||||||
|
return cm;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getEditors() {
|
||||||
|
return sections.filter(s => !s.isRemoved()).map(s => s.cm);
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleStyle() {
|
||||||
|
const newValue = !style.enabled;
|
||||||
|
dirty.modify('enabled', style.enabled, newValue);
|
||||||
|
style.enabled = newValue;
|
||||||
|
enabledEl.checked = newValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
function nextEditor(cm, cycle = true) {
|
||||||
|
if (!cycle) {
|
||||||
|
for (const section of sections) {
|
||||||
|
if (section.isRemoved()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (cm === section.cm) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nextPrevEditor(cm, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
function prevEditor(cm, cycle = true) {
|
||||||
|
if (!cycle) {
|
||||||
|
for (let i = sections.length - 1; i >= 0; i--) {
|
||||||
|
if (sections[i].isRemoved()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (cm === sections[i].cm) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nextPrevEditor(cm, -1);
|
||||||
|
}
|
||||||
|
|
||||||
|
function nextPrevEditor(cm, direction) {
|
||||||
|
const editors = getEditors();
|
||||||
|
cm = editors[(editors.indexOf(cm) + direction + editors.length) % editors.length];
|
||||||
|
scrollToEditor(cm);
|
||||||
|
cm.focus();
|
||||||
|
return cm;
|
||||||
|
}
|
||||||
|
|
||||||
|
function scrollToEditor(cm) {
|
||||||
|
const section = sections.find(s => s.cm === cm).el;
|
||||||
|
const bounds = section.getBoundingClientRect();
|
||||||
|
if (
|
||||||
|
(bounds.bottom > window.innerHeight && bounds.top > 0) ||
|
||||||
|
(bounds.top < 0 && bounds.bottom < window.innerHeight)
|
||||||
|
) {
|
||||||
|
if (bounds.top < 0) {
|
||||||
|
window.scrollBy(0, bounds.top - 1);
|
||||||
|
} else {
|
||||||
|
window.scrollBy(0, bounds.bottom - window.innerHeight + 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getLastActivatedEditor() {
|
||||||
|
let result;
|
||||||
|
for (const section of sections) {
|
||||||
|
if (section.isRemoved()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// .lastActive is initiated by codemirror-factory
|
||||||
|
if (!result || section.cm.lastActive > result.lastActive) {
|
||||||
|
result = section.cm;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
function scrollEntirePageOnCtrlShift(event) {
|
||||||
|
// make Shift-Ctrl-Wheel scroll entire page even when mouse is over a code editor
|
||||||
|
if (event.shiftKey && event.ctrlKey && !event.altKey && !event.metaKey) {
|
||||||
|
// Chrome scrolls horizontally when Shift is pressed but on some PCs this might be different
|
||||||
|
window.scrollBy(0, event.deltaX || event.deltaY);
|
||||||
|
event.preventDefault();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function showMozillaFormat() {
|
||||||
|
const popup = showCodeMirrorPopup(t('styleToMozillaFormatTitle'), '', {readOnly: true});
|
||||||
|
popup.codebox.setValue(sectionsToMozFormat(getModel()));
|
||||||
|
popup.codebox.execCommand('selectAll');
|
||||||
|
}
|
||||||
|
|
||||||
|
function showMozillaFormatImport(text = '') {
|
||||||
|
const popup = showCodeMirrorPopup(t('styleFromMozillaFormatPrompt'),
|
||||||
|
$create('.buttons', [
|
||||||
|
$create('button', {
|
||||||
|
name: 'import-replace',
|
||||||
|
textContent: t('importReplaceLabel'),
|
||||||
|
title: 'Ctrl-Shift-Enter:\n' + t('importReplaceTooltip'),
|
||||||
|
onclick: () => doImport({replaceOldStyle: true}),
|
||||||
|
}),
|
||||||
|
$create('button', {
|
||||||
|
name: 'import-append',
|
||||||
|
textContent: t('importAppendLabel'),
|
||||||
|
title: 'Ctrl-Enter:\n' + t('importAppendTooltip'),
|
||||||
|
onclick: doImport,
|
||||||
|
}),
|
||||||
|
]));
|
||||||
|
const contents = $('.contents', popup);
|
||||||
|
contents.insertBefore(popup.codebox.display.wrapper, contents.firstElementChild);
|
||||||
|
popup.codebox.focus();
|
||||||
|
popup.codebox.on('changes', cm => {
|
||||||
|
popup.classList.toggle('ready', !cm.isBlank());
|
||||||
|
cm.markClean();
|
||||||
|
});
|
||||||
|
if (text) {
|
||||||
|
popup.codebox.setValue(text);
|
||||||
|
popup.codebox.clearHistory();
|
||||||
|
popup.codebox.markClean();
|
||||||
|
}
|
||||||
|
// overwrite default extraKeys as those are inapplicable in popup context
|
||||||
|
popup.codebox.options.extraKeys = {
|
||||||
|
'Ctrl-Enter': doImport,
|
||||||
|
'Shift-Ctrl-Enter': () => doImport({replaceOldStyle: true}),
|
||||||
|
};
|
||||||
|
|
||||||
|
function doImport({replaceOldStyle = false}) {
|
||||||
|
lockPageUI(true);
|
||||||
|
API.parseCss({code: popup.codebox.getValue().trim()})
|
||||||
|
.then(({sections, errors}) => {
|
||||||
|
// shouldn't happen but just in case
|
||||||
|
if (!sections.length || errors.length) {
|
||||||
|
throw errors;
|
||||||
|
}
|
||||||
|
if (replaceOldStyle) {
|
||||||
|
return replaceSections(sections);
|
||||||
|
}
|
||||||
|
return new Promise(resolve => initSection({sections, done: resolve, focusOn: false}));
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
$('.dismiss').dispatchEvent(new Event('click'));
|
||||||
|
})
|
||||||
|
.catch(showError)
|
||||||
|
.then(() => lockPageUI(false));
|
||||||
|
}
|
||||||
|
|
||||||
|
function lockPageUI(locked) {
|
||||||
|
document.documentElement.style.pointerEvents = locked ? 'none' : '';
|
||||||
|
if (popup.codebox) {
|
||||||
|
popup.classList.toggle('ready', locked ? false : !popup.codebox.isBlank());
|
||||||
|
popup.codebox.options.readOnly = locked;
|
||||||
|
popup.codebox.display.wrapper.style.opacity = locked ? '.5' : '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function showError(errors) {
|
||||||
|
messageBox({
|
||||||
|
className: 'center danger',
|
||||||
|
title: t('styleFromMozillaFormatError'),
|
||||||
|
contents: $create('pre', Array.isArray(errors) ? errors.join('\n') : errors),
|
||||||
|
buttons: [t('confirmClose')],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateSectionOrder() {
|
||||||
|
const oldOrder = sectionOrder;
|
||||||
|
const validSections = sections.filter(s => !s.isRemoved());
|
||||||
|
sectionOrder = validSections.map(s => s.id).join(',');
|
||||||
|
dirty.modify('sectionOrder', oldOrder, sectionOrder);
|
||||||
|
container.dataset.sectionCount = validSections.length;
|
||||||
|
linter.refreshReport();
|
||||||
|
}
|
||||||
|
|
||||||
|
function getModel() {
|
||||||
|
return Object.assign({}, style, {
|
||||||
|
sections: sections.filter(s => !s.isRemoved()).map(s => s.getModel())
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function validate() {
|
||||||
|
if (!nameEl.reportValidity()) {
|
||||||
|
messageBox.alert(t('styleMissingName'));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
for (const section of sections) {
|
||||||
|
for (const apply of section.appliesTo) {
|
||||||
|
if (apply.getType() !== 'regexp') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!apply.valueEl.reportValidity()) {
|
||||||
|
messageBox.alert(t('styleBadRegexp'));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveStyle() {
|
||||||
|
if (!dirty.isDirty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const newStyle = getModel();
|
||||||
|
if (!validate(newStyle)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
API.editSave(newStyle)
|
||||||
|
.then(newStyle => {
|
||||||
|
destroyRemovedSections();
|
||||||
|
sessionStorage.justEditedStyleId = newStyle.id;
|
||||||
|
replaceStyle(newStyle, false);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function destroyRemovedSections() {
|
||||||
|
for (let i = 0; i < sections.length;) {
|
||||||
|
if (!sections[i].isRemoved()) {
|
||||||
|
i++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
sections[i].destroy();
|
||||||
|
sections[i].el.remove();
|
||||||
|
sections.splice(i, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateHeader() {
|
||||||
|
nameEl.value = style.name || '';
|
||||||
|
enabledEl.checked = style.enabled !== false;
|
||||||
|
$('#url').href = style.url || '';
|
||||||
|
updateTitle();
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateLivePreview() {
|
||||||
|
debounce(_updateLivePreview, 200);
|
||||||
|
}
|
||||||
|
|
||||||
|
function _updateLivePreview() {
|
||||||
|
livePreview.update(getModel());
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateTitle() {
|
||||||
|
const name = style.name;
|
||||||
|
const clean = !dirty.isDirty();
|
||||||
|
const title = !style.id ? t('addStyleTitle') : name;
|
||||||
|
document.title = (clean ? '' : '* ') + title;
|
||||||
|
$('#save-button').disabled = clean;
|
||||||
|
}
|
||||||
|
|
||||||
|
function initSection({
|
||||||
|
sections: originalSections,
|
||||||
|
total = originalSections.length,
|
||||||
|
focusOn = 0,
|
||||||
|
done
|
||||||
|
}) {
|
||||||
|
container.classList.add('hidden');
|
||||||
|
chunk();
|
||||||
|
|
||||||
|
function chunk() {
|
||||||
|
if (!originalSections.length) {
|
||||||
|
setGlobalProgress();
|
||||||
|
if (focusOn !== false) {
|
||||||
|
sections[focusOn].cm.focus();
|
||||||
|
}
|
||||||
|
container.classList.remove('hidden');
|
||||||
|
for (const section of sections) {
|
||||||
|
section.cm.refreshOnView();
|
||||||
|
}
|
||||||
|
if (done) {
|
||||||
|
done();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const t0 = performance.now();
|
||||||
|
while (originalSections.length && performance.now() - t0 < 100) {
|
||||||
|
insertSectionAfter(originalSections.shift());
|
||||||
|
}
|
||||||
|
setGlobalProgress(total - originalSections.length, total);
|
||||||
|
setTimeout(chunk);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeSection(section) {
|
||||||
|
if (sections.every(s => s.isRemoved() || s === section)) {
|
||||||
|
// TODO: hide remove button when `#sections[data-section-count=1]`
|
||||||
|
throw new Error('Cannot remove last section');
|
||||||
|
}
|
||||||
|
if (section.cm.isBlank()) {
|
||||||
|
const index = sections.indexOf(section);
|
||||||
|
sections.splice(index, 1);
|
||||||
|
section.el.remove();
|
||||||
|
section.remove();
|
||||||
|
section.destroy();
|
||||||
|
} else {
|
||||||
|
const lines = [];
|
||||||
|
const MAX_LINES = 10;
|
||||||
|
section.cm.doc.iter(0, MAX_LINES + 1, ({text}) => lines.push(text) && false);
|
||||||
|
const title = t('sectionCode') + '\n' +
|
||||||
|
'-'.repeat(20) + '\n' +
|
||||||
|
lines.slice(0, MAX_LINES).map(s => clipString(s, 100)).join('\n') +
|
||||||
|
(lines.length > MAX_LINES ? '\n...' : '');
|
||||||
|
$('.deleted-section', section.el).title = title;
|
||||||
|
section.remove();
|
||||||
|
}
|
||||||
|
dirty.remove(section, section);
|
||||||
|
updateSectionOrder();
|
||||||
|
section.off(updateLivePreview);
|
||||||
|
updateLivePreview();
|
||||||
|
}
|
||||||
|
|
||||||
|
function restoreSection(section) {
|
||||||
|
section.restore();
|
||||||
|
updateSectionOrder();
|
||||||
|
section.onChange(updateLivePreview);
|
||||||
|
updateLivePreview();
|
||||||
|
}
|
||||||
|
|
||||||
|
function insertSectionAfter(init, base) {
|
||||||
|
if (!init) {
|
||||||
|
init = {code: '', urlPrefixes: ['http://example.com']};
|
||||||
|
}
|
||||||
|
const section = createSection({
|
||||||
|
originalSection: init,
|
||||||
|
genId,
|
||||||
|
dirty,
|
||||||
|
showMozillaFormatImport,
|
||||||
|
removeSection,
|
||||||
|
restoreSection,
|
||||||
|
insertSectionAfter,
|
||||||
|
moveSectionUp,
|
||||||
|
moveSectionDown,
|
||||||
|
prevEditor,
|
||||||
|
nextEditor
|
||||||
|
});
|
||||||
|
if (base) {
|
||||||
|
const index = sections.indexOf(base);
|
||||||
|
sections.splice(index + 1, 0, section);
|
||||||
|
container.insertBefore(section.el, base.el.nextSibling);
|
||||||
|
} else {
|
||||||
|
sections.push(section);
|
||||||
|
container.appendChild(section.el);
|
||||||
|
}
|
||||||
|
section.render();
|
||||||
|
updateSectionOrder();
|
||||||
|
section.onChange(updateLivePreview);
|
||||||
|
updateLivePreview();
|
||||||
|
}
|
||||||
|
|
||||||
|
function moveSectionUp(section) {
|
||||||
|
const index = sections.indexOf(section);
|
||||||
|
if (index === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
container.insertBefore(section.el, sections[index - 1].el);
|
||||||
|
sections[index] = sections[index - 1];
|
||||||
|
sections[index - 1] = section;
|
||||||
|
updateSectionOrder();
|
||||||
|
}
|
||||||
|
|
||||||
|
function moveSectionDown(section) {
|
||||||
|
const index = sections.indexOf(section);
|
||||||
|
if (index === sections.length - 1) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
container.insertBefore(sections[index + 1].el, section.el);
|
||||||
|
sections[index] = sections[index + 1];
|
||||||
|
sections[index + 1] = section;
|
||||||
|
updateSectionOrder();
|
||||||
|
}
|
||||||
|
|
||||||
|
function replaceSections(originalSections) {
|
||||||
|
for (const section of sections) {
|
||||||
|
section.remove(true);
|
||||||
|
}
|
||||||
|
sections.length = 0;
|
||||||
|
container.textContent = '';
|
||||||
|
return new Promise(resolve => initSection({sections: originalSections, done: resolve}));
|
||||||
|
}
|
||||||
|
|
||||||
|
function replaceStyle(newStyle, codeIsUpdated) {
|
||||||
|
// FIXME: avoid recreating all editors?
|
||||||
|
reinit().then(() => {
|
||||||
|
Object.assign(style, newStyle);
|
||||||
|
updateHeader();
|
||||||
|
dirty.clear();
|
||||||
|
// Go from new style URL to edit style URL
|
||||||
|
if (location.href.indexOf('id=') === -1 && style.id) {
|
||||||
|
history.replaceState({}, document.title, 'edit.html?id=' + style.id);
|
||||||
|
$('#heading').textContent = t('editStyleHeading');
|
||||||
|
}
|
||||||
|
livePreview.show(Boolean(style.id));
|
||||||
|
});
|
||||||
|
|
||||||
|
function reinit() {
|
||||||
|
if (codeIsUpdated !== false) {
|
||||||
|
return replaceSections(newStyle.sections.slice());
|
||||||
|
}
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
582
edit/sections.js
582
edit/sections.js
|
@ -1,582 +0,0 @@
|
||||||
/*
|
|
||||||
global CodeMirror
|
|
||||||
global editors propertyToCss CssToProperty
|
|
||||||
global onChange initHooks setCleanGlobal
|
|
||||||
global fromMozillaFormat maximizeCodeHeight toggleContextMenuDelete
|
|
||||||
global setCleanItem updateTitle
|
|
||||||
global showAppliesToHelp beautify regExpTester setGlobalProgress setCleanSection
|
|
||||||
global clipString linter
|
|
||||||
*/
|
|
||||||
'use strict';
|
|
||||||
|
|
||||||
function initWithSectionStyle(style, codeIsUpdated) {
|
|
||||||
$('#name').value = style.name || '';
|
|
||||||
$('#enabled').checked = style.enabled !== false;
|
|
||||||
$('#url').href = style.url || '';
|
|
||||||
if (codeIsUpdated !== false) {
|
|
||||||
editors.length = 0;
|
|
||||||
$('#sections').textContent = '';
|
|
||||||
addSections(style.sections.length ? style.sections : [{code: ''}]);
|
|
||||||
initHooks();
|
|
||||||
}
|
|
||||||
setCleanGlobal();
|
|
||||||
updateTitle();
|
|
||||||
}
|
|
||||||
|
|
||||||
function addSections(sections, onAdded = () => {}) {
|
|
||||||
if (addSections.running) {
|
|
||||||
console.error('addSections cannot be re-entered: please report to the developers');
|
|
||||||
// TODO: handle this properly e.g. on update/import
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
addSections.running = true;
|
|
||||||
maximizeCodeHeight.stats = null;
|
|
||||||
// make a shallow copy since we might run asynchronously
|
|
||||||
// and the original array might get modified
|
|
||||||
sections = sections.slice();
|
|
||||||
const t0 = performance.now();
|
|
||||||
const divs = [];
|
|
||||||
let index = 0;
|
|
||||||
|
|
||||||
return new Promise(function run(resolve) {
|
|
||||||
while (index < sections.length) {
|
|
||||||
const div = addSection(null, sections[index]);
|
|
||||||
maximizeCodeHeight(div, index === sections.length - 1);
|
|
||||||
onAdded(div, index);
|
|
||||||
divs.push(div);
|
|
||||||
maybeFocusFirstCM();
|
|
||||||
index++;
|
|
||||||
const elapsed = performance.now() - t0;
|
|
||||||
if (elapsed > 500) {
|
|
||||||
setGlobalProgress(index, sections.length);
|
|
||||||
}
|
|
||||||
if (elapsed > 100) {
|
|
||||||
// after 100ms the sections are added asynchronously
|
|
||||||
setTimeout(run, 0, resolve);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
editors.last.state.renderLintReportNow = true;
|
|
||||||
addSections.running = false;
|
|
||||||
setGlobalProgress();
|
|
||||||
resolve(divs);
|
|
||||||
});
|
|
||||||
|
|
||||||
function maybeFocusFirstCM() {
|
|
||||||
const isPageLocked = document.documentElement.style.pointerEvents;
|
|
||||||
if (divs[0] && (isPageLocked ? divs.length === sections.length : index === 0)) {
|
|
||||||
makeSectionVisible(divs[0].CodeMirror);
|
|
||||||
setTimeout(() => {
|
|
||||||
if ((document.activeElement || {}).localName !== 'input') {
|
|
||||||
divs[0].CodeMirror.focus();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function addSection(event, section) {
|
|
||||||
const div = template.section.cloneNode(true);
|
|
||||||
$('.applies-to-help', div).addEventListener('click', showAppliesToHelp);
|
|
||||||
$('.remove-section', div).addEventListener('click', removeSection);
|
|
||||||
$('.add-section', div).addEventListener('click', addSection);
|
|
||||||
$('.clone-section', div).addEventListener('click', cloneSection);
|
|
||||||
$('.move-section-up', div).addEventListener('click', moveSection);
|
|
||||||
$('.move-section-down', div).addEventListener('click', moveSection);
|
|
||||||
$('.beautify-section', div).addEventListener('click', beautify);
|
|
||||||
|
|
||||||
const code = (section || {}).code || '';
|
|
||||||
|
|
||||||
const appliesTo = $('.applies-to-list', div);
|
|
||||||
let appliesToAdded = false;
|
|
||||||
|
|
||||||
if (section) {
|
|
||||||
for (const i in propertyToCss) {
|
|
||||||
if (section[i]) {
|
|
||||||
section[i].forEach(url => {
|
|
||||||
addAppliesTo(appliesTo, propertyToCss[i], url);
|
|
||||||
appliesToAdded = true;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!appliesToAdded) {
|
|
||||||
addAppliesTo(appliesTo, event && 'url-prefix', '');
|
|
||||||
}
|
|
||||||
|
|
||||||
appliesTo.addEventListener('change', onChange);
|
|
||||||
appliesTo.addEventListener('input', onChange);
|
|
||||||
|
|
||||||
toggleTestRegExpVisibility();
|
|
||||||
appliesTo.addEventListener('change', toggleTestRegExpVisibility);
|
|
||||||
$('.test-regexp', div).onclick = () => {
|
|
||||||
regExpTester.toggle();
|
|
||||||
regExpTester.update(getRegExps());
|
|
||||||
};
|
|
||||||
|
|
||||||
function getRegExps() {
|
|
||||||
return [...appliesTo.children]
|
|
||||||
.map(item =>
|
|
||||||
!item.matches('.applies-to-everything') &&
|
|
||||||
$('.applies-type', item).value === 'regexp' &&
|
|
||||||
$('.applies-value', item).value.trim()
|
|
||||||
)
|
|
||||||
.filter(item => item);
|
|
||||||
}
|
|
||||||
|
|
||||||
function toggleTestRegExpVisibility() {
|
|
||||||
const show = getRegExps().length > 0;
|
|
||||||
div.classList.toggle('has-regexp', show);
|
|
||||||
appliesTo.oninput = appliesTo.oninput || show && (event => {
|
|
||||||
if (event.target.matches('.applies-value') &&
|
|
||||||
$('.applies-type', event.target.closest('.applies-to-item')).value === 'regexp') {
|
|
||||||
regExpTester.update(getRegExps());
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const sections = $('#sections');
|
|
||||||
let cm;
|
|
||||||
if (event) {
|
|
||||||
let clickedSection = event && getSectionForChild(event.target, {includeDeleted: true});
|
|
||||||
clickedSection.insertAdjacentElement('afterend', div);
|
|
||||||
while (clickedSection && !clickedSection.matches('.section')) {
|
|
||||||
clickedSection = clickedSection.previousElementSibling;
|
|
||||||
}
|
|
||||||
const newIndex = getSections().indexOf(clickedSection) + 1;
|
|
||||||
cm = setupCodeMirror(div, code, newIndex);
|
|
||||||
makeSectionVisible(cm);
|
|
||||||
cm.focus();
|
|
||||||
} else {
|
|
||||||
sections.appendChild(div);
|
|
||||||
cm = setupCodeMirror(div, code);
|
|
||||||
}
|
|
||||||
linter.enableForEditor(cm);
|
|
||||||
linter.refreshReport();
|
|
||||||
div.CodeMirror = cm;
|
|
||||||
setCleanSection(div);
|
|
||||||
return div;
|
|
||||||
}
|
|
||||||
|
|
||||||
// may be invoked as a DOM listener
|
|
||||||
function addAppliesTo(list, type, value) {
|
|
||||||
let clickedItem;
|
|
||||||
if (this instanceof Node) {
|
|
||||||
clickedItem = this.closest('.applies-to-item');
|
|
||||||
list = this.closest('.applies-to-list');
|
|
||||||
// dummy <a> wrapper was clicked
|
|
||||||
if (arguments[0] instanceof Event) arguments[0].preventDefault();
|
|
||||||
}
|
|
||||||
const showingEverything = $('.applies-to-everything', list);
|
|
||||||
// blow away 'Everything' if it's there
|
|
||||||
if (showingEverything) {
|
|
||||||
list.removeChild(list.firstChild);
|
|
||||||
}
|
|
||||||
let item, toFocus;
|
|
||||||
|
|
||||||
// a section is added with known applies-to
|
|
||||||
if (type) {
|
|
||||||
item = template.appliesTo.cloneNode(true);
|
|
||||||
$('[name=applies-type]', item).value = type;
|
|
||||||
$('[name=applies-value]', item).value = value;
|
|
||||||
$('.remove-applies-to', item).addEventListener('click', removeAppliesTo);
|
|
||||||
|
|
||||||
// a "+" button was clicked
|
|
||||||
} else if (showingEverything || clickedItem) {
|
|
||||||
item = template.appliesTo.cloneNode(true);
|
|
||||||
toFocus = $('[name=applies-type]', item);
|
|
||||||
if (clickedItem) {
|
|
||||||
$('[name=applies-type]', item).value = $('[name="applies-type"]', clickedItem).value;
|
|
||||||
}
|
|
||||||
$('.remove-applies-to', item).addEventListener('click', removeAppliesTo);
|
|
||||||
|
|
||||||
// a global section is added
|
|
||||||
} else {
|
|
||||||
item = template.appliesToEverything.cloneNode(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
$('.add-applies-to', item).addEventListener('click', addAppliesTo);
|
|
||||||
list.insertBefore(item, clickedItem && clickedItem.nextElementSibling);
|
|
||||||
if (toFocus) toFocus.focus();
|
|
||||||
}
|
|
||||||
|
|
||||||
function cloneSection(event) {
|
|
||||||
const section = getSectionForChild(event.target);
|
|
||||||
addSection(event, getSectionsHashes([section]).pop());
|
|
||||||
setCleanItem($('#sections'), false);
|
|
||||||
updateTitle();
|
|
||||||
}
|
|
||||||
|
|
||||||
function moveSection(event) {
|
|
||||||
const section = getSectionForChild(event.target);
|
|
||||||
const dir = event.target.closest('.move-section-up') ? -1 : 1;
|
|
||||||
const cm = section.CodeMirror;
|
|
||||||
const index = editors.indexOf(cm);
|
|
||||||
const newIndex = (index + dir + editors.length) % editors.length;
|
|
||||||
const currentNextEl = section.nextElementSibling;
|
|
||||||
const newSection = editors[newIndex].getSection();
|
|
||||||
newSection.insertAdjacentElement('afterend', section);
|
|
||||||
section.parentNode.insertBefore(newSection, currentNextEl || null);
|
|
||||||
cm.focus();
|
|
||||||
editors[index] = editors[newIndex];
|
|
||||||
editors[newIndex] = cm;
|
|
||||||
setCleanItem($('#sections'), false);
|
|
||||||
updateTitle();
|
|
||||||
}
|
|
||||||
|
|
||||||
function setupCodeMirror(sectionDiv, code, index = editors.length) {
|
|
||||||
const cm = CodeMirror(wrapper => {
|
|
||||||
$('.code-label', sectionDiv).insertAdjacentElement('afterend', wrapper);
|
|
||||||
}, {
|
|
||||||
value: code,
|
|
||||||
});
|
|
||||||
const wrapper = cm.display.wrapper;
|
|
||||||
|
|
||||||
let onChangeTimer;
|
|
||||||
cm.on('changes', (cm, changes) => {
|
|
||||||
clearTimeout(onChangeTimer);
|
|
||||||
onChangeTimer = setTimeout(indicateCodeChange, 200, cm, changes);
|
|
||||||
});
|
|
||||||
wrapper.addEventListener('keydown', event => nextPrevEditorOnKeydown(cm, event), true);
|
|
||||||
cm.on('paste', (cm, event) => {
|
|
||||||
const text = event.clipboardData.getData('text') || '';
|
|
||||||
if (
|
|
||||||
text.includes('@-moz-document') &&
|
|
||||||
text.replace(/\/\*[\s\S]*?(?:\*\/|$)/g, '')
|
|
||||||
.match(/@-moz-document[\s\r\n]+(url|url-prefix|domain|regexp)\(/)
|
|
||||||
) {
|
|
||||||
event.preventDefault();
|
|
||||||
fromMozillaFormat();
|
|
||||||
$('#help-popup').codebox.setValue(text);
|
|
||||||
$('#help-popup').codebox.clearHistory();
|
|
||||||
$('#help-popup').codebox.markClean();
|
|
||||||
}
|
|
||||||
if (editors.length === 1) {
|
|
||||||
setTimeout(() => {
|
|
||||||
if (cm.display.sizer.clientHeight > cm.display.wrapper.clientHeight) {
|
|
||||||
maximizeCodeHeight.stats = null;
|
|
||||||
maximizeCodeHeight(cm.getSection(), true);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
if (!FIREFOX) {
|
|
||||||
cm.on('mousedown', (cm, event) => toggleContextMenuDelete.call(cm, event));
|
|
||||||
}
|
|
||||||
|
|
||||||
wrapper.classList.add('resize-grip-enabled');
|
|
||||||
let lastClickTime = 0;
|
|
||||||
const resizeGrip = wrapper.appendChild(template.resizeGrip.cloneNode(true));
|
|
||||||
resizeGrip.onmousedown = event => {
|
|
||||||
if (event.button !== 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
event.preventDefault();
|
|
||||||
if (Date.now() - lastClickTime < 500) {
|
|
||||||
lastClickTime = 0;
|
|
||||||
toggleSectionHeight(cm);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
lastClickTime = Date.now();
|
|
||||||
const minHeight = cm.defaultTextHeight() +
|
|
||||||
/* .CodeMirror-lines padding */
|
|
||||||
cm.display.lineDiv.offsetParent.offsetTop +
|
|
||||||
/* borders */
|
|
||||||
wrapper.offsetHeight - wrapper.clientHeight;
|
|
||||||
wrapper.style.pointerEvents = 'none';
|
|
||||||
document.body.style.cursor = 's-resize';
|
|
||||||
function resize(e) {
|
|
||||||
const cmPageY = wrapper.getBoundingClientRect().top + window.scrollY;
|
|
||||||
const height = Math.max(minHeight, e.pageY - cmPageY);
|
|
||||||
if (height !== wrapper.clientHeight) {
|
|
||||||
cm.setSize(null, height);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
document.addEventListener('mousemove', resize);
|
|
||||||
document.addEventListener('mouseup', function resizeStop() {
|
|
||||||
document.removeEventListener('mouseup', resizeStop);
|
|
||||||
document.removeEventListener('mousemove', resize);
|
|
||||||
wrapper.style.pointerEvents = '';
|
|
||||||
document.body.style.cursor = '';
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
editors.splice(index, 0, cm);
|
|
||||||
return cm;
|
|
||||||
}
|
|
||||||
|
|
||||||
function indicateCodeChange(cm) {
|
|
||||||
const section = cm.getSection();
|
|
||||||
setCleanItem(section, cm.isClean(section.savedValue));
|
|
||||||
updateTitle();
|
|
||||||
}
|
|
||||||
|
|
||||||
function setupAutocomplete(cm, enable = true) {
|
|
||||||
const onOff = enable ? 'on' : 'off';
|
|
||||||
cm[onOff]('changes', autocompleteOnTyping);
|
|
||||||
cm[onOff]('pick', autocompletePicked);
|
|
||||||
}
|
|
||||||
|
|
||||||
function autocompleteOnTyping(cm, [info], debounced) {
|
|
||||||
if (
|
|
||||||
cm.state.completionActive ||
|
|
||||||
info.origin && !info.origin.includes('input') ||
|
|
||||||
!info.text.last
|
|
||||||
) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (cm.state.autocompletePicked) {
|
|
||||||
cm.state.autocompletePicked = false;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!debounced) {
|
|
||||||
debounce(autocompleteOnTyping, 100, cm, [info], true);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (info.text.last.match(/[-a-z!]+$/i)) {
|
|
||||||
cm.state.autocompletePicked = false;
|
|
||||||
cm.options.hintOptions.completeSingle = false;
|
|
||||||
cm.execCommand('autocomplete');
|
|
||||||
setTimeout(() => {
|
|
||||||
cm.options.hintOptions.completeSingle = true;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function autocompletePicked(cm) {
|
|
||||||
cm.state.autocompletePicked = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
function nextPrevEditorOnKeydown(cm, event) {
|
|
||||||
const key = event.which;
|
|
||||||
if (key < 37 || key > 40 || event.shiftKey || event.altKey || event.metaKey) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const {line, ch} = cm.getCursor();
|
|
||||||
switch (key) {
|
|
||||||
case 37:
|
|
||||||
// arrow Left
|
|
||||||
if (line || ch) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// fallthrough to arrow Up
|
|
||||||
case 38:
|
|
||||||
// arrow Up
|
|
||||||
if (line > 0 || cm === editors[0]) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
event.preventDefault();
|
|
||||||
event.stopPropagation();
|
|
||||||
cm = CodeMirror.commands.prevEditor(cm);
|
|
||||||
cm.setCursor(cm.doc.size - 1, key === 37 ? 1e20 : ch);
|
|
||||||
break;
|
|
||||||
case 39:
|
|
||||||
// arrow Right
|
|
||||||
if (line < cm.doc.size - 1 || ch < cm.getLine(line).length - 1) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// fallthrough to arrow Down
|
|
||||||
case 40:
|
|
||||||
// arrow Down
|
|
||||||
if (line < cm.doc.size - 1 || cm === editors.last) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
event.preventDefault();
|
|
||||||
event.stopPropagation();
|
|
||||||
cm = CodeMirror.commands.nextEditor(cm);
|
|
||||||
cm.setCursor(0, 0);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
const animation = (cm.getSection().firstElementChild.getAnimations() || [])[0];
|
|
||||||
if (animation) {
|
|
||||||
animation.playbackRate = -1;
|
|
||||||
animation.currentTime = 2000;
|
|
||||||
animation.play();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function toggleSectionHeight(cm) {
|
|
||||||
if (cm.state.toggleHeightSaved) {
|
|
||||||
// restore previous size
|
|
||||||
cm.setSize(null, cm.state.toggleHeightSaved);
|
|
||||||
cm.state.toggleHeightSaved = 0;
|
|
||||||
} else {
|
|
||||||
// maximize
|
|
||||||
const wrapper = cm.display.wrapper;
|
|
||||||
const allBounds = $('#sections').getBoundingClientRect();
|
|
||||||
const pageExtrasHeight = allBounds.top + window.scrollY +
|
|
||||||
parseFloat(getComputedStyle($('#sections')).paddingBottom);
|
|
||||||
const sectionExtrasHeight = cm.getSection().clientHeight - wrapper.offsetHeight;
|
|
||||||
cm.state.toggleHeightSaved = wrapper.clientHeight;
|
|
||||||
cm.setSize(null, window.innerHeight - sectionExtrasHeight - pageExtrasHeight);
|
|
||||||
const bounds = cm.getSection().getBoundingClientRect();
|
|
||||||
if (bounds.top < 0 || bounds.bottom > window.innerHeight) {
|
|
||||||
window.scrollBy(0, bounds.top);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function getSectionForChild(el, {includeDeleted} = {}) {
|
|
||||||
return el.closest(`#sections > ${includeDeleted ? '*' : '.section'}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
function getSections() {
|
|
||||||
return $$('#sections > .section');
|
|
||||||
}
|
|
||||||
|
|
||||||
function getSectionsHashes(elements = getSections()) {
|
|
||||||
const sections = [];
|
|
||||||
for (const div of elements) {
|
|
||||||
const meta = {urls: [], urlPrefixes: [], domains: [], regexps: []};
|
|
||||||
for (const li of $('.applies-to-list', div).childNodes) {
|
|
||||||
if (li.className === template.appliesToEverything.className) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
const type = $('[name=applies-type]', li).value;
|
|
||||||
const value = $('[name=applies-value]', li).value;
|
|
||||||
if (type && value) {
|
|
||||||
meta[CssToProperty[type]].push(value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const code = div.CodeMirror.getValue();
|
|
||||||
if (/^\s*$/.test(code) && Object.keys(meta).length === 0) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
meta.code = code;
|
|
||||||
sections.push(meta);
|
|
||||||
}
|
|
||||||
return sections;
|
|
||||||
}
|
|
||||||
|
|
||||||
function removeAppliesTo(event) {
|
|
||||||
event.preventDefault();
|
|
||||||
const appliesTo = event.target.closest('.applies-to-item');
|
|
||||||
const appliesToList = appliesTo.closest('.applies-to-list');
|
|
||||||
removeAreaAndSetDirty(appliesTo);
|
|
||||||
if (!appliesToList.hasChildNodes()) {
|
|
||||||
addAppliesTo(appliesToList);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function removeSection(event) {
|
|
||||||
const section = getSectionForChild(event.target);
|
|
||||||
const cm = section.CodeMirror;
|
|
||||||
if (event instanceof Event && (!cm.isClean() || !cm.isBlank())) {
|
|
||||||
const stub = template.deletedSection.cloneNode(true);
|
|
||||||
const MAX_LINES = 10;
|
|
||||||
const lines = [];
|
|
||||||
cm.doc.iter(0, MAX_LINES + 1, ({text}) => lines.push(text) && false);
|
|
||||||
stub.title = t('sectionCode') + '\n' +
|
|
||||||
'-'.repeat(20) + '\n' +
|
|
||||||
lines.slice(0, MAX_LINES).map(s => clipString(s, 100)).join('\n') +
|
|
||||||
(lines.length > MAX_LINES ? '\n...' : '');
|
|
||||||
$('.restore-section', stub).onclick = () => {
|
|
||||||
let el = stub;
|
|
||||||
while (el && !el.matches('.section')) {
|
|
||||||
el = el.previousElementSibling;
|
|
||||||
}
|
|
||||||
const index = el ? editors.indexOf(el) + 1 : 0;
|
|
||||||
editors.splice(index, 0, cm);
|
|
||||||
stub.parentNode.replaceChild(section, stub);
|
|
||||||
setCleanItem(section, false);
|
|
||||||
updateTitle();
|
|
||||||
cm.focus();
|
|
||||||
linter.enableForEditor(cm);
|
|
||||||
linter.refreshReport();
|
|
||||||
};
|
|
||||||
section.insertAdjacentElement('afterend', stub);
|
|
||||||
}
|
|
||||||
setCleanItem($('#sections'), false);
|
|
||||||
removeAreaAndSetDirty(section);
|
|
||||||
editors.splice(editors.indexOf(cm), 1);
|
|
||||||
linter.disableForEditor(cm);
|
|
||||||
linter.refreshReport();
|
|
||||||
}
|
|
||||||
|
|
||||||
function removeAreaAndSetDirty(area) {
|
|
||||||
const contributors = $$('.style-contributor', area);
|
|
||||||
if (!contributors.length) {
|
|
||||||
setCleanItem(area, false);
|
|
||||||
}
|
|
||||||
contributors.some(node => {
|
|
||||||
if (node.savedValue) {
|
|
||||||
// it's a saved section, so make it dirty and stop the enumeration
|
|
||||||
setCleanItem(area, false);
|
|
||||||
return true;
|
|
||||||
} else {
|
|
||||||
// it's an empty section, so undirty the applies-to items,
|
|
||||||
// otherwise orphaned ids would keep the style dirty
|
|
||||||
setCleanItem(node, true);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
updateTitle();
|
|
||||||
area.remove();
|
|
||||||
}
|
|
||||||
|
|
||||||
function makeSectionVisible(cm) {
|
|
||||||
if (editors.length === 1) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const section = cm.getSection();
|
|
||||||
const bounds = section.getBoundingClientRect();
|
|
||||||
if (
|
|
||||||
(bounds.bottom > window.innerHeight && bounds.top > 0) ||
|
|
||||||
(bounds.top < 0 && bounds.bottom < window.innerHeight)
|
|
||||||
) {
|
|
||||||
if (bounds.top < 0) {
|
|
||||||
window.scrollBy(0, bounds.top - 1);
|
|
||||||
} else {
|
|
||||||
window.scrollBy(0, bounds.bottom - window.innerHeight + 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function maximizeCodeHeight(sectionDiv, isLast) {
|
|
||||||
const cm = sectionDiv.CodeMirror;
|
|
||||||
const stats = maximizeCodeHeight.stats = maximizeCodeHeight.stats || {totalHeight: 0, deltas: []};
|
|
||||||
if (!stats.cmActualHeight) {
|
|
||||||
stats.cmActualHeight = getComputedHeight(cm.display.wrapper);
|
|
||||||
}
|
|
||||||
if (!stats.sectionMarginTop) {
|
|
||||||
stats.sectionMarginTop = parseFloat(getComputedStyle(sectionDiv).marginTop);
|
|
||||||
}
|
|
||||||
const sectionTop = sectionDiv.getBoundingClientRect().top - stats.sectionMarginTop;
|
|
||||||
if (!stats.firstSectionTop) {
|
|
||||||
stats.firstSectionTop = sectionTop;
|
|
||||||
}
|
|
||||||
const extrasHeight = getComputedHeight(sectionDiv) - stats.cmActualHeight;
|
|
||||||
const cmMaxHeight = window.innerHeight - extrasHeight - sectionTop - stats.sectionMarginTop;
|
|
||||||
const cmDesiredHeight = cm.display.sizer.clientHeight + 2 * cm.defaultTextHeight();
|
|
||||||
const cmGrantableHeight = Math.max(stats.cmActualHeight, Math.min(cmMaxHeight, cmDesiredHeight));
|
|
||||||
stats.deltas.push(cmGrantableHeight - stats.cmActualHeight);
|
|
||||||
stats.totalHeight += cmGrantableHeight + extrasHeight;
|
|
||||||
if (!isLast) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
stats.totalHeight += stats.firstSectionTop;
|
|
||||||
if (stats.totalHeight <= window.innerHeight) {
|
|
||||||
editors.forEach((cm, index) => {
|
|
||||||
cm.setSize(null, stats.deltas[index] + stats.cmActualHeight);
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// scale heights to fill the gap between last section and bottom edge of the window
|
|
||||||
const sections = $('#sections');
|
|
||||||
const available = window.innerHeight - sections.getBoundingClientRect().bottom -
|
|
||||||
parseFloat(getComputedStyle(sections).marginBottom);
|
|
||||||
if (available <= 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const totalDelta = stats.deltas.reduce((sum, d) => sum + d, 0);
|
|
||||||
const q = available / totalDelta;
|
|
||||||
const baseHeight = stats.cmActualHeight - stats.sectionMarginTop;
|
|
||||||
stats.deltas.forEach((delta, index) => {
|
|
||||||
editors[index].setSize(null, baseHeight + Math.floor(q * delta));
|
|
||||||
});
|
|
||||||
|
|
||||||
function getComputedHeight(el) {
|
|
||||||
const compStyle = getComputedStyle(el);
|
|
||||||
return el.getBoundingClientRect().height +
|
|
||||||
parseFloat(compStyle.marginTop) + parseFloat(compStyle.marginBottom);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,4 +1,5 @@
|
||||||
/* global CodeMirror showHelp */
|
/* global CodeMirror showHelp onDOMready $ $$ $create template t
|
||||||
|
prefs stringAsRegExp */
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
onDOMready().then(() => {
|
onDOMready().then(() => {
|
||||||
|
|
|
@ -1,11 +1,9 @@
|
||||||
/*
|
/* global dirtyReporter
|
||||||
global editors styleId: true
|
createAppliesToLineWidget messageBox
|
||||||
global CodeMirror dirtyReporter
|
sectionsToMozFormat
|
||||||
global createAppliesToLineWidget messageBox
|
createMetaCompiler linter createLivePreview cmFactory $ $create API prefs t
|
||||||
global sectionsToMozFormat
|
chromeSync */
|
||||||
global beforeUnload
|
/* exported createSourceEditor */
|
||||||
global createMetaCompiler linter
|
|
||||||
*/
|
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
function createSourceEditor(style) {
|
function createSourceEditor(style) {
|
||||||
|
@ -20,7 +18,6 @@ function createSourceEditor(style) {
|
||||||
const dirty = dirtyReporter();
|
const dirty = dirtyReporter();
|
||||||
dirty.onChange(() => {
|
dirty.onChange(() => {
|
||||||
const isDirty = dirty.isDirty();
|
const isDirty = dirty.isDirty();
|
||||||
window.onbeforeunload = isDirty ? beforeUnload : null;
|
|
||||||
document.body.classList.toggle('dirty', isDirty);
|
document.body.classList.toggle('dirty', isDirty);
|
||||||
$('#save-button').disabled = !isDirty;
|
$('#save-button').disabled = !isDirty;
|
||||||
updateTitle();
|
updateTitle();
|
||||||
|
@ -29,29 +26,26 @@ function createSourceEditor(style) {
|
||||||
// normalize style
|
// normalize style
|
||||||
if (!style.id) setupNewStyle(style);
|
if (!style.id) setupNewStyle(style);
|
||||||
|
|
||||||
const cm = CodeMirror($('.single-editor'), {
|
const cm = cmFactory.create($('.single-editor'), {
|
||||||
value: style.sourceCode,
|
value: style.sourceCode,
|
||||||
});
|
});
|
||||||
let savedGeneration = cm.changeGeneration();
|
let savedGeneration = cm.changeGeneration();
|
||||||
|
|
||||||
editors.push(cm);
|
const livePreview = createLivePreview(preprocess);
|
||||||
|
livePreview.show(Boolean(style.id));
|
||||||
|
|
||||||
$('#enabled').onchange = function () {
|
$('#enabled').onchange = function () {
|
||||||
const value = this.checked;
|
const value = this.checked;
|
||||||
dirty.modify('enabled', style.enabled, value);
|
dirty.modify('enabled', style.enabled, value);
|
||||||
style.enabled = value;
|
style.enabled = value;
|
||||||
|
updateLivePreview();
|
||||||
};
|
};
|
||||||
|
|
||||||
cm.on('changes', () => {
|
cm.on('changes', () => {
|
||||||
dirty.modify('sourceGeneration', savedGeneration, cm.changeGeneration());
|
dirty.modify('sourceGeneration', savedGeneration, cm.changeGeneration());
|
||||||
|
updateLivePreview();
|
||||||
});
|
});
|
||||||
|
|
||||||
CodeMirror.commands.prevEditor = cm => nextPrevMozDocument(cm, -1);
|
|
||||||
CodeMirror.commands.nextEditor = cm => nextPrevMozDocument(cm, 1);
|
|
||||||
CodeMirror.commands.toggleStyle = toggleStyle;
|
|
||||||
CodeMirror.commands.save = save;
|
|
||||||
CodeMirror.closestVisible = () => cm;
|
|
||||||
|
|
||||||
cm.operation(initAppliesToLineWidget);
|
cm.operation(initAppliesToLineWidget);
|
||||||
|
|
||||||
const metaCompiler = createMetaCompiler(cm);
|
const metaCompiler = createMetaCompiler(cm);
|
||||||
|
@ -86,6 +80,24 @@ function createSourceEditor(style) {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function preprocess(style) {
|
||||||
|
return API.buildUsercss({
|
||||||
|
sourceCode: style.sourceCode,
|
||||||
|
assignVars: true
|
||||||
|
})
|
||||||
|
.then(({style: newStyle}) => {
|
||||||
|
delete newStyle.enabled;
|
||||||
|
return Object.assign(style, newStyle);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateLivePreview() {
|
||||||
|
if (!style.id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
livePreview.update(Object.assign({}, style, {sourceCode: cm.getValue()}));
|
||||||
|
}
|
||||||
|
|
||||||
function initAppliesToLineWidget() {
|
function initAppliesToLineWidget() {
|
||||||
const PREF_NAME = 'editor.appliesToLineWidget';
|
const PREF_NAME = 'editor.appliesToLineWidget';
|
||||||
const widget = createAppliesToLineWidget(cm);
|
const widget = createAppliesToLineWidget(cm);
|
||||||
|
@ -179,6 +191,7 @@ function createSourceEditor(style) {
|
||||||
if (codeIsUpdated === false || sameCode) {
|
if (codeIsUpdated === false || sameCode) {
|
||||||
updateEnvironment();
|
updateEnvironment();
|
||||||
dirty.clear('enabled');
|
dirty.clear('enabled');
|
||||||
|
updateLivePreview();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -191,6 +204,10 @@ function createSourceEditor(style) {
|
||||||
cm.setCursor(cursor);
|
cm.setCursor(cursor);
|
||||||
savedGeneration = cm.changeGeneration();
|
savedGeneration = cm.changeGeneration();
|
||||||
}
|
}
|
||||||
|
if (sameCode) {
|
||||||
|
// the code is same but the environment is changed
|
||||||
|
updateLivePreview();
|
||||||
|
}
|
||||||
dirty.clear();
|
dirty.clear();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -199,10 +216,10 @@ function createSourceEditor(style) {
|
||||||
history.replaceState({}, '', `?id=${newStyle.id}`);
|
history.replaceState({}, '', `?id=${newStyle.id}`);
|
||||||
}
|
}
|
||||||
sessionStorage.justEditedStyleId = newStyle.id;
|
sessionStorage.justEditedStyleId = newStyle.id;
|
||||||
style = newStyle;
|
Object.assign(style, newStyle);
|
||||||
styleId = style.id;
|
|
||||||
$('#preview-label').classList.remove('hidden');
|
$('#preview-label').classList.remove('hidden');
|
||||||
updateMeta();
|
updateMeta();
|
||||||
|
livePreview.show(Boolean(style.id));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -218,19 +235,15 @@ function createSourceEditor(style) {
|
||||||
if (!dirty.isDirty()) return;
|
if (!dirty.isDirty()) return;
|
||||||
const code = cm.getValue();
|
const code = cm.getValue();
|
||||||
return ensureUniqueStyle(code)
|
return ensureUniqueStyle(code)
|
||||||
.then(() => API.saveUsercssUnsafe({
|
.then(() => API.editSaveUsercss({
|
||||||
id: style.id,
|
id: style.id,
|
||||||
reason: 'editSave',
|
|
||||||
enabled: style.enabled,
|
enabled: style.enabled,
|
||||||
sourceCode: code,
|
sourceCode: code,
|
||||||
}))
|
}))
|
||||||
.then(({style, errors}) => {
|
.then(replaceStyle)
|
||||||
replaceStyle(style);
|
|
||||||
if (errors) return Promise.reject(errors);
|
|
||||||
})
|
|
||||||
.catch(err => {
|
.catch(err => {
|
||||||
if (err.handled) return;
|
if (err.handled) return;
|
||||||
if (err.message === t('styleMissingMeta', 'name')) {
|
if (err.code === 'missingMandatory' && err.args.includes('name')) {
|
||||||
messageBox.confirm(t('usercssReplaceTemplateConfirmation')).then(ok => ok &&
|
messageBox.confirm(t('usercssReplaceTemplateConfirmation')).then(ok => ok &&
|
||||||
chromeSync.setLZValue('usercssTemplate', code)
|
chromeSync.setLZValue('usercssTemplate', code)
|
||||||
.then(() => chromeSync.getLZValue('usercssTemplate'))
|
.then(() => chromeSync.getLZValue('usercssTemplate'))
|
||||||
|
@ -239,7 +252,7 @@ function createSourceEditor(style) {
|
||||||
}
|
}
|
||||||
const contents = Array.isArray(err) ?
|
const contents = Array.isArray(err) ?
|
||||||
$create('pre', err.join('\n')) :
|
$create('pre', err.join('\n')) :
|
||||||
[String(err)];
|
[err.message || String(err)];
|
||||||
if (Number.isInteger(err.index)) {
|
if (Number.isInteger(err.index)) {
|
||||||
const pos = cm.posFromIndex(err.index);
|
const pos = cm.posFromIndex(err.index);
|
||||||
contents[0] += ` (line ${pos.line + 1} col ${pos.ch + 1})`;
|
contents[0] += ` (line ${pos.line + 1} col ${pos.ch + 1})`;
|
||||||
|
@ -373,5 +386,15 @@ function createSourceEditor(style) {
|
||||||
replaceStyle,
|
replaceStyle,
|
||||||
isDirty: dirty.isDirty,
|
isDirty: dirty.isDirty,
|
||||||
getStyle: () => style,
|
getStyle: () => style,
|
||||||
|
getEditors: () => [cm],
|
||||||
|
scrollToEditor: () => {},
|
||||||
|
getStyleId: () => style.id,
|
||||||
|
getEditorTitle: () => '',
|
||||||
|
save,
|
||||||
|
toggleStyle,
|
||||||
|
prevEditor: cm => nextPrevMozDocument(cm, -1),
|
||||||
|
nextEditor: cm => nextPrevMozDocument(cm, 1),
|
||||||
|
closestVisible: () => cm,
|
||||||
|
getSearchableInputs: () => []
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
/* exported dirtyReporter memoize clipString sectionsToMozFormat */
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
function dirtyReporter() {
|
function dirtyReporter() {
|
||||||
|
|
|
@ -9,6 +9,8 @@
|
||||||
<link href="global.css" rel="stylesheet">
|
<link href="global.css" rel="stylesheet">
|
||||||
<link href="install-usercss/install-usercss.css" rel="stylesheet">
|
<link href="install-usercss/install-usercss.css" rel="stylesheet">
|
||||||
|
|
||||||
|
<script src="js/promisify.js"></script>
|
||||||
|
<script src="js/msg.js"></script>
|
||||||
<script src="js/messaging.js"></script>
|
<script src="js/messaging.js"></script>
|
||||||
<script src="js/prefs.js"></script>
|
<script src="js/prefs.js"></script>
|
||||||
<script src="js/dom.js"></script>
|
<script src="js/dom.js"></script>
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/* global CodeMirror semverCompare closeCurrentTab */
|
/* global CodeMirror semverCompare closeCurrentTab messageBox download
|
||||||
/* global messageBox download chromeLocal */
|
$ $$ $create $createLink t prefs API getTab */
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
(() => {
|
(() => {
|
||||||
|
@ -86,13 +86,11 @@
|
||||||
cm.setCursor(cursor);
|
cm.setCursor(cursor);
|
||||||
cm.scrollTo(scrollInfo.left, scrollInfo.top);
|
cm.scrollTo(scrollInfo.left, scrollInfo.top);
|
||||||
|
|
||||||
API.saveUsercssUnsafe({
|
API.installUsercss({
|
||||||
id: (installed || installedDup).id,
|
id: (installed || installedDup).id,
|
||||||
reason: 'update',
|
|
||||||
sourceCode
|
sourceCode
|
||||||
}).then(({style, errors}) => {
|
}).then(style => {
|
||||||
updateMeta(style);
|
updateMeta(style);
|
||||||
if (errors) return Promise.reject(errors);
|
|
||||||
}).catch(showError);
|
}).catch(showError);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -242,7 +240,7 @@
|
||||||
const contents = Array.isArray(err) ?
|
const contents = Array.isArray(err) ?
|
||||||
[$create('pre', err.join('\n'))] :
|
[$create('pre', err.join('\n'))] :
|
||||||
[err && err.message && $create('pre', err.message) || err || 'Unknown error'];
|
[err && err.message && $create('pre', err.message) || err || 'Unknown error'];
|
||||||
if (Number.isInteger(err.index)) {
|
if (Number.isInteger(err.index) && typeof contents[0] === 'string') {
|
||||||
const pos = cm.posFromIndex(err.index);
|
const pos = cm.posFromIndex(err.index);
|
||||||
contents[0] = `${pos.line + 1}:${pos.ch + 1} ` + contents[0];
|
contents[0] = `${pos.line + 1}:${pos.ch + 1} ` + contents[0];
|
||||||
contents.push($create('pre', drawLinePointer(pos)));
|
contents.push($create('pre', drawLinePointer(pos)));
|
||||||
|
@ -301,7 +299,7 @@
|
||||||
data.version,
|
data.version,
|
||||||
]))
|
]))
|
||||||
).then(ok => ok &&
|
).then(ok => ok &&
|
||||||
API.saveUsercss(Object.assign(style, dup && {reason: 'update'}))
|
API.installUsercss(style)
|
||||||
.then(install)
|
.then(install)
|
||||||
.catch(err => messageBox.alert(t('styleInstallFailed', err), 'pre'))
|
.catch(err => messageBox.alert(t('styleInstallFailed', err), 'pre'))
|
||||||
);
|
);
|
||||||
|
|
71
js/cache.js
Normal file
71
js/cache.js
Normal file
|
@ -0,0 +1,71 @@
|
||||||
|
/* exported createCache */
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
// create a FIFO limit-size map.
|
||||||
|
function createCache({size = 1000, onDeleted} = {}) {
|
||||||
|
const map = new Map();
|
||||||
|
const buffer = Array(size);
|
||||||
|
let index = 0;
|
||||||
|
let lastIndex = 0;
|
||||||
|
return {
|
||||||
|
get,
|
||||||
|
set,
|
||||||
|
delete: delete_,
|
||||||
|
clear,
|
||||||
|
has: id => map.has(id),
|
||||||
|
entries: function *() {
|
||||||
|
for (const [id, item] of map) {
|
||||||
|
yield [id, item.data];
|
||||||
|
}
|
||||||
|
},
|
||||||
|
values: function *() {
|
||||||
|
for (const item of map.values()) {
|
||||||
|
yield item.data;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
get size() {
|
||||||
|
return map.size;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
function get(id) {
|
||||||
|
const item = map.get(id);
|
||||||
|
return item && item.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
function set(id, data) {
|
||||||
|
if (map.size === size) {
|
||||||
|
// full
|
||||||
|
map.delete(buffer[lastIndex].id);
|
||||||
|
if (onDeleted) {
|
||||||
|
onDeleted(buffer[lastIndex].id, buffer[lastIndex].data);
|
||||||
|
}
|
||||||
|
lastIndex = (lastIndex + 1) % size;
|
||||||
|
}
|
||||||
|
const item = {id, data, index};
|
||||||
|
map.set(id, item);
|
||||||
|
buffer[index] = item;
|
||||||
|
index = (index + 1) % size;
|
||||||
|
}
|
||||||
|
|
||||||
|
function delete_(id) {
|
||||||
|
const item = map.get(id);
|
||||||
|
if (!item) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
map.delete(item.id);
|
||||||
|
const lastItem = buffer[lastIndex];
|
||||||
|
lastItem.index = item.index;
|
||||||
|
buffer[item.index] = lastItem;
|
||||||
|
lastIndex = (lastIndex + 1) % size;
|
||||||
|
if (onDeleted) {
|
||||||
|
onDeleted(item.id, item.data);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function clear() {
|
||||||
|
map.clear();
|
||||||
|
index = lastIndex = 0;
|
||||||
|
}
|
||||||
|
}
|
61
js/dom.js
61
js/dom.js
|
@ -1,9 +1,18 @@
|
||||||
|
/* global prefs */
|
||||||
|
/* exported scrollElementIntoView animateElement enforceInputRange $createLink
|
||||||
|
setupLivePrefs moveFocus */
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
if (!/^Win\d+/.test(navigator.platform)) {
|
if (!/^Win\d+/.test(navigator.platform)) {
|
||||||
document.documentElement.classList.add('non-windows');
|
document.documentElement.classList.add('non-windows');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// make querySelectorAll enumeration code readable
|
||||||
|
// FIXME: avoid extending native?
|
||||||
|
['forEach', 'some', 'indexOf', 'map'].forEach(method => {
|
||||||
|
NodeList.prototype[method] = Array.prototype[method];
|
||||||
|
});
|
||||||
|
|
||||||
// polyfill for old browsers to enable [...results] and for-of
|
// polyfill for old browsers to enable [...results] and for-of
|
||||||
for (const type of [NodeList, NamedNodeMap, HTMLCollection, HTMLAllCollection]) {
|
for (const type of [NodeList, NamedNodeMap, HTMLCollection, HTMLAllCollection]) {
|
||||||
if (!type.prototype[Symbol.iterator]) {
|
if (!type.prototype[Symbol.iterator]) {
|
||||||
|
@ -392,3 +401,55 @@ function moveFocus(rootElement, step) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Accepts an array of pref names (values are fetched via prefs.get)
|
||||||
|
// and establishes a two-way connection between the document elements and the actual prefs
|
||||||
|
function setupLivePrefs(
|
||||||
|
IDs = Object.getOwnPropertyNames(prefs.defaults)
|
||||||
|
.filter(id => $('#' + id))
|
||||||
|
) {
|
||||||
|
for (const id of IDs) {
|
||||||
|
const element = $('#' + id);
|
||||||
|
updateElement({id, element, force: true});
|
||||||
|
element.addEventListener('change', onChange);
|
||||||
|
}
|
||||||
|
prefs.subscribe(IDs, (id, value) => updateElement({id, value}));
|
||||||
|
|
||||||
|
function onChange() {
|
||||||
|
const value = getInputValue(this);
|
||||||
|
if (prefs.get(this.id) !== value) {
|
||||||
|
prefs.set(this.id, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function updateElement({
|
||||||
|
id,
|
||||||
|
value = prefs.get(id),
|
||||||
|
element = $('#' + id),
|
||||||
|
force,
|
||||||
|
}) {
|
||||||
|
if (!element) {
|
||||||
|
prefs.unsubscribe(IDs, updateElement);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setInputValue(element, value, force);
|
||||||
|
}
|
||||||
|
function getInputValue(input) {
|
||||||
|
if (input.type === 'checkbox') {
|
||||||
|
return input.checked;
|
||||||
|
}
|
||||||
|
if (input.type === 'number') {
|
||||||
|
return Number(input.value);
|
||||||
|
}
|
||||||
|
return input.value;
|
||||||
|
}
|
||||||
|
function setInputValue(input, value, force = false) {
|
||||||
|
if (force || getInputValue(input) !== value) {
|
||||||
|
if (input.type === 'checkbox') {
|
||||||
|
input.checked = value;
|
||||||
|
} else {
|
||||||
|
input.value = value;
|
||||||
|
}
|
||||||
|
input.dispatchEvent(new Event('change', {bubbles: true, cancelable: true}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
/* global tryCatch */
|
||||||
|
/* exported tHTML formatDate */
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
const template = {};
|
const template = {};
|
||||||
|
|
228
js/messaging.js
228
js/messaging.js
|
@ -1,17 +1,13 @@
|
||||||
/*
|
/* exported getActiveTab onTabReady stringAsRegExp getTabRealURL openURL
|
||||||
global BG: true
|
getStyleWithNoCode tryRegExp sessionStorageHash download
|
||||||
global FIREFOX: true
|
closeCurrentTab */
|
||||||
global onRuntimeMessage applyOnMessage
|
|
||||||
*/
|
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
// keep message channel open for sendResponse in chrome.runtime.onMessage listener
|
|
||||||
const KEEP_CHANNEL_OPEN = true;
|
|
||||||
|
|
||||||
const CHROME = Boolean(chrome.app) && parseInt(navigator.userAgent.match(/Chrom\w+\/(?:\d+\.){2}(\d+)|$/)[1]);
|
const CHROME = Boolean(chrome.app) && parseInt(navigator.userAgent.match(/Chrom\w+\/(?:\d+\.){2}(\d+)|$/)[1]);
|
||||||
const OPERA = Boolean(chrome.app) && parseFloat(navigator.userAgent.match(/\bOPR\/(\d+\.\d+)|$/)[1]);
|
const OPERA = Boolean(chrome.app) && parseFloat(navigator.userAgent.match(/\bOPR\/(\d+\.\d+)|$/)[1]);
|
||||||
const VIVALDI = Boolean(chrome.app) && navigator.userAgent.includes('Vivaldi');
|
const VIVALDI = Boolean(chrome.app) && navigator.userAgent.includes('Vivaldi');
|
||||||
const ANDROID = !chrome.windows;
|
// FIXME: who use this?
|
||||||
|
// const ANDROID = !chrome.windows;
|
||||||
let FIREFOX = !chrome.app && parseFloat(navigator.userAgent.match(/\bFirefox\/(\d+\.\d+)|$/)[1]);
|
let FIREFOX = !chrome.app && parseFloat(navigator.userAgent.match(/\bFirefox\/(\d+\.\d+)|$/)[1]);
|
||||||
|
|
||||||
if (!CHROME && !chrome.browserAction.openPopup) {
|
if (!CHROME && !chrome.browserAction.openPopup) {
|
||||||
|
@ -72,14 +68,9 @@ const URLS = {
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
|
|
||||||
let BG = chrome.extension.getBackgroundPage();
|
const IS_BG = chrome.extension.getBackgroundPage && chrome.extension.getBackgroundPage() === window;
|
||||||
if (BG && !BG.getStyles && BG !== window) {
|
|
||||||
// own page like editor/manage is being loaded on browser startup
|
if (!IS_BG) {
|
||||||
// before the background page has been fully initialized;
|
|
||||||
// it'll be resolved in onBackgroundReady() instead
|
|
||||||
BG = null;
|
|
||||||
}
|
|
||||||
if (!BG || BG !== window) {
|
|
||||||
if (FIREFOX) {
|
if (FIREFOX) {
|
||||||
document.documentElement.classList.add('firefox');
|
document.documentElement.classList.add('firefox');
|
||||||
} else if (OPERA) {
|
} else if (OPERA) {
|
||||||
|
@ -87,169 +78,15 @@ if (!BG || BG !== window) {
|
||||||
} else {
|
} else {
|
||||||
if (VIVALDI) document.documentElement.classList.add('vivaldi');
|
if (VIVALDI) document.documentElement.classList.add('vivaldi');
|
||||||
}
|
}
|
||||||
// TODO: remove once our manifest's minimum_chrome_version is 50+
|
|
||||||
// Chrome 49 doesn't report own extension pages in webNavigation apparently
|
|
||||||
if (CHROME && CHROME < 2661) {
|
|
||||||
getActiveTab().then(tab =>
|
|
||||||
window.API.updateIcon({tab}));
|
|
||||||
}
|
|
||||||
} else if (!BG.API_METHODS) {
|
|
||||||
BG.API_METHODS = {};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const FIREFOX_NO_DOM_STORAGE = FIREFOX && !tryCatch(() => localStorage);
|
if (IS_BG) {
|
||||||
if (FIREFOX_NO_DOM_STORAGE) {
|
window.API_METHODS = {};
|
||||||
// may be disabled via dom.storage.enabled
|
|
||||||
Object.defineProperty(window, 'localStorage', {value: {}});
|
|
||||||
Object.defineProperty(window, 'sessionStorage', {value: {}});
|
|
||||||
}
|
|
||||||
|
|
||||||
// eslint-disable-next-line no-var
|
|
||||||
var API = (() => {
|
|
||||||
return new Proxy(() => {}, {
|
|
||||||
get: (target, name) =>
|
|
||||||
name === 'remoteCall' ?
|
|
||||||
remoteCall :
|
|
||||||
arg => invokeBG(name, arg),
|
|
||||||
});
|
|
||||||
|
|
||||||
function remoteCall(name, arg, remoteWindow) {
|
|
||||||
let thing = window[name] || window.API_METHODS[name];
|
|
||||||
if (typeof thing === 'function') {
|
|
||||||
thing = thing(arg);
|
|
||||||
}
|
|
||||||
if (!thing || typeof thing !== 'object') {
|
|
||||||
return thing;
|
|
||||||
} else if (thing instanceof Promise) {
|
|
||||||
return thing.then(product => remoteWindow.deepCopy(product));
|
|
||||||
} else {
|
|
||||||
return remoteWindow.deepCopy(thing);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function invokeBG(name, arg = {}) {
|
|
||||||
if (BG && (name in BG || name in BG.API_METHODS)) {
|
|
||||||
const call = BG !== window ?
|
|
||||||
BG.API.remoteCall(name, BG.deepCopy(arg), window) :
|
|
||||||
remoteCall(name, arg, BG);
|
|
||||||
return Promise.resolve(call);
|
|
||||||
}
|
|
||||||
if (BG && BG.getStyles) {
|
|
||||||
throw new Error('Bad API method', name, arg);
|
|
||||||
}
|
|
||||||
if (FIREFOX) {
|
|
||||||
arg.method = name;
|
|
||||||
return sendMessage(arg);
|
|
||||||
}
|
|
||||||
return onBackgroundReady().then(() => invokeBG(name, arg));
|
|
||||||
}
|
|
||||||
|
|
||||||
function onBackgroundReady() {
|
|
||||||
return BG && BG.getStyles ? Promise.resolve() : new Promise(function ping(resolve) {
|
|
||||||
sendMessage({method: 'healthCheck'}, health => {
|
|
||||||
if (health !== undefined) {
|
|
||||||
BG = chrome.extension.getBackgroundPage();
|
|
||||||
resolve();
|
|
||||||
} else {
|
|
||||||
setTimeout(ping, 0, resolve);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
|
|
||||||
|
|
||||||
function notifyAllTabs(msg) {
|
|
||||||
const originalMessage = msg;
|
|
||||||
const styleUpdated = msg.method === 'styleUpdated';
|
|
||||||
if (styleUpdated || msg.method === 'styleAdded') {
|
|
||||||
// apply/popup/manage use only meta for these two methods,
|
|
||||||
// editor may need the full code but can fetch it directly,
|
|
||||||
// so we send just the meta to avoid spamming lots of tabs with huge styles
|
|
||||||
msg = Object.assign({}, msg, {
|
|
||||||
style: getStyleWithNoCode(msg.style)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
const affectsAll = !msg.affects || msg.affects.all;
|
|
||||||
const affectsOwnOriginOnly = !affectsAll && (msg.affects.editor || msg.affects.manager);
|
|
||||||
const affectsTabs = affectsAll || affectsOwnOriginOnly;
|
|
||||||
const affectsIcon = affectsAll || msg.affects.icon;
|
|
||||||
const affectsPopup = affectsAll || msg.affects.popup;
|
|
||||||
const affectsSelf = affectsPopup || msg.prefs;
|
|
||||||
// notify all open extension pages and popups
|
|
||||||
if (affectsSelf) {
|
|
||||||
msg.tabId = undefined;
|
|
||||||
sendMessage(msg, ignoreChromeError);
|
|
||||||
}
|
|
||||||
// notify tabs
|
|
||||||
if (affectsTabs || affectsIcon) {
|
|
||||||
const notifyTab = tab => {
|
|
||||||
if (!styleUpdated
|
|
||||||
&& (affectsTabs || URLS.optionsUI.includes(tab.url))
|
|
||||||
// own pages are already notified via sendMessage
|
|
||||||
&& !(affectsSelf && tab.url.startsWith(URLS.ownOrigin))
|
|
||||||
// skip lazy-loaded aka unloaded tabs that seem to start loading on message in FF
|
|
||||||
&& (!FIREFOX || tab.width)) {
|
|
||||||
msg.tabId = tab.id;
|
|
||||||
sendMessage(msg, ignoreChromeError);
|
|
||||||
}
|
|
||||||
if (affectsIcon) {
|
|
||||||
// eslint-disable-next-line no-use-before-define
|
|
||||||
debounce(API.updateIcon, 0, {tab});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
// list all tabs including chrome-extension:// which can be ours
|
|
||||||
Promise.all([
|
|
||||||
queryTabs(affectsOwnOriginOnly ? {url: URLS.ownOrigin + '*'} : {}),
|
|
||||||
getActiveTab(),
|
|
||||||
]).then(([tabs, activeTab]) => {
|
|
||||||
const activeTabId = activeTab && activeTab.id;
|
|
||||||
for (const tab of tabs) {
|
|
||||||
invokeOrPostpone(tab.id === activeTabId, notifyTab, tab);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
// notify self: the message no longer is sent to the origin in new Chrome
|
|
||||||
if (typeof onRuntimeMessage !== 'undefined') {
|
|
||||||
onRuntimeMessage(originalMessage);
|
|
||||||
}
|
|
||||||
// notify apply.js on own pages
|
|
||||||
if (typeof applyOnMessage !== 'undefined') {
|
|
||||||
applyOnMessage(originalMessage);
|
|
||||||
}
|
|
||||||
// propagate saved style state/code efficiently
|
|
||||||
if (styleUpdated) {
|
|
||||||
msg.refreshOwnTabs = false;
|
|
||||||
API.refreshAllTabs(msg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function sendMessage(msg, callback) {
|
|
||||||
/*
|
|
||||||
Promise mode [default]:
|
|
||||||
- rejects on receiving {__ERROR__: message} created by background.js::onRuntimeMessage
|
|
||||||
- automatically suppresses chrome.runtime.lastError because it's autogenerated
|
|
||||||
by browserAction.setText which lacks a callback param in chrome API
|
|
||||||
Standard callback mode:
|
|
||||||
- enabled by passing a second param
|
|
||||||
*/
|
|
||||||
const {tabId, frameId} = msg;
|
|
||||||
const fn = tabId >= 0 ? chrome.tabs.sendMessage : chrome.runtime.sendMessage;
|
|
||||||
const args = tabId >= 0 ? [tabId, msg, {frameId}] : [msg];
|
|
||||||
if (callback) {
|
|
||||||
fn(...args, callback);
|
|
||||||
} else {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
fn(...args, r => {
|
|
||||||
const err = r && r.__ERROR__;
|
|
||||||
(err ? reject : resolve)(err || r);
|
|
||||||
ignoreChromeError();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// FIXME: `localStorage` and `sessionStorage` may be disabled via dom.storage.enabled
|
||||||
|
// Object.defineProperty(window, 'localStorage', {value: {}});
|
||||||
|
// Object.defineProperty(window, 'sessionStorage', {value: {}});
|
||||||
|
|
||||||
function queryTabs(options = {}) {
|
function queryTabs(options = {}) {
|
||||||
return new Promise(resolve =>
|
return new Promise(resolve =>
|
||||||
|
@ -276,13 +113,6 @@ function getActiveTab() {
|
||||||
.then(tabs => tabs[0]);
|
.then(tabs => tabs[0]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function getActiveTabRealURL() {
|
|
||||||
return getActiveTab()
|
|
||||||
.then(getTabRealURL);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function getTabRealURL(tab) {
|
function getTabRealURL(tab) {
|
||||||
return new Promise(resolve => {
|
return new Promise(resolve => {
|
||||||
if (tab.url !== 'chrome://newtab/' || URLS.chromeProtectsNTP) {
|
if (tab.url !== 'chrome://newtab/' || URLS.chromeProtectsNTP) {
|
||||||
|
@ -385,7 +215,6 @@ function openURL({
|
||||||
index,
|
index,
|
||||||
active,
|
active,
|
||||||
currentWindow = true,
|
currentWindow = true,
|
||||||
message,
|
|
||||||
}) {
|
}) {
|
||||||
url = url.includes('://') ? url : chrome.runtime.getURL(url);
|
url = url.includes('://') ? url : chrome.runtime.getURL(url);
|
||||||
// [some] chromium forks don't handle their fake branded protocols
|
// [some] chromium forks don't handle their fake branded protocols
|
||||||
|
@ -401,15 +230,7 @@ function openURL({
|
||||||
url.replace(/%2F.*/, '*').replace(/#.*/, '') :
|
url.replace(/%2F.*/, '*').replace(/#.*/, '') :
|
||||||
url.replace(/#.*/, '');
|
url.replace(/#.*/, '');
|
||||||
|
|
||||||
const task = queryTabs({url: urlQuery, currentWindow}).then(maybeSwitch);
|
return queryTabs({url: urlQuery, currentWindow}).then(maybeSwitch);
|
||||||
if (!message) {
|
|
||||||
return task;
|
|
||||||
} else {
|
|
||||||
return task.then(onTabReady).then(tab => {
|
|
||||||
message.tabId = tab.id;
|
|
||||||
return sendMessage(message).then(() => tab);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function maybeSwitch(tabs = []) {
|
function maybeSwitch(tabs = []) {
|
||||||
const urlWithSlash = url + '/';
|
const urlWithSlash = url + '/';
|
||||||
|
@ -652,27 +473,6 @@ function download(url, {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function invokeOrPostpone(isInvoke, fn, ...args) {
|
|
||||||
return isInvoke
|
|
||||||
? fn(...args)
|
|
||||||
: setTimeout(invokeOrPostpone, 0, true, fn, ...args);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function openEditor({id}) {
|
|
||||||
let url = '/edit.html';
|
|
||||||
if (id) {
|
|
||||||
url += `?id=${id}`;
|
|
||||||
}
|
|
||||||
if (chrome.windows && prefs.get('openEditInWindow')) {
|
|
||||||
chrome.windows.create(Object.assign({url}, prefs.get('windowPosition')));
|
|
||||||
} else {
|
|
||||||
openURL({url});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function closeCurrentTab() {
|
function closeCurrentTab() {
|
||||||
// https://bugzilla.mozilla.org/show_bug.cgi?id=1409375
|
// https://bugzilla.mozilla.org/show_bug.cgi?id=1409375
|
||||||
getOwnTab().then(tab => {
|
getOwnTab().then(tab => {
|
||||||
|
|
78
js/meta-parser.js
Normal file
78
js/meta-parser.js
Normal file
|
@ -0,0 +1,78 @@
|
||||||
|
/* global usercssMeta colorConverter */
|
||||||
|
/* exported metaParser */
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const metaParser = (() => {
|
||||||
|
const {createParser, ParseError} = usercssMeta;
|
||||||
|
const PREPROCESSORS = new Set(['default', 'uso', 'stylus', 'less']);
|
||||||
|
const options = {
|
||||||
|
validateKey: {
|
||||||
|
preprocessor: state => {
|
||||||
|
if (!PREPROCESSORS.has(state.value)) {
|
||||||
|
throw new ParseError({
|
||||||
|
code: 'unknownPreprocessor',
|
||||||
|
args: [state.value],
|
||||||
|
index: state.valueIndex
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
validateVar: {
|
||||||
|
select: state => {
|
||||||
|
if (state.varResult.options.every(o => o.name !== state.value)) {
|
||||||
|
throw new ParseError({
|
||||||
|
code: 'invalidSelectValueMismatch',
|
||||||
|
index: state.valueIndex
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
color: state => {
|
||||||
|
const color = colorConverter.parse(state.value);
|
||||||
|
if (!color) {
|
||||||
|
throw new ParseError({
|
||||||
|
code: 'invalidColor',
|
||||||
|
args: [state.value],
|
||||||
|
index: state.valueIndex
|
||||||
|
});
|
||||||
|
}
|
||||||
|
state.value = colorConverter.format(color, 'rgb');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const parser = createParser(options);
|
||||||
|
const looseParser = createParser(Object.assign({}, options, {allowErrors: true, unknownKey: 'throw'}));
|
||||||
|
return {
|
||||||
|
parse,
|
||||||
|
lint,
|
||||||
|
nullifyInvalidVars
|
||||||
|
};
|
||||||
|
|
||||||
|
function parse(text, indexOffset) {
|
||||||
|
try {
|
||||||
|
return parser.parse(text);
|
||||||
|
} catch (err) {
|
||||||
|
if (typeof err.index === 'number') {
|
||||||
|
err.index += indexOffset;
|
||||||
|
}
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function lint(text) {
|
||||||
|
return looseParser.parse(text);
|
||||||
|
}
|
||||||
|
|
||||||
|
function nullifyInvalidVars(vars) {
|
||||||
|
for (const va of Object.values(vars)) {
|
||||||
|
if (va.value === null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
parser.validateVar(va);
|
||||||
|
} catch (err) {
|
||||||
|
va.value = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return vars;
|
||||||
|
}
|
||||||
|
})();
|
|
@ -1,4 +1,5 @@
|
||||||
/* global parserlib */
|
/* global parserlib */
|
||||||
|
/* exported parseMozFormat */
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
329
js/msg.js
Normal file
329
js/msg.js
Normal file
|
@ -0,0 +1,329 @@
|
||||||
|
/* global promisify deepCopy */
|
||||||
|
/* exported msg API */
|
||||||
|
// deepCopy is only used if the script is executed in extension pages.
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const msg = (() => {
|
||||||
|
let isBg = false;
|
||||||
|
if (chrome.extension.getBackgroundPage && chrome.extension.getBackgroundPage() === window) {
|
||||||
|
isBg = true;
|
||||||
|
window._msg = {
|
||||||
|
id: 1,
|
||||||
|
storage: new Map(),
|
||||||
|
handler: null,
|
||||||
|
clone: deepCopy
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const runtimeSend = promisify(chrome.runtime.sendMessage.bind(chrome.runtime));
|
||||||
|
const tabSend = chrome.tabs && promisify(chrome.tabs.sendMessage.bind(chrome.tabs));
|
||||||
|
const tabQuery = chrome.tabs && promisify(chrome.tabs.query.bind(chrome.tabs));
|
||||||
|
let bg;
|
||||||
|
const preparing = !isBg && chrome.runtime.getBackgroundPage &&
|
||||||
|
promisify(chrome.runtime.getBackgroundPage.bind(chrome.runtime))()
|
||||||
|
.catch(() => null)
|
||||||
|
.then(_bg => {
|
||||||
|
bg = _bg;
|
||||||
|
});
|
||||||
|
bg = isBg ? window : !preparing ? null : undefined;
|
||||||
|
const EXTENSION_URL = chrome.runtime.getURL('');
|
||||||
|
let handler;
|
||||||
|
const from_ = location.href.startsWith(EXTENSION_URL) ? 'extension' : 'content';
|
||||||
|
const RX_NO_RECEIVER = /Receiving end does not exist/;
|
||||||
|
const RX_PORT_CLOSED = /The message port closed before a response was received/;
|
||||||
|
return {
|
||||||
|
send,
|
||||||
|
sendTab,
|
||||||
|
sendBg,
|
||||||
|
broadcast,
|
||||||
|
broadcastTab,
|
||||||
|
broadcastExtension,
|
||||||
|
ignoreError,
|
||||||
|
on,
|
||||||
|
onTab,
|
||||||
|
onExtension,
|
||||||
|
off,
|
||||||
|
RX_NO_RECEIVER,
|
||||||
|
RX_PORT_CLOSED
|
||||||
|
};
|
||||||
|
|
||||||
|
function send(data, target = 'extension') {
|
||||||
|
if (bg === undefined) {
|
||||||
|
return preparing.then(() => send(data, target));
|
||||||
|
}
|
||||||
|
const message = {type: 'direct', data, target, from: from_};
|
||||||
|
if (bg) {
|
||||||
|
exchangeSet(message);
|
||||||
|
}
|
||||||
|
const request = runtimeSend(message).then(unwrapData);
|
||||||
|
if (message.id) {
|
||||||
|
return withCleanup(request, () => bg._msg.storage.delete(message.id));
|
||||||
|
}
|
||||||
|
return request;
|
||||||
|
}
|
||||||
|
|
||||||
|
function sendTab(tabId, data, options, target = 'tab') {
|
||||||
|
return tabSend(tabId, {type: 'direct', data, target, from: from_}, options)
|
||||||
|
.then(unwrapData);
|
||||||
|
}
|
||||||
|
|
||||||
|
function sendBg(data) {
|
||||||
|
if (bg === undefined) {
|
||||||
|
return preparing.then(doSend);
|
||||||
|
}
|
||||||
|
return withPromiseError(doSend);
|
||||||
|
|
||||||
|
function doSend() {
|
||||||
|
if (bg) {
|
||||||
|
if (!bg._msg.handler) {
|
||||||
|
throw new Error('there is no bg handler');
|
||||||
|
}
|
||||||
|
const handlers = bg._msg.handler.extension.concat(bg._msg.handler.both);
|
||||||
|
// in FF, the object would become a dead object when the window
|
||||||
|
// is closed, so we have to clone the object into background.
|
||||||
|
return Promise.resolve(executeCallbacks(handlers, bg._msg.clone(data), {url: location.href}))
|
||||||
|
.then(deepCopy);
|
||||||
|
}
|
||||||
|
return send(data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function ignoreError(err) {
|
||||||
|
if (err.message && (
|
||||||
|
RX_NO_RECEIVER.test(err.message) ||
|
||||||
|
RX_PORT_CLOSED.test(err.message)
|
||||||
|
)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
console.warn(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
function broadcast(data, filter) {
|
||||||
|
return Promise.all([
|
||||||
|
send(data, 'both').catch(ignoreError),
|
||||||
|
broadcastTab(data, filter, null, true, 'both')
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
function broadcastTab(data, filter, options, ignoreExtension = false, target = 'tab') {
|
||||||
|
return tabQuery({})
|
||||||
|
// TODO: send to activated tabs first?
|
||||||
|
.then(tabs => {
|
||||||
|
const requests = [];
|
||||||
|
for (const tab of tabs) {
|
||||||
|
const isExtension = tab.url.startsWith(EXTENSION_URL);
|
||||||
|
if (
|
||||||
|
tab.discarded ||
|
||||||
|
// FIXME: use `URLS.supported`?
|
||||||
|
!/^(http|ftp|file)/.test(tab.url) &&
|
||||||
|
!tab.url.startsWith('chrome://newtab/') &&
|
||||||
|
!isExtension ||
|
||||||
|
isExtension && ignoreExtension ||
|
||||||
|
filter && !filter(tab)
|
||||||
|
) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const dataObj = typeof data === 'function' ? data(tab) : data;
|
||||||
|
if (!dataObj) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const message = {type: 'direct', data: dataObj, target, from: from_};
|
||||||
|
if (isExtension) {
|
||||||
|
exchangeSet(message);
|
||||||
|
}
|
||||||
|
let request = tabSend(tab.id, message, options).then(unwrapData);
|
||||||
|
if (message.id) {
|
||||||
|
request = withCleanup(request, () => bg._msg.storage.delete(message.id));
|
||||||
|
}
|
||||||
|
requests.push(request.catch(ignoreError));
|
||||||
|
}
|
||||||
|
return Promise.all(requests);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function broadcastExtension(...args) {
|
||||||
|
return send(...args).catch(ignoreError);
|
||||||
|
}
|
||||||
|
|
||||||
|
function on(fn) {
|
||||||
|
initHandler();
|
||||||
|
handler.both.push(fn);
|
||||||
|
}
|
||||||
|
|
||||||
|
function onTab(fn) {
|
||||||
|
initHandler();
|
||||||
|
handler.tab.push(fn);
|
||||||
|
}
|
||||||
|
|
||||||
|
function onExtension(fn) {
|
||||||
|
initHandler();
|
||||||
|
handler.extension.push(fn);
|
||||||
|
}
|
||||||
|
|
||||||
|
function off(fn) {
|
||||||
|
for (const type of ['both', 'tab', 'extension']) {
|
||||||
|
const index = handler[type].indexOf(fn);
|
||||||
|
if (index >= 0) {
|
||||||
|
handler[type].splice(index, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function initHandler() {
|
||||||
|
if (handler) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
handler = {
|
||||||
|
both: [],
|
||||||
|
tab: [],
|
||||||
|
extension: []
|
||||||
|
};
|
||||||
|
if (isBg) {
|
||||||
|
bg._msg.handler = handler;
|
||||||
|
}
|
||||||
|
chrome.runtime.onMessage.addListener(handleMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
function executeCallbacks(callbacks, ...args) {
|
||||||
|
let result;
|
||||||
|
for (const fn of callbacks) {
|
||||||
|
const data = withPromiseError(fn, ...args);
|
||||||
|
if (data !== undefined && result === undefined) {
|
||||||
|
result = data;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleMessage(message, sender, sendResponse) {
|
||||||
|
const handlers = message.target === 'tab' ?
|
||||||
|
handler.tab.concat(handler.both) : message.target === 'extension' ?
|
||||||
|
handler.extension.concat(handler.both) :
|
||||||
|
handler.both.concat(handler.extension, handler.tab);
|
||||||
|
if (!handlers.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (message.type === 'exchange') {
|
||||||
|
const pending = exchangeGet(message, true);
|
||||||
|
if (pending) {
|
||||||
|
pending.then(response);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return response();
|
||||||
|
|
||||||
|
function response() {
|
||||||
|
const result = executeCallbacks(handlers, message.data, sender);
|
||||||
|
if (result === undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Promise.resolve(result)
|
||||||
|
.then(
|
||||||
|
data => ({
|
||||||
|
error: false,
|
||||||
|
data
|
||||||
|
}),
|
||||||
|
err => ({
|
||||||
|
error: true,
|
||||||
|
data: Object.assign({
|
||||||
|
message: err.message || String(err),
|
||||||
|
// FIXME: do we want to pass the entire stack?
|
||||||
|
stack: err.stack
|
||||||
|
}, err) // this allows us to pass custom properties e.g. `err.index`
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.then(function doResponse(responseMessage) {
|
||||||
|
if (message.from === 'extension' && bg === undefined) {
|
||||||
|
return preparing.then(() => doResponse(responseMessage));
|
||||||
|
}
|
||||||
|
if (message.from === 'extension' && bg) {
|
||||||
|
exchangeSet(responseMessage);
|
||||||
|
} else {
|
||||||
|
responseMessage.type = 'direct';
|
||||||
|
}
|
||||||
|
return responseMessage;
|
||||||
|
})
|
||||||
|
.then(sendResponse);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function exchangeGet(message, keepStorage = false) {
|
||||||
|
if (bg === undefined) {
|
||||||
|
return preparing.then(() => exchangeGet(message, keepStorage));
|
||||||
|
}
|
||||||
|
message.data = bg._msg.storage.get(message.id);
|
||||||
|
if (keepStorage) {
|
||||||
|
message.data = deepCopy(message.data);
|
||||||
|
} else {
|
||||||
|
bg._msg.storage.delete(message.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function exchangeSet(message) {
|
||||||
|
const id = bg._msg.id;
|
||||||
|
bg._msg.storage.set(id, message.data);
|
||||||
|
bg._msg.id++;
|
||||||
|
message.type = 'exchange';
|
||||||
|
message.id = id;
|
||||||
|
delete message.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
function withPromiseError(fn, ...args) {
|
||||||
|
try {
|
||||||
|
return fn(...args);
|
||||||
|
} catch (err) {
|
||||||
|
return Promise.reject(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function withCleanup(p, fn) {
|
||||||
|
return p.then(
|
||||||
|
result => {
|
||||||
|
cleanup();
|
||||||
|
return result;
|
||||||
|
},
|
||||||
|
err => {
|
||||||
|
cleanup();
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
function cleanup() {
|
||||||
|
try {
|
||||||
|
fn();
|
||||||
|
} catch (err) {
|
||||||
|
// pass
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// {type, error, data, id}
|
||||||
|
function unwrapData(result) {
|
||||||
|
if (result === undefined) {
|
||||||
|
throw new Error('Receiving end does not exist');
|
||||||
|
}
|
||||||
|
if (result.type === 'exchange') {
|
||||||
|
const pending = exchangeGet(result);
|
||||||
|
if (pending) {
|
||||||
|
return pending.then(unwrap);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return unwrap();
|
||||||
|
|
||||||
|
function unwrap() {
|
||||||
|
if (result.error) {
|
||||||
|
throw Object.assign(new Error(result.data.message), result.data);
|
||||||
|
}
|
||||||
|
return result.data;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
|
const API = new Proxy({}, {
|
||||||
|
get: (target, name) =>
|
||||||
|
(...args) => Promise.resolve(msg.sendBg({
|
||||||
|
method: 'invokeAPI',
|
||||||
|
name,
|
||||||
|
args
|
||||||
|
}))
|
||||||
|
});
|
316
js/prefs.js
316
js/prefs.js
|
@ -1,8 +1,8 @@
|
||||||
/* global prefs: true, contextMenus, FIREFOX_NO_DOM_STORAGE */
|
/* global promisify */
|
||||||
|
/* exported prefs */
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
// eslint-disable-next-line no-var
|
const prefs = (() => {
|
||||||
var prefs = new function Prefs() {
|
|
||||||
const defaults = {
|
const defaults = {
|
||||||
'openEditInWindow': false, // new editor opens in a own browser window
|
'openEditInWindow': false, // new editor opens in a own browser window
|
||||||
'windowPosition': {}, // detached window position
|
'windowPosition': {}, // detached window position
|
||||||
|
@ -98,29 +98,33 @@ var prefs = new function Prefs() {
|
||||||
};
|
};
|
||||||
const values = deepCopy(defaults);
|
const values = deepCopy(defaults);
|
||||||
|
|
||||||
const affectsIcon = [
|
|
||||||
'show-badge',
|
|
||||||
'disableAll',
|
|
||||||
'badgeDisabled',
|
|
||||||
'badgeNormal',
|
|
||||||
'iconset',
|
|
||||||
];
|
|
||||||
|
|
||||||
const onChange = {
|
const onChange = {
|
||||||
any: new Set(),
|
any: new Set(),
|
||||||
specific: new Map(),
|
specific: new Map(),
|
||||||
};
|
};
|
||||||
|
|
||||||
// coalesce multiple pref changes in broadcast
|
const initializing = promisify(chrome.storage.sync.get.bind(chrome.storage.sync))('settings')
|
||||||
let broadcastPrefs = {};
|
.then(result => {
|
||||||
|
if (result.settings) {
|
||||||
|
setAll(result.settings, true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
Object.defineProperties(this, {
|
chrome.storage.onChanged.addListener((changes, area) => {
|
||||||
defaults: {value: deepCopy(defaults)},
|
if (area !== 'sync' || !changes.settings || !changes.settings.newValue) {
|
||||||
readOnlyValues: {value: {}},
|
return;
|
||||||
|
}
|
||||||
|
initializing.then(() => setAll(changes.settings.newValue, true));
|
||||||
});
|
});
|
||||||
|
|
||||||
Object.assign(Prefs.prototype, {
|
let timer;
|
||||||
|
|
||||||
|
// coalesce multiple pref changes in broadcast
|
||||||
|
// let changes = {};
|
||||||
|
|
||||||
|
return {
|
||||||
|
initializing,
|
||||||
|
defaults,
|
||||||
get(key, defaultValue) {
|
get(key, defaultValue) {
|
||||||
if (key in values) {
|
if (key in values) {
|
||||||
return values[key];
|
return values[key];
|
||||||
|
@ -133,62 +137,11 @@ var prefs = new function Prefs() {
|
||||||
}
|
}
|
||||||
console.warn("No default preference for '%s'", key);
|
console.warn("No default preference for '%s'", key);
|
||||||
},
|
},
|
||||||
|
|
||||||
getAll() {
|
getAll() {
|
||||||
return deepCopy(values);
|
return deepCopy(values);
|
||||||
},
|
},
|
||||||
|
set,
|
||||||
set(key, value, {broadcast = true, sync = true, fromBroadcast} = {}) {
|
reset: key => set(key, deepCopy(defaults[key])),
|
||||||
const oldValue = values[key];
|
|
||||||
switch (typeof defaults[key]) {
|
|
||||||
case typeof value:
|
|
||||||
break;
|
|
||||||
case 'string':
|
|
||||||
value = String(value);
|
|
||||||
break;
|
|
||||||
case 'number':
|
|
||||||
value |= 0;
|
|
||||||
break;
|
|
||||||
case 'boolean':
|
|
||||||
value = value === true || value === 'true';
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
values[key] = value;
|
|
||||||
defineReadonlyProperty(this.readOnlyValues, key, value);
|
|
||||||
const hasChanged = !equal(value, oldValue);
|
|
||||||
if (!fromBroadcast || FIREFOX_NO_DOM_STORAGE) {
|
|
||||||
localStorage[key] = typeof defaults[key] === 'object'
|
|
||||||
? JSON.stringify(value)
|
|
||||||
: value;
|
|
||||||
}
|
|
||||||
if (!fromBroadcast && broadcast && hasChanged) {
|
|
||||||
this.broadcast(key, value, {sync});
|
|
||||||
}
|
|
||||||
if (hasChanged) {
|
|
||||||
const specific = onChange.specific.get(key);
|
|
||||||
if (typeof specific === 'function') {
|
|
||||||
specific(key, value);
|
|
||||||
} else if (specific instanceof Set) {
|
|
||||||
for (const listener of specific.values()) {
|
|
||||||
listener(key, value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for (const listener of onChange.any.values()) {
|
|
||||||
listener(key, value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
reset: key => this.set(key, deepCopy(defaults[key])),
|
|
||||||
|
|
||||||
broadcast(key, value, {sync = true} = {}) {
|
|
||||||
broadcastPrefs[key] = value;
|
|
||||||
debounce(doBroadcast);
|
|
||||||
if (sync) {
|
|
||||||
debounce(doSyncSet);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
subscribe(keys, listener) {
|
subscribe(keys, listener) {
|
||||||
// keys: string[] ids
|
// keys: string[] ids
|
||||||
// or a falsy value to subscribe to everything
|
// or a falsy value to subscribe to everything
|
||||||
|
@ -208,7 +161,6 @@ var prefs = new function Prefs() {
|
||||||
onChange.any.add(listener);
|
onChange.any.add(listener);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
unsubscribe(keys, listener) {
|
unsubscribe(keys, listener) {
|
||||||
if (keys) {
|
if (keys) {
|
||||||
for (const key of keys) {
|
for (const key of keys) {
|
||||||
|
@ -226,147 +178,58 @@ var prefs = new function Prefs() {
|
||||||
onChange.all.remove(listener);
|
onChange.all.remove(listener);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
});
|
};
|
||||||
|
|
||||||
{
|
function setAll(settings, synced) {
|
||||||
const importFromBG = () =>
|
for (const [key, value] of Object.entries(settings)) {
|
||||||
API.getPrefs().then(prefs => {
|
set(key, value, synced);
|
||||||
const props = {};
|
}
|
||||||
for (const id in prefs) {
|
|
||||||
const value = prefs[id];
|
|
||||||
values[id] = value;
|
|
||||||
props[id] = {value: deepCopy(value)};
|
|
||||||
}
|
|
||||||
Object.defineProperties(this.readOnlyValues, props);
|
|
||||||
});
|
|
||||||
// Unlike chrome.storage or messaging, HTML5 localStorage is synchronous and always ready,
|
|
||||||
// so we'll mirror the prefs to avoid using the wrong defaults during the startup phase
|
|
||||||
const importFromLocalStorage = () => {
|
|
||||||
forgetOutdatedDefaults(localStorage);
|
|
||||||
for (const key in defaults) {
|
|
||||||
const defaultValue = defaults[key];
|
|
||||||
let value = localStorage[key];
|
|
||||||
if (typeof value === 'string') {
|
|
||||||
switch (typeof defaultValue) {
|
|
||||||
case 'boolean':
|
|
||||||
value = value.toLowerCase() === 'true';
|
|
||||||
break;
|
|
||||||
case 'number':
|
|
||||||
value |= 0;
|
|
||||||
break;
|
|
||||||
case 'object':
|
|
||||||
value = tryJSONparse(value) || defaultValue;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
} else if (FIREFOX_NO_DOM_STORAGE && BG) {
|
|
||||||
value = BG.localStorage[key];
|
|
||||||
value = value === undefined ? defaultValue : value;
|
|
||||||
localStorage[key] = value;
|
|
||||||
} else {
|
|
||||||
value = defaultValue;
|
|
||||||
}
|
|
||||||
if (BG === window) {
|
|
||||||
// when in bg page, .set() will write to localStorage
|
|
||||||
this.set(key, value, {broadcast: false, sync: false});
|
|
||||||
} else {
|
|
||||||
values[key] = value;
|
|
||||||
defineReadonlyProperty(this.readOnlyValues, key, value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return Promise.resolve();
|
|
||||||
};
|
|
||||||
(FIREFOX_NO_DOM_STORAGE && !BG ? importFromBG() : importFromLocalStorage()).then(() => {
|
|
||||||
if (BG && BG !== window) return;
|
|
||||||
if (BG === window) {
|
|
||||||
affectsIcon.forEach(key => this.broadcast(key, values[key], {sync: false}));
|
|
||||||
chromeSync.getValue('settings').then(settings => importFromSync.call(this, settings));
|
|
||||||
}
|
|
||||||
chrome.storage.onChanged.addListener((changes, area) => {
|
|
||||||
if (area === 'sync' && 'settings' in changes) {
|
|
||||||
importFromSync.call(this, changes.settings.newValue);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// any access to chrome API takes time due to initialization of bindings
|
function set(key, value, synced = false) {
|
||||||
window.addEventListener('load', function _() {
|
const oldValue = values[key];
|
||||||
window.removeEventListener('load', _);
|
switch (typeof defaults[key]) {
|
||||||
chrome.runtime.onMessage.addListener(msg => {
|
case typeof value:
|
||||||
if (msg.prefs) {
|
break;
|
||||||
for (const id in msg.prefs) {
|
case 'string':
|
||||||
prefs.set(id, msg.prefs[id], {fromBroadcast: true});
|
value = String(value);
|
||||||
}
|
break;
|
||||||
}
|
case 'number':
|
||||||
});
|
value |= 0;
|
||||||
});
|
break;
|
||||||
|
case 'boolean':
|
||||||
// register hotkeys
|
value = value === true || value === 'true';
|
||||||
if (FIREFOX && (browser.commands || {}).update) {
|
break;
|
||||||
const hotkeyPrefs = Object.keys(values).filter(k => k.startsWith('hotkey.'));
|
}
|
||||||
this.subscribe(hotkeyPrefs, (name, value) => {
|
if (equal(value, oldValue)) {
|
||||||
try {
|
|
||||||
name = name.split('.')[1];
|
|
||||||
if (value.trim()) {
|
|
||||||
browser.commands.update({name, shortcut: value}).catch(ignoreChromeError);
|
|
||||||
} else {
|
|
||||||
browser.commands.reset(name).catch(ignoreChromeError);
|
|
||||||
}
|
|
||||||
} catch (e) {}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return;
|
|
||||||
|
|
||||||
function doBroadcast() {
|
|
||||||
if (BG && BG === window && !BG.dbExec.initialized) {
|
|
||||||
window.addEventListener('storageReady', function _() {
|
|
||||||
window.removeEventListener('storageReady', _);
|
|
||||||
doBroadcast();
|
|
||||||
});
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const affects = {
|
values[key] = value;
|
||||||
all: 'disableAll' in broadcastPrefs
|
emitChange(key, value);
|
||||||
|| 'exposeIframes' in broadcastPrefs,
|
if (synced || timer) {
|
||||||
};
|
return;
|
||||||
if (!affects.all) {
|
}
|
||||||
for (const key in broadcastPrefs) {
|
timer = setTimeout(syncPrefs);
|
||||||
affects.icon = affects.icon || affectsIcon.includes(key);
|
}
|
||||||
affects.popup = affects.popup || key.startsWith('popup');
|
|
||||||
affects.editor = affects.editor || key.startsWith('editor');
|
function emitChange(key, value) {
|
||||||
affects.manager = affects.manager || key.startsWith('manage');
|
const specific = onChange.specific.get(key);
|
||||||
|
if (typeof specific === 'function') {
|
||||||
|
specific(key, value);
|
||||||
|
} else if (specific instanceof Set) {
|
||||||
|
for (const listener of specific.values()) {
|
||||||
|
listener(key, value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
notifyAllTabs({method: 'prefChanged', prefs: broadcastPrefs, affects});
|
for (const listener of onChange.any.values()) {
|
||||||
broadcastPrefs = {};
|
listener(key, value);
|
||||||
}
|
|
||||||
|
|
||||||
function doSyncSet() {
|
|
||||||
chromeSync.setValue('settings', values);
|
|
||||||
}
|
|
||||||
|
|
||||||
function importFromSync(synced = {}) {
|
|
||||||
forgetOutdatedDefaults(synced);
|
|
||||||
for (const key in defaults) {
|
|
||||||
if (key in synced) {
|
|
||||||
this.set(key, synced[key], {sync: false});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function forgetOutdatedDefaults(storage) {
|
function syncPrefs() {
|
||||||
// our linter runs as a worker so we can reduce the delay and forget the old default values
|
// FIXME: we always set the entire object? Ideally, this should only use `changes`.
|
||||||
if (Number(storage['editor.lintDelay']) === 500) delete storage['editor.lintDelay'];
|
chrome.storage.sync.set({settings: values});
|
||||||
if (Number(storage['editor.lintReportDelay']) === 4500) delete storage['editor.lintReportDelay'];
|
timer = null;
|
||||||
}
|
|
||||||
|
|
||||||
function defineReadonlyProperty(obj, key, value) {
|
|
||||||
const copy = deepCopy(value);
|
|
||||||
if (typeof copy === 'object') {
|
|
||||||
Object.freeze(copy);
|
|
||||||
}
|
|
||||||
Object.defineProperty(obj, key, {value: copy, configurable: true});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function equal(a, b) {
|
function equal(a, b) {
|
||||||
|
@ -389,7 +252,7 @@ var prefs = new function Prefs() {
|
||||||
}
|
}
|
||||||
|
|
||||||
function contextDeleteMissing() {
|
function contextDeleteMissing() {
|
||||||
return CHROME && (
|
return /Chrome\/\d+/.test(navigator.userAgent) && (
|
||||||
// detect browsers without Delete by looking at the end of UA string
|
// detect browsers without Delete by looking at the end of UA string
|
||||||
/Vivaldi\/[\d.]+$/.test(navigator.userAgent) ||
|
/Vivaldi\/[\d.]+$/.test(navigator.userAgent) ||
|
||||||
// Chrome and co.
|
// Chrome and co.
|
||||||
|
@ -398,44 +261,17 @@ var prefs = new function Prefs() {
|
||||||
!Array.from(navigator.plugins).some(p => p.name === 'Shockwave Flash')
|
!Array.from(navigator.plugins).some(p => p.name === 'Shockwave Flash')
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}();
|
|
||||||
|
|
||||||
|
function deepCopy(obj) {
|
||||||
// Accepts an array of pref names (values are fetched via prefs.get)
|
if (!obj || typeof obj !== 'object') {
|
||||||
// and establishes a two-way connection between the document elements and the actual prefs
|
return obj;
|
||||||
function setupLivePrefs(
|
|
||||||
IDs = Object.getOwnPropertyNames(prefs.readOnlyValues)
|
|
||||||
.filter(id => $('#' + id))
|
|
||||||
) {
|
|
||||||
const checkedProps = {};
|
|
||||||
for (const id of IDs) {
|
|
||||||
const element = $('#' + id);
|
|
||||||
checkedProps[id] = element.type === 'checkbox' ? 'checked' : 'value';
|
|
||||||
updateElement({id, element, force: true});
|
|
||||||
element.addEventListener('change', onChange);
|
|
||||||
}
|
|
||||||
prefs.subscribe(IDs, (id, value) => updateElement({id, value}));
|
|
||||||
|
|
||||||
function onChange() {
|
|
||||||
const value = this[checkedProps[this.id]];
|
|
||||||
if (prefs.get(this.id) !== value) {
|
|
||||||
prefs.set(this.id, value);
|
|
||||||
}
|
}
|
||||||
}
|
if (Array.isArray(obj)) {
|
||||||
function updateElement({
|
return obj.map(deepCopy);
|
||||||
id,
|
|
||||||
value = prefs.get(id),
|
|
||||||
element = $('#' + id),
|
|
||||||
force,
|
|
||||||
}) {
|
|
||||||
if (!element) {
|
|
||||||
prefs.unsubscribe(IDs, updateElement);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const prop = checkedProps[id];
|
|
||||||
if (force || element[prop] !== value) {
|
|
||||||
element[prop] = value;
|
|
||||||
element.dispatchEvent(new Event('change', {bubbles: true, cancelable: true}));
|
|
||||||
}
|
}
|
||||||
|
return Object.keys(obj).reduce((output, key) => {
|
||||||
|
output[key] = deepCopy(obj[key]);
|
||||||
|
return output;
|
||||||
|
}, {});
|
||||||
}
|
}
|
||||||
}
|
})();
|
||||||
|
|
24
js/promisify.js
Normal file
24
js/promisify.js
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
/* exported promisify */
|
||||||
|
'use strict';
|
||||||
|
/*
|
||||||
|
Convert chrome APIs into promises. Example:
|
||||||
|
|
||||||
|
const storageSyncGet = promisify(chrome.storage.sync.get.bind(chrome.storage.sync));
|
||||||
|
storageSyncGet(['key']).then(result => {...});
|
||||||
|
|
||||||
|
*/
|
||||||
|
function promisify(fn) {
|
||||||
|
return (...args) =>
|
||||||
|
new Promise((resolve, reject) => {
|
||||||
|
fn(...args, (...result) => {
|
||||||
|
if (chrome.runtime.lastError) {
|
||||||
|
reject(chrome.runtime.lastError);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
resolve(
|
||||||
|
result.length === 0 ? undefined :
|
||||||
|
result.length === 1 ? result[0] : result
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
|
@ -1,8 +1,8 @@
|
||||||
|
/* exported loadScript */
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
// loadScript(script: Array<Promise|string>|string): Promise
|
// loadScript(script: Array<Promise|string>|string): Promise
|
||||||
// eslint-disable-next-line no-var
|
const loadScript = (() => {
|
||||||
var loadScript = (() => {
|
|
||||||
const cache = new Map();
|
const cache = new Map();
|
||||||
|
|
||||||
function inject(file) {
|
function inject(file) {
|
||||||
|
@ -26,7 +26,7 @@ var loadScript = (() => {
|
||||||
el.onload = () => {
|
el.onload = () => {
|
||||||
el.onload = null;
|
el.onload = null;
|
||||||
el.onerror = null;
|
el.onerror = null;
|
||||||
resolve();
|
resolve(el);
|
||||||
};
|
};
|
||||||
el.onerror = () => {
|
el.onerror = () => {
|
||||||
el.onload = null;
|
el.onload = null;
|
||||||
|
@ -37,11 +37,15 @@ var loadScript = (() => {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return files => {
|
return (files, noCache = false) => {
|
||||||
if (!Array.isArray(files)) {
|
if (!Array.isArray(files)) {
|
||||||
files = [files];
|
files = [files];
|
||||||
}
|
}
|
||||||
return Promise.all(files.map(f => (typeof f === 'string' ? inject(f) : f)));
|
return Promise.all(files.map(f =>
|
||||||
|
typeof f !== 'string' ? f :
|
||||||
|
noCache ? doInject(f) :
|
||||||
|
inject(f)
|
||||||
|
));
|
||||||
};
|
};
|
||||||
})();
|
})();
|
||||||
|
|
||||||
|
@ -65,7 +69,10 @@ var loadScript = (() => {
|
||||||
subscribers.set(srcSuffix, [resolve]);
|
subscribers.set(srcSuffix, [resolve]);
|
||||||
}
|
}
|
||||||
// a resolved Promise won't reject anymore
|
// a resolved Promise won't reject anymore
|
||||||
setTimeout(() => emptyAfterCleanup(srcSuffix) + reject(), timeout);
|
setTimeout(() => {
|
||||||
|
emptyAfterCleanup(srcSuffix);
|
||||||
|
reject(new Error('Timeout'));
|
||||||
|
}, timeout);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,19 @@
|
||||||
|
/* exported styleSectionsEqual styleCodeEmpty calcStyleDigest */
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
|
function styleCodeEmpty(code) {
|
||||||
|
if (!code) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
const rx = /\s+|\/\*[\s\S]*?\*\/|@namespace[^;]+;|@charset[^;]+;/giy;
|
||||||
|
while (rx.exec(code)) {
|
||||||
|
if (rx.lastIndex === code.length) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {Style} a - first style object
|
* @param {Style} a - first style object
|
||||||
* @param {Style} b - second style object
|
* @param {Style} b - second style object
|
||||||
|
@ -54,3 +68,31 @@ function styleSectionsEqual(a, b, {ignoreCode, checkSource} = {}) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalizeStyleSections({sections}) {
|
||||||
|
// retain known properties in an arbitrarily predefined order
|
||||||
|
return (sections || []).map(section => ({
|
||||||
|
code: section.code || '',
|
||||||
|
urls: section.urls || [],
|
||||||
|
urlPrefixes: section.urlPrefixes || [],
|
||||||
|
domains: section.domains || [],
|
||||||
|
regexps: section.regexps || [],
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
function calcStyleDigest(style) {
|
||||||
|
const jsonString = style.usercssData ?
|
||||||
|
style.sourceCode : JSON.stringify(normalizeStyleSections(style));
|
||||||
|
const text = new TextEncoder('utf-8').encode(jsonString);
|
||||||
|
return crypto.subtle.digest('SHA-1', text).then(hex);
|
||||||
|
|
||||||
|
function hex(buffer) {
|
||||||
|
const parts = [];
|
||||||
|
const PAD8 = '00000000';
|
||||||
|
const view = new DataView(buffer);
|
||||||
|
for (let i = 0; i < view.byteLength; i += 4) {
|
||||||
|
parts.push((PAD8 + view.getUint32(i).toString(16)).slice(-8));
|
||||||
|
}
|
||||||
|
return parts.join('');
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,20 +1,14 @@
|
||||||
/* global loadScript */
|
/* global loadScript tryJSONparse */
|
||||||
|
/* exported chromeLocal chromeSync */
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
// eslint-disable-next-line no-var
|
const [chromeLocal, chromeSync] = (() => {
|
||||||
var [chromeLocal, chromeSync] = (() => {
|
|
||||||
const native = 'sync' in chrome.storage &&
|
|
||||||
!chrome.runtime.id.includes('@temporary');
|
|
||||||
if (!native && BG !== window) {
|
|
||||||
setupOnChangeRelay();
|
|
||||||
}
|
|
||||||
return [
|
return [
|
||||||
createWrapper('local'),
|
createWrapper('local'),
|
||||||
createWrapper('sync'),
|
createWrapper('sync'),
|
||||||
];
|
];
|
||||||
|
|
||||||
function createWrapper(name) {
|
function createWrapper(name) {
|
||||||
if (!native) createDummyStorage(name);
|
|
||||||
const storage = chrome.storage[name];
|
const storage = chrome.storage[name];
|
||||||
const wrapper = {
|
const wrapper = {
|
||||||
get: data => new Promise(resolve => storage.get(data, resolve)),
|
get: data => new Promise(resolve => storage.get(data, resolve)),
|
||||||
|
@ -58,39 +52,10 @@ var [chromeLocal, chromeSync] = (() => {
|
||||||
return wrapper;
|
return wrapper;
|
||||||
}
|
}
|
||||||
|
|
||||||
function createDummyStorage(name) {
|
|
||||||
chrome.storage[name] = {
|
|
||||||
get: (data, cb) => API.dummyStorageGet({data, name}).then(cb),
|
|
||||||
set: (data, cb) => API.dummyStorageSet({data, name}).then(cb),
|
|
||||||
remove: (data, cb) => API.dummyStorageRemove({data, name}).then(cb),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function loadLZStringScript() {
|
function loadLZStringScript() {
|
||||||
return window.LZString ?
|
return window.LZString ?
|
||||||
Promise.resolve(window.LZString) :
|
Promise.resolve(window.LZString) :
|
||||||
loadScript('/vendor/lz-string-unsafe/lz-string-unsafe.min.js').then(() =>
|
loadScript('/vendor/lz-string-unsafe/lz-string-unsafe.min.js').then(() =>
|
||||||
(window.LZString = window.LZString || window.LZStringUnsafe));
|
(window.LZString = window.LZString || window.LZStringUnsafe));
|
||||||
}
|
}
|
||||||
|
|
||||||
function setupOnChangeRelay() {
|
|
||||||
const listeners = new Set();
|
|
||||||
const onMessage = msg => {
|
|
||||||
if (!msg.dummyStorageChanges) return;
|
|
||||||
for (const fn of listeners.values()) {
|
|
||||||
fn(msg.dummyStorageChanges, msg.dummyStorageName);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
Object.assign(chrome.storage.onChanged, {
|
|
||||||
addListener(fn) {
|
|
||||||
if (!listeners.size) chrome.runtime.onMessage.addListener(onMessage);
|
|
||||||
listeners.add(fn);
|
|
||||||
},
|
|
||||||
hasListener: fn => listeners.has(fn),
|
|
||||||
removeListener(fn) {
|
|
||||||
listeners.delete(fn);
|
|
||||||
if (!listeners.size) chrome.runtime.onMessage.removeListener(onMessage);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
})();
|
})();
|
||||||
|
|
654
js/usercss.js
654
js/usercss.js
|
@ -1,514 +1,55 @@
|
||||||
/* global loadScript semverCompare colorConverter styleCodeEmpty */
|
/* global backgroundWorker */
|
||||||
|
/* exported usercss */
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
// eslint-disable-next-line no-var
|
const usercss = (() => {
|
||||||
var usercss = (() => {
|
const GLOBAL_METAS = {
|
||||||
// true = global
|
author: undefined,
|
||||||
// false or 0 = private
|
description: undefined,
|
||||||
// <string> = global key name
|
homepageURL: 'url',
|
||||||
// <function> = (style, newValue)
|
// updateURL: 'updateUrl',
|
||||||
const KNOWN_META = new Map([
|
name: undefined,
|
||||||
['author', true],
|
|
||||||
['advanced', 0],
|
|
||||||
['description', true],
|
|
||||||
['homepageURL', 'url'],
|
|
||||||
['icon', 0],
|
|
||||||
['license', 0],
|
|
||||||
['name', true],
|
|
||||||
['namespace', 0],
|
|
||||||
//['noframes', 0],
|
|
||||||
['preprocessor', 0],
|
|
||||||
['supportURL', 0],
|
|
||||||
['updateURL', (style, newValue) => {
|
|
||||||
// always preserve locally installed style's updateUrl
|
|
||||||
if (!/^file:/.test(style.updateUrl)) {
|
|
||||||
style.updateUrl = newValue;
|
|
||||||
}
|
|
||||||
}],
|
|
||||||
['var', 0],
|
|
||||||
['version', 0],
|
|
||||||
]);
|
|
||||||
const MANDATORY_META = ['name', 'namespace', 'version'];
|
|
||||||
const META_VARS = ['text', 'color', 'checkbox', 'select', 'dropdown', 'image', 'number', 'range'];
|
|
||||||
const META_URLS = [...KNOWN_META.keys()].filter(k => k.endsWith('URL'));
|
|
||||||
|
|
||||||
const BUILDER = {
|
|
||||||
default: {
|
|
||||||
postprocess(sections, vars) {
|
|
||||||
let varDef = Object.keys(vars).map(k => ` --${k}: ${vars[k].value};\n`).join('');
|
|
||||||
if (!varDef) return;
|
|
||||||
varDef = ':root {\n' + varDef + '}\n';
|
|
||||||
for (const section of sections) {
|
|
||||||
if (!styleCodeEmpty(section.code)) {
|
|
||||||
section.code = varDef + section.code;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
stylus: {
|
|
||||||
preprocess(source, vars) {
|
|
||||||
return loadScript('/vendor/stylus-lang-bundle/stylus.min.js').then(() => (
|
|
||||||
new Promise((resolve, reject) => {
|
|
||||||
const varDef = Object.keys(vars).map(key => `${key} = ${vars[key].value};\n`).join('');
|
|
||||||
if (!Error.captureStackTrace) Error.captureStackTrace = () => {};
|
|
||||||
window.stylus(varDef + source).render((err, output) => {
|
|
||||||
if (err) {
|
|
||||||
reject(err);
|
|
||||||
} else {
|
|
||||||
resolve(output);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
})
|
|
||||||
));
|
|
||||||
}
|
|
||||||
},
|
|
||||||
less: {
|
|
||||||
preprocess(source, vars) {
|
|
||||||
window.less = window.less || {
|
|
||||||
logLevel: 0,
|
|
||||||
useFileCache: false,
|
|
||||||
};
|
|
||||||
const varDefs = Object.keys(vars).map(key => `@${key}:${vars[key].value};\n`).join('');
|
|
||||||
return loadScript('/vendor/less/less.min.js')
|
|
||||||
.then(() => window.less.render(varDefs + source))
|
|
||||||
.then(({css}) => css);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
uso: {
|
|
||||||
preprocess(source, vars) {
|
|
||||||
const pool = new Map();
|
|
||||||
return Promise.resolve(doReplace(source));
|
|
||||||
|
|
||||||
function getValue(name, rgb) {
|
|
||||||
if (!vars.hasOwnProperty(name)) {
|
|
||||||
if (name.endsWith('-rgb')) {
|
|
||||||
return getValue(name.slice(0, -4), true);
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
if (rgb) {
|
|
||||||
if (vars[name].type === 'color') {
|
|
||||||
const color = colorConverter.parse(vars[name].value);
|
|
||||||
if (!color) return null;
|
|
||||||
const {r, g, b} = color;
|
|
||||||
return `${r}, ${g}, ${b}`;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
if (vars[name].type === 'dropdown' || vars[name].type === 'select') {
|
|
||||||
// prevent infinite recursion
|
|
||||||
pool.set(name, '');
|
|
||||||
return doReplace(vars[name].value);
|
|
||||||
}
|
|
||||||
return vars[name].value;
|
|
||||||
}
|
|
||||||
|
|
||||||
function doReplace(text) {
|
|
||||||
return text.replace(/\/\*\[\[([\w-]+)\]\]\*\//g, (match, name) => {
|
|
||||||
if (!pool.has(name)) {
|
|
||||||
const value = getValue(name);
|
|
||||||
pool.set(name, value === null ? match : value);
|
|
||||||
}
|
|
||||||
return pool.get(name);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
const RX_META = /\/\*\s*==userstyle==[\s\S]*?==\/userstyle==\s*\*\//i;
|
||||||
const RX_NUMBER = /-?\d+(\.\d+)?\s*/y;
|
const ERR_ARGS_IS_LIST = new Set(['missingMandatory', 'missingChar']);
|
||||||
const RX_WHITESPACE = /\s*/y;
|
return {buildMeta, buildCode, assignVars};
|
||||||
const RX_WORD = /([\w-]+)\s*/y;
|
|
||||||
const RX_STRING_BACKTICK = /(`(?:\\`|[\s\S])*?`)\s*/y;
|
|
||||||
const RX_STRING_QUOTED = /((['"])(?:\\\2|[^\n])*?\2|\w+)\s*/y;
|
|
||||||
|
|
||||||
const worker = {};
|
|
||||||
|
|
||||||
function getMetaSource(source) {
|
|
||||||
const commentRe = /\/\*[\s\S]*?\*\//g;
|
|
||||||
const metaRe = /==userstyle==[\s\S]*?==\/userstyle==/i;
|
|
||||||
|
|
||||||
let m;
|
|
||||||
// iterate through each comment
|
|
||||||
while ((m = commentRe.exec(source))) {
|
|
||||||
const commentSource = source.slice(m.index, m.index + m[0].length);
|
|
||||||
const n = commentSource.match(metaRe);
|
|
||||||
if (n) {
|
|
||||||
return {
|
|
||||||
index: m.index + n.index,
|
|
||||||
text: n[0]
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return {text: '', index: 0};
|
|
||||||
}
|
|
||||||
|
|
||||||
function parseWord(state, error = 'invalid word') {
|
|
||||||
RX_WORD.lastIndex = state.re.lastIndex;
|
|
||||||
const match = RX_WORD.exec(state.text);
|
|
||||||
if (!match) {
|
|
||||||
throw new Error((state.errorPrefix || '') + error);
|
|
||||||
}
|
|
||||||
state.value = match[1];
|
|
||||||
state.re.lastIndex += match[0].length;
|
|
||||||
}
|
|
||||||
|
|
||||||
function parseVar(state) {
|
|
||||||
const result = {
|
|
||||||
type: null,
|
|
||||||
label: null,
|
|
||||||
name: null,
|
|
||||||
value: null,
|
|
||||||
default: null,
|
|
||||||
options: null
|
|
||||||
};
|
|
||||||
|
|
||||||
parseWord(state, 'missing type');
|
|
||||||
result.type = state.type = state.value;
|
|
||||||
|
|
||||||
if (!META_VARS.includes(state.type)) {
|
|
||||||
throw new Error(`unknown type: ${state.type}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
parseWord(state, 'missing name');
|
|
||||||
result.name = state.value;
|
|
||||||
|
|
||||||
parseString(state);
|
|
||||||
result.label = state.value;
|
|
||||||
|
|
||||||
const {re, type, text} = state;
|
|
||||||
|
|
||||||
switch (type === 'image' && state.key === 'var' ? '@image@var' : type) {
|
|
||||||
case 'checkbox': {
|
|
||||||
const match = text.slice(re.lastIndex).match(/([01])\s+/);
|
|
||||||
if (!match) {
|
|
||||||
throw new Error('value must be 0 or 1');
|
|
||||||
}
|
|
||||||
re.lastIndex += match[0].length;
|
|
||||||
result.default = match[1];
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case 'select':
|
|
||||||
case '@image@var': {
|
|
||||||
state.errorPrefix = 'Invalid JSON: ';
|
|
||||||
parseJSONValue(state);
|
|
||||||
state.errorPrefix = '';
|
|
||||||
const extractDefaultOption = (key, value) => {
|
|
||||||
if (key.endsWith('*')) {
|
|
||||||
const option = createOption(key.slice(0, -1), value);
|
|
||||||
result.default = option.name;
|
|
||||||
return option;
|
|
||||||
}
|
|
||||||
return createOption(key, value);
|
|
||||||
};
|
|
||||||
if (Array.isArray(state.value)) {
|
|
||||||
result.options = state.value.map(k => extractDefaultOption(k));
|
|
||||||
} else {
|
|
||||||
result.options = Object.keys(state.value).map(k => extractDefaultOption(k, state.value[k]));
|
|
||||||
}
|
|
||||||
if (result.default === null) {
|
|
||||||
result.default = (result.options[0] || {}).name || '';
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case 'number':
|
|
||||||
case 'range': {
|
|
||||||
state.errorPrefix = 'Invalid JSON: ';
|
|
||||||
parseJSONValue(state);
|
|
||||||
state.errorPrefix = '';
|
|
||||||
// [default, start, end, step, units] (start, end, step & units are optional)
|
|
||||||
if (Array.isArray(state.value) && state.value.length) {
|
|
||||||
// label may be placed anywhere
|
|
||||||
result.units = (state.value.find(i => typeof i === 'string') || '').replace(/[\d.+-]/g, '');
|
|
||||||
const range = state.value.filter(i => typeof i === 'number' || i === null);
|
|
||||||
result.default = range[0];
|
|
||||||
result.min = range[1];
|
|
||||||
result.max = range[2];
|
|
||||||
result.step = range[3] === 0 ? 1 : range[3];
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case 'dropdown':
|
|
||||||
case 'image': {
|
|
||||||
if (text[re.lastIndex] !== '{') {
|
|
||||||
throw new Error('no open {');
|
|
||||||
}
|
|
||||||
result.options = [];
|
|
||||||
re.lastIndex++;
|
|
||||||
while (text[re.lastIndex] !== '}') {
|
|
||||||
const option = {};
|
|
||||||
|
|
||||||
parseStringUnquoted(state);
|
|
||||||
option.name = state.value;
|
|
||||||
|
|
||||||
parseString(state);
|
|
||||||
option.label = state.value;
|
|
||||||
|
|
||||||
if (type === 'dropdown') {
|
|
||||||
parseEOT(state);
|
|
||||||
} else {
|
|
||||||
parseString(state);
|
|
||||||
}
|
|
||||||
option.value = state.value;
|
|
||||||
|
|
||||||
result.options.push(option);
|
|
||||||
}
|
|
||||||
re.lastIndex++;
|
|
||||||
eatWhitespace(state);
|
|
||||||
result.default = result.options[0].name;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
default: {
|
|
||||||
// text, color
|
|
||||||
parseStringToEnd(state);
|
|
||||||
result.default = state.value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
state.usercssData.vars[result.name] = result;
|
|
||||||
validateVar(result);
|
|
||||||
}
|
|
||||||
|
|
||||||
function createOption(label, value) {
|
|
||||||
let name;
|
|
||||||
const match = label.match(/^(\w+):(.*)/);
|
|
||||||
if (match) {
|
|
||||||
([, name, label] = match);
|
|
||||||
}
|
|
||||||
if (!name) {
|
|
||||||
name = label;
|
|
||||||
}
|
|
||||||
if (!value) {
|
|
||||||
value = name;
|
|
||||||
}
|
|
||||||
return {name, label, value};
|
|
||||||
}
|
|
||||||
|
|
||||||
function parseEOT(state) {
|
|
||||||
const re = /<<<EOT([\s\S]+?)EOT;/y;
|
|
||||||
re.lastIndex = state.re.lastIndex;
|
|
||||||
const match = state.text.match(re);
|
|
||||||
if (!match) {
|
|
||||||
throw new Error('missing EOT');
|
|
||||||
}
|
|
||||||
state.re.lastIndex += match[0].length;
|
|
||||||
state.value = match[1].trim().replace(/\*\\\//g, '*/');
|
|
||||||
eatWhitespace(state);
|
|
||||||
}
|
|
||||||
|
|
||||||
function parseStringUnquoted(state) {
|
|
||||||
const pos = state.re.lastIndex;
|
|
||||||
const nextQuoteOrEOL = posOrEnd(state.text, '"', pos);
|
|
||||||
state.re.lastIndex = nextQuoteOrEOL;
|
|
||||||
state.value = state.text.slice(pos, nextQuoteOrEOL).trim().replace(/\s+/g, '-');
|
|
||||||
}
|
|
||||||
|
|
||||||
function parseString(state) {
|
|
||||||
const pos = state.re.lastIndex;
|
|
||||||
const rx = state.text[pos] === '`' ? RX_STRING_BACKTICK : RX_STRING_QUOTED;
|
|
||||||
rx.lastIndex = pos;
|
|
||||||
const match = rx.exec(state.text);
|
|
||||||
if (!match) {
|
|
||||||
throw new Error((state.errorPrefix || '') + 'Quoted string expected');
|
|
||||||
}
|
|
||||||
state.re.lastIndex += match[0].length;
|
|
||||||
state.value = unquote(match[1]);
|
|
||||||
}
|
|
||||||
|
|
||||||
function parseJSONValue(state) {
|
|
||||||
const JSON_PRIME = {
|
|
||||||
__proto__: null,
|
|
||||||
'null': null,
|
|
||||||
'true': true,
|
|
||||||
'false': false
|
|
||||||
};
|
|
||||||
const {text, re, errorPrefix} = state;
|
|
||||||
if (text[re.lastIndex] === '{') {
|
|
||||||
// object
|
|
||||||
const obj = {};
|
|
||||||
re.lastIndex++;
|
|
||||||
eatWhitespace(state);
|
|
||||||
while (text[re.lastIndex] !== '}') {
|
|
||||||
parseString(state);
|
|
||||||
const key = state.value;
|
|
||||||
if (text[re.lastIndex] !== ':') {
|
|
||||||
throw new Error(`${errorPrefix}missing ':'`);
|
|
||||||
}
|
|
||||||
re.lastIndex++;
|
|
||||||
eatWhitespace(state);
|
|
||||||
parseJSONValue(state);
|
|
||||||
obj[key] = state.value;
|
|
||||||
if (text[re.lastIndex] === ',') {
|
|
||||||
re.lastIndex++;
|
|
||||||
eatWhitespace(state);
|
|
||||||
} else if (text[re.lastIndex] !== '}') {
|
|
||||||
throw new Error(`${errorPrefix}missing ',' or '}'`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
re.lastIndex++;
|
|
||||||
eatWhitespace(state);
|
|
||||||
state.value = obj;
|
|
||||||
} else if (text[re.lastIndex] === '[') {
|
|
||||||
// array
|
|
||||||
const arr = [];
|
|
||||||
re.lastIndex++;
|
|
||||||
eatWhitespace(state);
|
|
||||||
while (text[re.lastIndex] !== ']') {
|
|
||||||
parseJSONValue(state);
|
|
||||||
arr.push(state.value);
|
|
||||||
if (text[re.lastIndex] === ',') {
|
|
||||||
re.lastIndex++;
|
|
||||||
eatWhitespace(state);
|
|
||||||
} else if (text[re.lastIndex] !== ']') {
|
|
||||||
throw new Error(`${errorPrefix}missing ',' or ']'`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
re.lastIndex++;
|
|
||||||
eatWhitespace(state);
|
|
||||||
state.value = arr;
|
|
||||||
} else if (text[re.lastIndex] === '"' || text[re.lastIndex] === '`') {
|
|
||||||
// string
|
|
||||||
parseString(state);
|
|
||||||
} else if (/\d/.test(text[re.lastIndex])) {
|
|
||||||
// number
|
|
||||||
parseNumber(state);
|
|
||||||
} else {
|
|
||||||
parseWord(state);
|
|
||||||
if (!(state.value in JSON_PRIME)) {
|
|
||||||
throw new Error(`${errorPrefix}unknown literal '${state.value}'`);
|
|
||||||
}
|
|
||||||
state.value = JSON_PRIME[state.value];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function parseNumber(state) {
|
|
||||||
RX_NUMBER.lastIndex = state.re.lastIndex;
|
|
||||||
const match = RX_NUMBER.exec(state.text);
|
|
||||||
if (!match) {
|
|
||||||
throw new Error((state.errorPrefix || '') + 'invalid number');
|
|
||||||
}
|
|
||||||
state.value = Number(match[0].trim());
|
|
||||||
state.re.lastIndex += match[0].length;
|
|
||||||
}
|
|
||||||
|
|
||||||
function eatWhitespace(state) {
|
|
||||||
RX_WHITESPACE.lastIndex = state.re.lastIndex;
|
|
||||||
state.re.lastIndex += RX_WHITESPACE.exec(state.text)[0].length;
|
|
||||||
}
|
|
||||||
|
|
||||||
function parseStringToEnd(state) {
|
|
||||||
const EOL = posOrEnd(state.text, '\n', state.re.lastIndex);
|
|
||||||
const match = state.text.slice(state.re.lastIndex, EOL);
|
|
||||||
state.value = unquote(match.trim());
|
|
||||||
state.re.lastIndex += match.length;
|
|
||||||
}
|
|
||||||
|
|
||||||
function unquote(s) {
|
|
||||||
const q = s[0];
|
|
||||||
if (q === s[s.length - 1] && (q === '"' || q === "'" || q === '`')) {
|
|
||||||
// http://www.json.org/
|
|
||||||
return s.slice(1, -1).replace(
|
|
||||||
new RegExp(`\\\\([${q}\\\\/bfnrt]|u[0-9a-fA-F]{4})`, 'g'),
|
|
||||||
s => {
|
|
||||||
if (s[1] === q) {
|
|
||||||
return q;
|
|
||||||
}
|
|
||||||
return JSON.parse(`"${s}"`);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return s;
|
|
||||||
}
|
|
||||||
|
|
||||||
function posOrEnd(haystack, needle, start) {
|
|
||||||
const pos = haystack.indexOf(needle, start);
|
|
||||||
return pos < 0 ? haystack.length : pos;
|
|
||||||
}
|
|
||||||
|
|
||||||
function buildMeta(sourceCode) {
|
function buildMeta(sourceCode) {
|
||||||
sourceCode = sourceCode.replace(/\r\n?/g, '\n');
|
sourceCode = sourceCode.replace(/\r\n?/g, '\n');
|
||||||
|
|
||||||
const usercssData = {
|
|
||||||
vars: {}
|
|
||||||
};
|
|
||||||
|
|
||||||
const style = {
|
const style = {
|
||||||
reason: 'install',
|
|
||||||
enabled: true,
|
enabled: true,
|
||||||
sourceCode,
|
sourceCode,
|
||||||
sections: [],
|
sections: []
|
||||||
usercssData
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const {text, index: metaIndex} = getMetaSource(sourceCode);
|
const match = sourceCode.match(RX_META);
|
||||||
const re = /@(\w+)[ \t\xA0]*/mg;
|
if (!match) {
|
||||||
const state = {style, re, text, usercssData};
|
throw new Error('can not find metadata');
|
||||||
|
}
|
||||||
|
|
||||||
function doParse() {
|
return backgroundWorker.parseUsercssMeta(match[0], match.index)
|
||||||
let match;
|
.catch(err => {
|
||||||
while ((match = re.exec(text))) {
|
if (err.code) {
|
||||||
const key = state.key = match[1];
|
const args = ERR_ARGS_IS_LIST.has(err.code) ? drawList(err.args) : err.args;
|
||||||
const route = KNOWN_META.get(key);
|
const message = chrome.i18n.getMessage(`meta_${err.code}`, args);
|
||||||
if (route === undefined) {
|
if (message) {
|
||||||
continue;
|
err.message = message;
|
||||||
}
|
|
||||||
if (key === 'var' || key === 'advanced') {
|
|
||||||
if (key === 'advanced') {
|
|
||||||
state.maybeUSO = true;
|
|
||||||
}
|
}
|
||||||
parseVar(state);
|
|
||||||
} else {
|
|
||||||
parseStringToEnd(state);
|
|
||||||
usercssData[key] = state.value;
|
|
||||||
}
|
}
|
||||||
let value = state.value;
|
throw err;
|
||||||
if (key === 'version') {
|
})
|
||||||
value = usercssData[key] = normalizeVersion(value);
|
.then(({metadata}) => {
|
||||||
validateVersion(value);
|
style.usercssData = metadata;
|
||||||
|
for (const [key, value = key] of Object.entries(GLOBAL_METAS)) {
|
||||||
|
style[value] = metadata[key];
|
||||||
}
|
}
|
||||||
if (META_URLS.includes(key)) {
|
return style;
|
||||||
validateUrl(key, value);
|
});
|
||||||
}
|
|
||||||
switch (typeof route) {
|
|
||||||
case 'function':
|
|
||||||
route(style, value);
|
|
||||||
break;
|
|
||||||
case 'string':
|
|
||||||
style[route] = value;
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
if (route) {
|
|
||||||
style[key] = value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
doParse();
|
|
||||||
} catch (e) {
|
|
||||||
// the source code string offset
|
|
||||||
e.index = metaIndex + state.re.lastIndex;
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (state.maybeUSO && !usercssData.preprocessor) {
|
|
||||||
usercssData.preprocessor = 'uso';
|
|
||||||
}
|
|
||||||
|
|
||||||
validateStyle(style);
|
|
||||||
return style;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function normalizeVersion(version) {
|
function drawList(items) {
|
||||||
// https://docs.npmjs.com/misc/semver#versions
|
return items.map(i => i.length === 1 ? JSON.stringify(i) : i).join(', ');
|
||||||
if (version[0] === 'v' || version[0] === '=') {
|
|
||||||
return version.slice(1);
|
|
||||||
}
|
|
||||||
return version;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -518,136 +59,37 @@ var usercss = (() => {
|
||||||
* when allowErrors is falsy or {style, errors} object when allowErrors is truthy
|
* when allowErrors is falsy or {style, errors} object when allowErrors is truthy
|
||||||
*/
|
*/
|
||||||
function buildCode(style, allowErrors) {
|
function buildCode(style, allowErrors) {
|
||||||
const {usercssData: {preprocessor, vars}, sourceCode} = style;
|
const match = style.sourceCode.match(RX_META);
|
||||||
let builder;
|
return backgroundWorker.compileUsercss(
|
||||||
if (preprocessor) {
|
style.usercssData.preprocessor,
|
||||||
if (!BUILDER[preprocessor]) {
|
style.sourceCode.slice(0, match.index) + style.sourceCode.slice(match.index + match[0].length),
|
||||||
return Promise.reject(chrome.i18n.getMessage('styleMetaErrorPreprocessor', preprocessor));
|
style.usercssData.vars
|
||||||
}
|
)
|
||||||
builder = BUILDER[preprocessor];
|
|
||||||
} else {
|
|
||||||
builder = BUILDER.default;
|
|
||||||
}
|
|
||||||
|
|
||||||
const sVars = simpleVars(vars);
|
|
||||||
|
|
||||||
return (
|
|
||||||
Promise.resolve(
|
|
||||||
builder.preprocess && builder.preprocess(sourceCode, sVars) ||
|
|
||||||
sourceCode)
|
|
||||||
.then(mozStyle => invokeWorker({
|
|
||||||
action: 'parse',
|
|
||||||
styleId: style.id,
|
|
||||||
code: mozStyle,
|
|
||||||
}))
|
|
||||||
.then(({sections, errors}) => {
|
.then(({sections, errors}) => {
|
||||||
if (!errors.length) errors = false;
|
if (!errors.length) errors = false;
|
||||||
if (!sections.length || errors && !allowErrors) {
|
if (!sections.length || errors && !allowErrors) {
|
||||||
return Promise.reject(errors);
|
throw errors;
|
||||||
}
|
}
|
||||||
style.sections = sections;
|
style.sections = sections;
|
||||||
if (builder.postprocess) builder.postprocess(style.sections, sVars);
|
|
||||||
return allowErrors ? {style, errors} : style;
|
return allowErrors ? {style, errors} : style;
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
function simpleVars(vars) {
|
|
||||||
// simplify vars by merging `va.default` to `va.value`, so BUILDER don't
|
|
||||||
// need to test each va's default value.
|
|
||||||
return Object.keys(vars).reduce((output, key) => {
|
|
||||||
const va = vars[key];
|
|
||||||
output[key] = Object.assign({}, va, {
|
|
||||||
value: va.value === null || va.value === undefined ?
|
|
||||||
getVarValue(va, 'default') : getVarValue(va, 'value')
|
|
||||||
});
|
});
|
||||||
return output;
|
|
||||||
}, {});
|
|
||||||
}
|
|
||||||
|
|
||||||
function getVarValue(va, prop) {
|
|
||||||
if (va.type === 'select' || va.type === 'dropdown' || va.type === 'image') {
|
|
||||||
// TODO: handle customized image
|
|
||||||
return va.options.find(o => o.name === va[prop]).value;
|
|
||||||
}
|
|
||||||
if ((va.type === 'number' || va.type === 'range') && va.units) {
|
|
||||||
return va[prop] + va.units;
|
|
||||||
}
|
|
||||||
return va[prop];
|
|
||||||
}
|
|
||||||
|
|
||||||
function validateStyle({usercssData: data}) {
|
|
||||||
for (const prop of MANDATORY_META) {
|
|
||||||
if (!data[prop]) {
|
|
||||||
throw new Error(chrome.i18n.getMessage('styleMissingMeta', prop));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
validateVersion(data.version);
|
|
||||||
META_URLS.forEach(k => validateUrl(k, data[k]));
|
|
||||||
Object.keys(data.vars).forEach(k => validateVar(data.vars[k]));
|
|
||||||
}
|
|
||||||
|
|
||||||
function validateVersion(version) {
|
|
||||||
semverCompare(version, '0.0.0');
|
|
||||||
}
|
|
||||||
|
|
||||||
function validateUrl(key, url) {
|
|
||||||
if (!url) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
url = new URL(url);
|
|
||||||
if (!/^https?:/.test(url.protocol)) {
|
|
||||||
throw new Error(`${url.protocol} is not a valid protocol in ${key}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function validateVar(va, value = 'default') {
|
|
||||||
if (va.type === 'select' || va.type === 'dropdown') {
|
|
||||||
if (va.options.every(o => o.name !== va[value])) {
|
|
||||||
throw new Error(chrome.i18n.getMessage('styleMetaErrorSelectValueMismatch'));
|
|
||||||
}
|
|
||||||
} else if (va.type === 'checkbox' && !/^[01]$/.test(va[value])) {
|
|
||||||
throw new Error(chrome.i18n.getMessage('styleMetaErrorCheckbox'));
|
|
||||||
} else if (va.type === 'color') {
|
|
||||||
va[value] = colorConverter.format(colorConverter.parse(va[value]), 'rgb');
|
|
||||||
} else if ((va.type === 'number' || va.type === 'range') && typeof va[value] !== 'number') {
|
|
||||||
throw new Error(chrome.i18n.getMessage('styleMetaErrorRangeOrNumber', va.type));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function assignVars(style, oldStyle) {
|
function assignVars(style, oldStyle) {
|
||||||
const {usercssData: {vars}} = style;
|
const {usercssData: {vars}} = style;
|
||||||
const {usercssData: {vars: oldVars}} = oldStyle;
|
const {usercssData: {vars: oldVars}} = oldStyle;
|
||||||
|
if (!vars || !oldVars) {
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
// The type of var might be changed during the update. Set value to null if the value is invalid.
|
// The type of var might be changed during the update. Set value to null if the value is invalid.
|
||||||
for (const key of Object.keys(vars)) {
|
for (const key of Object.keys(vars)) {
|
||||||
if (oldVars[key] && oldVars[key].value) {
|
if (oldVars[key] && oldVars[key].value) {
|
||||||
vars[key].value = oldVars[key].value;
|
vars[key].value = oldVars[key].value;
|
||||||
try {
|
|
||||||
validateVar(vars[key], 'value');
|
|
||||||
} catch (e) {
|
|
||||||
vars[key].value = null;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return backgroundWorker.nullifyInvalidVars(vars)
|
||||||
|
.then(vars => {
|
||||||
|
style.usercssData.vars = vars;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function invokeWorker(message) {
|
|
||||||
if (!worker.queue) {
|
|
||||||
worker.instance = new Worker('/background/parserlib-loader.js');
|
|
||||||
worker.queue = [];
|
|
||||||
worker.instance.onmessage = ({data}) => {
|
|
||||||
worker.queue.shift().resolve(data.__ERROR__ ? Promise.reject(data.__ERROR__) : data);
|
|
||||||
if (worker.queue.length) {
|
|
||||||
worker.instance.postMessage(worker.queue[0].message);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return new Promise(resolve => {
|
|
||||||
worker.queue.push({message, resolve});
|
|
||||||
if (worker.queue.length === 1) {
|
|
||||||
worker.instance.postMessage(message);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return {buildMeta, buildCode, assignVars, invokeWorker};
|
|
||||||
})();
|
})();
|
||||||
|
|
98
js/worker-util.js
Normal file
98
js/worker-util.js
Normal file
|
@ -0,0 +1,98 @@
|
||||||
|
/* global importScripts */
|
||||||
|
/* exported workerUtil */
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const workerUtil = (() => {
|
||||||
|
const loadedScripts = new Set();
|
||||||
|
return {createWorker, createAPI, loadScript, cloneError};
|
||||||
|
|
||||||
|
function createWorker({url, lifeTime = 30}) {
|
||||||
|
let worker;
|
||||||
|
let id;
|
||||||
|
let timer;
|
||||||
|
const pendingResponse = new Map();
|
||||||
|
|
||||||
|
return new Proxy({}, {
|
||||||
|
get: (target, prop) =>
|
||||||
|
(...args) => {
|
||||||
|
if (!worker) {
|
||||||
|
init();
|
||||||
|
}
|
||||||
|
return invoke(prop, args);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function init() {
|
||||||
|
id = 0;
|
||||||
|
worker = new Worker(url);
|
||||||
|
worker.onmessage = onMessage;
|
||||||
|
}
|
||||||
|
|
||||||
|
function uninit() {
|
||||||
|
worker.onmessage = null;
|
||||||
|
worker.terminate();
|
||||||
|
worker = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function onMessage(e) {
|
||||||
|
const message = e.data;
|
||||||
|
pendingResponse.get(message.id)[message.error ? 'reject' : 'resolve'](message.data);
|
||||||
|
pendingResponse.delete(message.id);
|
||||||
|
if (!pendingResponse.size && lifeTime >= 0) {
|
||||||
|
timer = setTimeout(uninit, lifeTime * 1000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function invoke(action, args) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
pendingResponse.set(id, {resolve, reject});
|
||||||
|
clearTimeout(timer);
|
||||||
|
worker.postMessage({
|
||||||
|
id,
|
||||||
|
action,
|
||||||
|
args
|
||||||
|
});
|
||||||
|
id++;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function createAPI(methods) {
|
||||||
|
self.onmessage = e => {
|
||||||
|
const message = e.data;
|
||||||
|
Promise.resolve()
|
||||||
|
.then(() => methods[message.action](...message.args))
|
||||||
|
.then(result => ({
|
||||||
|
id: message.id,
|
||||||
|
error: false,
|
||||||
|
data: result
|
||||||
|
}))
|
||||||
|
.catch(err => ({
|
||||||
|
id: message.id,
|
||||||
|
error: true,
|
||||||
|
data: cloneError(err)
|
||||||
|
}))
|
||||||
|
.then(data => self.postMessage(data));
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function cloneError(err) {
|
||||||
|
return Object.assign({
|
||||||
|
name: err.name,
|
||||||
|
stack: err.stack,
|
||||||
|
message: err.message,
|
||||||
|
lineNumber: err.lineNumber,
|
||||||
|
columnNumber: err.columnNumber,
|
||||||
|
fileName: err.fileName
|
||||||
|
}, err);
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadScript(...scripts) {
|
||||||
|
const urls = scripts.filter(u => !loadedScripts.has(u));
|
||||||
|
if (!urls.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
importScripts(...urls);
|
||||||
|
urls.forEach(u => loadedScripts.add(u));
|
||||||
|
}
|
||||||
|
})();
|
23
manage.html
23
manage.html
|
@ -146,26 +146,27 @@
|
||||||
</details>
|
</details>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<script src="js/promisify.js"></script>
|
||||||
<script src="js/dom.js"></script>
|
<script src="js/dom.js"></script>
|
||||||
<script src="js/messaging.js"></script>
|
<script src="js/messaging.js"></script>
|
||||||
<script src="js/prefs.js"></script>
|
<script src="js/prefs.js"></script>
|
||||||
|
<script src="js/msg.js"></script>
|
||||||
<script src="content/apply.js"></script>
|
<script src="content/apply.js"></script>
|
||||||
<script src="js/localization.js"></script>
|
<script src="js/localization.js"></script>
|
||||||
<script src="manage/filters.js"></script>
|
<script src="manage/filters.js"></script>
|
||||||
<script src="manage/sort.js"></script>
|
<script src="manage/sort.js"></script>
|
||||||
<script src="manage/manage.js"></script>
|
<script src="manage/manage.js"></script>
|
||||||
|
|
||||||
<script src="vendor-overwrites/colorpicker/colorconverter.js" async></script>
|
<script src="vendor-overwrites/colorpicker/colorconverter.js"></script>
|
||||||
<script src="vendor-overwrites/colorpicker/colorpicker.js" async></script>
|
<script src="vendor-overwrites/colorpicker/colorpicker.js"></script>
|
||||||
<script src="manage/config-dialog.js" async></script>
|
<script src="manage/config-dialog.js"></script>
|
||||||
<script src="manage/updater-ui.js" async></script>
|
<script src="manage/updater-ui.js"></script>
|
||||||
<script src="manage/object-diff.js" async></script>
|
<script src="manage/object-diff.js"></script>
|
||||||
<script src="manage/import-export.js" async></script>
|
<script src="manage/import-export.js"></script>
|
||||||
|
<script src="manage/incremental-search.js"></script>
|
||||||
<script src="manage/incremental-search.js" async></script>
|
<script src="msgbox/msgbox.js"></script>
|
||||||
<script src="msgbox/msgbox.js" async></script>
|
<script src="js/sections-util.js"></script>
|
||||||
<script src="js/sections-equal.js" async></script>
|
<script src="js/storage-util.js"></script>
|
||||||
<script src="js/storage-util.js" async></script>
|
|
||||||
|
|
||||||
<script src="sync/vendor/dropbox/dropbox-sdk.js" async></script>
|
<script src="sync/vendor/dropbox/dropbox-sdk.js" async></script>
|
||||||
<script src="sync/vendor/zipjs/zip.js" defer></script>
|
<script src="sync/vendor/zipjs/zip.js" defer></script>
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
/* global messageBox */
|
/* global messageBox deepCopy $create $createLink $ t tWordBreak
|
||||||
|
prefs setupLivePrefs debounce API */
|
||||||
|
/* exported configDialog */
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
function configDialog(style) {
|
function configDialog(style) {
|
||||||
|
@ -117,13 +119,13 @@ function configDialog(style) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!bgStyle) {
|
if (!bgStyle) {
|
||||||
API.getStyles({id: style.id, omitCode: !BG})
|
API.getStyle(style.id, true)
|
||||||
.then(([bgStyle]) => save({anyChangeIsDirty}, bgStyle || {}));
|
.catch(() => ({}))
|
||||||
|
.then(bgStyle => save({anyChangeIsDirty}, bgStyle));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
style = style.sections ? Object.assign({}, style) : style;
|
style = style.sections ? Object.assign({}, style) : style;
|
||||||
style.enabled = true;
|
style.enabled = true;
|
||||||
style.reason = 'config';
|
|
||||||
style.sourceCode = null;
|
style.sourceCode = null;
|
||||||
style.sections = null;
|
style.sections = null;
|
||||||
const styleVars = style.usercssData.vars;
|
const styleVars = style.usercssData.vars;
|
||||||
|
@ -171,9 +173,9 @@ function configDialog(style) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
saving = true;
|
saving = true;
|
||||||
return API.saveUsercss(style)
|
return API.configUsercssVars(style.id, style.usercssData.vars)
|
||||||
.then(saved => {
|
.then(newVars => {
|
||||||
varsInitial = getInitialValues(saved.usercssData.vars);
|
varsInitial = getInitialValues(newVars);
|
||||||
vars.forEach(va => onchange({target: va.input, justSaved: true}));
|
vars.forEach(va => onchange({target: va.input, justSaved: true}));
|
||||||
renderValues();
|
renderValues();
|
||||||
updateButtons();
|
updateButtons();
|
||||||
|
@ -182,7 +184,7 @@ function configDialog(style) {
|
||||||
.catch(errors => {
|
.catch(errors => {
|
||||||
const el = $('.config-error', messageBox.element) ||
|
const el = $('.config-error', messageBox.element) ||
|
||||||
$('#message-box-buttons').insertAdjacentElement('afterbegin', $create('.config-error'));
|
$('#message-box-buttons').insertAdjacentElement('afterbegin', $create('.config-error'));
|
||||||
el.textContent = el.title = Array.isArray(errors) ? errors.join('\n') : errors;
|
el.textContent = el.title = Array.isArray(errors) ? errors.join('\n') : errors.message || String(errors);
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
saving = false;
|
saving = false;
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/* global installed messageBox */
|
/* global installed messageBox sorter $ $$ $create t debounce prefs API onDOMready */
|
||||||
/* global sorter */
|
/* exported filterAndAppend */
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
const filtersSelector = {
|
const filtersSelector = {
|
||||||
|
@ -114,7 +114,7 @@ onDOMready().then(() => {
|
||||||
}
|
}
|
||||||
if (value !== undefined) {
|
if (value !== undefined) {
|
||||||
el.lastValue = value;
|
el.lastValue = value;
|
||||||
if (el.id in prefs.readOnlyValues) {
|
if (el.id in prefs.defaults) {
|
||||||
prefs.set(el.id, false);
|
prefs.set(el.id, false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +1,54 @@
|
||||||
/* global messageBox handleUpdate handleDelete applyOnMessage styleSectionsEqual */
|
/* global messageBox styleSectionsEqual getOwnTab API onDOMready
|
||||||
|
tryJSONparse scrollElementIntoView $ $$ API $create t animateElement */
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
const STYLISH_DUMP_FILE_EXT = '.txt';
|
const STYLISH_DUMP_FILE_EXT = '.txt';
|
||||||
const STYLUS_BACKUP_FILE_EXT = '.json';
|
const STYLUS_BACKUP_FILE_EXT = '.json';
|
||||||
|
|
||||||
|
onDOMready().then(() => {
|
||||||
|
$('#file-all-styles').onclick = exportToFile;
|
||||||
|
$('#unfile-all-styles').onclick = () => {
|
||||||
|
importFromFile({fileTypeFilter: STYLUS_BACKUP_FILE_EXT});
|
||||||
|
};
|
||||||
|
|
||||||
|
Object.assign(document.body, {
|
||||||
|
ondragover(event) {
|
||||||
|
const hasFiles = event.dataTransfer.types.includes('Files');
|
||||||
|
event.dataTransfer.dropEffect = hasFiles || event.target.type === 'search' ? 'copy' : 'none';
|
||||||
|
this.classList.toggle('dropzone', hasFiles);
|
||||||
|
if (hasFiles) {
|
||||||
|
event.preventDefault();
|
||||||
|
clearTimeout(this.fadeoutTimer);
|
||||||
|
this.classList.remove('fadeout');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
ondragend() {
|
||||||
|
animateElement(this, {className: 'fadeout', removeExtraClasses: ['dropzone']}).then(() => {
|
||||||
|
this.style.animationDuration = '';
|
||||||
|
});
|
||||||
|
},
|
||||||
|
ondragleave(event) {
|
||||||
|
try {
|
||||||
|
// in Firefox event.target could be XUL browser and hence there is no permission to access it
|
||||||
|
if (event.target === this) {
|
||||||
|
this.ondragend();
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
this.ondragend();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
ondrop(event) {
|
||||||
|
this.ondragend();
|
||||||
|
if (event.dataTransfer.files.length) {
|
||||||
|
event.preventDefault();
|
||||||
|
if ($('#only-updates input').checked) {
|
||||||
|
$('#only-updates input').click();
|
||||||
|
}
|
||||||
|
importFromFile({file: event.dataTransfer.files[0]});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
function importFromFile({fileTypeFilter, file} = {}) {
|
function importFromFile({fileTypeFilter, file} = {}) {
|
||||||
return new Promise(resolve => {
|
return new Promise(resolve => {
|
||||||
|
@ -41,7 +86,7 @@ function importFromFile({fileTypeFilter, file} = {}) {
|
||||||
importFromString(text) :
|
importFromString(text) :
|
||||||
getOwnTab().then(tab => {
|
getOwnTab().then(tab => {
|
||||||
tab.url = URL.createObjectURL(new Blob([text], {type: 'text/css'}));
|
tab.url = URL.createObjectURL(new Blob([text], {type: 'text/css'}));
|
||||||
return API.installUsercss({direct: true, tab})
|
return API.openUsercssInstallPage({direct: true, tab})
|
||||||
.then(() => URL.revokeObjectURL(tab.url));
|
.then(() => URL.revokeObjectURL(tab.url));
|
||||||
})
|
})
|
||||||
).then(numStyles => {
|
).then(numStyles => {
|
||||||
|
@ -56,19 +101,14 @@ function importFromFile({fileTypeFilter, file} = {}) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function importFromString(jsonString, oldStyles) {
|
function importFromString(jsonString) {
|
||||||
if (!oldStyles) {
|
const json = tryJSONparse(jsonString);
|
||||||
return API.getStyles().then(styles => importFromString(jsonString, styles));
|
if (!Array.isArray(json)) {
|
||||||
|
return Promise.reject(new Error('the backup is not a valid JSON file'));
|
||||||
}
|
}
|
||||||
const json = tryJSONparse(jsonString) || [];
|
let oldStyles;
|
||||||
if (typeof json.slice !== 'function') {
|
let oldStylesById;
|
||||||
json.length = 0;
|
let oldStylesByName;
|
||||||
}
|
|
||||||
const oldStylesById = new Map(
|
|
||||||
oldStyles.map(style => [style.id, style]));
|
|
||||||
const oldStylesByName = json.length && new Map(
|
|
||||||
oldStyles.map(style => [style.name.trim(), style]));
|
|
||||||
|
|
||||||
const stats = {
|
const stats = {
|
||||||
added: {names: [], ids: [], legend: 'importReportLegendAdded'},
|
added: {names: [], ids: [], legend: 'importReportLegendAdded'},
|
||||||
unchanged: {names: [], ids: [], legend: 'importReportLegendIdentical'},
|
unchanged: {names: [], ids: [], legend: 'importReportLegendIdentical'},
|
||||||
|
@ -78,31 +118,25 @@ function importFromString(jsonString, oldStyles) {
|
||||||
invalid: {names: [], legend: 'importReportLegendInvalid'},
|
invalid: {names: [], legend: 'importReportLegendInvalid'},
|
||||||
};
|
};
|
||||||
|
|
||||||
let index = 0;
|
return API.getAllStyles().then(styles => {
|
||||||
let lastRenderTime = performance.now();
|
// make a copy of the current database, that may be used when we want to
|
||||||
const renderQueue = [];
|
// undo
|
||||||
const RENDER_NAP_TIME_MAX = 1000; // ms
|
oldStyles = styles;
|
||||||
const RENDER_QUEUE_MAX = 50; // number of styles
|
oldStylesById = new Map(
|
||||||
const SAVE_OPTIONS = {reason: 'import', notify: false};
|
oldStyles.map(style => [style.id, style]));
|
||||||
|
oldStylesByName = json.length && new Map(
|
||||||
return new Promise(proceed);
|
oldStyles.map(style => [style.name.trim(), style]));
|
||||||
|
return Promise.all(json.map((item, i) => {
|
||||||
function proceed(resolve) {
|
const info = analyze(item, i);
|
||||||
while (index < json.length) {
|
|
||||||
const item = json[index++];
|
|
||||||
const info = analyze(item);
|
|
||||||
if (info) {
|
if (info) {
|
||||||
// using saveStyle directly since json was parsed in background page context
|
return API.importStyle(item)
|
||||||
return API.saveStyle(Object.assign(item, SAVE_OPTIONS))
|
.then(style => updateStats(style, info));
|
||||||
.then(style => account({style, info, resolve}));
|
|
||||||
}
|
}
|
||||||
}
|
}));
|
||||||
renderQueue.forEach(style => handleUpdate(style, {reason: 'import'}));
|
})
|
||||||
renderQueue.length = 0;
|
.then(done);
|
||||||
done(resolve);
|
|
||||||
}
|
|
||||||
|
|
||||||
function analyze(item) {
|
function analyze(item, index) {
|
||||||
if (typeof item !== 'object' ||
|
if (typeof item !== 'object' ||
|
||||||
!item ||
|
!item ||
|
||||||
!item.name ||
|
!item.name ||
|
||||||
|
@ -146,17 +180,7 @@ function importFromString(jsonString, oldStyles) {
|
||||||
.some(field => oldStyle[field] && oldStyle[field] === newStyle[field]);
|
.some(field => oldStyle[field] && oldStyle[field] === newStyle[field]);
|
||||||
}
|
}
|
||||||
|
|
||||||
function account({style, info, resolve}) {
|
function updateStats(style, {oldStyle, metaEqual, codeEqual}) {
|
||||||
renderQueue.push(style);
|
|
||||||
if (performance.now() - lastRenderTime > RENDER_NAP_TIME_MAX
|
|
||||||
|| renderQueue.length > RENDER_QUEUE_MAX) {
|
|
||||||
renderQueue.forEach(style => handleUpdate(style, {reason: 'import'}));
|
|
||||||
setTimeout(scrollElementIntoView, 0, $('#style-' + renderQueue.pop().id));
|
|
||||||
renderQueue.length = 0;
|
|
||||||
lastRenderTime = performance.now();
|
|
||||||
}
|
|
||||||
setTimeout(proceed, 0, resolve);
|
|
||||||
const {oldStyle, metaEqual, codeEqual} = info;
|
|
||||||
if (!oldStyle) {
|
if (!oldStyle) {
|
||||||
stats.added.names.push(style.name);
|
stats.added.names.push(style.name);
|
||||||
stats.added.ids.push(style.id);
|
stats.added.ids.push(style.id);
|
||||||
|
@ -176,42 +200,41 @@ function importFromString(jsonString, oldStyles) {
|
||||||
stats.metaOnly.ids.push(style.id);
|
stats.metaOnly.ids.push(style.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
function done(resolve) {
|
function done() {
|
||||||
const numChanged = stats.metaAndCode.names.length +
|
const numChanged = stats.metaAndCode.names.length +
|
||||||
stats.metaOnly.names.length +
|
stats.metaOnly.names.length +
|
||||||
stats.codeOnly.names.length +
|
stats.codeOnly.names.length +
|
||||||
stats.added.names.length;
|
stats.added.names.length;
|
||||||
Promise.resolve(numChanged && API.refreshAllTabs()).then(() => {
|
const report = Object.keys(stats)
|
||||||
const report = Object.keys(stats)
|
.filter(kind => stats[kind].names.length)
|
||||||
.filter(kind => stats[kind].names.length)
|
.map(kind => {
|
||||||
.map(kind => {
|
const {ids, names, legend} = stats[kind];
|
||||||
const {ids, names, legend} = stats[kind];
|
const listItemsWithId = (name, i) =>
|
||||||
const listItemsWithId = (name, i) =>
|
$create('div', {dataset: {id: ids[i]}}, name);
|
||||||
$create('div', {dataset: {id: ids[i]}}, name);
|
const listItems = name =>
|
||||||
const listItems = name =>
|
$create('div', name);
|
||||||
$create('div', name);
|
const block =
|
||||||
const block =
|
$create('details', {dataset: {id: kind}}, [
|
||||||
$create('details', {dataset: {id: kind}}, [
|
$create('summary',
|
||||||
$create('summary',
|
$create('b', names.length + ' ' + t(legend))),
|
||||||
$create('b', names.length + ' ' + t(legend))),
|
$create('small',
|
||||||
$create('small',
|
names.map(ids ? listItemsWithId : listItems)),
|
||||||
names.map(ids ? listItemsWithId : listItems)),
|
]);
|
||||||
]);
|
return block;
|
||||||
return block;
|
});
|
||||||
});
|
scrollTo(0, 0);
|
||||||
scrollTo(0, 0);
|
messageBox({
|
||||||
messageBox({
|
title: t('importReportTitle'),
|
||||||
title: t('importReportTitle'),
|
contents: report.length ? report : t('importReportUnchanged'),
|
||||||
contents: report.length ? report : t('importReportUnchanged'),
|
buttons: [t('confirmClose'), numChanged && t('undo')],
|
||||||
buttons: [t('confirmClose'), numChanged && t('undo')],
|
onshow: bindClick,
|
||||||
onshow: bindClick,
|
})
|
||||||
}).then(({button}) => {
|
.then(({button}) => {
|
||||||
if (button === 1) {
|
if (button === 1) {
|
||||||
undo();
|
undo();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
resolve(numChanged);
|
return Promise.resolve(numChanged);
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function undo() {
|
function undo() {
|
||||||
|
@ -222,27 +245,20 @@ function importFromString(jsonString, oldStyles) {
|
||||||
...stats.added.ids,
|
...stats.added.ids,
|
||||||
];
|
];
|
||||||
let tasks = Promise.resolve();
|
let tasks = Promise.resolve();
|
||||||
let tasksUI = Promise.resolve();
|
|
||||||
for (const id of newIds) {
|
for (const id of newIds) {
|
||||||
tasks = tasks.then(() => API.deleteStyle({id, notify: false}));
|
tasks = tasks.then(() => API.deleteStyle(id));
|
||||||
tasksUI = tasksUI.then(() => handleDelete(id));
|
|
||||||
const oldStyle = oldStylesById.get(id);
|
const oldStyle = oldStylesById.get(id);
|
||||||
if (oldStyle) {
|
if (oldStyle) {
|
||||||
Object.assign(oldStyle, SAVE_OPTIONS);
|
tasks = tasks.then(() => API.importStyle(oldStyle));
|
||||||
tasks = tasks.then(() => API.saveStyle(oldStyle));
|
|
||||||
tasksUI = tasksUI.then(() => handleUpdate(oldStyle, {reason: 'import'}));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// taskUI is superfast and updates style list only in this page,
|
// taskUI is superfast and updates style list only in this page,
|
||||||
// which should account for 99.99999999% of cases, supposedly
|
// which should account for 99.99999999% of cases, supposedly
|
||||||
return tasks
|
return tasks.then(() => messageBox({
|
||||||
.then(tasksUI)
|
title: t('importReportUndoneTitle'),
|
||||||
.then(API.refreshAllTabs)
|
contents: newIds.length + ' ' + t('importReportUndone'),
|
||||||
.then(() => messageBox({
|
buttons: [t('confirmClose')],
|
||||||
title: t('importReportUndoneTitle'),
|
}));
|
||||||
contents: newIds.length + ' ' + t('importReportUndone'),
|
|
||||||
buttons: [t('confirmClose')],
|
|
||||||
}));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function bindClick() {
|
function bindClick() {
|
||||||
|
@ -273,8 +289,8 @@ function importFromString(jsonString, oldStyles) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
$('#file-all-styles').onclick = () => {
|
function exportToFile() {
|
||||||
API.getStyles().then(styles => {
|
API.getAllStyles().then(styles => {
|
||||||
// https://crbug.com/714373
|
// https://crbug.com/714373
|
||||||
document.documentElement.appendChild(
|
document.documentElement.appendChild(
|
||||||
$create('iframe', {
|
$create('iframe', {
|
||||||
|
@ -313,47 +329,4 @@ $('#file-all-styles').onclick = () => {
|
||||||
const yyyy = today.getFullYear();
|
const yyyy = today.getFullYear();
|
||||||
return `stylus-${yyyy}-${mm}-${dd}${STYLUS_BACKUP_FILE_EXT}`;
|
return `stylus-${yyyy}-${mm}-${dd}${STYLUS_BACKUP_FILE_EXT}`;
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
|
|
||||||
$('#unfile-all-styles').onclick = () => {
|
|
||||||
importFromFile({fileTypeFilter: STYLUS_BACKUP_FILE_EXT});
|
|
||||||
};
|
|
||||||
|
|
||||||
Object.assign(document.body, {
|
|
||||||
ondragover(event) {
|
|
||||||
const hasFiles = event.dataTransfer.types.includes('Files');
|
|
||||||
event.dataTransfer.dropEffect = hasFiles || event.target.type === 'search' ? 'copy' : 'none';
|
|
||||||
this.classList.toggle('dropzone', hasFiles);
|
|
||||||
if (hasFiles) {
|
|
||||||
event.preventDefault();
|
|
||||||
clearTimeout(this.fadeoutTimer);
|
|
||||||
this.classList.remove('fadeout');
|
|
||||||
}
|
|
||||||
},
|
|
||||||
ondragend() {
|
|
||||||
animateElement(this, {className: 'fadeout', removeExtraClasses: ['dropzone']}).then(() => {
|
|
||||||
this.style.animationDuration = '';
|
|
||||||
});
|
|
||||||
},
|
|
||||||
ondragleave(event) {
|
|
||||||
try {
|
|
||||||
// in Firefox event.target could be XUL browser and hence there is no permission to access it
|
|
||||||
if (event.target === this) {
|
|
||||||
this.ondragend();
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
this.ondragend();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
ondrop(event) {
|
|
||||||
this.ondragend();
|
|
||||||
if (event.dataTransfer.files.length) {
|
|
||||||
event.preventDefault();
|
|
||||||
if ($('#only-updates input').checked) {
|
|
||||||
$('#only-updates input').click();
|
|
||||||
}
|
|
||||||
importFromFile({file: event.dataTransfer.files[0]});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
/* global installed */
|
/* global installed onDOMready $create debounce $ scrollElementIntoView
|
||||||
|
animateElement */
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
onDOMready().then(() => {
|
onDOMready().then(() => {
|
||||||
|
|
|
@ -1,10 +1,13 @@
|
||||||
/*
|
/*
|
||||||
global messageBox getStyleWithNoCode retranslateCSS
|
global messageBox getStyleWithNoCode
|
||||||
global filtersSelector filterAndAppend urlFilterParam showFiltersStats
|
filterAndAppend urlFilterParam showFiltersStats
|
||||||
global checkUpdate handleUpdateInstalled
|
checkUpdate handleUpdateInstalled
|
||||||
global objectDiff
|
objectDiff
|
||||||
global configDialog
|
configDialog
|
||||||
global sorter
|
sorter msg prefs API onDOMready $ $$ $create template setupLivePrefs
|
||||||
|
URLS enforceInputRange t tWordBreak formatDate
|
||||||
|
getOwnTab getActiveTab openURL animateElement sessionStorageHash debounce
|
||||||
|
scrollElementIntoView CHROME VIVALDI FIREFOX
|
||||||
*/
|
*/
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
|
@ -23,7 +26,6 @@ const newUI = {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
newUI.renderClass();
|
newUI.renderClass();
|
||||||
requestAnimationFrame(usePrefsDuringPageLoad);
|
|
||||||
|
|
||||||
const TARGET_TYPES = ['domains', 'urls', 'urlPrefixes', 'regexps'];
|
const TARGET_TYPES = ['domains', 'urls', 'urlPrefixes', 'regexps'];
|
||||||
const GET_FAVICON_URL = 'https://www.google.com/s2/favicons?domain=';
|
const GET_FAVICON_URL = 'https://www.google.com/s2/favicons?domain=';
|
||||||
|
@ -32,23 +34,38 @@ const OWN_ICON = chrome.runtime.getManifest().icons['16'];
|
||||||
const handleEvent = {};
|
const handleEvent = {};
|
||||||
|
|
||||||
Promise.all([
|
Promise.all([
|
||||||
API.getStyles({omitCode: !BG}),
|
API.getAllStyles(true),
|
||||||
urlFilterParam && API.searchDB({query: 'url:' + urlFilterParam}),
|
urlFilterParam && API.searchDB({query: 'url:' + urlFilterParam}),
|
||||||
onDOMready().then(initGlobalEvents),
|
Promise.all([
|
||||||
|
onDOMready(),
|
||||||
|
prefs.initializing,
|
||||||
|
])
|
||||||
|
.then(() => {
|
||||||
|
initGlobalEvents();
|
||||||
|
if (!VIVALDI) {
|
||||||
|
$$('#header select').forEach(el => el.adjustWidth());
|
||||||
|
}
|
||||||
|
if (FIREFOX && 'update' in (chrome.commands || {})) {
|
||||||
|
const btn = $('#manage-shortcuts-button');
|
||||||
|
btn.classList.remove('chromium-only');
|
||||||
|
btn.onclick = API.optionsCustomizeHotkeys;
|
||||||
|
}
|
||||||
|
}),
|
||||||
]).then(args => {
|
]).then(args => {
|
||||||
showStyles(...args);
|
showStyles(...args);
|
||||||
});
|
});
|
||||||
|
|
||||||
chrome.runtime.onMessage.addListener(onRuntimeMessage);
|
msg.onExtension(onRuntimeMessage);
|
||||||
|
|
||||||
function onRuntimeMessage(msg) {
|
function onRuntimeMessage(msg) {
|
||||||
switch (msg.method) {
|
switch (msg.method) {
|
||||||
case 'styleUpdated':
|
case 'styleUpdated':
|
||||||
case 'styleAdded':
|
case 'styleAdded':
|
||||||
handleUpdate(msg.style, msg);
|
API.getStyle(msg.style.id, true)
|
||||||
|
.then(style => handleUpdate(style, msg));
|
||||||
break;
|
break;
|
||||||
case 'styleDeleted':
|
case 'styleDeleted':
|
||||||
handleDelete(msg.id);
|
handleDelete(msg.style.id);
|
||||||
break;
|
break;
|
||||||
case 'styleApply':
|
case 'styleApply':
|
||||||
case 'styleReplaceAll':
|
case 'styleReplaceAll':
|
||||||
|
@ -96,8 +113,7 @@ function initGlobalEvents() {
|
||||||
setupLivePrefs();
|
setupLivePrefs();
|
||||||
sorter.init();
|
sorter.init();
|
||||||
|
|
||||||
$$('[id^="manage.newUI"]')
|
prefs.subscribe(['manage.newUI'], () => switchUI());
|
||||||
.forEach(el => (el.oninput = (el.onchange = switchUI)));
|
|
||||||
|
|
||||||
switchUI({styleOnly: true});
|
switchUI({styleOnly: true});
|
||||||
|
|
||||||
|
@ -119,7 +135,7 @@ function showStyles(styles = [], matchUrlIds) {
|
||||||
const sorted = sorter.sort({
|
const sorted = sorter.sort({
|
||||||
styles: styles.map(style => ({
|
styles: styles.map(style => ({
|
||||||
style,
|
style,
|
||||||
name: style.name.toLocaleLowerCase() + '\n' + style.name,
|
name: (style.name || '').toLocaleLowerCase() + '\n' + style.name,
|
||||||
})),
|
})),
|
||||||
});
|
});
|
||||||
let index = 0;
|
let index = 0;
|
||||||
|
@ -195,7 +211,7 @@ function createStyleElement({style, name}) {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
const parts = createStyleElement.parts;
|
const parts = createStyleElement.parts;
|
||||||
const configurable = style.usercssData && Object.keys(style.usercssData.vars).length > 0;
|
const configurable = style.usercssData && style.usercssData.vars && Object.keys(style.usercssData.vars).length > 0;
|
||||||
parts.checker.checked = style.enabled;
|
parts.checker.checked = style.enabled;
|
||||||
parts.nameLink.textContent = tWordBreak(style.name);
|
parts.nameLink.textContent = tWordBreak(style.name);
|
||||||
parts.nameLink.href = parts.editLink.href = parts.editHrefBase + style.id;
|
parts.nameLink.href = parts.editLink.href = parts.editHrefBase + style.id;
|
||||||
|
@ -395,10 +411,7 @@ Object.assign(handleEvent, {
|
||||||
},
|
},
|
||||||
|
|
||||||
toggle(event, entry) {
|
toggle(event, entry) {
|
||||||
API.saveStyle({
|
API.toggleStyle(entry.styleId, this.matches('.enable') || this.checked);
|
||||||
id: entry.styleId,
|
|
||||||
enabled: this.matches('.enable') || this.checked,
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
|
|
||||||
check(event, entry) {
|
check(event, entry) {
|
||||||
|
@ -410,8 +423,7 @@ Object.assign(handleEvent, {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
const json = entry.updatedCode;
|
const json = entry.updatedCode;
|
||||||
json.id = entry.styleId;
|
json.id = entry.styleId;
|
||||||
json.reason = 'update';
|
API[json.usercssData ? 'installUsercss' : 'installStyle'](json);
|
||||||
API[json.usercssData ? 'saveUsercss' : 'saveStyle'](json);
|
|
||||||
},
|
},
|
||||||
|
|
||||||
delete(event, entry) {
|
delete(event, entry) {
|
||||||
|
@ -426,7 +438,7 @@ Object.assign(handleEvent, {
|
||||||
})
|
})
|
||||||
.then(({button}) => {
|
.then(({button}) => {
|
||||||
if (button === 0) {
|
if (button === 0) {
|
||||||
API.deleteStyle({id});
|
API.deleteStyle(id);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
@ -510,10 +522,7 @@ Object.assign(handleEvent, {
|
||||||
|
|
||||||
|
|
||||||
function handleUpdate(style, {reason, method} = {}) {
|
function handleUpdate(style, {reason, method} = {}) {
|
||||||
if (reason === 'editPreview') return;
|
if (reason === 'editPreview' || reason === 'editPreviewEnd') return;
|
||||||
// the style was toggled and refreshAllTabs() sent a mini-notification,
|
|
||||||
// but we've already processed 'styleUpdated' sent directly from notifyAllTabs()
|
|
||||||
if (!style.sections) return;
|
|
||||||
let entry;
|
let entry;
|
||||||
let oldEntry = $(ENTRY_ID_PREFIX + style.id);
|
let oldEntry = $(ENTRY_ID_PREFIX + style.id);
|
||||||
if (oldEntry && method === 'styleUpdated') {
|
if (oldEntry && method === 'styleUpdated') {
|
||||||
|
@ -638,7 +647,7 @@ function switchUI({styleOnly} = {}) {
|
||||||
const missingFavicons = newUI.enabled && newUI.favicons && !$('.applies-to img');
|
const missingFavicons = newUI.enabled && newUI.favicons && !$('.applies-to img');
|
||||||
if (changed.enabled || (missingFavicons && !createStyleElement.parts)) {
|
if (changed.enabled || (missingFavicons && !createStyleElement.parts)) {
|
||||||
installed.textContent = '';
|
installed.textContent = '';
|
||||||
API.getStyles().then(showStyles);
|
API.getAllStyles(true).then(showStyles);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (changed.targets) {
|
if (changed.targets) {
|
||||||
|
@ -662,9 +671,10 @@ function onVisibilityChange() {
|
||||||
// assuming other changes aren't important enough to justify making a complicated DOM sync
|
// assuming other changes aren't important enough to justify making a complicated DOM sync
|
||||||
case 'visible':
|
case 'visible':
|
||||||
if (sessionStorage.justEditedStyleId) {
|
if (sessionStorage.justEditedStyleId) {
|
||||||
API.getStyles({id: sessionStorage.justEditedStyleId}).then(([style]) => {
|
API.getStyle(Number(sessionStorage.justEditedStyleId), true)
|
||||||
handleUpdate(style, {method: 'styleUpdated'});
|
.then(style => {
|
||||||
});
|
handleUpdate(style, {method: 'styleUpdated'});
|
||||||
|
});
|
||||||
delete sessionStorage.justEditedStyleId;
|
delete sessionStorage.justEditedStyleId;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
@ -685,30 +695,3 @@ function highlightEditedStyle() {
|
||||||
requestAnimationFrame(() => scrollElementIntoView(entry));
|
requestAnimationFrame(() => scrollElementIntoView(entry));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function usePrefsDuringPageLoad() {
|
|
||||||
for (const id of Object.getOwnPropertyNames(prefs.readOnlyValues)) {
|
|
||||||
const value = prefs.readOnlyValues[id];
|
|
||||||
if (value !== true) continue;
|
|
||||||
const el = document.getElementById(id) ||
|
|
||||||
id.includes('expanded') && $(`details[data-pref="${id}"]`);
|
|
||||||
if (!el) continue;
|
|
||||||
if (el.type === 'checkbox') {
|
|
||||||
el.checked = value;
|
|
||||||
} else if (el.localName === 'details') {
|
|
||||||
el.open = value;
|
|
||||||
} else {
|
|
||||||
el.value = value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!VIVALDI) {
|
|
||||||
$$('#header select').forEach(el => el.adjustWidth());
|
|
||||||
}
|
|
||||||
|
|
||||||
if (FIREFOX && 'update' in (chrome.commands || {})) {
|
|
||||||
const btn = $('#manage-shortcuts-button');
|
|
||||||
btn.classList.remove('chromium-only');
|
|
||||||
btn.onclick = API.optionsCustomizeHotkeys;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
/* exported objectDiff */
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
function objectDiff(first, second, path = '') {
|
function objectDiff(first, second, path = '') {
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/* global installed */
|
/* global installed messageBox t $ $create prefs */
|
||||||
/* global messageBox */
|
/* exported sorter */
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
const sorter = (() => {
|
const sorter = (() => {
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
/* global messageBox */
|
/* global messageBox ENTRY_ID_PREFIX newUI filtersSelector filterAndAppend
|
||||||
/* global ENTRY_ID_PREFIX, newUI */
|
sorter $ $$ $create API onDOMready scrollElementIntoView t chromeLocal */
|
||||||
/* global filtersSelector, filterAndAppend, sorter */
|
/* exported handleUpdateInstalled */
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
onDOMready().then(() => {
|
onDOMready().then(() => {
|
||||||
|
|
|
@ -24,23 +24,27 @@
|
||||||
],
|
],
|
||||||
"background": {
|
"background": {
|
||||||
"scripts": [
|
"scripts": [
|
||||||
|
"js/promisify.js",
|
||||||
"js/messaging.js",
|
"js/messaging.js",
|
||||||
|
"js/msg.js",
|
||||||
"js/storage-util.js",
|
"js/storage-util.js",
|
||||||
"js/sections-equal.js",
|
"js/sections-util.js",
|
||||||
"background/storage-dummy.js",
|
"js/worker-util.js",
|
||||||
"background/storage.js",
|
|
||||||
"js/prefs.js",
|
"js/prefs.js",
|
||||||
"js/script-loader.js",
|
"js/script-loader.js",
|
||||||
"js/usercss.js",
|
"js/usercss.js",
|
||||||
|
"js/cache.js",
|
||||||
|
"background/db.js",
|
||||||
|
"background/style-manager.js",
|
||||||
|
"background/navigator-util.js",
|
||||||
|
"background/icon-util.js",
|
||||||
"background/background.js",
|
"background/background.js",
|
||||||
"background/usercss-helper.js",
|
"background/usercss-helper.js",
|
||||||
"background/style-via-api.js",
|
"background/style-via-api.js",
|
||||||
"background/search-db.js",
|
"background/search-db.js",
|
||||||
"background/update.js",
|
"background/update.js",
|
||||||
"background/refresh-all-tabs.js",
|
|
||||||
"background/openusercss-api.js",
|
"background/openusercss-api.js",
|
||||||
"vendor/semver-bundle/semver.js",
|
"vendor/semver-bundle/semver.js"
|
||||||
"vendor-overwrites/colorpicker/colorconverter.js"
|
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"commands": {
|
"commands": {
|
||||||
|
@ -58,7 +62,7 @@
|
||||||
"run_at": "document_start",
|
"run_at": "document_start",
|
||||||
"all_frames": true,
|
"all_frames": true,
|
||||||
"match_about_blank": true,
|
"match_about_blank": true,
|
||||||
"js": ["content/apply.js"]
|
"js": ["js/promisify.js", "js/msg.js", "js/prefs.js", "content/apply.js"]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"matches": ["http://userstyles.org/*", "https://userstyles.org/*"],
|
"matches": ["http://userstyles.org/*", "https://userstyles.org/*"],
|
||||||
|
@ -111,5 +115,11 @@
|
||||||
"options_ui": {
|
"options_ui": {
|
||||||
"page": "options.html",
|
"page": "options.html",
|
||||||
"chrome_style": false
|
"chrome_style": false
|
||||||
|
},
|
||||||
|
"applications": {
|
||||||
|
"gecko": {
|
||||||
|
"id": "{7a7a4a92-a2a0-41d1-9fd7-1e92480d612d}",
|
||||||
|
"strict_min_version": "53"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
/* global focusAccessibility */
|
/* global focusAccessibility moveFocus $ $create t tHTML animateElement */
|
||||||
/* global moveFocus */
|
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -21,6 +21,8 @@
|
||||||
|
|
||||||
<script src="js/dom.js"></script>
|
<script src="js/dom.js"></script>
|
||||||
<script src="js/messaging.js"></script>
|
<script src="js/messaging.js"></script>
|
||||||
|
<script src="js/promisify.js"></script>
|
||||||
|
<script src="js/msg.js"></script>
|
||||||
<script src="js/localization.js"></script>
|
<script src="js/localization.js"></script>
|
||||||
<script src="js/prefs.js"></script>
|
<script src="js/prefs.js"></script>
|
||||||
<script src="js/storage-util.js" async></script>
|
<script src="js/storage-util.js" async></script>
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
/* global messageBox */
|
/* global messageBox msg setupLivePrefs enforceInputRange
|
||||||
|
$ $$ $create $createLink
|
||||||
|
FIREFOX OPERA CHROME URLS openURL prefs t API ignoreChromeError */
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
setupLivePrefs();
|
setupLivePrefs();
|
||||||
|
@ -21,7 +23,7 @@ if (!FIREFOX && !OPERA && CHROME < 3343) {
|
||||||
|
|
||||||
if (FIREFOX && 'update' in (chrome.commands || {})) {
|
if (FIREFOX && 'update' in (chrome.commands || {})) {
|
||||||
$('[data-cmd="open-keyboard"]').classList.remove('chromium-only');
|
$('[data-cmd="open-keyboard"]').classList.remove('chromium-only');
|
||||||
chrome.runtime.onMessage.addListener(msg => {
|
msg.onExtension(msg => {
|
||||||
if (msg.method === 'optionsCustomizeHotkeys') {
|
if (msg.method === 'optionsCustomizeHotkeys') {
|
||||||
customizeHotkeys();
|
customizeHotkeys();
|
||||||
}
|
}
|
||||||
|
@ -57,7 +59,7 @@ document.onclick = e => {
|
||||||
|
|
||||||
case 'reset':
|
case 'reset':
|
||||||
$$('input')
|
$$('input')
|
||||||
.filter(input => input.id in prefs.readOnlyValues)
|
.filter(input => input.id in prefs.defaults)
|
||||||
.forEach(input => prefs.reset(input.id));
|
.forEach(input => prefs.reset(input.id));
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
|
|
@ -18,6 +18,8 @@
|
||||||
"stylelint-bundle": "^8.0.0",
|
"stylelint-bundle": "^8.0.0",
|
||||||
"stylus-lang-bundle": "^0.54.5",
|
"stylus-lang-bundle": "^0.54.5",
|
||||||
"updates": "^5.1.2",
|
"updates": "^5.1.2",
|
||||||
|
"web-ext": "^2.9.1",
|
||||||
|
"usercss-meta": "^0.8.1",
|
||||||
"webext-tx-fix": "^0.3.1"
|
"webext-tx-fix": "^0.3.1"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
@ -30,6 +32,7 @@
|
||||||
"update-transifex": "tx push -s",
|
"update-transifex": "tx push -s",
|
||||||
"update-vendor": "node tools/update-libraries.js && node tools/update-codemirror-themes.js",
|
"update-vendor": "node tools/update-libraries.js && node tools/update-codemirror-themes.js",
|
||||||
"update-versions": "node tools/update-versions",
|
"update-versions": "node tools/update-versions",
|
||||||
"zip": "npm run update-versions && node tools/zip.js"
|
"zip": "npm run update-versions && node tools/zip.js",
|
||||||
|
"start": "web-ext run"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -151,10 +151,12 @@
|
||||||
<link rel="stylesheet" href="manage/config-dialog.css">
|
<link rel="stylesheet" href="manage/config-dialog.css">
|
||||||
<script src="manage/config-dialog.js"></script>
|
<script src="manage/config-dialog.js"></script>
|
||||||
|
|
||||||
|
<script src="js/promisify.js"></script>
|
||||||
<script src="js/dom.js"></script>
|
<script src="js/dom.js"></script>
|
||||||
<script src="js/messaging.js"></script>
|
<script src="js/messaging.js"></script>
|
||||||
<script src="js/localization.js"></script>
|
<script src="js/localization.js"></script>
|
||||||
<script src="js/prefs.js"></script>
|
<script src="js/prefs.js"></script>
|
||||||
|
<script src="js/msg.js"></script>
|
||||||
<script src="content/apply.js"></script>
|
<script src="content/apply.js"></script>
|
||||||
|
|
||||||
<link rel="stylesheet" href="popup/popup.css">
|
<link rel="stylesheet" href="popup/popup.css">
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
/* global applyOnMessage installed */
|
/* global $ $$ API debounce $create t */
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
// eslint-disable-next-line no-var
|
/* exported hotkeys */
|
||||||
var hotkeys = (() => {
|
const hotkeys = (() => {
|
||||||
const entries = document.getElementsByClassName('entry');
|
const entries = document.getElementsByClassName('entry');
|
||||||
let togglablesShown;
|
let togglablesShown;
|
||||||
let togglables;
|
let togglables;
|
||||||
|
@ -101,15 +101,13 @@ var hotkeys = (() => {
|
||||||
entry = typeof entry === 'string' ? $('#' + entry) : entry;
|
entry = typeof entry === 'string' ? $('#' + entry) : entry;
|
||||||
if (!match && $('.checker', entry).checked !== enable || entry.classList.contains(match)) {
|
if (!match && $('.checker', entry).checked !== enable || entry.classList.contains(match)) {
|
||||||
results.push(entry.id);
|
results.push(entry.id);
|
||||||
task = task.then(() => API.saveStyle({
|
task = task
|
||||||
id: entry.styleId,
|
.then(() => API.toggleStyle(entry.styleId, enable))
|
||||||
enabled: enable,
|
.then(() => {
|
||||||
notify: false,
|
entry.classList.toggle('enabled', enable);
|
||||||
})).then(() => {
|
entry.classList.toggle('disabled', !enable);
|
||||||
entry.classList.toggle('enabled', enable);
|
$('.checker', entry).checked = enable;
|
||||||
entry.classList.toggle('disabled', !enable);
|
});
|
||||||
$('.checker', entry).checked = enable;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (results.length) task.then(API.refreshAllTabs);
|
if (results.length) task.then(API.refreshAllTabs);
|
||||||
|
|
|
@ -301,6 +301,10 @@ html[style*="border"] .entry:nth-child(11):before {
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font-size: 90%;
|
font-size: 90%;
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.regexp-partial .regexp-problem-indicator {
|
||||||
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
.regexp-partial .actions,
|
.regexp-partial .actions,
|
||||||
|
@ -311,6 +315,8 @@ html[style*="border"] .entry:nth-child(11):before {
|
||||||
#regexp-explanation {
|
#regexp-explanation {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
background-color: white;
|
background-color: white;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
left: 0;
|
left: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
padding: .5rem;
|
padding: .5rem;
|
||||||
|
|
345
popup/popup.js
345
popup/popup.js
|
@ -1,4 +1,7 @@
|
||||||
/* global configDialog hotkeys */
|
/* global configDialog hotkeys onTabReady msg
|
||||||
|
getActiveTab FIREFOX getTabRealURL URLS API onDOMready $ $$ prefs CHROME
|
||||||
|
setupLivePrefs template t $create tWordBreak animateElement
|
||||||
|
tryJSONparse debounce */
|
||||||
|
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
|
@ -11,44 +14,45 @@ const ENTRY_ID_PREFIX = '#' + ENTRY_ID_PREFIX_RAW;
|
||||||
|
|
||||||
toggleSideBorders();
|
toggleSideBorders();
|
||||||
|
|
||||||
getActiveTab().then(tab =>
|
getActiveTab()
|
||||||
FIREFOX && tab.url === 'about:blank' && tab.status === 'loading'
|
.then(tab =>
|
||||||
? getTabRealURLFirefox(tab)
|
FIREFOX && tab.url === 'about:blank' && tab.status === 'loading'
|
||||||
: getTabRealURL(tab)
|
? getTabRealURLFirefox(tab)
|
||||||
).then(url => Promise.all([
|
: getTabRealURL(tab)
|
||||||
(tabURL = URLS.supported(url) ? url : '') &&
|
)
|
||||||
API.getStyles({
|
.then(url => Promise.all([
|
||||||
matchUrl: tabURL,
|
(tabURL = URLS.supported(url) ? url : '') &&
|
||||||
omitCode: !BG,
|
API.getStylesByUrl(tabURL),
|
||||||
}),
|
onDOMready().then(initPopup),
|
||||||
onDOMready().then(initPopup),
|
]))
|
||||||
])).then(([styles]) => {
|
.then(([results]) => {
|
||||||
showStyles(styles);
|
if (!results) {
|
||||||
});
|
// unsupported URL;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
showStyles(results.map(r => Object.assign(r.data, r)));
|
||||||
|
})
|
||||||
|
.catch(console.error);
|
||||||
|
|
||||||
chrome.runtime.onMessage.addListener(onRuntimeMessage);
|
msg.onExtension(onRuntimeMessage);
|
||||||
|
|
||||||
|
prefs.subscribe(['popup.stylesFirst'], (key, stylesFirst) => {
|
||||||
|
const actions = $('body > .actions');
|
||||||
|
const before = stylesFirst ? actions : actions.nextSibling;
|
||||||
|
document.body.insertBefore(installed, before);
|
||||||
|
});
|
||||||
|
prefs.subscribe(['popupWidth'], (key, value) => setPopupWidth(value));
|
||||||
|
prefs.subscribe(['popup.borders'], (key, value) => toggleSideBorders(value));
|
||||||
|
|
||||||
function onRuntimeMessage(msg) {
|
function onRuntimeMessage(msg) {
|
||||||
switch (msg.method) {
|
switch (msg.method) {
|
||||||
case 'styleAdded':
|
case 'styleAdded':
|
||||||
case 'styleUpdated':
|
case 'styleUpdated':
|
||||||
if (msg.reason === 'editPreview') return;
|
if (msg.reason === 'editPreview' || msg.reason === 'editPreviewEnd') return;
|
||||||
handleUpdate(msg.style);
|
handleUpdate(msg);
|
||||||
break;
|
break;
|
||||||
case 'styleDeleted':
|
case 'styleDeleted':
|
||||||
handleDelete(msg.id);
|
handleDelete(msg.style.id);
|
||||||
break;
|
|
||||||
case 'prefChanged':
|
|
||||||
if ('popup.stylesFirst' in msg.prefs) {
|
|
||||||
const stylesFirst = msg.prefs['popup.stylesFirst'];
|
|
||||||
const actions = $('body > .actions');
|
|
||||||
const before = stylesFirst ? actions : actions.nextSibling;
|
|
||||||
document.body.insertBefore(installed, before);
|
|
||||||
} else if ('popupWidth' in msg.prefs) {
|
|
||||||
setPopupWidth(msg.prefs.popupWidth);
|
|
||||||
} else if ('popup.borders' in msg.prefs) {
|
|
||||||
toggleSideBorders(msg.prefs['popup.borders']);
|
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
dispatchEvent(new CustomEvent(msg.method, {detail: msg}));
|
dispatchEvent(new CustomEvent(msg.method, {detail: msg}));
|
||||||
|
@ -111,34 +115,35 @@ function initPopup() {
|
||||||
}
|
}
|
||||||
|
|
||||||
getActiveTab().then(function ping(tab, retryCountdown = 10) {
|
getActiveTab().then(function ping(tab, retryCountdown = 10) {
|
||||||
sendMessage({tabId: tab.id, method: 'ping', frameId: 0}, pong => {
|
msg.sendTab(tab.id, {method: 'ping'}, {frameId: 0})
|
||||||
if (pong) {
|
.catch(() => false)
|
||||||
return;
|
.then(pong => {
|
||||||
}
|
if (pong) {
|
||||||
ignoreChromeError();
|
return;
|
||||||
// FF and some Chrome forks (e.g. CentBrowser) implement tab-on-demand
|
}
|
||||||
// so we'll wait a bit to handle popup being invoked right after switching
|
// FF and some Chrome forks (e.g. CentBrowser) implement tab-on-demand
|
||||||
if (retryCountdown > 0 && (
|
// so we'll wait a bit to handle popup being invoked right after switching
|
||||||
tab.status !== 'complete' ||
|
if (retryCountdown > 0 && (
|
||||||
FIREFOX && tab.url === 'about:blank')) {
|
tab.status !== 'complete' ||
|
||||||
setTimeout(ping, 100, tab, --retryCountdown);
|
FIREFOX && tab.url === 'about:blank')) {
|
||||||
return;
|
setTimeout(ping, 100, tab, --retryCountdown);
|
||||||
}
|
return;
|
||||||
const info = template.unreachableInfo;
|
}
|
||||||
if (FIREFOX && tabURL.startsWith(URLS.browserWebStore)) {
|
const info = template.unreachableInfo;
|
||||||
$('label', info).textContent = t('unreachableAMO');
|
if (FIREFOX && tabURL.startsWith(URLS.browserWebStore)) {
|
||||||
const note = (FIREFOX < 59 ? t('unreachableAMOHintOldFF') : t('unreachableAMOHint')) +
|
$('label', info).textContent = t('unreachableAMO');
|
||||||
(FIREFOX < 60 ? '' : '\n' + t('unreachableAMOHintNewFF'));
|
const note = (FIREFOX < 59 ? t('unreachableAMOHintOldFF') : t('unreachableAMOHint')) +
|
||||||
const renderToken = s => s[0] === '<' ? $create('b', tWordBreak(s.slice(1, -1))) : s;
|
(FIREFOX < 60 ? '' : '\n' + t('unreachableAMOHintNewFF'));
|
||||||
const renderLine = line => $create('p', line.split(/(<.*?>)/).map(renderToken));
|
const renderToken = s => s[0] === '<' ? $create('b', tWordBreak(s.slice(1, -1))) : s;
|
||||||
const noteNode = $create('fragment', note.split('\n').map(renderLine));
|
const renderLine = line => $create('p', line.split(/(<.*?>)/).map(renderToken));
|
||||||
const target = $('p', info);
|
const noteNode = $create('fragment', note.split('\n').map(renderLine));
|
||||||
target.parentNode.insertBefore(noteNode, target);
|
const target = $('p', info);
|
||||||
target.remove();
|
target.parentNode.insertBefore(noteNode, target);
|
||||||
}
|
target.remove();
|
||||||
document.body.classList.add('unreachable');
|
}
|
||||||
document.body.insertBefore(info, document.body.firstChild);
|
document.body.classList.add('unreachable');
|
||||||
});
|
document.body.insertBefore(info, document.body.firstChild);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Write new style links
|
// Write new style links
|
||||||
|
@ -227,93 +232,92 @@ function showStyles(styles) {
|
||||||
const container = document.createDocumentFragment();
|
const container = document.createDocumentFragment();
|
||||||
styles.forEach(style => createStyleElement({style, container}));
|
styles.forEach(style => createStyleElement({style, container}));
|
||||||
installed.appendChild(container);
|
installed.appendChild(container);
|
||||||
setTimeout(detectSloppyRegexps, 100, styles);
|
window.dispatchEvent(new Event('showStyles:done'));
|
||||||
|
|
||||||
API.getStyles({
|
|
||||||
matchUrl: tabURL,
|
|
||||||
strictRegexp: false,
|
|
||||||
omitCode: true,
|
|
||||||
}).then(unscreenedStyles => {
|
|
||||||
for (const style of unscreenedStyles) {
|
|
||||||
if (!styles.find(({id}) => id === style.id)) {
|
|
||||||
createStyleElement({style, check: true});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
window.dispatchEvent(new Event('showStyles:done'));
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function createStyleElement({
|
function createStyleElement({
|
||||||
style,
|
style,
|
||||||
check = false,
|
|
||||||
container = installed,
|
container = installed,
|
||||||
}) {
|
}) {
|
||||||
const entry = template.style.cloneNode(true);
|
let entry = $(ENTRY_ID_PREFIX + style.id);
|
||||||
entry.setAttribute('style-id', style.id);
|
if (!entry) {
|
||||||
Object.assign(entry, {
|
entry = template.style.cloneNode(true);
|
||||||
id: ENTRY_ID_PREFIX_RAW + style.id,
|
entry.setAttribute('style-id', style.id);
|
||||||
styleId: style.id,
|
Object.assign(entry, {
|
||||||
styleIsUsercss: Boolean(style.usercssData),
|
id: ENTRY_ID_PREFIX_RAW + style.id,
|
||||||
className: entry.className + ' ' + (style.enabled ? 'enabled' : 'disabled'),
|
styleId: style.id,
|
||||||
onmousedown: handleEvent.maybeEdit,
|
styleIsUsercss: Boolean(style.usercssData),
|
||||||
});
|
onmousedown: handleEvent.maybeEdit,
|
||||||
|
styleMeta: style
|
||||||
|
});
|
||||||
|
const checkbox = $('.checker', entry);
|
||||||
|
Object.assign(checkbox, {
|
||||||
|
id: ENTRY_ID_PREFIX_RAW + style.id,
|
||||||
|
// title: t('exclusionsPopupTip'),
|
||||||
|
onclick: handleEvent.toggle,
|
||||||
|
// oncontextmenu: handleEvent.openExcludeMenu
|
||||||
|
});
|
||||||
|
const editLink = $('.style-edit-link', entry);
|
||||||
|
Object.assign(editLink, {
|
||||||
|
href: editLink.getAttribute('href') + style.id,
|
||||||
|
onclick: handleEvent.openLink,
|
||||||
|
});
|
||||||
|
const styleName = $('.style-name', entry);
|
||||||
|
Object.assign(styleName, {
|
||||||
|
htmlFor: ENTRY_ID_PREFIX_RAW + style.id,
|
||||||
|
onclick: handleEvent.name,
|
||||||
|
});
|
||||||
|
styleName.checkbox = checkbox;
|
||||||
|
styleName.appendChild(document.createTextNode(' '));
|
||||||
|
const config = $('.configure', entry);
|
||||||
|
if (!style.usercssData && style.updateUrl && style.updateUrl.includes('?') && style.url) {
|
||||||
|
config.target = '_blank';
|
||||||
|
config.title = t('configureStyleOnHomepage');
|
||||||
|
config.dataset.sendMessage = JSON.stringify({method: 'openSettings'});
|
||||||
|
$('use', config).attributes['xlink:href'].nodeValue = '#svg-icon-config-uso';
|
||||||
|
}
|
||||||
|
$('.enable', entry).onclick = handleEvent.toggle;
|
||||||
|
$('.disable', entry).onclick = handleEvent.toggle;
|
||||||
|
$('.delete', entry).onclick = handleEvent.delete;
|
||||||
|
$('.configure', entry).onclick = handleEvent.configure;
|
||||||
|
|
||||||
const checkbox = $('.checker', entry);
|
const indicator = template.regexpProblemIndicator.cloneNode(true);
|
||||||
Object.assign(checkbox, {
|
indicator.appendChild(document.createTextNode('!'));
|
||||||
id: ENTRY_ID_PREFIX_RAW + style.id,
|
indicator.onclick = handleEvent.indicator;
|
||||||
checked: style.enabled,
|
$('.main-controls', entry).appendChild(indicator);
|
||||||
onclick: handleEvent.toggle,
|
}
|
||||||
});
|
|
||||||
|
|
||||||
const editLink = $('.style-edit-link', entry);
|
style = Object.assign(entry.styleMeta, style);
|
||||||
Object.assign(editLink, {
|
|
||||||
href: editLink.getAttribute('href') + style.id,
|
entry.classList.toggle('disabled', !style.enabled);
|
||||||
onclick: handleEvent.openLink,
|
entry.classList.toggle('enabled', style.enabled);
|
||||||
});
|
$('.checker', entry).checked = style.enabled;
|
||||||
|
|
||||||
const styleName = $('.style-name', entry);
|
const styleName = $('.style-name', entry);
|
||||||
Object.assign(styleName, {
|
styleName.lastChild.textContent = style.name;
|
||||||
htmlFor: ENTRY_ID_PREFIX_RAW + style.id,
|
setTimeout(() => {
|
||||||
onclick: handleEvent.name,
|
styleName.title = entry.styleMeta.sloppy ?
|
||||||
});
|
t('styleNotAppliedRegexpProblemTooltip') :
|
||||||
styleName.checkbox = checkbox;
|
styleName.scrollWidth > styleName.clientWidth + 1 ?
|
||||||
styleName.appendChild(document.createTextNode(style.name));
|
styleName.textContent : '';
|
||||||
setTimeout((el = styleName) => {
|
|
||||||
if (el.scrollWidth > el.clientWidth + 1) {
|
|
||||||
el.title = el.textContent;
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const config = $('.configure', entry);
|
const config = $('.configure', entry);
|
||||||
if (!style.usercssData && style.updateUrl && style.updateUrl.includes('?') && style.url) {
|
if (!style.usercssData && style.updateUrl && style.updateUrl.includes('?') && style.url) {
|
||||||
config.href = style.url;
|
config.href = style.url;
|
||||||
config.target = '_blank';
|
|
||||||
config.title = t('configureStyleOnHomepage');
|
|
||||||
config.dataset.sendMessage = JSON.stringify({method: 'openSettings'});
|
|
||||||
$('use', config).attributes['xlink:href'].nodeValue = '#svg-icon-config-uso';
|
|
||||||
} else if (!style.usercssData || !Object.keys(style.usercssData.vars || {}).length) {
|
|
||||||
config.style.display = 'none';
|
|
||||||
}
|
|
||||||
|
|
||||||
$('.enable', entry).onclick = handleEvent.toggle;
|
|
||||||
$('.disable', entry).onclick = handleEvent.toggle;
|
|
||||||
$('.delete', entry).onclick = handleEvent.delete;
|
|
||||||
$('.configure', entry).onclick = handleEvent.configure;
|
|
||||||
|
|
||||||
if (check) detectSloppyRegexps([style]);
|
|
||||||
|
|
||||||
const oldElement = $(ENTRY_ID_PREFIX + style.id);
|
|
||||||
if (oldElement && oldElement.contains(document.activeElement)) {
|
|
||||||
// preserve the focused element inside
|
|
||||||
const {className} = document.activeElement;
|
|
||||||
oldElement.parentNode.replaceChild(entry, oldElement);
|
|
||||||
// we're not using $() since className may contain multiple tokens
|
|
||||||
const el = entry.getElementsByClassName(className)[0];
|
|
||||||
if (el) el.focus();
|
|
||||||
} else if (oldElement) {
|
|
||||||
oldElement.parentNode.replaceChild(entry, oldElement);
|
|
||||||
} else {
|
} else {
|
||||||
|
config.removeAttribute('href');
|
||||||
|
}
|
||||||
|
config.style.display =
|
||||||
|
!style.usercssData && config.href ||
|
||||||
|
style.usercssData && Object.keys(style.usercssData.vars || {}).length ?
|
||||||
|
'' : 'none';
|
||||||
|
|
||||||
|
entry.classList.toggle('not-applied', style.excluded || style.sloppy);
|
||||||
|
entry.classList.toggle('regexp-partial', style.sloppy);
|
||||||
|
|
||||||
|
if (entry.parentNode !== container) {
|
||||||
container.appendChild(entry);
|
container.appendChild(entry);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -337,10 +341,10 @@ Object.assign(handleEvent, {
|
||||||
toggle(event) {
|
toggle(event) {
|
||||||
// when fired on checkbox, prevent the parent label from seeing the event, see #501
|
// when fired on checkbox, prevent the parent label from seeing the event, see #501
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
API.saveStyle({
|
API.toggleStyle(
|
||||||
id: handleEvent.getClickedStyleId(event),
|
handleEvent.getClickedStyleId(event),
|
||||||
enabled: this.matches('.enable') || this.checked,
|
this.matches('.enable') || this.checked
|
||||||
});
|
);
|
||||||
},
|
},
|
||||||
|
|
||||||
delete(event) {
|
delete(event) {
|
||||||
|
@ -367,14 +371,14 @@ Object.assign(handleEvent, {
|
||||||
className: 'lights-on',
|
className: 'lights-on',
|
||||||
onComplete: () => (box.dataset.display = false),
|
onComplete: () => (box.dataset.display = false),
|
||||||
});
|
});
|
||||||
if (ok) API.deleteStyle({id});
|
if (ok) API.deleteStyle(id);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
configure(event) {
|
configure(event) {
|
||||||
const {styleId, styleIsUsercss} = handleEvent.getClickedStyleElement(event);
|
const {styleId, styleIsUsercss} = handleEvent.getClickedStyleElement(event);
|
||||||
if (styleIsUsercss) {
|
if (styleIsUsercss) {
|
||||||
API.getStyles({id: styleId}).then(([style]) => {
|
API.getStyle(styleId, true).then(style => {
|
||||||
hotkeys.setState(false);
|
hotkeys.setState(false);
|
||||||
configDialog(style).then(() => {
|
configDialog(style).then(() => {
|
||||||
hotkeys.setState(true);
|
hotkeys.setState(true);
|
||||||
|
@ -436,12 +440,18 @@ Object.assign(handleEvent, {
|
||||||
|
|
||||||
openURLandHide(event) {
|
openURLandHide(event) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
const message = tryJSONparse(this.dataset.sendMessage);
|
||||||
getActiveTab()
|
getActiveTab()
|
||||||
.then(activeTab => API.openURL({
|
.then(activeTab => API.openURL({
|
||||||
url: this.href || this.dataset.href,
|
url: this.href || this.dataset.href,
|
||||||
index: activeTab.index + 1,
|
index: activeTab.index + 1
|
||||||
message: tryJSONparse(this.dataset.sendMessage),
|
|
||||||
}))
|
}))
|
||||||
|
.then(tab => {
|
||||||
|
if (message) {
|
||||||
|
return onTabReady(tab)
|
||||||
|
.then(() => msg.sendTab(tab.id, message));
|
||||||
|
}
|
||||||
|
})
|
||||||
.then(window.close);
|
.then(window.close);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -457,24 +467,31 @@ Object.assign(handleEvent, {
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
function handleUpdate(style) {
|
function handleUpdate({style, reason}) {
|
||||||
if ($(ENTRY_ID_PREFIX + style.id)) {
|
|
||||||
createStyleElement({style, check: true});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!tabURL) return;
|
if (!tabURL) return;
|
||||||
// Add an entry when a new style for the current url is installed
|
|
||||||
API.getStyles({
|
fetchStyle()
|
||||||
matchUrl: tabURL,
|
.then(style => {
|
||||||
stopOnFirst: true,
|
if (!style) {
|
||||||
omitCode: true,
|
return;
|
||||||
}).then(([style]) => {
|
}
|
||||||
if (style) {
|
if ($(ENTRY_ID_PREFIX + style.id)) {
|
||||||
|
createStyleElement({style});
|
||||||
|
return;
|
||||||
|
}
|
||||||
document.body.classList.remove('blocked');
|
document.body.classList.remove('blocked');
|
||||||
$$.remove('.blocked-info, #no-styles');
|
$$.remove('.blocked-info, #no-styles');
|
||||||
createStyleElement({style, check: true});
|
createStyleElement({style});
|
||||||
|
})
|
||||||
|
.catch(console.error);
|
||||||
|
|
||||||
|
function fetchStyle() {
|
||||||
|
if (reason === 'toggle' && $(ENTRY_ID_PREFIX + style.id)) {
|
||||||
|
return Promise.resolve(style);
|
||||||
}
|
}
|
||||||
});
|
return API.getStylesByUrl(tabURL, style.id)
|
||||||
|
.then(([result]) => result && Object.assign(result.data, result));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -485,32 +502,6 @@ function handleDelete(id) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function detectSloppyRegexps(styles) {
|
|
||||||
API.detectSloppyRegexps({
|
|
||||||
matchUrl: tabURL,
|
|
||||||
ids: styles.map(({id}) => id),
|
|
||||||
}).then(results => {
|
|
||||||
for (const {id, applied, skipped, hasInvalidRegexps} of results) {
|
|
||||||
const entry = $(ENTRY_ID_PREFIX + id);
|
|
||||||
if (!entry) continue;
|
|
||||||
if (!applied) {
|
|
||||||
entry.classList.add('not-applied');
|
|
||||||
$('.style-name', entry).title = t('styleNotAppliedRegexpProblemTooltip');
|
|
||||||
}
|
|
||||||
if (skipped || hasInvalidRegexps) {
|
|
||||||
entry.classList.toggle('regexp-partial', Boolean(skipped));
|
|
||||||
entry.classList.toggle('regexp-invalid', Boolean(hasInvalidRegexps));
|
|
||||||
const indicator = template.regexpProblemIndicator.cloneNode(true);
|
|
||||||
indicator.appendChild(document.createTextNode(entry.skipped || '!'));
|
|
||||||
indicator.onclick = handleEvent.indicator;
|
|
||||||
$('.main-controls', entry).appendChild(indicator);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function getTabRealURLFirefox(tab) {
|
function getTabRealURLFirefox(tab) {
|
||||||
// wait for FF tab-on-demand to get a real URL (initially about:blank), 5 sec max
|
// wait for FF tab-on-demand to get a real URL (initially about:blank), 5 sec max
|
||||||
return new Promise(resolve => {
|
return new Promise(resolve => {
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
/* global tabURL handleEvent */
|
/* global tabURL handleEvent $ $$ prefs template FIREFOX chromeLocal debounce
|
||||||
|
$create t API tWordBreak formatDate tryCatch tryJSONparse LZString
|
||||||
|
ignoreChromeError */
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
window.addEventListener('showStyles:done', function _() {
|
window.addEventListener('showStyles:done', function _() {
|
||||||
|
@ -135,7 +137,7 @@ window.addEventListener('showStyles:done', function _() {
|
||||||
if (result) {
|
if (result) {
|
||||||
result.installed = false;
|
result.installed = false;
|
||||||
result.installedStyleId = -1;
|
result.installedStyleId = -1;
|
||||||
(BG || window).clearTimeout(result.pingbackTimer);
|
window.clearTimeout(result.pingbackTimer);
|
||||||
renderActionButtons($('#' + RESULT_ID_PREFIX + result.id));
|
renderActionButtons($('#' + RESULT_ID_PREFIX + result.id));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -287,14 +289,14 @@ window.addEventListener('showStyles:done', function _() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const md5Url = UPDATE_URL.replace('%', result.id);
|
const md5Url = UPDATE_URL.replace('%', result.id);
|
||||||
API.getStyles({md5Url}).then(([installedStyle]) => {
|
API.styleExists({md5Url}).then(exist => {
|
||||||
if (installedStyle) {
|
if (exist) {
|
||||||
totalResults = Math.max(0, totalResults - 1);
|
totalResults = Math.max(0, totalResults - 1);
|
||||||
} else {
|
} else {
|
||||||
processedResults.push(result);
|
processedResults.push(result);
|
||||||
render();
|
render();
|
||||||
}
|
}
|
||||||
setTimeout(processNextResult, !installedStyle && DELAY_AFTER_FETCHING_STYLES);
|
setTimeout(processNextResult, !exist && DELAY_AFTER_FETCHING_STYLES);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -529,7 +531,7 @@ window.addEventListener('showStyles:done', function _() {
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
const entry = this.closest('.search-result');
|
const entry = this.closest('.search-result');
|
||||||
saveScrollPosition(entry);
|
saveScrollPosition(entry);
|
||||||
API.deleteStyle({id: entry._result.installedStyleId})
|
API.deleteStyle(entry._result.installedStyleId)
|
||||||
.then(restoreScrollPosition);
|
.then(restoreScrollPosition);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -555,9 +557,7 @@ window.addEventListener('showStyles:done', function _() {
|
||||||
pingback(result);
|
pingback(result);
|
||||||
// show a 'config-on-homepage' icon in the popup
|
// show a 'config-on-homepage' icon in the popup
|
||||||
style.updateUrl += settings.length ? '?' : '';
|
style.updateUrl += settings.length ? '?' : '';
|
||||||
// show a 'style installed' tooltip in the manager
|
return API.installStyle(style);
|
||||||
style.reason = 'install';
|
|
||||||
return API.saveStyle(style);
|
|
||||||
})
|
})
|
||||||
.catch(reason => {
|
.catch(reason => {
|
||||||
const usoId = result.id;
|
const usoId = result.id;
|
||||||
|
@ -581,7 +581,8 @@ window.addEventListener('showStyles:done', function _() {
|
||||||
}
|
}
|
||||||
|
|
||||||
function pingback(result) {
|
function pingback(result) {
|
||||||
const wnd = BG || window;
|
const wnd = window;
|
||||||
|
// FIXME: move this to background page and create an API like installUSOStyle
|
||||||
result.pingbackTimer = wnd.setTimeout(wnd.download, PINGBACK_DELAY,
|
result.pingbackTimer = wnd.setTimeout(wnd.download, PINGBACK_DELAY,
|
||||||
BASE_URL + '/styles/install/' + result.id + '?source=stylish-ch');
|
BASE_URL + '/styles/install/' + result.id + '?source=stylish-ch');
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/* global messageBox */
|
/* global zip onDOMready */
|
||||||
/* global zip */
|
/* exported createZipFileFromText readZipFileFromBlob */
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
onDOMready().then(() => {
|
onDOMready().then(() => {
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
/* exported getRedirectUrlAuthFlow launchWebAuthFlow */
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
/* global messageBox Dropbox createZipFileFromText readZipFileFromBlob launchWebAuthFlow getRedirectUrlAuthFlow importFromString resolve */
|
/* global messageBox Dropbox createZipFileFromText readZipFileFromBlob
|
||||||
|
launchWebAuthFlow getRedirectUrlAuthFlow importFromString resolve
|
||||||
|
$ $create t chromeLocal API getOwnTab */
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
const DROPBOX_API_KEY = 'zg52vphuapvpng9';
|
const DROPBOX_API_KEY = 'zg52vphuapvpng9';
|
||||||
|
@ -66,7 +68,7 @@ $('#sync-dropbox-export').onclick = () => {
|
||||||
// file deleted with success, get styles and create a file
|
// file deleted with success, get styles and create a file
|
||||||
.then(() => {
|
.then(() => {
|
||||||
messageProgressBar({title: title, text: t('gettingStyles')});
|
messageProgressBar({title: title, text: t('gettingStyles')});
|
||||||
return API.getStyles().then(styles => JSON.stringify(styles, null, '\t'));
|
return API.getAllStyles().then(styles => JSON.stringify(styles, null, '\t'));
|
||||||
})
|
})
|
||||||
// create zip file
|
// create zip file
|
||||||
.then(stylesText => {
|
.then(stylesText => {
|
||||||
|
@ -85,7 +87,7 @@ $('#sync-dropbox-export').onclick = () => {
|
||||||
console.log(error);
|
console.log(error);
|
||||||
// saving file first time
|
// saving file first time
|
||||||
if (error.status === API_ERROR_STATUS_FILE_NOT_FOUND) {
|
if (error.status === API_ERROR_STATUS_FILE_NOT_FOUND) {
|
||||||
API.getStyles()
|
API.getAllStyles()
|
||||||
.then(styles => {
|
.then(styles => {
|
||||||
messageProgressBar({title: title, text: t('gettingStyles')});
|
messageProgressBar({title: title, text: t('gettingStyles')});
|
||||||
return JSON.stringify(styles, null, '\t');
|
return JSON.stringify(styles, null, '\t');
|
||||||
|
@ -141,7 +143,7 @@ $('#sync-dropbox-import').onclick = () => {
|
||||||
importFromString(text) :
|
importFromString(text) :
|
||||||
getOwnTab().then(tab => {
|
getOwnTab().then(tab => {
|
||||||
tab.url = URL.createObjectURL(new Blob([text], {type: 'text/css'}));
|
tab.url = URL.createObjectURL(new Blob([text], {type: 'text/css'}));
|
||||||
return API.installUsercss({direct: true, tab})
|
return API.openUsercssInstallPage({direct: true, tab})
|
||||||
.then(() => URL.revokeObjectURL(tab.url));
|
.then(() => URL.revokeObjectURL(tab.url));
|
||||||
})
|
})
|
||||||
).then(numStyles => {
|
).then(numStyles => {
|
||||||
|
|
|
@ -28,6 +28,9 @@ const files = {
|
||||||
],
|
],
|
||||||
'stylus-lang-bundle': [
|
'stylus-lang-bundle': [
|
||||||
'stylus.min.js'
|
'stylus.min.js'
|
||||||
|
],
|
||||||
|
'usercss-meta': [
|
||||||
|
'dist/usercss-meta.min.js → usercss-meta.min.js'
|
||||||
]
|
]
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -35,7 +38,7 @@ async function updateReadme(lib) {
|
||||||
const pkg = await fs.readJson(`${root}/node_modules/${lib}/package.json`);
|
const pkg = await fs.readJson(`${root}/node_modules/${lib}/package.json`);
|
||||||
const file = `${root}/vendor/${lib}/README.md`;
|
const file = `${root}/vendor/${lib}/README.md`;
|
||||||
const txt = await fs.readFile(file, 'utf8');
|
const txt = await fs.readFile(file, 'utf8');
|
||||||
return fs.writeFile(file, txt.replace(/\bv[\d.]+[-\w]*\b/g, `v${pkg.version}`));
|
return fs.writeFile(file, txt.replace(/\b([v@])[\d.]+[-\w]*\b/g, `$1${pkg.version}`));
|
||||||
}
|
}
|
||||||
|
|
||||||
function isFolder(fileOrFolder) {
|
function isFolder(fileOrFolder) {
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
/* global CodeMirror colorConverter */
|
/* global colorConverter $create debounce */
|
||||||
|
/* exported colorMimicry */
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
(window.CodeMirror ? window.CodeMirror.prototype : window).colorpicker = function () {
|
(window.CodeMirror ? window.CodeMirror.prototype : window).colorpicker = function () {
|
||||||
|
|
|
@ -5505,3 +5505,5 @@ self.parserlib = (() => {
|
||||||
|
|
||||||
//endregion
|
//endregion
|
||||||
})();
|
})();
|
||||||
|
|
||||||
|
self.parserlib.css.Tokens[self.parserlib.css.Tokens.COMMENT].hide = false;
|
||||||
|
|
4
vendor/README.md
vendored
4
vendor/README.md
vendored
|
@ -9,7 +9,8 @@ Using this repo, run `npm install`... the latest versions of:
|
||||||
* `less` (https://github.com/less/less.js) is installed.
|
* `less` (https://github.com/less/less.js) is installed.
|
||||||
* `lz-string-unsafe` (https://github.com/openstyles/lz-string-unsafe) is installed.
|
* `lz-string-unsafe` (https://github.com/openstyles/lz-string-unsafe) is installed.
|
||||||
* `semver-bundle` (https://github.com/openstyles/semver-bundle) is installed.
|
* `semver-bundle` (https://github.com/openstyles/semver-bundle) is installed.
|
||||||
* `stylus-lang` (https://github.com/openstyles/stylus-lang-bundle) is installed.<br><br>
|
* `stylus-lang` (https://github.com/openstyles/stylus-lang-bundle) is installed.
|
||||||
|
* `usercss-meta` (https://github.com/StylishThemes/parse-usercss) is installed.
|
||||||
* The necessary build tools are installed; see `devDependencies` in the `package.json`.
|
* The necessary build tools are installed; see `devDependencies` in the `package.json`.
|
||||||
|
|
||||||
## Running the build script
|
## Running the build script
|
||||||
|
@ -24,6 +25,7 @@ The following changes are made:
|
||||||
* `lz-string-unsafe`: The compressed `lz-string-unsafe.min.js` file is copied directly into `vendor/lz-string-unsafe`.
|
* `lz-string-unsafe`: The compressed `lz-string-unsafe.min.js` file is copied directly into `vendor/lz-string-unsafe`.
|
||||||
* `semver-bundle`: The `dist/semver.js` file is copied directly into `vendor/semver`.
|
* `semver-bundle`: The `dist/semver.js` file is copied directly into `vendor/semver`.
|
||||||
* `stylus-lang-bundle`: The `stylus.min.js` file is copied directly into `vendor/stylus-lang-bundle`.
|
* `stylus-lang-bundle`: The `stylus.min.js` file is copied directly into `vendor/stylus-lang-bundle`.
|
||||||
|
* `usercss-meta`: The `dist/usercss-meta.min.js` file is copied directly into `vendor/usercss-meta`.
|
||||||
|
|
||||||
## Creating the ZIP
|
## Creating the ZIP
|
||||||
|
|
||||||
|
|
22
vendor/usercss-meta/LICENSE
vendored
Normal file
22
vendor/usercss-meta/LICENSE
vendored
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
The MIT License (MIT)
|
||||||
|
|
||||||
|
Original code: Copyright (c) Stylus team (github.com/openstyles/stylus)
|
||||||
|
Modified code: Copyright (c) StylishThemes (github.com/StylishThemes/parse-usercss)
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in
|
||||||
|
all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
|
THE SOFTWARE.
|
5
vendor/usercss-meta/README.md
vendored
Normal file
5
vendor/usercss-meta/README.md
vendored
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
## usercss-meta v0.8.1
|
||||||
|
|
||||||
|
usercss-meta installed via npm - source repo:
|
||||||
|
|
||||||
|
https://unpkg.com/usercss-meta@0.8.1/dist/usercss-meta.min.js
|
2
vendor/usercss-meta/usercss-meta.min.js
vendored
Normal file
2
vendor/usercss-meta/usercss-meta.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
Loading…
Reference in New Issue
Block a user