1
0

utils.js 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296
  1. import path from 'path/path.js'
  2. if (typeof window === 'undefined') {
  3. global.window = {}
  4. }
  5. // Copy from https://github.com/primetwig/react-nestable/blob/dacea9dc191399a3520f5dc7623f5edebc83e7b7/dist/utils.js
  6. export const closest = (target, selector) => {
  7. // closest(e.target, '.field')
  8. while (target) {
  9. if (target.matches && target.matches(selector)) return target
  10. target = target.parentNode
  11. }
  12. return null
  13. }
  14. export const getOffsetRect = (elem) => {
  15. // (1)
  16. const box = elem.getBoundingClientRect(),
  17. body = document.body,
  18. docElem = document.documentElement,
  19. // (2)
  20. scrollTop = window.pageYOffset || docElem.scrollTop || body.scrollTop,
  21. scrollLeft = window.pageXOffset || docElem.scrollLeft || body.scrollLeft,
  22. // (3)
  23. clientTop = docElem.clientTop || body.clientTop || 0,
  24. clientLeft = docElem.clientLeft || body.clientLeft || 0,
  25. // (4)
  26. top = box.top + scrollTop - clientTop,
  27. left = box.left + scrollLeft - clientLeft;
  28. return {
  29. top: Math.round(top),
  30. left: Math.round(left)
  31. }
  32. }
  33. // jquery focus
  34. export const focus = (elem) => {
  35. return elem === document.activeElement &&
  36. document.hasFocus() &&
  37. !!(elem.type || elem.href || ~elem.tabIndex)
  38. }
  39. // copied from https://stackoverflow.com/a/32180863
  40. export const timeConversion = (millisec) => {
  41. let seconds = (millisec / 1000).toFixed(0),
  42. minutes = (millisec / (1000 * 60)).toFixed(0),
  43. hours = (millisec / (1000 * 60 * 60)).toFixed(1),
  44. days = (millisec / (1000 * 60 * 60 * 24)).toFixed(1);
  45. if (seconds < 60) {
  46. return seconds + 's'
  47. } else if (minutes < 60) {
  48. return minutes + 'm'
  49. } else if (hours < 24) {
  50. return hours + 'h'
  51. } else {
  52. return days + 'd'
  53. }
  54. }
  55. export const getSelectionText = () => {
  56. const selection = (window.getSelection() || '').toString().trim()
  57. if (selection) {
  58. return selection
  59. }
  60. // Firefox fix
  61. const activeElement = window.document.activeElement
  62. if (activeElement) {
  63. if (activeElement.tagName === 'INPUT' || activeElement.tagName === 'TEXTAREA') {
  64. const el = activeElement
  65. return el.value.slice(el.selectionStart || 0, el.selectionEnd || 0)
  66. }
  67. }
  68. return ''
  69. }
  70. // Modified from https://github.com/GoogleChromeLabs/browser-nativefs
  71. // because shadow-cljs doesn't handle this babel transform
  72. export const getFiles = async (dirHandle, recursive, cb, path = dirHandle.name) => {
  73. const dirs = []
  74. const files = []
  75. for await (const entry of dirHandle.values()) {
  76. const nestedPath = `${path}/${entry.name}`
  77. if (entry.kind === 'file') {
  78. cb(nestedPath, entry)
  79. files.push(
  80. entry.getFile().then((file) => {
  81. Object.defineProperty(file, 'webkitRelativePath', {
  82. configurable: true,
  83. enumerable: true,
  84. get: () => nestedPath,
  85. })
  86. Object.defineProperty(file, 'handle', {
  87. configurable: true,
  88. enumerable: true,
  89. get: () => entry,
  90. })
  91. return file
  92. })
  93. )
  94. } else if (entry.kind === 'directory' && recursive) {
  95. cb(nestedPath, entry)
  96. dirs.push(getFiles(entry, recursive, cb, nestedPath))
  97. }
  98. }
  99. return [(await Promise.all(dirs)), (await Promise.all(files))]
  100. }
  101. export const verifyPermission = async (handle, readWrite) => {
  102. const options = {}
  103. if (readWrite) {
  104. options.mode = 'readwrite'
  105. }
  106. // Check if permission was already granted.
  107. if ((await handle.queryPermission(options)) === 'granted') {
  108. return
  109. }
  110. // Request permission. If the user grants permission, just return.
  111. if ((await handle.requestPermission(options)) === 'granted') {
  112. return
  113. }
  114. // The user didn't grant permission, throw an error.
  115. throw new Error('Permission is not granted')
  116. }
  117. // NOTE: Need externs to prevent `options.recursive` been munged
  118. // When building with release.
  119. export const openDirectory = async (options = {}, cb) => {
  120. options.recursive = options.recursive || false;
  121. const handle = await window.showDirectoryPicker({
  122. mode: 'readwrite'
  123. });
  124. const _ask = await verifyPermission(handle, true);
  125. return [handle, getFiles(handle, options.recursive, cb)];
  126. };
  127. export const writeFile = async (fileHandle, contents) => {
  128. // Create a FileSystemWritableFileStream to write to.
  129. const writable = await fileHandle.createWritable()
  130. if (contents instanceof ReadableStream) {
  131. await contents.pipeTo(writable)
  132. } else {
  133. // Write the contents of the file to the stream.
  134. await writable.write(contents)
  135. // Close the file and write the contents to disk.
  136. await writable.close()
  137. }
  138. }
  139. export const nfsSupported = () => {
  140. if ('chooseFileSystemEntries' in self) {
  141. return 'chooseFileSystemEntries'
  142. } else if ('showOpenFilePicker' in self) {
  143. return 'showOpenFilePicker'
  144. }
  145. return false
  146. }
  147. const inputTypes = [
  148. window.HTMLInputElement,
  149. window.HTMLSelectElement,
  150. window.HTMLTextAreaElement,
  151. ]
  152. export const triggerInputChange = (node, value = '', name = 'change') => {
  153. // only process the change on elements we know have a value setter in their constructor
  154. if (inputTypes.indexOf(node.__proto__.constructor) > -1) {
  155. const setValue = Object.getOwnPropertyDescriptor(node.__proto__, 'value').set
  156. const event = new Event('change', {
  157. bubbles: true
  158. })
  159. setValue.call(node, value)
  160. node.dispatchEvent(event)
  161. }
  162. }
  163. // Copied from https://github.com/google/diff-match-patch/issues/29#issuecomment-647627182
  164. export const reversePatch = patch => {
  165. return patch.map(patchObj => ({
  166. diffs: patchObj.diffs.map(([op, val]) => [
  167. op * -1, // The money maker
  168. val
  169. ]),
  170. start1: patchObj.start2,
  171. start2: patchObj.start1,
  172. length1: patchObj.length2,
  173. length2: patchObj.length1
  174. }));
  175. };
  176. // Copied from https://github.com/sindresorhus/path-is-absolute/blob/main/index.js
  177. export const win32 = path => {
  178. // https://github.com/nodejs/node/blob/b3fcc245fb25539909ef1d5eaa01dbf92e168633/lib/path.js#L56
  179. const splitDeviceRe = /^([a-zA-Z]:|[\\/]{2}[^\\/]+[\\/]+[^\\/]+)?([\\/])?([\s\S]*?)$/,
  180. result = splitDeviceRe.exec(path),
  181. device = result[1] || '',
  182. isUnc = Boolean(device && device.charAt(1) !== ':');
  183. // UNC paths are always absolute
  184. return Boolean(result[2] || isUnc);
  185. };
  186. export const ios = () => {
  187. return [
  188. 'iPad Simulator',
  189. 'iPhone Simulator',
  190. 'iPod Simulator',
  191. 'iPad',
  192. 'iPhone',
  193. 'iPod'
  194. ].includes(navigator.platform)
  195. // iPad on iOS 13 detection
  196. ||
  197. (navigator.userAgent.includes("Mac") && "ontouchend" in document)
  198. }
  199. export const getClipText = (cb, errorHandler) => {
  200. navigator.permissions.query({
  201. name: "clipboard-read"
  202. }).then((result) => {
  203. if (result.state == "granted" || result.state == "prompt") {
  204. navigator.clipboard.readText()
  205. .then(text => {
  206. cb(text);
  207. })
  208. .catch(err => {
  209. errorHandler(err)
  210. });
  211. }
  212. })
  213. }
  214. export const writeClipboard = (text, isHtml) => {
  215. let blob = new Blob([text], {
  216. type: ["text/plain"]
  217. });
  218. let data = [new ClipboardItem({
  219. ["text/plain"]: blob
  220. })];
  221. if (isHtml) {
  222. blob = new Blob([text], {
  223. type: ["text/plain", "text/html"]
  224. })
  225. data = [new ClipboardItem({
  226. ["text/plain"]: blob,
  227. ["text/html"]: blob
  228. })];
  229. }
  230. navigator.permissions.query({
  231. name: "clipboard-write"
  232. }).then((result) => {
  233. if (result.state == "granted" || result.state == "prompt") {
  234. navigator.clipboard.write(data).then(() => {
  235. /* success */
  236. }).catch(e => {
  237. console.log(e, "fail")
  238. })
  239. }
  240. })
  241. }
  242. export const toPosixPath = (input) => {
  243. return input && input.replace(/\\+/g, '/')
  244. }
  245. export const nodePath = Object.assign({}, path, {
  246. basename (input) {
  247. input = toPosixPath(input)
  248. return path.basename(input)
  249. },
  250. name (input) {
  251. input = toPosixPath(input)
  252. return path.parse(input).name
  253. },
  254. dirname (input) {
  255. input = toPosixPath(input)
  256. return path.dirname(input)
  257. },
  258. extname (input) {
  259. input = toPosixPath(input)
  260. return path.extname(input)
  261. }
  262. })