Skip to content

Instantly share code, notes, and snippets.

@voratham
Created March 27, 2022 12:01
Show Gist options
  • Save voratham/4f77d49182a82d6ea84fb244fde857e1 to your computer and use it in GitHub Desktop.
Save voratham/4f77d49182a82d6ea84fb244fde857e1 to your computer and use it in GitHub Desktop.
migration from vu-clamp to vue3-clamp typescript
<script lang="ts">
import { addListener, removeListener } from "resize-detector";
import {
computed,
defineComponent,
h,
nextTick,
onBeforeUnmount,
onMounted,
onUpdated,
ref,
watch,
} from "vue";
export default defineComponent({
name: "Vue3Clamp",
props: {
tag: {
type: String,
default: "div",
required: false,
},
autoResize: {
type: Boolean,
default: false,
required: false,
},
maxLines: {
type: Number,
default: 2,
required: false,
},
maxHeight: {
type: [Number, String],
required: true,
},
ellipsis: {
type: String,
default: "…",
required: false,
},
location: {
type: String,
default: "end",
validator: (value: string) => {
return ["start", "middle", "end"].indexOf(value) !== -1;
},
required: false,
},
expanded: {
type: Boolean,
default: false,
required: false,
},
},
emits: ["update:expanded", "clampchange"],
setup(props, context) {
const getText = (): string => {
if (context.slots.default) {
const [content] = (context.slots.default() || []).filter(
(node) => node.children !== ""
);
return content && content.children ? content.children.toString() : "";
}
return "";
};
const offset = ref(0);
const text = ref(getText());
const localExpanded = ref<boolean>(!!props.expanded);
const unregisterResizeCallback = ref<() => void>();
const rootRef = ref<null | HTMLElement>(null);
const textRef = ref<null | HTMLElement>(null);
const contentRef = ref<null | HTMLElement>(null);
watch(
() => props.expanded,
(val) => {
localExpanded.value = val;
}
);
onMounted(() => {
init();
watch(
() => [props.maxLines, props.maxHeight, props.ellipsis, isClamped],
() => {
update();
}
);
watch(
() => [props.tag, text, props.autoResize],
() => {
init();
}
);
});
onUpdated(() => {
text.value = getText();
applyChange();
});
onBeforeUnmount(() => {
cleanUp();
});
watch(localExpanded, (val) => {
if (val) {
clampAt(text.value.length);
} else {
update();
}
if (props.expanded !== val) {
context.emit("update:expanded", val);
}
});
const init = (): void => {
if (!context.slots.default) {
return;
}
const contents = context.slots.default();
if (!contents) {
return;
}
offset.value = text.value.length;
cleanUp();
if (props.autoResize && rootRef.value != null) {
addListener(rootRef.value, () => {
update();
});
unregisterResizeCallback.value = () => {
if (rootRef.value != null) {
removeListener(rootRef.value, update);
}
};
}
update();
};
const cleanUp = () => {
if (typeof unregisterResizeCallback.value === "function") {
unregisterResizeCallback.value();
}
};
const clampedText = computed<string>(() => {
if (props.location === "start") {
return (
props.ellipsis + (text.value.slice(0, offset.value) || "").trim()
);
} else if (props.location === "middle") {
const split = Math.floor(offset.value / 2);
return (
(text.value.slice(0, split) || "").trim() +
props.ellipsis +
(text.value.slice(-split) || "").trim()
);
}
return (text.value.slice(0, offset.value) || "").trim() + props.ellipsis;
});
const isClamped = computed<boolean>(() => {
if (!text.value) return false;
return offset.value !== text.value.length;
});
watch(
isClamped,
(val) => {
nextTick(() => context.emit("clampchange", val));
},
{
immediate: true,
}
);
const realText = computed<string>(() => {
return isClamped.value ? clampedText.value : text.value;
});
const realMaxHeight = computed<string | number | null>(() => {
if (localExpanded.value) {
return null;
}
if (!props.maxHeight) {
return null;
}
return typeof props.maxHeight === "number"
? `${props.maxHeight}px`
: props.maxHeight;
});
const update = (): void => {
if (localExpanded.value) {
return;
}
applyChange();
if (isOverflow() || isClamped) {
search();
}
};
const clampAt = (_offset: number) => {
offset.value = _offset;
applyChange();
};
const moveEdge = (steps: number) => {
clampAt(offset.value + steps);
};
const fill = () => {
while (
(!isOverflow() || getLines() < 2) &&
offset.value < text.value.length
) {
moveEdge(1);
}
};
const clamp = () => {
while (isOverflow() && getLines() > 1 && offset.value > 0) {
moveEdge(-1);
}
};
const stepToFit = (): void => {
fill();
clamp();
};
const search = (...range: number[]) => {
const [from = 0, to = offset.value] = range;
if (to - from <= 3) {
stepToFit();
return;
}
const target = Math.floor((to + from) / 2);
clampAt(target);
if (isOverflow()) {
search(from, target);
} else {
search(target, to);
}
};
const getLines = (): number => {
const result = Object.keys(
Array.prototype.slice
.call(contentRef.value?.getClientRects())
.reduce<Record<string, boolean>>((acc, bound: DOMRect) => {
const key = `${bound.top}/${bound.bottom}`;
if (!acc[key]) {
acc[key] = true;
}
return acc;
}, {})
);
return result.length;
};
const isOverflow = (): boolean => {
if (!props.maxLines && !props.maxHeight) {
return false;
}
if (props.maxLines) {
if (getLines() > props.maxLines) {
return true;
}
}
if (props.maxHeight && rootRef.value) {
if (rootRef.value.scrollHeight > rootRef.value.offsetHeight) {
return true;
}
}
return false;
};
const applyChange = (): void => {
if (textRef.value != null) {
textRef.value.textContent = realText.value;
}
};
const expand = (): void => {
localExpanded.value = true;
};
const collapse = (): void => {
localExpanded.value = false;
};
const toggle = (): void => {
localExpanded.value = !localExpanded.value;
};
return () => {
const contents = [
h(
"span",
{
ref: textRef,
attrs: {
"aria-label": text.value.trim(),
},
},
realText.value
),
];
const scope = {
expand,
collapse,
toggle,
clamped: isClamped.value,
expanded: localExpanded.value,
};
const before = context.slots.before
? context.slots.before(scope)
: context.slots.before;
if (before) {
contents.unshift(...(Array.isArray(before) ? before : [before]));
}
const after = context.slots.after
? context.slots.after(scope)
: context.slots.after;
if (after) {
contents.push(...(Array.isArray(after) ? after : [after]));
}
const lines = [
h(
"span",
{
style: {
boxShadow: "transparent 0 0",
background: "red",
},
ref: contentRef,
},
contents
),
];
return h(
props.tag,
{
ref: rootRef,
style: {
maxHeight: realMaxHeight.value,
overflow: "hidden",
},
},
lines
);
};
},
});
</script>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment