mirror of
https://gitlab.com/wgp/dougal/software.git
synced 2025-12-06 13:27:08 +00:00
Make Vue component reusable.
This converts <dougal-fixed-width-format/> into a more reusable <dougal-fixed-string-decoder/> component.
This commit is contained in:
@@ -0,0 +1,140 @@
|
||||
<template>
|
||||
<v-row dense no-gutters>
|
||||
|
||||
<v-col cols="1">
|
||||
<slot name="prepend"></slot>
|
||||
</v-col>
|
||||
|
||||
<v-col cols="2">
|
||||
<v-chip outlined label small :color="colour">{{name}}</v-chip>
|
||||
</v-col>
|
||||
|
||||
<v-col cols="2">
|
||||
<v-text-field
|
||||
class="ml-3"
|
||||
dense
|
||||
label="From"
|
||||
type="number"
|
||||
min="0"
|
||||
v-model.number="value.offset"
|
||||
:readonly="readonly"
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
|
||||
<v-col cols="2">
|
||||
<v-text-field
|
||||
class="ml-3"
|
||||
dense
|
||||
label="Length"
|
||||
type="number"
|
||||
min="0"
|
||||
v-model.number="value.length"
|
||||
:readonly="readonly"
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
|
||||
<v-col cols="2">
|
||||
<dougal-field-content-dialog
|
||||
:readonly="readonly"
|
||||
:value="value"
|
||||
@input="$emit('input', $event)"
|
||||
></dougal-field-content-dialog>
|
||||
|
||||
</v-col>
|
||||
|
||||
<v-col cols="1">
|
||||
<slot name="append"></slot>
|
||||
</v-col>
|
||||
|
||||
</v-row>
|
||||
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
.input {
|
||||
flex: 1 1 auto;
|
||||
line-height: 20px;
|
||||
padding: 8px 0 8px;
|
||||
min-height: 32px;
|
||||
max-height: 32px;
|
||||
max-width: 100%;
|
||||
min-width: 0px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.input >>> .chunk {
|
||||
padding-inline: 1px;
|
||||
border: 1px solid;
|
||||
}
|
||||
|
||||
.input >>> .chunk-empty {
|
||||
padding-inline: 1px;
|
||||
}
|
||||
|
||||
.input >>> .chunk-overlap {
|
||||
padding-inline: 1px;
|
||||
border: 1px solid grey;
|
||||
color: grey;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import DougalFieldContentDialog from '../fields/field-content-dialog'
|
||||
|
||||
export default {
|
||||
name: "DougalFixedStringDecoderField",
|
||||
|
||||
components: {
|
||||
DougalFieldContentDialog
|
||||
},
|
||||
|
||||
props: {
|
||||
value: Object,
|
||||
name: String,
|
||||
colour: String,
|
||||
readonly: Boolean,
|
||||
},
|
||||
|
||||
data () {
|
||||
return {
|
||||
name_: "",
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
},
|
||||
|
||||
watch: {
|
||||
|
||||
name () {
|
||||
if (this.name != this.name_) {
|
||||
this.name_ = this.name;
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
|
||||
addField () {
|
||||
if (!this.fieldNameErrors) {
|
||||
this.$emit("update:fields", {
|
||||
...this.fields,
|
||||
[this.fieldName]: { offset: 0, length: 0 }
|
||||
});
|
||||
this.fieldName = "";
|
||||
}
|
||||
},
|
||||
|
||||
reset () {
|
||||
this.text_ = this.text;
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
mounted () {
|
||||
this.reset();
|
||||
}
|
||||
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,370 @@
|
||||
<template>
|
||||
<v-card flat elevation="0">
|
||||
<v-card-title v-if="title">{{ title }}</v-card-title>
|
||||
<v-card-subtitle v-if="subtitle">{{ subtitle }}</v-card-subtitle>
|
||||
<v-card-text>
|
||||
<v-form>
|
||||
<div v-if="isMultiline"
|
||||
class="multiline mb-5"
|
||||
:style="multilineElementStyle"
|
||||
v-html="html"
|
||||
>
|
||||
</div>
|
||||
<v-input v-else
|
||||
class="v-text-field"
|
||||
:hint="hint"
|
||||
persistent-hint
|
||||
v-model="text_"
|
||||
>
|
||||
<label
|
||||
class="v-label"
|
||||
:class="[ $vuetify.theme.isDark && 'theme--dark', text_ && text_.length && 'v-label--active' ]"
|
||||
style="left: 0px; right: auto; position: absolute;"
|
||||
>{{ label }}</label>
|
||||
<div class="input"
|
||||
:class="isMultiline ? 'multiline' : ''"
|
||||
v-html="html"
|
||||
>
|
||||
</div>
|
||||
</v-input>
|
||||
|
||||
<v-container>
|
||||
|
||||
<dougal-fixed-string-decoder-field v-for="(field, key) in fields" :key="key"
|
||||
v-model="fields[key]"
|
||||
:name="key"
|
||||
:colour="getHSLColourFor(key)"
|
||||
:readonly="readonly"
|
||||
>
|
||||
<template v-slot:append v-if="editableFieldList && !readonly">
|
||||
<v-btn
|
||||
class="ml-3"
|
||||
fab
|
||||
text
|
||||
small
|
||||
title="Remove this property"
|
||||
>
|
||||
<v-icon
|
||||
color="error"
|
||||
@click="removeField(key)"
|
||||
>mdi-minus</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
</dougal-fixed-string-decoder-field>
|
||||
|
||||
<v-row dense no-gutters v-if="editableFieldList && !readonly">
|
||||
<v-col cols="3">
|
||||
<v-text-field
|
||||
label="Add new field"
|
||||
hint="Enter the name of a new field"
|
||||
:error-messages="fieldNameErrors"
|
||||
v-model="fieldName"
|
||||
append-outer-icon="mdi-plus-circle"
|
||||
@keydown.enter.prevent="addField"
|
||||
>
|
||||
<template v-slot:append-outer>
|
||||
<v-icon
|
||||
color="primary"
|
||||
:disabled="fieldName && !!fieldNameErrors"
|
||||
@click="addField"
|
||||
>mdi-plus</v-icon>
|
||||
</template>
|
||||
</v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
</v-container>
|
||||
|
||||
|
||||
|
||||
</v-form>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
.input {
|
||||
flex: 1 1 auto;
|
||||
line-height: 20px;
|
||||
padding: 8px 0 8px;
|
||||
min-height: 32px;
|
||||
max-height: 32px;
|
||||
max-width: 100%;
|
||||
min-width: 0px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.multiline {
|
||||
font-family: mono;
|
||||
white-space: pre;
|
||||
overflow-x: auto;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.multiline >>> .line-number {
|
||||
display: inline-block;
|
||||
font-size: 75%;
|
||||
width: 5ex;
|
||||
margin-inline-end: 1ex;
|
||||
text-align: right;
|
||||
border: none;
|
||||
position: relative;
|
||||
top: -1px;
|
||||
}
|
||||
|
||||
.input, .multiline >>> .chunk {
|
||||
padding-inline: 1px;
|
||||
border: 1px solid;
|
||||
}
|
||||
|
||||
.input, .multiline >>> .chunk-empty {
|
||||
padding-inline: 1px;
|
||||
}
|
||||
|
||||
.input, .multiline >>> .chunk-overlap {
|
||||
padding-inline: 1px;
|
||||
border: 1px solid grey;
|
||||
color: grey;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import { getHSLColourFor } from '@/lib/hsl'
|
||||
import DougalFixedStringDecoderField from './fixed-string-decoder-field'
|
||||
|
||||
export default {
|
||||
name: "DougalFixedStringDecoder",
|
||||
|
||||
components: {
|
||||
DougalFixedStringDecoderField
|
||||
},
|
||||
|
||||
mixins: [
|
||||
{
|
||||
methods: {
|
||||
getHSLColourFor
|
||||
}
|
||||
}
|
||||
],
|
||||
|
||||
props: {
|
||||
text: String,
|
||||
fields: Object,
|
||||
multiline: Boolean,
|
||||
numberedLines: [ Boolean, Number ],
|
||||
maxHeight: String,
|
||||
editableFieldList: { type: Boolean, default: true },
|
||||
readonly: Boolean,
|
||||
title: String,
|
||||
subtitle: String,
|
||||
label: String,
|
||||
hint: String,
|
||||
},
|
||||
|
||||
data () {
|
||||
return {
|
||||
//< The reason for not using this.text directly is that at some point
|
||||
//< we might extend this component to allow editing the sample text.
|
||||
text_: "",
|
||||
//< The name of a new field to add.
|
||||
fieldName: ""
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
|
||||
/** Whether to treat the sample text as multiline.
|
||||
*/
|
||||
isMultiline () {
|
||||
return this.multiline === true || this.text.includes("\n");
|
||||
},
|
||||
|
||||
/* Return the fields as an array sorted by offset
|
||||
*/
|
||||
parts () {
|
||||
return Object.entries(this.fields).sort( (a, b) => a.offset - b.offset );
|
||||
},
|
||||
|
||||
/* Transform this.parts into {start, end} intervals.
|
||||
*/
|
||||
chunks () {
|
||||
const chunks = [];
|
||||
const chunk_num = 0;
|
||||
for (const [name, part] of this.parts) {
|
||||
const chunk = {};
|
||||
chunk.start = part.offset;
|
||||
chunk.end = part.offset + part.length - 1;
|
||||
//chunk.text = this.text_.slice(chunk.start, chunk.end);
|
||||
chunk.colour = this.getHSLColourFor(name)
|
||||
|
||||
chunks.push(chunk);
|
||||
}
|
||||
|
||||
return chunks;
|
||||
},
|
||||
|
||||
multilineElementStyle () {
|
||||
if (this.maxHeight) {
|
||||
return `max-height: ${this.maxHeight};`;
|
||||
}
|
||||
return "";
|
||||
},
|
||||
|
||||
/** Return a colourised HTML version of this.text.
|
||||
*/
|
||||
html () {
|
||||
if (!this.text_) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.isMultiline) {
|
||||
if (typeof this.numberedLines == "number" || this.numberedLines) {
|
||||
const offset = typeof this.numberedLines == "number" ? Math.abs(this.numberedLines) : 0;
|
||||
return this.text_.split("\n").map( (line, idx) =>
|
||||
this.numberLine(offset+idx, this.renderTextLine(line))).join("<br/>");
|
||||
} else {
|
||||
return this.text_.split("\n").map(this.renderTextLine).join("<br/>");
|
||||
}
|
||||
} else {
|
||||
return this.renderTextLine(this.text_);
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
fieldNameErrors () {
|
||||
return this.parts.find( i => i[0] == this.fieldName )
|
||||
? "A field with this name already exists"
|
||||
: null;
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
watch: {
|
||||
|
||||
text () {
|
||||
if (this.text != this.text_) {
|
||||
this.reset();
|
||||
}
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
methods: {
|
||||
|
||||
addField () {
|
||||
if (!this.fieldNameErrors) {
|
||||
this.$emit("update:fields", {
|
||||
...this.fields,
|
||||
[this.fieldName]: { offset: 0, length: 0 }
|
||||
});
|
||||
this.fieldName = "";
|
||||
}
|
||||
},
|
||||
|
||||
// NOTE Not used
|
||||
updateField (field, key, value) {
|
||||
const fields = {
|
||||
...this.fields,
|
||||
[field]: {
|
||||
...this.fields[field],
|
||||
[key]: value
|
||||
}
|
||||
};
|
||||
this.$emit("update:fields", fields);
|
||||
},
|
||||
|
||||
removeField (key) {
|
||||
const fields = {...this.fields};
|
||||
delete fields[key];
|
||||
this.$emit("update:fields", fields);
|
||||
},
|
||||
|
||||
/** Return an HSL colour as a function of an input value
|
||||
* `str`.
|
||||
*/
|
||||
xgetHSLColourFor () {
|
||||
console.log("WILL BE DEFINED ON MOUNT");
|
||||
},
|
||||
|
||||
/** Return a `<span>` opening tag.
|
||||
*/
|
||||
style (name, colour) {
|
||||
return colour
|
||||
? `<span class="${name}" style="color:${colour};border-color:${colour}">`
|
||||
: `<span class="${name}">`;
|
||||
},
|
||||
|
||||
/** Return an array of the intervals that intersect `pos`.
|
||||
* May be empty.
|
||||
*/
|
||||
chunksFor (pos) {
|
||||
return this.chunks.filter( chunk =>
|
||||
pos >= chunk.start &&
|
||||
pos <= chunk.end
|
||||
)
|
||||
},
|
||||
|
||||
/*
|
||||
* Algorithm:
|
||||
*
|
||||
* Go through every character of one line of text and determine in which
|
||||
* part(s) it falls in, if any. Collect adjacent same parts into <span/>
|
||||
* elements.
|
||||
*/
|
||||
renderTextLine (text) {
|
||||
const parts = [];
|
||||
|
||||
let prevStyle;
|
||||
|
||||
for (const pos in text) {
|
||||
const chunks = this.chunksFor(pos);
|
||||
const isEmpty = chunks.length == 0;
|
||||
const isOverlap = chunks.length > 1;
|
||||
|
||||
const style = isEmpty
|
||||
? this.style("chunk-empty")
|
||||
: isOverlap
|
||||
? this.style("chunk-overlap")
|
||||
: this.style("chunk", chunks[0].colour);
|
||||
|
||||
if (style != prevStyle) {
|
||||
if (prevStyle) {
|
||||
parts.push("</span>");
|
||||
}
|
||||
parts.push(style);
|
||||
}
|
||||
parts.push(text[pos]);
|
||||
prevStyle = style;
|
||||
}
|
||||
|
||||
if (parts.length) {
|
||||
parts.push("</span>");
|
||||
}
|
||||
|
||||
return parts.join("");
|
||||
},
|
||||
|
||||
numberLine (number, line) {
|
||||
return `<span class="line-number">${number}</span>${line}`;
|
||||
},
|
||||
|
||||
setText (v) {
|
||||
//console.log(v);
|
||||
this.text_ = v;
|
||||
},
|
||||
|
||||
reset () {
|
||||
this.text_ = this.text.replaceAll("\r", "");
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
mounted () {
|
||||
this.reset();
|
||||
}
|
||||
|
||||
}
|
||||
</script>
|
||||
@@ -1,322 +0,0 @@
|
||||
<template>
|
||||
<v-card flat>
|
||||
<v-card-text>
|
||||
<div class="sample" v-html="sampleHtml" ref="sample">
|
||||
</div>
|
||||
|
||||
<v-divider></v-divider>
|
||||
|
||||
<v-form>
|
||||
<v-container>
|
||||
|
||||
<v-row no-gutters v-for="(field, key) in {line, point, easting, northing}" :key="key">
|
||||
<v-col>
|
||||
<v-chip outlined :color="field.colour">{{field.label}}</v-chip>
|
||||
</v-col>
|
||||
<v-col>
|
||||
<v-text-field
|
||||
class="ml-3"
|
||||
dense
|
||||
label="From"
|
||||
type="number"
|
||||
min="0"
|
||||
:ref="key"
|
||||
v-model.number="field.offset"
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
<v-col>
|
||||
<v-text-field
|
||||
class="ml-3"
|
||||
dense
|
||||
label="Width"
|
||||
type="number"
|
||||
min="0"
|
||||
v-model.number="field.width"
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
</v-container>
|
||||
</v-form>
|
||||
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.sample {
|
||||
font-family: mono;
|
||||
white-space: pre;
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
|
||||
|
||||
function arraysAreEqual(arr0, arr1) {
|
||||
return (arr0 && arr1 && arr0.length == arr1.length) &&
|
||||
arr0.reduce( (acc, cur, idx) =>
|
||||
acc && (arr1[idx] == cur), true );
|
||||
}
|
||||
|
||||
function repeatsLength(arr, comparator=(a, b) => a == b) {
|
||||
function checkRepeat(arr) {
|
||||
let idx = 0;
|
||||
if (arr.length) {
|
||||
while (comparator(arr[idx], arr[++idx]));
|
||||
}
|
||||
return idx;
|
||||
}
|
||||
|
||||
const repeats = [];
|
||||
let offset = 0;
|
||||
let count;
|
||||
while (count = checkRepeat(arr.slice(offset))) {
|
||||
repeats.push(count);
|
||||
offset += count;
|
||||
}
|
||||
|
||||
return repeats;
|
||||
}
|
||||
|
||||
export default {
|
||||
name: "DougalProjectSettingsFixedWidthFormat",
|
||||
|
||||
props: [ "value", "sample" ],
|
||||
|
||||
data () {
|
||||
return {
|
||||
line: {
|
||||
name: "line_name",
|
||||
type: "int",
|
||||
label: "Line",
|
||||
offset: 0,
|
||||
width: 4,
|
||||
colour: "green"
|
||||
},
|
||||
point: {
|
||||
name: "point_number",
|
||||
type: "int",
|
||||
label: "Point",
|
||||
offset: 4,
|
||||
width: 4,
|
||||
colour: "blue"
|
||||
},
|
||||
easting: {
|
||||
name: "easting",
|
||||
type: "float",
|
||||
label: "Easting",
|
||||
offset: 8,
|
||||
width: 12,
|
||||
colour: "red"
|
||||
},
|
||||
northing: {
|
||||
name: "northing",
|
||||
type: "float",
|
||||
label: "Northing",
|
||||
offset: 20,
|
||||
width: 12,
|
||||
colour: "orange"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
watch: {
|
||||
value (newValue) {
|
||||
this.reset();
|
||||
},
|
||||
|
||||
fields: {
|
||||
handler () {
|
||||
if (this.overlappingFields) {
|
||||
this.$emit("input", null);
|
||||
} else {
|
||||
this.$emit("input", {
|
||||
names: this.names,
|
||||
types: this.types,
|
||||
widths: this.widths
|
||||
});
|
||||
}
|
||||
},
|
||||
deep: true
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
|
||||
sampleHtml () {
|
||||
if (!this.sample) {
|
||||
return "";
|
||||
}
|
||||
|
||||
const parts = this.fieldsDescending;
|
||||
|
||||
const partsForColumn = (col) => {
|
||||
return parts.filter(part =>
|
||||
part.offset <= col && part.offset+part.width > col
|
||||
);
|
||||
}
|
||||
|
||||
const partsForLine = (line) => {
|
||||
return Array.from({length: line.length}, (_, idx) => partsForColumn(idx));
|
||||
}
|
||||
|
||||
function getDecorators(arr, comparator, specialValue) {
|
||||
const repeats = repeatsLength(arr, comparator);
|
||||
let sum = 0;
|
||||
const res = [];
|
||||
repeats.slice(0, -1).forEach( count => {
|
||||
const idx = sum;
|
||||
sum += count;
|
||||
const el = arr[idx];
|
||||
if (el.length) {
|
||||
if (el.length == 1) {
|
||||
// Only one field, return it
|
||||
res.push({
|
||||
...el[0],
|
||||
offset: idx,
|
||||
width: Math.min(Math.min(el[0].offset+el[0].width, sum+idx) - idx, sum-idx)
|
||||
});
|
||||
} else {
|
||||
// More than one element, return special value
|
||||
res.push({
|
||||
...specialValue,
|
||||
offset: idx,
|
||||
width: sum-idx
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return res.sort( (a, b) => b.offset-a.offset );
|
||||
}
|
||||
|
||||
const lines = this.sample.split("\n").map( line => {
|
||||
const decorators = getDecorators(partsForLine(line), arraysAreEqual, {name: "Overlap", colour: "grey"});
|
||||
let s = line;
|
||||
for (const part of decorators) {
|
||||
const s0 = s.slice(0, part.offset);
|
||||
const s1 = s.slice(part.offset, part.offset+part.width);
|
||||
const s2 = s.slice(part.offset+part.width);
|
||||
s = s0+`<span class="${part.colour}--text" title="${part.name}" style="border: 1px solid;">${s1}</span>${s2}`;
|
||||
}
|
||||
return s;
|
||||
});
|
||||
|
||||
return lines.join("<br/>");
|
||||
},
|
||||
|
||||
overlappingFields () {
|
||||
function isOverlapping (a, b) {
|
||||
return a.offset < b.offset+b.width && b.offset < a.offset+a.width;
|
||||
}
|
||||
|
||||
for (const field of this.fields) {
|
||||
for (const otherField of this.fields) {
|
||||
if (field != otherField && isOverlapping(field, otherField)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
},
|
||||
|
||||
names () {
|
||||
return this.fieldsAscending.map(f => f.name);
|
||||
},
|
||||
|
||||
types () {
|
||||
return this.fieldsAscending.map(f => f.type);
|
||||
},
|
||||
|
||||
widths () {
|
||||
if (this.overlappingFields) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const fields = this.fieldsAscending;
|
||||
const w = [ ];
|
||||
|
||||
if (fields[0].offset) {
|
||||
w.push(-fields[0].offset);
|
||||
}
|
||||
w.push(fields[0].width);
|
||||
|
||||
for (let idx=1; idx<fields.length; idx++) {
|
||||
const f0 = fields[idx-1];
|
||||
const f1 = fields[idx];
|
||||
const gap = f0.offset+f0.width-f1.offset;
|
||||
|
||||
if (gap) {
|
||||
w.push(gap);
|
||||
}
|
||||
w.push(f1.width);
|
||||
}
|
||||
return w;
|
||||
},
|
||||
|
||||
fields () {
|
||||
return [ this.line, this.point, this.easting, this.northing ];
|
||||
},
|
||||
|
||||
fieldsAscending () {
|
||||
return Object.values(this.fields).sort( (a, b) => a.offset - b.offset );
|
||||
},
|
||||
|
||||
fieldsDescending () {
|
||||
return Object.values(this.fields).sort( (a, b) => b.offset - a.offset );
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
methods: {
|
||||
|
||||
reset () {
|
||||
if (!this.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
const fieldFor = (index) => {
|
||||
return this.fields.find(field => field.name == this.value.names[index]);
|
||||
}
|
||||
|
||||
let offset=0;
|
||||
let index=0;
|
||||
|
||||
for (const width of this.value.widths) {
|
||||
if (width < 0) {
|
||||
offset -= width;
|
||||
} else {
|
||||
fieldFor(index).offset = offset;
|
||||
fieldFor(index).width = width;
|
||||
offset += width;
|
||||
index++;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
save () {
|
||||
this.$emit('input', {...this.$data.values});
|
||||
},
|
||||
|
||||
back () {
|
||||
this.$emit('close');
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
mounted () {
|
||||
this.reset();
|
||||
document.addEventListener("selectionchange", this.handleSelection);
|
||||
},
|
||||
|
||||
beforeUnmount () {
|
||||
document.removeEventListener("selectionchange", this.handleSelection);
|
||||
}
|
||||
|
||||
}
|
||||
</script>
|
||||
Reference in New Issue
Block a user