Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion packages/super-editor/src/components/SuperEditor.vue
Original file line number Diff line number Diff line change
Expand Up @@ -35,14 +35,15 @@ const editorElem = ref(null);
const initEditor = async () => {
console.debug('[super-editor] Loading file...', props.fileSource);

const content = await Editor.loadXmlData(props.fileSource);
const [content, media] = await Editor.loadXmlData(props.fileSource);
editor.value = new Editor({
mode: 'docx',
element: editorElem.value,
fileSource: props.fileSource,
extensions: getStarterExtensions(),
documentId: props.documentId,
content,
media,
...props.options,
});
};
Expand Down
13 changes: 11 additions & 2 deletions packages/super-editor/src/components/toolbar/ButtonGroup.vue
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ const isDropdown = (item) => item.type === 'dropdown';
const isSeparator = (item) => item.type === 'separator';
const handleToolbarButtonClick = (item, argument = null) => {
currentItem.value = item;
currentItem.value.expand = true;
if (item.disabled.value) return;
emit('command', { item, argument });
}
Expand All @@ -58,14 +59,21 @@ const handleToolbarButtonTextSubmit = (item, argument) => {
emit('command', { item, argument });
}

const handleSelect = (item, argument) => {
const closeDropdowns = () => {
if (!currentItem.value) return;
currentItem.value.expand = false;
currentItem.value = null;
}

const handleSelect = (item, argument) => {
closeDropdowns();
emit('command', { item, argument });
}

const handleClickOutside = (e) => {
currentItem.value = null;
closeDropdowns();
}

</script>

<template>
Expand All @@ -91,6 +99,7 @@ const handleClickOutside = (e) => {
v-if="isDropdown(item) && item.nestedOptions?.value?.length"
:options="item.nestedOptions.value"
:trigger="item.disabled.value ? null : 'click'"
:show="item.expand.value"
size="medium"
placement="bottom-start"
class="toolbar-button"
Expand Down
118 changes: 88 additions & 30 deletions packages/super-editor/src/components/toolbar/LinkInput.vue
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
<script setup>
import { ref, computed } from "vue";
import { ref, computed, watch } from "vue";

const emit = defineEmits(["submit", "cancel"]);
const props = defineProps({
initialText: {
type: String,
default: "",
},
initialUrl: {
href: {
type: String,
default: "",
},
Expand All @@ -19,6 +19,10 @@ const props = defineProps({
type: Boolean,
default: true,
},
goToAnchor: {
type: Function,
default: () => {},
},
});

const handleSubmit = () => {
Expand All @@ -33,10 +37,13 @@ const handleSubmit = () => {
urlError.value = true;
};

const handleRemove = () => {
emit("submit", { text: text.value, href: null });
};

const urlError = ref(false);
const text = ref(props.initialText);
const rawUrl = ref(props.initialUrl);
const rawUrl = ref(props.href);
const url = computed(() => {
if (!rawUrl.value?.startsWith("http")) return "http://" + rawUrl.value;
return rawUrl.value;
Expand All @@ -47,48 +54,100 @@ const validUrl = computed(() => {
return url.value.includes(".") && urlSplit.length > 1;
});

const getApplyText = computed(() => {
return showApply.value ? "Apply" : "Remove";
});
const isDisabled = computed(() => {
return !validUrl.value;
});
const showApply = computed(() => {
return !showRemove.value;
});
const showRemove = computed(() => {
return props.initialUrl && !rawUrl.value
const getApplyText = computed(() => showApply.value ? "Apply" : "Remove");
const isDisabled = computed(() => !validUrl.value);
const showApply = computed(() => !showRemove.value);
const showRemove = computed(() => props.href && !rawUrl.value);
const isAnchor = computed(() => props.href.startsWith("#"));

const openLink = () => {
window.open(url.value, "_blank");
};
watch(() => props.href, (newVal) => {
rawUrl.value = newVal;
});
</script>

<template>
<div class="link-input-ctn">
<div class="link-title">
Add link
<div class="link-title" v-if="!href">Add link</div>
<div class="link-title" v-else-if="isAnchor">Page anchor</div>
<div class="link-title" v-else>Edit link</div>

<div v-if="showInput && !isAnchor">
<div class="input-row">
<i class="fas fa-link input-icon"></i>
<input
type="text"
placeholder="Type or paste a link"
:class="{ error: urlError }"
v-model="rawUrl"
@keydown.enter.stop.prevent="handleSubmit"
@keydown="urlError = false"
/>
<i :class="{disabled: !validUrl}" class="fal fa-external-link-alt open-link-icon" @click="openLink"></i>
</div>
<div class="input-row link-buttons">
<button class="remove-btn" @click="handleRemove" v-if="href">
<i class="fal fa-times"></i>
Remove
</button>
<button class="submit-btn" v-if="showApply" @click="handleSubmit" :class="{ 'disable-btn': isDisabled }">{{ getApplyText }}</button>
</div>
</div>
<div class="input-row" v-if="showInput">
<i class="fas fa-link input-icon"></i>
<input
type="text"
placeholder="Type or paste a link"
:class="{ error: urlError }"
v-model="rawUrl"
@keydown.enter.stop.prevent="handleSubmit"
@keydown="urlError = false"
/>

<button class="submit-btn" v-if="showApply" @click="handleSubmit" :class="{ 'disable-btn': isDisabled }">{{ getApplyText }}</button>
<button class="remove-btn" v-if="showRemove" @click="handleSubmit">Remove</button>

<div v-else-if="isAnchor" class="input-row go-to-anchor clickable">
<a @click.stop.prevent="goToAnchor">Go to {{ href.startsWith('#_') ? href.substring(2) : href }}</a>
</div>
</div>
</template>

<style scoped>
.open-link-icon {
margin-left: 10px;
width: 30px;
height: 30px;
border-radius: 50%;
border: 1px solid transparent;
display: flex;
justify-content: center;
align-items: center;
transition: all 0.2s ease;
cursor: pointer;
}
.open-link-icon:hover {
color: #1355FF;
background-color: white;
border: 1px solid #DBDBDB;
}
.disabled {
opacity: 0.6;
cursor: not-allowed;
pointer-events: none;
}
.link-buttons {
display: flex;
justify-content: flex-end;
margin-top: 10px;
}
.remove-btn i {
margin-right: 5px;
}
.link-buttons button {
margin-left: 5px;
}
.disable-btn {
opacity: 0.6;
cursor: not-allowed;
pointer-events: none;
}
.go-to-anchor a {
font-size: 14px;
text-decoration: underline;
}
.clickable {
cursor: pointer;
}
.link-title {
font-size: 14px;
font-weight: 600;
Expand Down Expand Up @@ -154,7 +213,6 @@ const showRemove = computed(() => {
font-size: 13px;
flex-grow: 1;
padding: 5px;
margin-right: 1em;
padding: 10px;
border-radius: 8px;
padding-left: 32px;
Expand Down
37 changes: 31 additions & 6 deletions packages/super-editor/src/components/toolbar/defaultItems.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { undoDepth, redoDepth } from "prosemirror-history";
import { h } from "vue";
import { h, onDeactivated } from "vue";

import { sanitizeNumber } from "./helpers";
import { useToolbarItem } from "./use-toolbar-item";
Expand Down Expand Up @@ -300,21 +300,47 @@ export const makeDefaultItems = (superToolbar) => {
render: () => renderLinkDropdown(link),
}
],
onActivate: ({ href }) => {
if (href) link.attributes.value = { href };
else link.attributes.value = {};
link.expand.value = true;
},
onDeactivate: () => {
link.attributes.value = {};
link.expand.value = false;
}
});

function renderLinkDropdown(link) {
const handleSubmit = ({ href, text }) => {
const handleSubmit = ({ href }) => {
link.attributes.value.link = { href };
const itemWithCommand = { ...link, command: "toggleLink", };
superToolbar.emitCommand({ item: itemWithCommand, argument: { href, text: "test" } });

if (!href) link.active.value = false
};

return h('div', {}, [
h(LinkInput, {
onSubmit: handleSubmit,
initialUrl: link.attributes.value?.link?.href
href: link.attributes.value.href,
goToAnchor: () => {
if (!superToolbar.activeEditor || !link.attributes.value?.href) return;
const anchorName = link.attributes.value?.href?.slice(1);
const container = superToolbar.activeEditor.element;
const anchor = container.querySelector(`a[name='${anchorName}']`);
if (anchor) {
switch (anchorName) {
case '_top':
container.scrollIntoView({ behavior: 'smooth', block: 'start' });
break;
case '_bottom':
container.scrollIntoView({ behavior: 'smooth', block: 'end' });
break;
default:
anchor.scrollIntoView({ behavior: 'smooth', block: 'start' });
}
}
}
})
]);
}
Expand All @@ -332,11 +358,10 @@ export const makeDefaultItems = (superToolbar) => {
const image = useToolbarItem({
type: "button",
name: "image",
command: "toggleImage",
command: "setImage",
icon: "fas fa-image",
active: false,
tooltip: "Image",
disabled: true,
});

// alignment
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ export const useToolbarItem = (options) => {
const initiallyDisabled = options.disabled || false;
const disabled = ref(options.disabled);
const active = ref(false);
const expand = ref(false);

// top-level style
const style = ref(options.style);
Expand Down Expand Up @@ -116,6 +117,7 @@ export const useToolbarItem = (options) => {
attributes,
disabled,
active,
expand,
nestedOptions,

style,
Expand Down
16 changes: 14 additions & 2 deletions packages/super-editor/src/core/DocxZipper.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,11 @@ class DocxZipper {
this.debug = params.debug || false;
this.zip = new JSZip();
this.files = [];
this.media = {};
}

/**
* Get XML data from the zipped docx
* Get all docx data from the zipped docx
*
* [Content_Types].xml
* _rels/.rels
Expand All @@ -29,10 +30,11 @@ class DocxZipper {
* docProps/core.xml
* docProps/app.xml
* */
async getXmlData(file) {
async getDocxData(file) {
const extractedFiles = await this.unzip(file);
const files = Object.entries(extractedFiles.files);

const mediaObjects = {};
const validTypes = ['xml', 'rels'];
for (const file of files) {
const [_, zipEntry] = file;
Expand All @@ -43,9 +45,19 @@ class DocxZipper {
content,
});
}

else if (zipEntry.name.startsWith('word/media')) {
const mediaContent = await zipEntry.async("base64");
this.media[zipEntry.name] = `data:image/${this.getFileExtension(zipEntry.name)};base64,${mediaContent}`;
}
}

return this.files;
}

getFileExtension(fileName) {
return fileName.split('.').pop();
}

async unzip(file) {
const zip = await this.zip.loadAsync(file);
Expand Down
2 changes: 1 addition & 1 deletion packages/super-editor/src/core/DocxZipper.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ describe('DocxZipper - file extraction', () => {
it('It can extract xml files', async () => {
const fileContent = await readFileAsBuffer('../tests/fixtures/sample/sample.docx');
const fileObject = Buffer.from(fileContent);
const unzippedXml = await zipper.getXmlData(fileObject);
const unzippedXml = await zipper.getDocxData(fileObject);
expect(unzippedXml).toBeInstanceOf(Array);

unzippedXml.forEach((file) => {
Expand Down
Loading