WME ClickSaver

Various UI changes to make editing faster and easier.

  1. // ==UserScript==
  2. // @name WME ClickSaver
  3. // @namespace https://gf.qytechs.cn/users/45389
  4. // @version 2025.07.11.001
  5. // @description Various UI changes to make editing faster and easier.
  6. // @author MapOMatic
  7. // @include /^https:\/\/(www|beta)\.waze\.com\/(?!user\/)(.{2,6}\/)?editor\/?.*$/
  8. // @license GNU GPLv3
  9. // @connect sheets.googleapis.com
  10. // @connect gf.qytechs.cn
  11. // @contributionURL https://github.com/WazeDev/Thank-The-Authors
  12. // @grant GM_xmlhttpRequest
  13. // @grant GM_addElement
  14. // @require https://gf.qytechs.cn/scripts/24851-wazewrap/code/WazeWrap.js
  15. // @require https://update.gf.qytechs.cn/scripts/509664/WME%20Utils%20-%20Bootstrap.js
  16. // ==/UserScript==
  17.  
  18. /* global I18n */
  19. /* global WazeWrap */
  20. /* global bootstrap */
  21.  
  22. /* eslint-disable max-classes-per-file */
  23.  
  24. (function main() {
  25. 'use strict';
  26.  
  27. const updateMessage = 'Keep the address element contained in the selection view';
  28. const scriptName = GM_info.script.name;
  29. const scriptVersion = GM_info.script.version;
  30. const downloadUrl = 'https://gf.qytechs.cn/scripts/369629-wme-clicksaver/code/WME%20ClickSaver.user.js';
  31. const forumUrl = 'https://www.waze.com/forum/viewtopic.php?f=819&t=199894';
  32. const translationsUrl = 'https://sheets.googleapis.com/v4/spreadsheets/1ZlE9yhNncP9iZrPzFFa-FCtYuK58wNOEcmKqng4sH1M/values/ClickSaver';
  33. const apiKey = 'YTJWNVBVRkplbUZUZVVGMFl6aFVjMjVOTW0wNU5GaG5kVE40TUZoNWJVZEhWbU5rUjNacVdtdFlWUT09';
  34. const DEC = s => atob(atob(s));
  35. let sdk;
  36.  
  37. // This function is injected into the page.
  38. async function clicksaver(argsObject) {
  39. /* eslint-disable object-curly-newline */
  40. const roadTypeDropdownSelector = 'wz-select[name="roadType"]';
  41. const roadTypeChipSelector = 'wz-chip-select[class="road-type-chip-select"]';
  42. // const PARKING_SPACES_DROPDOWN_SELECTOR = 'select[name="estimatedNumberOfSpots"]';
  43. // const PARKING_COST_DROPDOWN_SELECTOR = 'select[name="costType"]';
  44. const settingsStoreName = 'clicksaver_settings';
  45. const defaultTranslation = {
  46. roadTypeButtons: {
  47. St: { text: 'St' },
  48. PS: { text: 'PS' },
  49. mH: { text: 'mH' },
  50. MH: { text: 'MH' },
  51. Fw: { text: 'Fw' },
  52. Rmp: { text: 'Rmp' },
  53. OR: { text: 'OR' },
  54. PLR: { text: 'PLR' },
  55. PR: { text: 'PR' },
  56. Fer: { text: 'Fer' },
  57. WT: { text: 'WT' },
  58. PB: { text: 'PB' },
  59. Sw: { text: 'Sw' },
  60. RR: { text: 'RR' },
  61. RT: { text: 'RT' },
  62. Pw: { text: 'Pw' }
  63. },
  64. prefs: {
  65. dropdownHelperGroup: 'DROPDOWN HELPERS',
  66. roadTypeButtons: 'Add road type buttons',
  67. useOldRoadColors: 'Use old road colors (requires refresh)',
  68. setCityToDefault: 'Keep default value',
  69. setStreetCityToNone: 'Set Street/City to None (new seg\'s only)',
  70. // eslint-disable-next-line camelcase
  71. setStreetCityToNone_Title: 'NOTE: Only works if connected directly or indirectly'
  72. + ' to a segment with State / Country already set.',
  73. setCityToConnectedSegCity: 'Set City to connected segment\'s City',
  74. parkingCostButtons: 'Add PLA cost buttons',
  75. parkingSpacesButtons: 'Add PLA estimated spaces buttons',
  76. timeSaversGroup: 'TIME SAVERS',
  77. discussionForumLinkText: 'Discussion Forum',
  78. showAddAltCityButton: 'Show "Add alt city" button',
  79. showSwapDrivingWalkingButton: 'Show "Swap driving<->walking segment type" button',
  80. // eslint-disable-next-line camelcase
  81. showSwapDrivingWalkingButton_Title: 'Swap between driving-type and walking-type segments. WARNING! This will DELETE and recreate the segment. Nodes may need to be reconnected.',
  82. showSwapStreetNamesButton: 'Show swap primary and alternative street name button',
  83. swapWholeAddress: 'Include city name when swapping street names',
  84. addCompactColors: 'Add colors to compact mode road type buttons',
  85. hideUncheckedRoadTypeButtons: 'Hide unchecked road type buttons in compact mode',
  86. enableAddressRemovalButton: 'Enable address removal button',
  87. addressRemovalButtonTooltipText: 'Select at least one, choosing both will combine the buttons',
  88. showRemoveStreetNameButton: 'Show "Remove street" button',
  89. removeStreetNameTooltipText: 'If you have different cities selected and you remove the street name, the street name will display as "No common street".',
  90. showRemoveCityNameButton: 'Show "Remove city" button'
  91. },
  92. swapSegmentTypeWarning: 'This will DELETE the segment and recreate it. Any speed data will be lost, and nodes will need to be reconnected. This message will only be displayed once. Continue?',
  93. // eslint-disable-next-line camelcase
  94. swapSegmentTypeError_Paths: 'Paths must be removed from segment before changing between driving and pedestrian road type.',
  95. addAltCityButtonText: 'Add alt city',
  96. removeStreetNameButtonText: 'Remove street',
  97. removeCityNameButtonText: 'Remove city',
  98. removeStreetAndCityNameButtonText: 'Remove street+city',
  99. segmentHasStreetNameAndHouseNumbers: 'Cannot remove street name from a segment with house numbers'
  100. };
  101.  
  102. const roadTypeDropdownOption = {
  103. DEFAULT: 'DEFAULT',
  104. NONE: 'NONE',
  105. CONNECTED_CITY: 'CONNECTED_CITY'
  106. };
  107.  
  108. // Road types defined in the WME SDK documentation
  109. const wmeRoadType = {
  110. ALLEY: 22,
  111. FERRY: 15,
  112. FREEWAY: 3,
  113. MAJOR_HIGHWAY: 6,
  114. MINOR_HIGHWAY: 7,
  115. OFF_ROAD: 8,
  116. PARKING_LOT_ROAD: 20,
  117. PEDESTRIAN_BOARDWALK: 10,
  118. PRIMARY_STREET: 2,
  119. PRIVATE_ROAD: 17,
  120. RAILROAD: 18,
  121. RAMP: 4,
  122. RUNWAY_TAXIWAY: 19,
  123. STAIRWAY: 16,
  124. STREET: 1,
  125. WALKING_TRAIL: 5,
  126. WALKWAY: 9
  127. };
  128. const roadTypeSettings = {
  129. St: { id: wmeRoadType.STREET, wmeColor: '#ffffeb', svColor: '#ffffff', category: 'streets', visible: true },
  130. PS: { id: wmeRoadType.PRIMARY_STREET, wmeColor: '#f0ea58', svColor: '#cba12e', category: 'streets', visible: true },
  131. Pw: { id: wmeRoadType.ALLEY, wmeColor: '#64799a', svColor: '#64799a', category: 'streets', visible: false },
  132. mH: { id: wmeRoadType.MINOR_HIGHWAY, wmeColor: '#69bf88', svColor: '#ece589', category: 'highways', visible: true },
  133. MH: { id: wmeRoadType.MAJOR_HIGHWAY, wmeColor: '#45b8d1', svColor: '#c13040', category: 'highways', visible: true },
  134. Fw: { id: wmeRoadType.FREEWAY, wmeColor: '#c577d2', svColor: '#387fb8', category: 'highways', visible: false },
  135. Rmp: { id: wmeRoadType.RAMP, wmeColor: '#b3bfb3', svColor: '#58c53b', category: 'highways', visible: false },
  136. OR: { id: wmeRoadType.OFF_ROAD, wmeColor: '#867342', svColor: '#82614a', category: 'otherDrivable', visible: false },
  137. PLR: { id: wmeRoadType.PARKING_LOT_ROAD, wmeColor: '#ababab', svColor: '#2282ab', category: 'otherDrivable', visible: true },
  138. PR: { id: wmeRoadType.PRIVATE_ROAD, wmeColor: '#beba6c', svColor: '#00ffb3', category: 'otherDrivable', visible: true },
  139. Fer: { id: wmeRoadType.FERRY, wmeColor: '#d7d8f8', svColor: '#ff8000', category: 'otherDrivable', visible: false },
  140. RR: { id: wmeRoadType.RAILROAD, wmeColor: '#c62925', svColor: '#ffffff', category: 'nonDrivable', visible: false },
  141. RT: { id: wmeRoadType.RUNWAY_TAXIWAY, wmeColor: '#ffffff', svColor: '#00ff00', category: 'nonDrivable', visible: false },
  142. WT: { id: wmeRoadType.WALKING_TRAIL, wmeColor: '#b0a790', svColor: '#00ff00', category: 'pedestrian', visible: false },
  143. PB: { id: wmeRoadType.PEDESTRIAN_BOARDWALK, wmeColor: '#9a9a9a', svColor: '#0000ff', category: 'pedestrian', visible: false },
  144. Sw: { id: wmeRoadType.STAIRWAY, wmeColor: '#999999', svColor: '#b700ff', category: 'pedestrian', visible: false }
  145. };
  146.  
  147. /* eslint-enable object-curly-newline */
  148. let _settings = {};
  149. let trans; // Translation object
  150.  
  151. // function log(message) {
  152. // console.log('ClickSaver:', message);
  153. // }
  154.  
  155. function logDebug(message) {
  156. console.debug('ClickSaver:', message);
  157. }
  158.  
  159. // function logWarning(message) {
  160. // console.warn('ClickSaver:', message);
  161. // }
  162.  
  163. // function logError(message) {
  164. // console.error('ClickSaver:', message);
  165. // }
  166.  
  167. function isChecked(checkboxId) {
  168. return $(`#${checkboxId}`).is(':checked');
  169. }
  170.  
  171. function isSwapPedestrianPermitted() {
  172. const userInfo = sdk.State.getUserInfo();
  173. const rank = userInfo.rank + 1;
  174. return rank >= 4 || (rank === 3 && userInfo.isAreaManager);
  175. }
  176.  
  177. function setChecked(checkboxId, checked) {
  178. $(`#${checkboxId}`).prop('checked', checked);
  179. }
  180. function loadSettingsFromStorage() {
  181. const loadedSettings = $.parseJSON(localStorage.getItem(settingsStoreName));
  182. const defaultSettings = {
  183. lastVersion: null,
  184. roadButtons: true,
  185. roadTypeButtons: ['St', 'PS', 'mH', 'MH', 'Fw', 'Rmp', 'PLR', 'PR', 'PB'],
  186. parkingCostButtons: true,
  187. parkingSpacesButtons: true,
  188. setNewPLRCity: roadTypeDropdownOption.DEFAULT,
  189. setNewPRCity: roadTypeDropdownOption.DEFAULT,
  190. setNewRRCity: roadTypeDropdownOption.DEFAULT,
  191. setNewPBCity: roadTypeDropdownOption.DEFAULT,
  192. setNewORCity: roadTypeDropdownOption.DEFAULT,
  193. addAltCityButton: true,
  194. addSwapPedestrianButton: false,
  195. useOldRoadColors: false,
  196. warnOnPedestrianTypeSwap: true,
  197. addCompactColors: true,
  198. addSwapPrimaryNameButton: false,
  199. swapWholeAddress: false,
  200. hideUncheckedRoadTypeButtons: false,
  201. addRemoveAddressButton: false,
  202. removeStreetName: false,
  203. removeCityName: false,
  204. shortcuts: {}
  205. };
  206. _settings = { ...defaultSettings, ...loadedSettings };
  207.  
  208. setChecked('csRoadTypeButtonsCheckBox', _settings.roadButtons);
  209. if (_settings.roadTypeButtons) {
  210. Object.keys(roadTypeSettings).forEach(roadTypeAbbr => {
  211. const checked = _settings.roadTypeButtons.indexOf(roadTypeAbbr) !== -1;
  212. const selector = `cs${roadTypeAbbr}CheckBox`;
  213. setChecked(selector, checked);
  214. if (!checked) {
  215. $(`#${selector}`).siblings('.csDropdownContainer').hide();
  216. }
  217. });
  218. }
  219.  
  220. $('.csRoadTypeButtonsCheckBoxContainer').toggle(_settings.roadButtons);
  221. $('.csAddRemoveAddressButtonCheckBoxContainer').toggle(_settings.addRemoveAddressButton);
  222. $('.csAddSwapPrimaryNameCheckBoxContainer').toggle(_settings.addSwapPrimaryNameButton);
  223.  
  224. // setChecked('csParkingSpacesButtonsCheckBox', _settings.parkingSpacesButtons);
  225. // setChecked('csParkingCostButtonsCheckBox', _settings.parkingCostButtons);
  226. setDropdownValue('csSetPLRCityDropdown', _settings.setNewPLRCity);
  227. setDropdownValue('csSetPRCityDropdown', _settings.setNewPRCity);
  228. setDropdownValue('csSetRRCityDropdown', _settings.setNewRRCity);
  229. setDropdownValue('csSetPBCityDropdown', _settings.setNewPBCity);
  230. setDropdownValue('csSetORCityDropdown', _settings.setNewORCity);
  231. setChecked('csUseOldRoadColorsCheckBox', _settings.useOldRoadColors);
  232. setChecked('csAddAltCityButtonCheckBox', _settings.addAltCityButton);
  233. setChecked('csAddSwapPedestrianButtonCheckBox', _settings.addSwapPedestrianButton);
  234. setChecked('csAddCompactColorsCheckBox', _settings.addCompactColors);
  235. setChecked('csAddSwapPrimaryNameCheckBox', _settings.addSwapPrimaryNameButton);
  236. setChecked('csSwapWholeAddressCheckBox', _settings.swapWholeAddress);
  237. setChecked('csHideUncheckedRoadTypeButtonsCheckBox', _settings.hideUncheckedRoadTypeButtons);
  238. setChecked('csAddRemoveAddressButtonCheckBox', _settings.addRemoveAddressButton);
  239. setChecked('csRemoveStreetNameCheckBox', _settings.removeStreetName);
  240. setChecked('csRemoveCityNameCheckBox', _settings.removeCityName);
  241. }
  242.  
  243. function setDropdownValue(dropdownId, value) {
  244. $(`#${dropdownId}`).val(value);
  245. }
  246.  
  247. function saveSettingsToStorage() {
  248. const settings = {
  249. lastVersion: argsObject.scriptVersion,
  250. roadButtons: _settings.roadButtons,
  251. parkingCostButtons: _settings.parkingCostButtons,
  252. parkingSpacesButtons: _settings.parkingSpacesButtons,
  253. setNewPLRCity: _settings.setNewPLRCity,
  254. setNewPRCity: _settings.setNewPRCity,
  255. setNewRRCity: _settings.setNewRRCity,
  256. setNewPBCity: _settings.setNewPBCity,
  257. setNewORCity: _settings.setNewORCity,
  258. useOldRoadColors: _settings.useOldRoadColors,
  259. addAltCityButton: _settings.addAltCityButton,
  260. addSwapPedestrianButton: _settings.addSwapPedestrianButton,
  261. warnOnPedestrianTypeSwap: _settings.warnOnPedestrianTypeSwap,
  262. addCompactColors: _settings.addCompactColors,
  263. addSwapPrimaryNameButton: _settings.addSwapPrimaryNameButton,
  264. swapWholeAddress: _settings.swapWholeAddress,
  265. hideUncheckedRoadTypeButtons: _settings.hideUncheckedRoadTypeButtons,
  266. addRemoveAddressButton: _settings.addRemoveAddressButton,
  267. removeStreetName: _settings.removeStreetName,
  268. removeCityName: _settings.removeCityName,
  269. shortcuts: {}
  270. };
  271. sdk.Shortcuts.getAllShortcuts().forEach(shortcut => {
  272. settings.shortcuts[shortcut.shortcutId] = shortcut.shortcutKeys;
  273. });
  274. settings.roadTypeButtons = [];
  275. Object.keys(roadTypeSettings).forEach(roadTypeAbbr => {
  276. if (_settings.roadTypeButtons.indexOf(roadTypeAbbr) !== -1) {
  277. settings.roadTypeButtons.push(roadTypeAbbr);
  278. }
  279. });
  280. localStorage.setItem(settingsStoreName, JSON.stringify(settings));
  281. logDebug('Settings saved');
  282. }
  283.  
  284. function isPedestrianTypeSegment(segment) {
  285. const pedRoadTypes = Object.values(roadTypeSettings)
  286. .filter(roadType => roadType.category === 'pedestrian')
  287. .map(roadType => roadType.id);
  288. return pedRoadTypes.includes(segment.roadType);
  289. }
  290.  
  291. function getConnectedSegmentIDs(segmentId) {
  292. return [
  293. ...sdk.DataModel.Segments.getConnectedSegments({ segmentId, reverseDirection: false }),
  294. ...sdk.DataModel.Segments.getConnectedSegments({ segmentId, reverseDirection: true })
  295. ].map(segment => segment.id);
  296. }
  297.  
  298. function getFirstConnectedSegmentAddress(segmentId) {
  299. const nonMatches = [];
  300. const segmentIDsToSearch = [segmentId];
  301. const hasAddress = id => !sdk.DataModel.Segments.getAddress({ segmentId: id }).isEmpty;
  302. while (segmentIDsToSearch.length > 0) {
  303. const startSegmentID = segmentIDsToSearch.pop();
  304. const connectedSegmentIDs = getConnectedSegmentIDs(startSegmentID);
  305. const hasAddrSegmentId = connectedSegmentIDs.find(hasAddress);
  306. if (hasAddrSegmentId) return sdk.DataModel.Segments.getAddress({ segmentId: hasAddrSegmentId });
  307.  
  308. nonMatches.push(startSegmentID);
  309. connectedSegmentIDs.forEach(segmentID => {
  310. if (nonMatches.indexOf(segmentID) === -1 && segmentIDsToSearch.indexOf(segmentID) === -1) {
  311. segmentIDsToSearch.push(segmentID);
  312. }
  313. });
  314. }
  315. return null;
  316. }
  317.  
  318. function setStreetAndCity(setCity) {
  319. const selection = sdk.Editing.getSelection();
  320.  
  321. selection?.ids.forEach(segmentId => {
  322. if (sdk.DataModel.Segments.getAddress({ segmentId }).isEmpty) {
  323. const addr = getFirstConnectedSegmentAddress(segmentId);
  324. if (addr) {
  325. // Process the city
  326. const newCityProperties = {
  327. cityName: setCity && !addr.city?.isEmpty ? addr.city.name : '',
  328. countryId: addr.country.id,
  329. stateId: addr.state.id
  330. };
  331. let newCityId = sdk.DataModel.Cities.getCity(newCityProperties)?.id;
  332. if (newCityId == null) {
  333. newCityId = sdk.DataModel.Cities.addCity(newCityProperties).id;
  334. }
  335.  
  336. // Process the street
  337. const newPrimaryStreetId = getOrCreateStreet('', newCityId).id;
  338.  
  339. // Update the segment with the new street
  340. sdk.DataModel.Segments.updateAddress({ segmentId, primaryStreetId: newPrimaryStreetId });
  341. }
  342. }
  343. });
  344. }
  345.  
  346. class WaitForElementError extends Error { }
  347.  
  348. function waitForElem(selector) {
  349. return new Promise((resolve, reject) => {
  350. function checkIt(tries = 0) {
  351. if (tries < 150) { // try for about 3 seconds;
  352. const elem = document.querySelector(selector);
  353. setTimeout(() => {
  354. if (!elem) {
  355. checkIt(++tries);
  356. } else {
  357. resolve(elem);
  358. }
  359. }, 20);
  360. } else {
  361. reject(new WaitForElementError(`Element was not found within 3 seconds: ${selector}`));
  362. }
  363. }
  364. checkIt();
  365. });
  366. }
  367.  
  368. async function waitForShadowElem(parentElemSelector, shadowElemSelectors) {
  369. const parentElem = await waitForElem(parentElemSelector);
  370. return new Promise((resolve, reject) => {
  371. shadowElemSelectors.forEach((shadowElemSelector, idx) => {
  372. function checkIt(parent, tries = 0) {
  373. if (tries < 150) { // try for about 3 seconds;
  374. const shadowElem = parent.shadowRoot.querySelector(shadowElemSelector);
  375. setTimeout(() => {
  376. if (!shadowElem) {
  377. checkIt(parent, ++tries);
  378. } else if (idx === shadowElemSelectors.length - 1) {
  379. resolve({ shadowElem, parentElem });
  380. } else {
  381. checkIt(shadowElem, 0);
  382. }
  383. }, 20);
  384. } else {
  385. reject(new WaitForElementError(`Shadow element was not found within 3 seconds: ${shadowElemSelector}`));
  386. }
  387. }
  388. checkIt(parentElem);
  389. });
  390. });
  391. }
  392.  
  393. async function onAddAltCityButtonClick() {
  394. const segmentId = sdk.Editing.getSelection().ids[0];
  395. const addr = sdk.DataModel.Segments.getAddress({ segmentId });
  396.  
  397. $('wz-button[class="add-alt-street-btn"]').click();
  398. await waitForElem('wz-autocomplete.alt-street-name');
  399.  
  400. // Set the street name field
  401. let result = await waitForShadowElem('wz-autocomplete.alt-street-name', ['wz-text-input']);
  402. result.shadowElem.focus();
  403. result.shadowElem.value = addr?.street?.name ?? '';
  404.  
  405. // Clear the city name field
  406. result = await waitForShadowElem('wz-autocomplete.alt-city-name', ['wz-text-input']);
  407. result.shadowElem.focus();
  408. result.shadowElem.value = null;
  409. }
  410.  
  411. function onRoadTypeButtonClick(roadType) {
  412. const selection = sdk.Editing.getSelection();
  413.  
  414. // Temporarily remove this while bugs are worked out.
  415. // WS.SDKMultiActionHack.groupActions(() => {
  416. selection?.ids.forEach(segmentId => {
  417. // Check for same roadType is necessary to prevent an error.
  418. if (sdk.DataModel.Segments.getById({ segmentId }).roadType !== roadType) {
  419. sdk.DataModel.Segments.updateSegment({ segmentId, roadType });
  420. }
  421. });
  422.  
  423. if (_settings.roadTypeButtons.map(rtb => roadTypeSettings[rtb].id).includes(roadType)) {
  424. const roadTypeSettingsMap = {
  425. [roadTypeSettings.PLR.id]: _settings.setNewPLRCity,
  426. [roadTypeSettings.PR.id]: _settings.setNewPRCity,
  427. [roadTypeSettings.RR.id]: _settings.setNewRRCity,
  428. [roadTypeSettings.PB.id]: _settings.setNewPBCity,
  429. [roadTypeSettings.OR.id]: _settings.setNewORCity
  430. };
  431. const setting = roadTypeSettingsMap[roadType];
  432.  
  433. if (!setting || setting === roadTypeDropdownOption.DEFAULT) {
  434. return;
  435. }
  436. setStreetAndCity(setting === roadTypeDropdownOption.CONNECTED_CITY);
  437. }
  438. }
  439.  
  440. function addRoadTypeButtons() {
  441. const selection = sdk.Editing.getSelection();
  442. if (selection?.objectType !== 'segment') return;
  443. const segmentId = selection.ids[0];
  444. if (segmentId == null) return;
  445. const segment = sdk.DataModel.Segments.getById({ segmentId });
  446. if (!segment) return;
  447. const isPed = isPedestrianTypeSegment(segment);
  448. const $dropDown = $(roadTypeDropdownSelector);
  449. $('#csRoadTypeButtonsContainer').remove();
  450. const $container = $('<div>', { id: 'csRoadTypeButtonsContainer', class: 'cs-rt-buttons-container', style: 'display: inline-table;' });
  451. const $street = $('<div>', { id: 'csStreetButtonContainer', class: 'cs-rt-buttons-group' });
  452. const $highway = $('<div>', { id: 'csHighwayButtonContainer', class: 'cs-rt-buttons-group' });
  453. const $otherDrivable = $('<div>', { id: 'csOtherDrivableButtonContainer', class: 'cs-rt-buttons-group' });
  454. const $nonDrivable = $('<div>', { id: 'csNonDrivableButtonContainer', class: 'cs-rt-buttons-group' });
  455. const $pedestrian = $('<div>', { id: 'csPedestrianButtonContainer', class: 'cs-rt-buttons-group' });
  456. const divs = {
  457. streets: $street,
  458. highways: $highway,
  459. otherDrivable: $otherDrivable,
  460. nonDrivable: $nonDrivable,
  461. pedestrian: $pedestrian
  462. };
  463. Object.keys(roadTypeSettings).forEach(roadTypeKey => {
  464. if (_settings.roadTypeButtons.includes(roadTypeKey)) {
  465. const roadTypeSetting = roadTypeSettings[roadTypeKey];
  466. const isDisabled = $dropDown[0].hasAttribute('disabled') && $dropDown[0].getAttribute('disabled') === 'true';
  467. if (!isDisabled && ((roadTypeSetting.category === 'pedestrian' && isPed) || (roadTypeSetting.category !== 'pedestrian' && !isPed))) {
  468. const $div = divs[roadTypeSetting.category];
  469. $div.append(
  470. $('<div>', {
  471. class: `btn cs-rt-button cs-rt-button-${roadTypeKey} btn-positive`,
  472. title: I18n.t('segment.road_types')[roadTypeSetting.id]
  473. })
  474. .text(trans.roadTypeButtons[roadTypeKey].text)
  475. .prop('checked', roadTypeSetting.visible)
  476. .data('rtId', roadTypeSetting.id)
  477. .click(function rtbClick() { onRoadTypeButtonClick($(this).data('rtId')); })
  478. );
  479. }
  480. }
  481. });
  482. if (isPed) {
  483. $container.append($pedestrian);
  484. } else {
  485. $container.append($street).append($highway).append($otherDrivable).append($nonDrivable);
  486. }
  487. $dropDown.before($container);
  488. }
  489.  
  490. // Function to add an event listener to the chip select for the road type in compact mode
  491. function addCompactRoadTypeChangeEvents() {
  492. const chipSelect = document.getElementsByClassName('road-type-chip-select')[0];
  493. chipSelect.addEventListener('chipSelected', evt => {
  494. const rtValue = evt.detail.value;
  495. onRoadTypeButtonClick(rtValue);
  496. });
  497. }
  498.  
  499. // Function to add road type colors to the chips in compact mode
  500. async function addCompactRoadTypeColors() {
  501. // TODO: Clean this up. Was combined from two functions.
  502. try {
  503. if (sdk.Settings.getUserSettings().isCompactMode
  504. && isChecked('csAddCompactColorsCheckBox')
  505. && sdk.Editing.getSelection()) {
  506. const useOldColors = _settings.useOldRoadColors;
  507. await waitForElem('.road-type-chip-select wz-checkable-chip');
  508. $('.road-type-chip-select wz-checkable-chip').addClass('cs-compact-button');
  509. Object.values(roadTypeSettings).forEach(roadType => {
  510. const bgColor = useOldColors ? roadType.svColor : roadType.wmeColor;
  511. const rtChip = $(`.road-type-chip-select wz-checkable-chip[value=${roadType.id}]`);
  512. if (rtChip.length !== 1) return;
  513. waitForShadowElem(`.road-type-chip-select wz-checkable-chip[value='${roadType.id}']`, ['div']).then(result => {
  514. const $elem = $(result.shadowElem);
  515. const padding = $elem.hasClass('checked') ? '0px 3px' : '0px 4px';
  516. $elem.css({ backgroundColor: bgColor, padding, color: 'black' });
  517. });
  518. });
  519.  
  520. const result = await waitForShadowElem('.road-type-chip-select wz-checkable-chip[checked=""]', ['div']);
  521. $(result.shadowElem).css({ border: 'black 2px solid', padding: '0px 3px' });
  522.  
  523. $('.road-type-chip-select wz-checkable-chip').each(function updateRoadTypeChip() {
  524. const style = {};
  525. if (this.getAttribute('checked') === 'false') {
  526. style.border = '';
  527. style.padding = '0px 4px';
  528. } else {
  529. style.border = 'black 2px solid';
  530. style.padding = '0px 3px';
  531. }
  532. $(this.shadowRoot.querySelector('div')).css(style);
  533. });
  534. }
  535. } catch (ex) {
  536. if (ex instanceof WaitForElementError) {
  537. // waitForElem will throw an error if Undo causes a deselection. Ignore it.
  538. } else {
  539. throw ex;
  540. }
  541. }
  542. }
  543.  
  544. // function isPLA(item) {
  545. // return (item.model.type === 'venue') && item.model.attributes.categories.includes('PARKING_LOT');
  546. // }
  547.  
  548. // function addParkingSpacesButtons() {
  549. // const $dropDown = $(PARKING_SPACES_DROPDOWN_SELECTOR);
  550. // const selItems = W.selectionManager.getSelectedFeatures();
  551. // const item = selItems[0];
  552.  
  553. // // If it's not a PLA, exit.
  554. // if (!isPLA(item)) return;
  555.  
  556. // $('#csParkingSpacesContainer').remove();
  557. // const $div = $('<div>', { id: 'csParkingSpacesContainer' });
  558. // const dropdownDisabled = $dropDown.attr('disabled') === 'disabled';
  559. // const optionNodes = $(`${PARKING_SPACES_DROPDOWN_SELECTOR} option`);
  560.  
  561. // for (let i = 0; i < optionNodes.length; i++) {
  562. // const $option = $(optionNodes[i]);
  563. // const text = $option.text();
  564. // const selected = $option.val() === $dropDown.val();
  565. // $div.append(
  566. // // TODO css
  567. // $('<div>', {
  568. // class: `btn waze-btn waze-btn-white${selected ? ' waze-btn-blue' : ''}${dropdownDisabled ? ' disabled' : ''}`,
  569. // style: 'margin-bottom: 5px; height: 22px; padding: 2px 8px 0px 8px; margin-right: 3px;'
  570. // })
  571. // .text(text)
  572. // .data('val', $option.val())
  573. // // eslint-disable-next-line func-names
  574. // .hover(() => { })
  575. // .click(function onParkingSpacesButtonClick() {
  576. // if (!dropdownDisabled) {
  577. // $(PARKING_SPACES_DROPDOWN_SELECTOR).val($(this).data('val')).change();
  578. // addParkingSpacesButtons();
  579. // }
  580. // })
  581. // );
  582. // }
  583.  
  584. // $dropDown.before($div);
  585. // $dropDown.hide();
  586. // }
  587.  
  588. // function addParkingCostButtons() {
  589. // const $dropDown = $(PARKING_COST_DROPDOWN_SELECTOR);
  590. // const selItems = W.selectionManager.getSelectedFeatures();
  591. // const item = selItems[0];
  592.  
  593. // // If it's not a PLA, exit.
  594. // if (!isPLA(item)) return;
  595.  
  596. // $('#csParkingCostContainer').remove();
  597. // const $div = $('<div>', { id: 'csParkingCostContainer' });
  598. // const dropdownDisabled = $dropDown.attr('disabled') === 'disabled';
  599. // const optionNodes = $(`${PARKING_COST_DROPDOWN_SELECTOR} option`);
  600. // for (let i = 0; i < optionNodes.length; i++) {
  601. // const $option = $(optionNodes[i]);
  602. // const text = $option.text();
  603. // const selected = $option.val() === $dropDown.val();
  604. // $div.append(
  605. // $('<div>', {
  606. // class: `btn waze-btn waze-btn-white${selected ? ' waze-btn-blue' : ''}${dropdownDisabled ? ' disabled' : ''}`,
  607. // // TODO css
  608. // style: 'margin-bottom: 5px; height: 22px; padding: 2px 8px 0px 8px; margin-right: 4px;'
  609. // })
  610. // .text(text !== '' ? text : '?')
  611. // .data('val', $option.val())
  612. // // eslint-disable-next-line func-names
  613. // .hover(() => { })
  614. // .click(function onParkingCostButtonClick() {
  615. // if (!dropdownDisabled) {
  616. // $(PARKING_COST_DROPDOWN_SELECTOR).val($(this).data('val')).change();
  617. // addParkingCostButtons();
  618. // }
  619. // })
  620. // );
  621. // }
  622.  
  623. // $dropDown.before($div);
  624. // $dropDown.hide();
  625. // }
  626.  
  627. function addAddAltCityButton() {
  628. // Only show the button if every segment has the same primary city and street.
  629. if (!selectedPrimaryStreetsAreEqual()) {
  630. return;
  631. }
  632.  
  633. const $button = $('<wz-button>')
  634. .text(trans.addAltCityButtonText)
  635. .click(onAddAltCityButtonClick)
  636. .attr({
  637. size: 'sm',
  638. color: 'text'
  639. });
  640.  
  641. $('#csAddressButtonContainer').append($button);
  642. }
  643.  
  644. async function addSwapPrimaryNameButton() {
  645. if (!isChecked('csAddSwapPrimaryNameCheckBox')) {
  646. return;
  647. }
  648. if (!selectedPrimaryStreetsAreEqual() || !selectedAltStreetsAreEqual()) {
  649. return;
  650. }
  651.  
  652. await waitForElem('.alt-streets-control');
  653.  
  654. // eslint-disable-next-line func-names
  655. $('span.alt-street-preview').each(function() {
  656. const id = 'csAddSwapPrimaryName';
  657. const altStreetId = Number($(this).attr('data-id'));
  658. const swappingIconElement = $(this).find(`#${id}`);
  659.  
  660. if (streetEqualsPrimaryStreetName(altStreetId)) {
  661. swappingIconElement.remove();
  662. return;
  663. }
  664.  
  665. const swappingIconExists = swappingIconElement.length > 0;
  666. if (swappingIconExists) {
  667. return;
  668. }
  669. const swapStreetNameButton = $('<i>', {
  670. id,
  671. class: 'w-icon w-icon-arrow-up alt-edit-button'
  672. });
  673.  
  674. $(this).append(swapStreetNameButton);
  675. swapStreetNameButton.click(onSwapStreetNamesClick);
  676. });
  677. }
  678.  
  679. function onSwapStreetNamesClick() {
  680. const selectedSegments = getSelectedSegments();
  681. const currentPrimaryStreet = sdk.DataModel.Segments.getAddress({ segmentId: selectedSegments[0] });
  682. const currentAltStreets = currentPrimaryStreet.altStreets.map(street => street.street);
  683. const selectedStreetId = Number($(this).parent().attr('data-id'));
  684. const newPrimary = currentAltStreets
  685. .find(street => street.id === selectedStreetId);
  686.  
  687. // WS.SDKMultiActionHack.groupActions(() => {
  688. const changeWithCityName = isChecked('csSwapWholeAddressCheckBox');
  689.  
  690. const newPrimaryStreet = getOrCreateStreet(
  691. newPrimary.name,
  692. changeWithCityName ? newPrimary.cityId : currentPrimaryStreet.city.id
  693. );
  694. const primaryToAltStreet = getOrCreateStreet(
  695. currentPrimaryStreet.street.name,
  696. changeWithCityName ? currentPrimaryStreet.city.id : newPrimary.cityId
  697. );
  698.  
  699. const newAltStreetsIds = [
  700. ...currentAltStreets.map(alt => alt.id)
  701. .filter(id => id !== selectedStreetId),
  702. primaryToAltStreet.id
  703. ];
  704. selectedSegments.forEach(segmentId => sdk.DataModel.Segments.updateAddress({
  705. segmentId,
  706. primaryStreetId: newPrimaryStreet.id,
  707. alternateStreetIds: newAltStreetsIds
  708. }));
  709. // });
  710. }
  711.  
  712. function addRemoveAddressButton() {
  713. if (!isChecked('csRemoveStreetNameCheckBox') && !isChecked('csRemoveCityNameCheckBox')) {
  714. return;
  715. }
  716.  
  717. const translation = getRemoveAddressButtonTranslation();
  718. const hasHouseNumbers = segmentWithStreetNameHasHouseNumbers();
  719. const $button = $('<wz-button>')
  720. .text(translation)
  721. .click(onRemoveAddressButton)
  722. .attr({
  723. size: 'sm',
  724. color: 'text',
  725. disabled: hasHouseNumbers,
  726. title: hasHouseNumbers ? trans.segmentHasStreetNameAndHouseNumbers : ''
  727. });
  728.  
  729. $('#csAddressButtonContainer').append($button);
  730. }
  731.  
  732. function segmentWithStreetNameHasHouseNumbers() {
  733. const selectedSegmentIds = getSelectedSegments();
  734. if (!selectedSegmentIds) {
  735. return false;
  736. }
  737.  
  738. const isStreetNameChecked = isChecked('csRemoveStreetNameCheckBox');
  739. if (!isStreetNameChecked) {
  740. return false;
  741. }
  742.  
  743. return selectedSegmentIds.some(segmentId => {
  744. const segment = sdk.DataModel.Segments.getById({ segmentId });
  745. return segment.hasHouseNumbers;
  746. });
  747. }
  748.  
  749. function getRemoveAddressButtonTranslation() {
  750. if (isChecked('csRemoveStreetNameCheckBox') && isChecked('csRemoveCityNameCheckBox')) {
  751. return trans.removeStreetAndCityNameButtonText;
  752. }
  753. if (isChecked('csRemoveCityNameCheckBox')) {
  754. return trans.removeCityNameButtonText;
  755. }
  756. if (isChecked('csRemoveStreetNameCheckBox')) {
  757. return trans.removeStreetNameButtonText;
  758. }
  759. return '';
  760. }
  761.  
  762. async function onRemoveAddressButton() {
  763. const selectedSegmentIds = getSelectedSegments();
  764. if (!selectedSegmentIds) {
  765. return;
  766. }
  767. const emptyCityId = getOrCreateEmptyCity().id;
  768. const isStreetNameChecked = isChecked('csRemoveStreetNameCheckBox');
  769. const isCityNameChecked = isChecked('csRemoveCityNameCheckBox');
  770.  
  771. selectedSegmentIds
  772. .forEach(segmentId => {
  773. const address = sdk.DataModel.Segments.getAddress({ segmentId });
  774. const streetName = isStreetNameChecked ? '' : address.street?.name ?? '';
  775. const cityId = isCityNameChecked ? emptyCityId : address.city?.id ?? '';
  776. const newStreetId = getOrCreateStreet(streetName, cityId).id;
  777.  
  778. sdk.DataModel.Segments.updateAddress({
  779. segmentId,
  780. primaryStreetId: newStreetId
  781. });
  782. });
  783. }
  784.  
  785. function getOrCreateEmptyCity() {
  786. return sdk.DataModel.Cities.getAll().find(city => city.isEmpty)
  787. ?? sdk.DataModel.Cities.addCity({ cityName: '' });
  788. }
  789.  
  790. function addSwapPedestrianButton() { // Added displayMode argument to identify compact vs. regular mode.
  791. const id = 'csSwapPedestrianContainer';
  792. $(`#${id}`).remove();
  793. const selection = sdk.Editing.getSelection();
  794. if (selection?.ids.length === 1 && selection.objectType === 'segment') {
  795. // TODO css
  796. const $container = $('<div>', { id, style: 'white-space: nowrap;float: right;display: inline;' });
  797. const $button = $('<div>', {
  798. id: 'csBtnSwapPedestrianRoadType',
  799. title: '',
  800. // TODO css
  801. style: 'display:inline-block;cursor:pointer;'
  802. });
  803. $button.append('<i class="w-icon w-icon-streetview w-icon-lg"></i><i class="fa fa-arrows-h fa-lg" style="color: #e84545;vertical-align: top;"></i><i class="w-icon w-icon-car w-icon-lg"></i>')
  804. .attr({
  805. title: trans.prefs.showSwapDrivingWalkingButton_Title
  806. });
  807. $container.append($button);
  808.  
  809. // Insert swap button in the correct location based on display mode.
  810. const $label = $('#segment-edit-general > form > div > div.road-type-control > wz-label');
  811. $label.css({ display: 'inline' }).append($container);
  812.  
  813. $('#csBtnSwapPedestrianRoadType').click(onSwapPedestrianButtonClick);
  814. }
  815. }
  816.  
  817. function onSwapPedestrianButtonClick() {
  818. if (_settings.warnOnPedestrianTypeSwap) {
  819. _settings.warnOnPedestrianTypeSwap = false;
  820. saveSettingsToStorage();
  821. if (!confirm(trans.swapSegmentTypeWarning)) {
  822. return;
  823. }
  824. }
  825.  
  826. const originalSegment = sdk.DataModel.Segments.getById({ segmentId: sdk.Editing.getSelection().ids[0] });
  827.  
  828. // Copy the selected segment geometry and attributes, then delete it.
  829. const oldPrimaryStreetId = originalSegment.primaryStreetId;
  830. const oldAltStreetIds = originalSegment.alternateStreetIds;
  831.  
  832. // WS.SDKMultiActionHack.groupActions(() => {
  833. const newRoadType = isPedestrianTypeSegment(originalSegment) ? wmeRoadType.STREET : wmeRoadType.WALKING_TRAIL;
  834. try {
  835. sdk.DataModel.Segments.deleteSegment({ segmentId: originalSegment.id });
  836. } catch (ex) {
  837. if (ex instanceof sdk.Errors.InvalidStateError) {
  838. WazeWrap.Alerts.error(scriptName, 'Something prevents this segment from being deleted.');
  839. return;
  840. }
  841. }
  842.  
  843. // create the replacement segment in the other segment type (pedestrian -> road & vice versa)
  844.  
  845. const newSegmentId = sdk.DataModel.Segments.addSegment({ geometry: originalSegment.geometry, roadType: newRoadType });
  846.  
  847. sdk.DataModel.Segments.updateAddress({
  848. segmentId: newSegmentId,
  849. primaryStreetId: oldPrimaryStreetId,
  850. alternateStreetIds: oldAltStreetIds
  851. });
  852.  
  853. sdk.Editing.setSelection({ selection: { ids: [newSegmentId], objectType: 'segment' } });
  854. // });
  855. }
  856.  
  857. function getSelectedSegments() {
  858. const selection = sdk.Editing.getSelection();
  859. if (selection?.objectType !== 'segment') {
  860. return null;
  861. }
  862. return selection.ids;
  863. }
  864.  
  865. function selectedPrimaryStreetsAreEqual() {
  866. const selection = getSelectedSegments();
  867. if (!selection) {
  868. return false;
  869. }
  870. if (selection.length === 1) {
  871. return true;
  872. }
  873.  
  874. const firstStreetId = sdk.DataModel.Segments.getAddress({ segmentId: selection[0] })?.street?.id;
  875. return selection
  876. .map(segmentId => sdk.DataModel.Segments.getAddress({ segmentId }))
  877. .every(addr => addr.street?.id === firstStreetId);
  878. }
  879.  
  880. function selectedAltStreetsAreEqual() {
  881. const selection = getSelectedSegments();
  882. if (!selection) {
  883. return false;
  884. }
  885. const addresses = selection.map(segmentId => sdk.DataModel.Segments.getAddress({ segmentId }))
  886. .map(street => street.altStreets.map(altStreet => altStreet.street.id))
  887. .map(addr => new Set(addr));
  888.  
  889. const firstAltAddresses = addresses[0];
  890. return addresses
  891. .every(address => address.size === firstAltAddresses.size && Array.from(address).every(value => firstAltAddresses.has(value)));
  892. }
  893.  
  894. function getOrCreateStreet(streetName, cityId) {
  895. return sdk.DataModel.Streets.getStreet({ streetName, cityId })
  896. ?? sdk.DataModel.Streets.addStreet({ streetName, cityId });
  897. }
  898.  
  899. function streetEqualsPrimaryStreetName(altStreetId) {
  900. const selection = getSelectedSegments();
  901. const primaryStreetName = selection
  902. .map(segmentId => sdk.DataModel.Segments.getAddress({ segmentId }))[0].street?.name;
  903. const selectedStreetName = sdk.DataModel.Streets.getById({ streetId: altStreetId })?.name;
  904. return primaryStreetName === selectedStreetName;
  905. }
  906.  
  907. /* eslint-disable no-bitwise, no-mixed-operators */
  908. function shadeColor2(color, percent) {
  909. const f = parseInt(color.slice(1), 16);
  910. const t = percent < 0 ? 0 : 255;
  911. const p = percent < 0 ? percent * -1 : percent;
  912. const R = f >> 16;
  913. const G = f >> 8 & 0x00FF;
  914. const B = f & 0x0000FF;
  915. return `#${(0x1000000 + (Math.round((t - R) * p) + R) * 0x10000 + (Math.round((t - G) * p) + G)
  916. * 0x100 + (Math.round((t - B) * p) + B)).toString(16).slice(1)}`;
  917. }
  918. /* eslint-enable no-bitwise, no-mixed-operators */
  919.  
  920. function buildRoadTypeButtonCss() {
  921. const lines = [];
  922. const useOldColors = _settings.useOldRoadColors;
  923. Object.keys(roadTypeSettings).forEach(roadTypeAbbr => {
  924. const roadType = roadTypeSettings[roadTypeAbbr];
  925. const bgColor = useOldColors ? roadType.svColor : roadType.wmeColor;
  926. let output = `.cs-rt-buttons-container .cs-rt-button-${roadTypeAbbr} {background-color:${
  927. bgColor};box-shadow:0 2px ${shadeColor2(bgColor, -0.5)};border-color:${shadeColor2(bgColor, -0.15)};}`;
  928. output += ` .cs-rt-buttons-container .cs-rt-button-${roadTypeAbbr}:hover {background-color:${
  929. shadeColor2(bgColor, 0.2)}}`;
  930. lines.push(output);
  931. });
  932. return lines.join(' ');
  933. }
  934.  
  935. function injectCss() {
  936. const css = [
  937. // Road type button formatting
  938. '.csRoadTypeButtonsCheckBoxContainer {margin-left:15px;}',
  939. '.cs-rt-buttons-container {margin-bottom:5px;height:21px;}',
  940. '.cs-rt-buttons-container .cs-rt-button {font-size:11px;line-height:20px;color:black;padding:0px 4px;height:20px;'
  941. + 'margin-right:2px;border-style:solid;border-width:1px;}',
  942. buildRoadTypeButtonCss(),
  943. '.btn.cs-rt-button:active {box-shadow:none;transform:translateY(2px)}',
  944. 'div .cs-rt-buttons-group {float:left; margin: 0px 5px 5px 0px;}',
  945. '#sidepanel-clicksaver .controls-container {padding:0px;}',
  946. '#sidepanel-clicksaver .controls-container label {white-space: normal;}',
  947. '#sidepanel-clicksaver {font-size:13px;}',
  948.  
  949. // Compact moad road type button formatting.
  950. '.cs-compact-button[checked="false"] {opacity: 0.65;}',
  951.  
  952. // Lock button formatting
  953. '.cs-group-label {font-size: 11px; width: 100%; font-family: Poppins, sans-serif;'
  954. + ' text-transform: uppercase; font-weight: 700; color: #354148; margin-bottom: 6px;}'
  955. ].join(' ');
  956. $(`<style type="text/css">${css}</style>`).appendTo('head');
  957. }
  958.  
  959. function createSettingsDropdown(id, settingName, titleText, divCss, options, optionalAttributes) {
  960. const $container = $('<div>', { class: 'controls-container' });
  961. const $select = $('<select>', {
  962. class: 'csSettingsControl',
  963. id,
  964. // TODO css
  965. style: 'font-size: 12px; border-color: #cbcbcb;border-radius: 10px; white-space: nowrap; width: 100%; text-overflow: ellipsis;',
  966. 'data-setting-name': settingName
  967. }).appendTo($container);
  968. // TODO css
  969. if (divCss) $container.css(divCss);
  970. // TODO css
  971. if (titleText) $container.attr({ title: titleText });
  972. if (optionalAttributes) $select.attr(optionalAttributes);
  973. options.forEach(option => {
  974. $select.append($('<option>', {
  975. value: option.value,
  976. text: option.text
  977. }));
  978. });
  979.  
  980. return $container;
  981. }
  982.  
  983. function createSettingsCheckbox(id, settingName, labelText, titleText, divCss, labelCss, optionalAttributes) {
  984. const $container = $('<div>', { class: 'controls-container' });
  985. const $input = $('<input>', {
  986. type: 'checkbox',
  987. class: 'csSettingsControl',
  988. name: id,
  989. id,
  990. 'data-setting-name': settingName
  991. }).appendTo($container);
  992. if (titleText) {
  993. labelText += '*';
  994. }
  995. const $label = $('<label>', { for: id }).text(labelText).appendTo($container);
  996. // TODO css
  997. if (divCss) $container.css(divCss);
  998. // TODO css
  999. if (labelCss) $label.css(labelCss);
  1000. if (titleText) $container.attr({ title: titleText });
  1001. if (optionalAttributes) $input.attr(optionalAttributes);
  1002. return $container;
  1003. }
  1004.  
  1005. async function initUserPanel() {
  1006. const $roadTypesDiv = $('<div>', { class: 'csRoadTypeButtonsCheckBoxContainer' });
  1007. $roadTypesDiv.append(
  1008. createSettingsCheckbox('csUseOldRoadColorsCheckBox', 'useOldRoadColors', trans.prefs.useOldRoadColors)
  1009. );
  1010. Object.keys(roadTypeSettings).forEach(roadTypeAbbr => {
  1011. const roadType = roadTypeSettings[roadTypeAbbr];
  1012. const id = `cs${roadTypeAbbr}CheckBox`;
  1013. const title = I18n.t('segment.road_types')[roadType.id];
  1014. const $roadTypeContainer = createSettingsCheckbox(id, 'roadType', title, null, null, null, {
  1015. 'data-road-type': roadTypeAbbr
  1016. });
  1017. $roadTypesDiv.append($roadTypeContainer);
  1018. if (['PLR', 'PR', 'RR', 'PB', 'OR'].includes(roadTypeAbbr)) { // added RR & PB by jm6087
  1019. const $dropdownContainer = $('<div>', { class: 'csDropdownContainer' });
  1020. const options = [
  1021. { value: roadTypeDropdownOption.DEFAULT, text: trans.prefs.setCityToDefault },
  1022. { value: roadTypeDropdownOption.NONE, text: trans.prefs.setStreetCityToNone },
  1023. { value: roadTypeDropdownOption.CONNECTED_CITY, text: trans.prefs.setCityToConnectedSegCity }
  1024. ];
  1025. $dropdownContainer.append(
  1026. // TODO css
  1027. createSettingsDropdown(
  1028. `csSet${roadTypeAbbr}CityDropdown`,
  1029. `setNew${roadTypeAbbr}City`,
  1030. '',
  1031. { paddingLeft: '20px', marginRight: '4px' },
  1032. options
  1033. )
  1034. );
  1035. $roadTypeContainer.append($dropdownContainer);
  1036. }
  1037. });
  1038.  
  1039. const $streetDetailDiv = $('<div>', { class: 'csAddRemoveAddressButtonCheckBoxContainer' }).append(
  1040. createSettingsCheckbox(
  1041. 'csRemoveStreetNameCheckBox',
  1042. 'removeStreetName',
  1043. trans.prefs.showRemoveStreetNameButton,
  1044. trans.prefs.removeStreetNameTooltipText,
  1045. { paddingLeft: '20px' }
  1046. ),
  1047. createSettingsCheckbox(
  1048. 'csRemoveCityNameCheckBox',
  1049. 'removeCityName',
  1050. trans.prefs.showRemoveCityNameButton,
  1051. '',
  1052. { paddingLeft: '20px' }
  1053. )
  1054. );
  1055.  
  1056. const $swapStreetDetailsDiv = $('<div>', { class: 'csAddSwapPrimaryNameCheckBoxContainer' }).append(
  1057. createSettingsCheckbox(
  1058. 'csSwapWholeAddressCheckBox',
  1059. 'swapWholeAddress',
  1060. trans.prefs.swapWholeAddress,
  1061. '',
  1062. { paddingLeft: '20px' }
  1063. )
  1064. );
  1065.  
  1066. const $panel = $('<div>', { id: 'sidepanel-clicksaver' }).append(
  1067. $('<div>', { class: 'side-panel-section>' }).append(
  1068. // TODO css
  1069. $('<div>', { style: 'margin-bottom:8px;' }).append(
  1070. $('<div>', { class: 'form-group' }).append(
  1071. $('<label>', { class: 'cs-group-label' }).text(trans.prefs.dropdownHelperGroup),
  1072. $('<div>').append(
  1073. createSettingsCheckbox(
  1074. 'csRoadTypeButtonsCheckBox',
  1075. 'roadButtons',
  1076. trans.prefs.roadTypeButtons
  1077. )
  1078. ).append($roadTypesDiv),
  1079. createSettingsCheckbox(
  1080. 'csAddCompactColorsCheckBox',
  1081. 'addCompactColors',
  1082. trans.prefs.addCompactColors
  1083. ),
  1084. createSettingsCheckbox(
  1085. 'csHideUncheckedRoadTypeButtonsCheckBox',
  1086. 'hideUncheckedRoadTypeButtons',
  1087. trans.prefs.hideUncheckedRoadTypeButtons
  1088. )
  1089. ),
  1090. $('<label>', { class: 'cs-group-label' }).text(trans.prefs.timeSaversGroup),
  1091. $('<div>', { style: 'margin-bottom:8px;' }).append(
  1092. createSettingsCheckbox(
  1093. 'csAddAltCityButtonCheckBox',
  1094. 'addAltCityButton',
  1095. trans.prefs.showAddAltCityButton
  1096. ),
  1097. createSettingsCheckbox(
  1098. 'csAddRemoveAddressButtonCheckBox',
  1099. 'addRemoveAddressButton',
  1100. trans.prefs.enableAddressRemovalButton,
  1101. trans.prefs.addressRemovalButtonTooltipText
  1102. ).append($streetDetailDiv),
  1103. isSwapPedestrianPermitted() ? createSettingsCheckbox(
  1104. 'csAddSwapPedestrianButtonCheckBox',
  1105. 'addSwapPedestrianButton',
  1106. trans.prefs.showSwapDrivingWalkingButton
  1107. ) : '',
  1108. createSettingsCheckbox(
  1109. 'csAddSwapPrimaryNameCheckBox',
  1110. 'addSwapPrimaryNameButton',
  1111. trans.prefs.showSwapStreetNamesButton
  1112. ).append($swapStreetDetailsDiv)
  1113. )
  1114. )
  1115. )
  1116. );
  1117.  
  1118. $panel.append(
  1119. // TODO css
  1120. $('<div>', { style: 'margin-top:20px;font-size:10px;color:#999999;' }).append(
  1121. $('<div>').text(`v. ${argsObject.scriptVersion}${argsObject.scriptName.toLowerCase().includes('beta') ? ' beta' : ''}`),
  1122. $('<div>').append(
  1123. $('<a>', { href: argsObject.forumUrl, target: '__blank' }).text(trans.prefs.discussionForumLinkText)
  1124. )
  1125. )
  1126. );
  1127.  
  1128. const { tabLabel, tabPane } = await sdk.Sidebar.registerScriptTab();
  1129. $(tabLabel).text('CS');
  1130. $(tabPane).append($panel);
  1131. // Decrease spacing around the tab contents.
  1132. $(tabPane).parent().css({ 'padding-top': '0px', 'padding-left': '8px' });
  1133.  
  1134. // Add change events
  1135. // Simple checkbox hierarchy
  1136. setupCheckboxChangeHandler('#csRoadTypeButtonsCheckBox', '.csRoadTypeButtonsCheckBoxContainer');
  1137. setupCheckboxChangeHandler('#csAddRemoveAddressButtonCheckBox', '.csAddRemoveAddressButtonCheckBoxContainer');
  1138. setupCheckboxChangeHandler('#csAddSwapPrimaryNameCheckBox', '.csAddSwapPrimaryNameCheckBoxContainer');
  1139.  
  1140. $('.csSettingsControl').change(function onSettingsCheckChanged() {
  1141. const { checked } = this;
  1142. const $this = $(this);
  1143. const settingName = $this.data('setting-name');
  1144. $this.siblings('.csDropdownContainer').toggle(checked);
  1145.  
  1146. if (settingName === 'roadType') {
  1147. const roadType = $this.data('road-type');
  1148. const array = _settings.roadTypeButtons;
  1149. const index = array.indexOf(roadType);
  1150. if (checked && index === -1) {
  1151. array.push(roadType);
  1152. } else if (!checked && index !== -1) {
  1153. array.splice(index, 1);
  1154. }
  1155. } else if (settingName.includes('setNew') && settingName.includes('City')) {
  1156. _settings[settingName] = $this.val();
  1157. } else {
  1158. _settings[settingName] = checked;
  1159. }
  1160. saveSettingsToStorage();
  1161. });
  1162. }
  1163.  
  1164. function setupCheckboxChangeHandler(checkboxSelector, containerSelector) {
  1165. $(checkboxSelector).change(function() {
  1166. $(containerSelector).toggle(this.checked);
  1167. saveSettingsToStorage();
  1168. });
  1169. }
  1170.  
  1171. function updateControls() {
  1172. if ($(roadTypeDropdownSelector).length > 0) {
  1173. if (isChecked('csRoadTypeButtonsCheckBox')) addRoadTypeButtons();
  1174. }
  1175. addCompactRoadTypeColors();
  1176. if (isSwapPedestrianPermitted() && isChecked('csAddSwapPedestrianButtonCheckBox')) {
  1177. addSwapPedestrianButton();
  1178. }
  1179. // if ($(PARKING_SPACES_DROPDOWN_SELECTOR).length > 0 && isChecked('csParkingSpacesButtonsCheckBox')) {
  1180. // addParkingSpacesButtons(); // TODO - add option setting
  1181. // }
  1182. // if ($(PARKING_COST_DROPDOWN_SELECTOR).length > 0 && isChecked('csParkingCostButtonsCheckBox')) {
  1183. // addParkingCostButtons(); // TODO - add option setting
  1184. // }
  1185. }
  1186.  
  1187. function replaceWord(target, searchWord, replaceWithWord) {
  1188. return target.replace(new RegExp(`\\b${searchWord}\\b`, 'g'), replaceWithWord);
  1189. }
  1190.  
  1191. function titleCase(word) {
  1192. return word.charAt(0).toUpperCase() + word.substring(1).toLowerCase();
  1193. }
  1194. function mcCase(word) {
  1195. return word.charAt(0).toUpperCase() + word.charAt(1).toLowerCase()
  1196. + word.charAt(2).toUpperCase() + word.substring(3).toLowerCase();
  1197. }
  1198. function upperCase(word) {
  1199. return word.toUpperCase();
  1200. }
  1201.  
  1202. function processSubstring(target, substringRegex, processFunction) {
  1203. const substrings = target.match(substringRegex);
  1204. if (substrings) {
  1205. for (let idx = 0; idx < substrings.length; idx++) {
  1206. const substring = substrings[idx];
  1207. const newSubstring = processFunction(substring);
  1208. target = replaceWord(target, substring, newSubstring);
  1209. }
  1210. }
  1211. return target;
  1212. }
  1213.  
  1214. function onPaste(e) {
  1215. const targetNode = e.target;
  1216. if (targetNode.name === 'streetName' || targetNode.className.includes('street-name')) {
  1217. // Get the text that's being pasted.
  1218. let pastedText = e.clipboardData.getData('text/plain');
  1219.  
  1220. // If pasting text in ALL CAPS...
  1221. if (/^[^a-z]*$/.test(pastedText)) {
  1222. [
  1223. // Title case all words first.
  1224. [/\b[a-zA-Z]+(?:'S)?\b/g, titleCase],
  1225.  
  1226. // Then process special cases.
  1227. [/\bMC\w+\b/ig, mcCase], // e.g. McCaulley
  1228. [/\b(?:I|US|SH|SR|CH|CR|CS|PR|PS)\s*-?\s*\d+\w*\b/ig, upperCase], // e.g. US-25, US25
  1229. /* eslint-disable-next-line max-len */
  1230. [/\b(?:AL|AK|AS|AZ|AR|CA|CO|CT|DE|DC|FM|FL|GA|GU|HI|ID|IL|IN|IA|KS|KY|LA|ME|MH|MD|MA|MI|MN|MS|MO|MT|NE|NV|NH|NJ|NM|NY|NC|ND|MP|OH|OK|OR|PW|PA|PR|RI|SC|SD|TN|TX|UT|VT|VI|VA|WA|WV|WI|WY)\s*-?\s*\d+\w*\b/ig, upperCase], // e.g. WV-52
  1231. [/\b(?:NE|NW|SE|SW)\b/ig, upperCase]
  1232. ].forEach(item => {
  1233. pastedText = processSubstring(pastedText, item[0], item[1]);
  1234. });
  1235.  
  1236. // Insert new text in the focused node.
  1237. document.execCommand('insertText', false, pastedText);
  1238.  
  1239. // Prevent the default paste behavior.
  1240. e.preventDefault();
  1241. return false;
  1242. }
  1243. }
  1244. return true;
  1245. }
  1246.  
  1247. function getTranslationObject() {
  1248. if (argsObject.useDefaultTranslation) {
  1249. return defaultTranslation;
  1250. }
  1251. let locale = I18n.currentLocale().toLowerCase();
  1252. if (!argsObject.translations.hasOwnProperty(locale)) {
  1253. locale = 'en-us';
  1254. }
  1255. return argsObject.translations[locale];
  1256. }
  1257.  
  1258. function errorHandler(callback) {
  1259. try {
  1260. callback();
  1261. } catch (ex) {
  1262. console.error(`${argsObject.scriptName}:`, ex);
  1263. }
  1264. }
  1265.  
  1266. /**
  1267. * This event handler is needed in the following scenarios:
  1268. * 1. When the user changes the selected compact road type chip to adjust its styling.
  1269. * 2. When the swap alternative name button is clicked.
  1270. */
  1271. function onSegmentsChanged() {
  1272. addCompactRoadTypeColors();
  1273. addSwapPrimaryNameButton();
  1274. }
  1275.  
  1276. async function onCopyCoordinatesShortcut() {
  1277. try {
  1278. const center = sdk.Map.getMapCenter();
  1279. const output = `${center.lat.toFixed(5)}, ${center.lon.toFixed(5)}`;
  1280. await navigator.clipboard.writeText(output);
  1281. WazeWrap.Alerts.info('WME ClickSaver', `Map center coordinate copied to clipboard:\n${output}`, false, false, 2000);
  1282. // console.debug('Map coordinates copied to clipboard:', center);
  1283. } catch (err) {
  1284. console.error('Failed to copy map center coordinates to clipboard: ', err);
  1285. }
  1286. }
  1287.  
  1288. function onToggleDrawNewRoadsAsTwoWayShortcut() {
  1289. const options = sdk.Settings.getUserSettings();
  1290. options.isCreateRoadsAsTwoWay = !options.isCreateRoadsAsTwoWay;
  1291. sdk.Settings.setUserSettings(options);
  1292. WazeWrap.Alerts.info('WME ClickSaver', `New segments will be drawn as <b>${options.isCreateRoadsAsTwoWay ? 'two-way' : 'one-way'}</b>.`, false, false, 2000);
  1293. }
  1294.  
  1295. function createShortcut(shortcutId, description, callback) {
  1296. let shortcutKeys = _settings.shortcuts?.[shortcutId] ?? null;
  1297. if (shortcutKeys && sdk.Shortcuts.areShortcutKeysInUse({ shortcutKeys })) {
  1298. shortcutKeys = null;
  1299. }
  1300. sdk.Shortcuts.createShortcut({
  1301. shortcutId,
  1302. shortcutKeys,
  1303. description,
  1304. callback
  1305. });
  1306. }
  1307.  
  1308. function hideUncheckedRoadTypeButtons() {
  1309. const selection = getSelectedSegments();
  1310. if (!selection) {
  1311. return;
  1312. }
  1313. const selectedRoadTypes = selection
  1314. .map(segmentId => sdk.DataModel.Segments.getById({ segmentId }))
  1315. .map(segment => segment.roadType);
  1316.  
  1317. const checkedRoadTypes = new Set(
  1318. _settings.roadTypeButtons
  1319. .map(roadType => roadTypeSettings[roadType])
  1320. .map(setting => setting.id)
  1321. .concat(selectedRoadTypes)
  1322. .map(id => id.toString())
  1323. );
  1324.  
  1325. // eslint-disable-next-line func-names
  1326. $('wz-chip-select.road-type-chip-select wz-checkable-chip').each(function() {
  1327. const buttonValue = $(this).attr('value');
  1328. if (buttonValue === 'MIXED') {
  1329. return;
  1330. }
  1331. if (!checkedRoadTypes.has(buttonValue)) {
  1332. $(this).parent().parent().remove();
  1333. }
  1334. });
  1335. }
  1336.  
  1337. async function init() {
  1338. logDebug('Initializing...');
  1339.  
  1340. trans = getTranslationObject();
  1341. Object.keys(roadTypeSettings).forEach(rtName => {
  1342. roadTypeSettings[rtName].text = trans.roadTypeButtons[rtName].text;
  1343. });
  1344.  
  1345. document.addEventListener('paste', onPaste);
  1346.  
  1347. sdk.Events.trackDataModelEvents({ dataModelName: 'segments' });
  1348. sdk.Events.on({
  1349. eventName: 'wme-data-model-objects-changed',
  1350. eventHandler: () => errorHandler(onSegmentsChanged)
  1351. });
  1352. sdk.Events.on({
  1353. eventName: 'wme-selection-changed',
  1354. eventHandler: () => errorHandler(updateControls)
  1355. });
  1356.  
  1357. // check for changes in the edit-panel
  1358. const observer = new MutationObserver(mutations => {
  1359. mutations.forEach(mutation => {
  1360. for (let i = 0; i < mutation.addedNodes.length; i++) {
  1361. const addedNode = mutation.addedNodes[i];
  1362.  
  1363. if (addedNode.nodeType === Node.ELEMENT_NODE) {
  1364. // Checks to identify if this is a segment in regular display mode.
  1365. if (addedNode.querySelector(roadTypeDropdownSelector)) {
  1366. if (isChecked('csRoadTypeButtonsCheckBox')) addRoadTypeButtons();
  1367. if (isSwapPedestrianPermitted() && isChecked('csAddSwapPedestrianButtonCheckBox')) {
  1368. addSwapPedestrianButton();
  1369. }
  1370. }
  1371. // Checks to identify if this is a segment in compact display mode.
  1372. if (addedNode.querySelector(roadTypeChipSelector)) {
  1373. if (isChecked('csRoadTypeButtonsCheckBox')) {
  1374. addCompactRoadTypeChangeEvents();
  1375. }
  1376. if (isSwapPedestrianPermitted() && isChecked('csAddSwapPedestrianButtonCheckBox')) {
  1377. addSwapPedestrianButton();
  1378. }
  1379. if (isChecked('csHideUncheckedRoadTypeButtonsCheckBox')) {
  1380. hideUncheckedRoadTypeButtons();
  1381. }
  1382. }
  1383. // if (addedNode.querySelector(PARKING_SPACES_DROPDOWN_SELECTOR) && isChecked('csParkingSpacesButtonsCheckBox')) {
  1384. // addParkingSpacesButtons();
  1385. // }
  1386. // if (addedNode.querySelector(PARKING_COST_DROPDOWN_SELECTOR)
  1387. // && isChecked('csParkingCostButtonsCheckBox')) {
  1388. // addParkingCostButtons();
  1389. // }
  1390. if (addedNode.querySelector('.side-panel-section')
  1391. && (isChecked('csAddAltCityButtonCheckBox') || isChecked('csAddRemoveAddressButtonCheckBox'))) {
  1392. createSharedAddressButtonContainer();
  1393. if (isChecked('csAddRemoveAddressButtonCheckBox')) {
  1394. addRemoveAddressButton();
  1395. }
  1396. if (isChecked('csAddAltCityButtonCheckBox')) {
  1397. addAddAltCityButton();
  1398. }
  1399. }
  1400. if (addedNode.querySelector('.alt-streets') && isChecked('csAddSwapPrimaryNameCheckBox')) {
  1401. // Cancel button doesn't change the datamodel so re-add the swap arrow on cancel click
  1402. // eslint-disable-next-line func-names
  1403. addedNode.addEventListener('click', event => {
  1404. if (event.target.classList.contains('alt-address-cancel-button')) {
  1405. addSwapPrimaryNameButton();
  1406. }
  1407. });
  1408. addSwapPrimaryNameButton();
  1409. }
  1410. }
  1411. }
  1412. });
  1413. });
  1414.  
  1415. observer.observe(document.getElementById('edit-panel'), { childList: true, subtree: true });
  1416. await initUserPanel();
  1417. loadSettingsFromStorage();
  1418. createShortcut('toggleTwoWaySegDrawingShortcut', 'Toggle new segment two-way drawing', onToggleDrawNewRoadsAsTwoWayShortcut);
  1419. createShortcut('copyCoordinatesShortcut', 'Copy map center coordinates', onCopyCoordinatesShortcut);
  1420. window.addEventListener('beforeunload', saveSettingsToStorage, false);
  1421. injectCss();
  1422. updateControls(); // In case of PL w/ segments selected.
  1423.  
  1424. logDebug('Initialized');
  1425. }
  1426.  
  1427. function createSharedAddressButtonContainer() {
  1428. const $addressEdit = $('#segment-edit-general div.address-edit');
  1429. const $wzLabel = $addressEdit.prev('wz-label');
  1430. const $container = $('<div>', {
  1431. style: 'display: flex; gap: 0.5em; place-content: flex-end;',
  1432. id: 'csAddressButtonContainer'
  1433. });
  1434.  
  1435. if ($wzLabel.css('display') === 'none') {
  1436. $container.css('padding-bottom', '4px');
  1437. } else {
  1438. $container.append($wzLabel);
  1439. }
  1440.  
  1441. $addressEdit.before($container);
  1442. }
  1443.  
  1444. function skipLoginDialog(tries = 0) {
  1445. if (sdk || tries === 1000) return;
  1446. if ($('wz-button.do-login').length) {
  1447. $('wz-button.do-login').click();
  1448. return;
  1449. }
  1450. setTimeout(skipLoginDialog, 100, ++tries);
  1451. }
  1452. skipLoginDialog();
  1453.  
  1454. sdk = await bootstrap({ scriptUpdateMonitor: { downloadUrl } });
  1455.  
  1456. init();
  1457. } // END clicksaver function (used to be injected, now just runs as a function)
  1458.  
  1459. // function exists(...objects) {
  1460. // return objects.every(object => typeof object !== 'undefined' && object !== null);
  1461. // }
  1462.  
  1463. function injectScript(argsObject) {
  1464. // 3/31/2023 - removing script injection due to loading errors that I can't track down ("require is not defined").
  1465. // Not sure if injection is needed anymore. I believe it was to get around an issue with Greasemonkey / Firefox.
  1466. clicksaver(argsObject);
  1467. // if (exists(require, $)) {
  1468. // GM_addElement('script', {
  1469. // textContent: `(function(){${clicksaver.toString()}\n clicksaver(${JSON.stringify(argsObject).replace('\'', '\\\'')})})();`
  1470. // });
  1471. // } else {
  1472. // setTimeout(() => injectScript(argsObject), 250);
  1473. // }
  1474. }
  1475.  
  1476. function setValue(object, path, value) {
  1477. const pathParts = path.split('.');
  1478. for (let i = 0; i < pathParts.length - 1; i++) {
  1479. const pathPart = pathParts[i];
  1480. if (pathPart in object) {
  1481. object = object[pathPart];
  1482. } else {
  1483. object[pathPart] = {};
  1484. object = object[pathPart];
  1485. }
  1486. }
  1487. object[pathParts[pathParts.length - 1]] = value;
  1488. }
  1489.  
  1490. function convertTranslationsArrayToObject(arrayIn) {
  1491. const translations = {};
  1492. let iRow;
  1493. let iCol;
  1494. const languages = arrayIn[0].map(lang => lang.toLowerCase());
  1495. for (iCol = 1; iCol < languages.length; iCol++) {
  1496. translations[languages[iCol]] = {};
  1497. }
  1498. for (iRow = 1; iRow < arrayIn.length; iRow++) {
  1499. const row = arrayIn[iRow];
  1500. const propertyPath = row[0];
  1501. for (iCol = 1; iCol < row.length; iCol++) {
  1502. setValue(translations[languages[iCol]], propertyPath, row[iCol]);
  1503. }
  1504. }
  1505. return translations;
  1506. }
  1507.  
  1508. function loadTranslations() {
  1509. if (typeof $ === 'undefined') {
  1510. setTimeout(loadTranslations, 250);
  1511. console.debug('ClickSaver:', 'jQuery not ready. Retry loading translations...');
  1512. } else {
  1513. // This call retrieves the data from the translations spreadsheet and then injects
  1514. // the main code into the page. If the spreadsheet call fails, the default English
  1515. // translation is used.
  1516. const args = {
  1517. scriptName,
  1518. scriptVersion,
  1519. forumUrl
  1520. };
  1521. $.getJSON(`${translationsUrl}?${DEC(apiKey)}`).then(res => {
  1522. args.translations = convertTranslationsArrayToObject(res.values);
  1523. console.debug('ClickSaver:', 'Translations loaded.');
  1524. }).fail(() => {
  1525. console.error('ClickSaver: Error loading translations spreadsheet. Using default translation (English).');
  1526. args.useDefaultTranslation = true;
  1527. }).always(() => {
  1528. // Leave this document.ready function. Some people randomly get a "require is not defined" error unless the injectMain function
  1529. // is called late enough. Even with a "typeof require !== 'undefined'" check.
  1530. $(document).ready(() => {
  1531. injectScript(args);
  1532. });
  1533. });
  1534. }
  1535. }
  1536.  
  1537. function sandboxBootstrap() {
  1538. if (WazeWrap?.Ready) {
  1539. WazeWrap.Interface.ShowScriptUpdate(scriptName, scriptVersion, updateMessage, forumUrl);
  1540. } else {
  1541. setTimeout(sandboxBootstrap, 250);
  1542. }
  1543. }
  1544.  
  1545. // Go ahead and start loading translations, and inject the main code into the page.
  1546. loadTranslations();
  1547.  
  1548. // Start the "sandboxed" code.
  1549. sandboxBootstrap();
  1550. })();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址