InputWithUnits.vue 8.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297
  1. <template>
  2. <div class="q-pa-xs">
  3. <q-tooltip
  4. v-model="isShowingTooltip"
  5. anchor="top middle"
  6. self="center middle"
  7. >
  8. {{ formatNumber(localTooltipText, inputWithUnitsTooltipDigits) }}
  9. </q-tooltip>
  10. <q-tooltip
  11. v-if="isShowingHelp && !isInfoMode"
  12. v-model="isShowingHelpLocal"
  13. anchor="bottom middle"
  14. self="center middle"
  15. >
  16. Input example: <b>{{ helpExpr }}</b
  17. ><br />
  18. </q-tooltip>
  19. <q-tooltip v-if="isInfoMode" anchor="top middle" self="center middle">
  20. current settings<br />
  21. </q-tooltip>
  22. <q-card bordered flat>
  23. <q-card-section class="items-center bg-grey-2" horizontal>
  24. <div
  25. class="side_note text-grey-9 q-px-xs text-center"
  26. :style="'width: ' + inputWithUnitsTitleWidthStyle"
  27. >
  28. {{ title }}
  29. </div>
  30. <div>
  31. <q-select
  32. :model-value="localQSelectModel"
  33. :options="qSelectOptions"
  34. :disable="isInfoMode"
  35. bg-color="white"
  36. dense
  37. fill-input
  38. hide-selected
  39. input-debounce="0"
  40. options-dense
  41. :style="'width: ' + inputWithUnitsBodyWidthStyle"
  42. use-input
  43. input-class="q-pl-xs"
  44. behavior="menu"
  45. @filter="filterQSelectOptions"
  46. @blur="handleQSelectBlur"
  47. @keydown.enter="handleQSelectBlur"
  48. @input-value="localQSelectModel = $event"
  49. >
  50. <template v-if="isError" #prepend>
  51. <q-tooltip> Input conflict </q-tooltip>
  52. <q-icon name="error" class="text-warning q-pl-xs" />
  53. </template>
  54. <template
  55. v-if="isShowingTooltipAppend && !isShowingTooltip"
  56. #append
  57. >
  58. <div style="font-size: 12px" class="q-py-sm">
  59. {{ formatNumber(localTooltipText, inputWithUnitsInlineDigits) }}
  60. </div>
  61. </template>
  62. <template #option="scope">
  63. <q-item v-bind="scope.itemProps" class="q-px-sm">
  64. <q-item-section>
  65. {{ scope.opt }}
  66. </q-item-section>
  67. <q-item-section side>
  68. {{
  69. formatNumber(
  70. evalString(scope.opt),
  71. inputWithUnitsInlineDigits
  72. )
  73. }}
  74. </q-item-section>
  75. </q-item>
  76. </template>
  77. </q-select>
  78. </div>
  79. <div
  80. class="side_note text-grey-9 q-px-xs text-center"
  81. :style="'width: ' + inputWithUnitsUnitsWidthStyle"
  82. >
  83. {{ units }}
  84. </div>
  85. </q-card-section>
  86. </q-card>
  87. </div>
  88. </template>
  89. <script lang="ts">
  90. import { evaluate } from 'mathjs';
  91. import { defineComponent, ref, watch } from 'vue';
  92. import {
  93. inputWithUnitsTitleWidthStyle,
  94. inputWithUnitsBodyWidthStyle,
  95. inputWithUnitsUnitsWidthStyle,
  96. inputWithUnitsHistoryLength,
  97. inputWithUnitsInlineDigits,
  98. inputWithUnitsTooltipDigits,
  99. } from 'components/config';
  100. export default defineComponent({
  101. name: 'InputWithUnits',
  102. props: {
  103. inputResult: {
  104. type: Number,
  105. required: true,
  106. default: 0,
  107. },
  108. initialExpression: {
  109. type: String,
  110. required: true,
  111. default: '',
  112. },
  113. title: {
  114. type: String,
  115. default: '',
  116. },
  117. units: {
  118. type: String,
  119. default: '',
  120. },
  121. isShowingHelp: {
  122. type: Boolean,
  123. default: false,
  124. },
  125. isError: {
  126. type: Boolean,
  127. default: false,
  128. },
  129. isInfoMode: {
  130. type: Boolean,
  131. default: false,
  132. },
  133. },
  134. emits: ['update:input-result', 'update:is-showing-help'],
  135. setup(props, { emit }) {
  136. let localQSelectModel = ref('');
  137. let localTooltipText = ref('');
  138. let isShowingTooltip = ref(false);
  139. let isShowingTooltipAppend = ref(false);
  140. let isShowingHelpLocal = ref(false);
  141. let helpExpr = ref('(1+2)*sqrt(2)');
  142. let evaluated = ref(0);
  143. let count_updates = 0;
  144. // Set some random values to get correct typing with
  145. // TypeScript and remove them after initialization.
  146. let qSelectOptions = ref(['a', 'b']);
  147. let qSelectOptionsHistory = ref(['a', 'b']);
  148. qSelectOptions.value.pop();
  149. qSelectOptions.value.pop();
  150. qSelectOptionsHistory.value.pop();
  151. qSelectOptionsHistory.value.pop();
  152. // evaluate current input, keeps the previous evaluateValue for invalid input
  153. function runEvaluate() {
  154. // Using try{} block to drop silently invalid input
  155. try {
  156. const tryEvaluate = Number(evaluate(localQSelectModel.value));
  157. if (!isNaN(tryEvaluate)) evaluated.value = tryEvaluate;
  158. } catch {}
  159. }
  160. watch(localQSelectModel, () => {
  161. runEvaluate();
  162. });
  163. function setTooltipVisibility() {
  164. if (evaluated.value != Number(localQSelectModel.value)) {
  165. isShowingTooltip.value = true;
  166. isShowingTooltipAppend.value = true;
  167. } else {
  168. isShowingTooltip.value = false;
  169. isShowingTooltipAppend.value = false;
  170. }
  171. }
  172. watch(isShowingTooltip, () => {
  173. // For a trivial case we would like to switch off showing tooltip
  174. if (isShowingTooltip.value) setTooltipVisibility();
  175. });
  176. function setTooltip() {
  177. localTooltipText.value = evaluated.value.toString();
  178. setTooltipVisibility();
  179. }
  180. watch(evaluated, () => {
  181. emit('update:input-result', evaluated.value);
  182. setTooltip();
  183. // Switch off showing help as soon as we have some input from user
  184. const threshold = 1;
  185. if (count_updates < threshold + 1) {
  186. // limit the unbound grow of count_updates
  187. count_updates += 1;
  188. if (props.isShowingHelp && count_updates > threshold) {
  189. qSelectOptionsHistory.value.unshift(helpExpr.value);
  190. emit('update:is-showing-help', false);
  191. }
  192. }
  193. });
  194. watch(isShowingHelpLocal, () => {
  195. // isShowingHelpLocal.value is set to be
  196. // true on hover. Disable it if needed.
  197. if (isShowingHelpLocal.value) {
  198. if (qSelectOptions.value.length > 0) isShowingHelpLocal.value = false;
  199. }
  200. });
  201. watch(qSelectOptions, () => {
  202. if (qSelectOptions.value.length > 0) isShowingHelpLocal.value = false;
  203. });
  204. watch(props, () => {
  205. // Using try{} block to drop silently invalid input
  206. try {
  207. // If props.inputResults changed and is not equal to local
  208. // expression localQSelectModel.value, then update local expression
  209. const tryEvaluate = Number(evaluate(localQSelectModel.value));
  210. if (!isNaN(tryEvaluate) && props.inputResult != tryEvaluate) {
  211. localQSelectModel.value = props.inputResult.toString();
  212. }
  213. } catch {}
  214. });
  215. localQSelectModel.value = props.initialExpression.toString();
  216. runEvaluate();
  217. localTooltipText.value = localQSelectModel.value;
  218. setTooltip();
  219. isShowingTooltip.value = false;
  220. return {
  221. inputWithUnitsTitleWidthStyle,
  222. inputWithUnitsBodyWidthStyle,
  223. inputWithUnitsUnitsWidthStyle,
  224. inputWithUnitsHistoryLength,
  225. inputWithUnitsInlineDigits,
  226. inputWithUnitsTooltipDigits,
  227. localTooltipText,
  228. isShowingTooltip,
  229. isShowingTooltipAppend,
  230. isShowingHelpLocal,
  231. helpExpr,
  232. qSelectOptions,
  233. localQSelectModel,
  234. handleQSelectBlur() {
  235. isShowingTooltip.value = false;
  236. const expr = localQSelectModel.value;
  237. if (!qSelectOptionsHistory.value.includes(expr))
  238. qSelectOptionsHistory.value.unshift(expr);
  239. if (qSelectOptionsHistory.value.length > inputWithUnitsHistoryLength)
  240. qSelectOptionsHistory.value.pop();
  241. },
  242. filterQSelectOptions(val: string, update: (data: () => void) => void) {
  243. update(() => {
  244. // To remove the selection from previously
  245. // selected option - we refill the options list
  246. qSelectOptions.value = qSelectOptionsHistory.value;
  247. });
  248. },
  249. formatNumber(value: string, digits: number, prepend: string): string {
  250. if (!prepend) prepend = '=';
  251. if (value === '') return '';
  252. const num = parseFloat(value);
  253. if (num < Math.pow(10, -digits) || num > 5 * Math.pow(10, digits + 2))
  254. return prepend + num.toExponential(digits);
  255. return (
  256. prepend +
  257. Number(
  258. Math.round(parseFloat(value + 'e' + digits.toString())).toString() +
  259. 'e-' +
  260. digits.toString()
  261. ).toString()
  262. );
  263. },
  264. // evaluate option items, returns empty string for trivial evaluations and errors
  265. evalString(val: string): string {
  266. // Using try{} block to drop silently invalid input
  267. try {
  268. const tryEvaluate = Number(evaluate(val));
  269. if (!isNaN(tryEvaluate) && tryEvaluate != Number(val)) {
  270. return tryEvaluate.toString();
  271. }
  272. } catch {}
  273. return '';
  274. },
  275. };
  276. },
  277. });
  278. </script>