mirror of
https://github.com/BookStackApp/BookStack.git
synced 2024-10-01 01:36:00 -04:00
Crawled forward slightly on table resizing
This commit is contained in:
parent
8b4f112462
commit
264966de02
3
TODO
3
TODO
@ -2,7 +2,8 @@
|
|||||||
|
|
||||||
- Table cell height resize & cell width resize via width style
|
- Table cell height resize & cell width resize via width style
|
||||||
- Column resize source: https://github.com/ProseMirror/prosemirror-tables/blob/master/src/columnresizing.js
|
- Column resize source: https://github.com/ProseMirror/prosemirror-tables/blob/master/src/columnresizing.js
|
||||||
- Looks like all the required internals are exported so we can copy out & modify easily.
|
- Have updated column resizing to set cell widths
|
||||||
|
- Now need to handle table overall size on change, then heights.
|
||||||
|
|
||||||
### In-Progress
|
### In-Progress
|
||||||
|
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
namespace BookStack\Uploads;
|
namespace BookStack\Uploads;
|
||||||
|
|
||||||
|
use BookStack\Auth\Access\LdapService;
|
||||||
use BookStack\Auth\User;
|
use BookStack\Auth\User;
|
||||||
use BookStack\Exceptions\HttpFetchException;
|
use BookStack\Exceptions\HttpFetchException;
|
||||||
use Exception;
|
use Exception;
|
||||||
@ -16,6 +17,7 @@ class UserAvatars
|
|||||||
{
|
{
|
||||||
$this->imageService = $imageService;
|
$this->imageService = $imageService;
|
||||||
$this->http = $http;
|
$this->http = $http;
|
||||||
|
$ldapService = app()->make(LdapService::class);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import {EditorState} from "prosemirror-state";
|
import {EditorState} from "prosemirror-state";
|
||||||
import {EditorView} from "prosemirror-view";
|
import {EditorView} from "prosemirror-view";
|
||||||
import {exampleSetup} from "prosemirror-example-setup";
|
import {exampleSetup} from "prosemirror-example-setup";
|
||||||
import {tableEditing, columnResizing} from "prosemirror-tables";
|
import {tableEditing} from "prosemirror-tables";
|
||||||
|
|
||||||
import {DOMParser} from "prosemirror-model";
|
import {DOMParser} from "prosemirror-model";
|
||||||
|
|
||||||
@ -9,6 +9,7 @@ import schema from "./schema";
|
|||||||
import menu from "./menu";
|
import menu from "./menu";
|
||||||
import nodeViews from "./node-views";
|
import nodeViews from "./node-views";
|
||||||
import {stateToHtml} from "./util";
|
import {stateToHtml} from "./util";
|
||||||
|
import {columnResizing} from "./plugins/table-resizing";
|
||||||
|
|
||||||
class ProseMirrorView {
|
class ProseMirrorView {
|
||||||
constructor(target, content) {
|
constructor(target, content) {
|
||||||
|
21
resources/js/editor/node-views/TableView.js
Normal file
21
resources/js/editor/node-views/TableView.js
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
class TableView {
|
||||||
|
/**
|
||||||
|
* @param {PmNode} node
|
||||||
|
* @param {PmView} view
|
||||||
|
* @param {(function(): number)} getPos
|
||||||
|
*/
|
||||||
|
constructor(node, view, getPos) {
|
||||||
|
this.dom = document.createElement("div")
|
||||||
|
this.dom.className = "ProseMirror-tableWrapper"
|
||||||
|
this.table = this.dom.appendChild(document.createElement("table"));
|
||||||
|
this.table.setAttribute('style', node.attrs.style);
|
||||||
|
this.colgroup = this.table.appendChild(document.createElement("colgroup"));
|
||||||
|
this.contentDOM = this.table.appendChild(document.createElement("tbody"));
|
||||||
|
}
|
||||||
|
|
||||||
|
ignoreMutation(record) {
|
||||||
|
return record.type == "attributes" && (record.target == this.table || this.colgroup.contains(record.target))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default TableView;
|
@ -1,9 +1,11 @@
|
|||||||
import ImageView from "./ImageView";
|
import ImageView from "./ImageView";
|
||||||
import IframeView from "./IframeView";
|
import IframeView from "./IframeView";
|
||||||
|
import TableView from "./TableView";
|
||||||
|
|
||||||
const views = {
|
const views = {
|
||||||
image: (node, view, getPos) => new ImageView(node, view, getPos),
|
image: (node, view, getPos) => new ImageView(node, view, getPos),
|
||||||
iframe: (node, view, getPos) => new IframeView(node, view, getPos),
|
iframe: (node, view, getPos) => new IframeView(node, view, getPos),
|
||||||
|
table: (node, view, getPos) => new TableView(node, view, getPos),
|
||||||
};
|
};
|
||||||
|
|
||||||
export default views;
|
export default views;
|
288
resources/js/editor/plugins/table-resizing.js
Normal file
288
resources/js/editor/plugins/table-resizing.js
Normal file
@ -0,0 +1,288 @@
|
|||||||
|
/**
|
||||||
|
* This file originates from https://github.com/ProseMirror/prosemirror-tables
|
||||||
|
* and is hence subject to the MIT license found here:
|
||||||
|
* https://github.com/ProseMirror/prosemirror-menu/blob/master/LICENSE
|
||||||
|
* @copyright Marijn Haverbeke and others
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {Plugin, PluginKey} from "prosemirror-state"
|
||||||
|
import {Decoration, DecorationSet} from "prosemirror-view"
|
||||||
|
import {
|
||||||
|
cellAround,
|
||||||
|
pointsAtCell,
|
||||||
|
setAttr,
|
||||||
|
TableMap,
|
||||||
|
} from "prosemirror-tables";
|
||||||
|
|
||||||
|
export const key = new PluginKey("tableColumnResizing")
|
||||||
|
|
||||||
|
export function columnResizing(options = {}) {
|
||||||
|
const {
|
||||||
|
handleWidth, cellMinWidth, lastColumnResizable
|
||||||
|
} = Object.assign({
|
||||||
|
handleWidth: 5,
|
||||||
|
cellMinWidth: 25,
|
||||||
|
lastColumnResizable: true
|
||||||
|
}, options);
|
||||||
|
|
||||||
|
let plugin = new Plugin({
|
||||||
|
key,
|
||||||
|
state: {
|
||||||
|
init(_, state) {
|
||||||
|
return new ResizeState(-1, false)
|
||||||
|
},
|
||||||
|
apply(tr, prev) {
|
||||||
|
return prev.apply(tr)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
attributes(state) {
|
||||||
|
let pluginState = key.getState(state)
|
||||||
|
return pluginState.activeHandle > -1 ? {class: "resize-cursor"} : null
|
||||||
|
},
|
||||||
|
|
||||||
|
handleDOMEvents: {
|
||||||
|
mousemove(view, event) {
|
||||||
|
handleMouseMove(view, event, handleWidth, cellMinWidth, lastColumnResizable)
|
||||||
|
},
|
||||||
|
mouseleave(view) {
|
||||||
|
handleMouseLeave(view)
|
||||||
|
},
|
||||||
|
mousedown(view, event) {
|
||||||
|
handleMouseDown(view, event, cellMinWidth)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
decorations(state) {
|
||||||
|
let pluginState = key.getState(state)
|
||||||
|
if (pluginState.activeHandle > -1) return handleDecorations(state, pluginState.activeHandle)
|
||||||
|
},
|
||||||
|
|
||||||
|
nodeViews: {}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return plugin
|
||||||
|
}
|
||||||
|
|
||||||
|
class ResizeState {
|
||||||
|
constructor(activeHandle, dragging) {
|
||||||
|
this.activeHandle = activeHandle
|
||||||
|
this.dragging = dragging
|
||||||
|
}
|
||||||
|
|
||||||
|
apply(tr) {
|
||||||
|
let state = this, action = tr.getMeta(key)
|
||||||
|
if (action && action.setHandle != null)
|
||||||
|
return new ResizeState(action.setHandle, null)
|
||||||
|
if (action && action.setDragging !== undefined)
|
||||||
|
return new ResizeState(state.activeHandle, action.setDragging)
|
||||||
|
if (state.activeHandle > -1 && tr.docChanged) {
|
||||||
|
let handle = tr.mapping.map(state.activeHandle, -1)
|
||||||
|
if (!pointsAtCell(tr.doc.resolve(handle))) handle = null
|
||||||
|
state = new ResizeState(handle, state.dragging)
|
||||||
|
}
|
||||||
|
return state
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleMouseMove(view, event, handleWidth, cellMinWidth, lastColumnResizable) {
|
||||||
|
let pluginState = key.getState(view.state)
|
||||||
|
|
||||||
|
if (!pluginState.dragging) {
|
||||||
|
let target = domCellAround(event.target), cell = -1
|
||||||
|
if (target) {
|
||||||
|
let {left, right} = target.getBoundingClientRect()
|
||||||
|
if (event.clientX - left <= handleWidth)
|
||||||
|
cell = edgeCell(view, event, "left")
|
||||||
|
else if (right - event.clientX <= handleWidth)
|
||||||
|
cell = edgeCell(view, event, "right")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cell != pluginState.activeHandle) {
|
||||||
|
if (!lastColumnResizable && cell !== -1) {
|
||||||
|
let $cell = view.state.doc.resolve(cell)
|
||||||
|
let table = $cell.node(-1), map = TableMap.get(table), start = $cell.start(-1)
|
||||||
|
let col = map.colCount($cell.pos - start) + $cell.nodeAfter.attrs.colspan - 1
|
||||||
|
|
||||||
|
if (col == map.width - 1) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updateHandle(view, cell)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleMouseLeave(view) {
|
||||||
|
let pluginState = key.getState(view.state)
|
||||||
|
if (pluginState.activeHandle > -1 && !pluginState.dragging) updateHandle(view, -1)
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleMouseDown(view, event, cellMinWidth) {
|
||||||
|
let pluginState = key.getState(view.state)
|
||||||
|
if (pluginState.activeHandle == -1 || pluginState.dragging) return false
|
||||||
|
|
||||||
|
let cell = view.state.doc.nodeAt(pluginState.activeHandle)
|
||||||
|
let width = currentColWidth(view, pluginState.activeHandle, cell.attrs)
|
||||||
|
view.dispatch(view.state.tr.setMeta(key, {setDragging: {startX: event.clientX, startWidth: width}}))
|
||||||
|
|
||||||
|
function finish(event) {
|
||||||
|
window.removeEventListener("mouseup", finish)
|
||||||
|
window.removeEventListener("mousemove", move)
|
||||||
|
let pluginState = key.getState(view.state)
|
||||||
|
if (pluginState.dragging) {
|
||||||
|
updateColumnWidth(view, pluginState.activeHandle, draggedWidth(pluginState.dragging, event, cellMinWidth))
|
||||||
|
view.dispatch(view.state.tr.setMeta(key, {setDragging: null}))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function move(event) {
|
||||||
|
if (!event.which) return finish(event)
|
||||||
|
let pluginState = key.getState(view.state)
|
||||||
|
let dragged = draggedWidth(pluginState.dragging, event, cellMinWidth)
|
||||||
|
displayColumnWidth(view, pluginState.activeHandle, dragged, cellMinWidth)
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener("mouseup", finish)
|
||||||
|
window.addEventListener("mousemove", move)
|
||||||
|
event.preventDefault()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
function currentColWidth(view, cellPos, {colspan, colwidth}) {
|
||||||
|
let width = colwidth && colwidth[colwidth.length - 1]
|
||||||
|
if (width) return width
|
||||||
|
let dom = view.domAtPos(cellPos)
|
||||||
|
let node = dom.node.childNodes[dom.offset]
|
||||||
|
let domWidth = node.offsetWidth, parts = colspan
|
||||||
|
if (colwidth) for (let i = 0; i < colspan; i++) if (colwidth[i]) {
|
||||||
|
domWidth -= colwidth[i]
|
||||||
|
parts--
|
||||||
|
}
|
||||||
|
return domWidth / parts
|
||||||
|
}
|
||||||
|
|
||||||
|
function domCellAround(target) {
|
||||||
|
while (target && target.nodeName != "TD" && target.nodeName != "TH")
|
||||||
|
target = target.classList.contains("ProseMirror") ? null : target.parentNode
|
||||||
|
return target
|
||||||
|
}
|
||||||
|
|
||||||
|
function edgeCell(view, event, side) {
|
||||||
|
let found = view.posAtCoords({left: event.clientX, top: event.clientY})
|
||||||
|
if (!found) return -1
|
||||||
|
let {pos} = found
|
||||||
|
let $cell = cellAround(view.state.doc.resolve(pos))
|
||||||
|
if (!$cell) return -1
|
||||||
|
if (side == "right") return $cell.pos
|
||||||
|
let map = TableMap.get($cell.node(-1)), start = $cell.start(-1)
|
||||||
|
let index = map.map.indexOf($cell.pos - start)
|
||||||
|
return index % map.width == 0 ? -1 : start + map.map[index - 1]
|
||||||
|
}
|
||||||
|
|
||||||
|
function draggedWidth(dragging, event, cellMinWidth) {
|
||||||
|
let offset = event.clientX - dragging.startX
|
||||||
|
return Math.max(cellMinWidth, dragging.startWidth + offset)
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateHandle(view, value) {
|
||||||
|
view.dispatch(view.state.tr.setMeta(key, {setHandle: value}))
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateColumnWidth(view, cell, width) {
|
||||||
|
let $cell = view.state.doc.resolve(cell);
|
||||||
|
let table = $cell.node(-1);
|
||||||
|
let map = TableMap.get(table);
|
||||||
|
let start = $cell.start(-1);
|
||||||
|
let col = map.colCount($cell.pos - start) + $cell.nodeAfter.attrs.colspan - 1;
|
||||||
|
let tr = view.state.tr;
|
||||||
|
|
||||||
|
for (let row = 0; row < map.height; row++) {
|
||||||
|
let mapIndex = row * map.width + col;
|
||||||
|
// Rowspanning cell that has already been handled
|
||||||
|
if (row && map.map[mapIndex] == map.map[mapIndex - map.width]) continue
|
||||||
|
let pos = map.map[mapIndex]
|
||||||
|
let {attrs} = table.nodeAt(pos);
|
||||||
|
const newWidth = (attrs.colspan * width) + 'px';
|
||||||
|
|
||||||
|
tr.setNodeMarkup(start + pos, null, setAttr(attrs, "width", newWidth));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tr.docChanged) view.dispatch(tr)
|
||||||
|
}
|
||||||
|
|
||||||
|
function displayColumnWidth(view, cell, width, cellMinWidth) {
|
||||||
|
const $cell = view.state.doc.resolve(cell)
|
||||||
|
const table = $cell.node(-1);
|
||||||
|
const start = $cell.start(-1);
|
||||||
|
const col = TableMap.get(table).colCount($cell.pos - start) + $cell.nodeAfter.attrs.colspan - 1
|
||||||
|
let dom = view.domAtPos($cell.start(-1)).node
|
||||||
|
while (dom.nodeName !== "TABLE") {
|
||||||
|
dom = dom.parentNode
|
||||||
|
}
|
||||||
|
updateColumnsOnResize(view, table, dom, cellMinWidth, col, width)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function updateColumnsOnResize(view, tableNode, tableDom, cellMinWidth, overrideCol, overrideValue) {
|
||||||
|
console.log({tableNode, tableDom, cellMinWidth, overrideCol, overrideValue});
|
||||||
|
let totalWidth = 0;
|
||||||
|
let fixedWidth = true;
|
||||||
|
const rows = tableDom.querySelectorAll('tr');
|
||||||
|
|
||||||
|
for (let y = 0; y < rows.length; y++) {
|
||||||
|
const row = rows[y];
|
||||||
|
const cell = row.children[overrideCol];
|
||||||
|
cell.style.width = `${overrideValue}px`;
|
||||||
|
if (y === 0) {
|
||||||
|
for (let x = 0; x < row.children.length; x++) {
|
||||||
|
const cell = row.children[x];
|
||||||
|
if (cell.style.width) {
|
||||||
|
const width = Number(cell.style.width.replace('px', ''));
|
||||||
|
totalWidth += width || cellMinWidth;
|
||||||
|
} else {
|
||||||
|
fixedWidth = false;
|
||||||
|
totalWidth += cellMinWidth;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(totalWidth);
|
||||||
|
if (fixedWidth) {
|
||||||
|
tableDom.style.width = totalWidth + "px"
|
||||||
|
tableDom.style.minWidth = ""
|
||||||
|
} else {
|
||||||
|
tableDom.style.width = ""
|
||||||
|
tableDom.style.minWidth = totalWidth + "px"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function zeroes(n) {
|
||||||
|
let result = []
|
||||||
|
for (let i = 0; i < n; i++) result.push(0)
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleDecorations(state, cell) {
|
||||||
|
let decorations = []
|
||||||
|
let $cell = state.doc.resolve(cell)
|
||||||
|
let table = $cell.node(-1), map = TableMap.get(table), start = $cell.start(-1)
|
||||||
|
let col = map.colCount($cell.pos - start) + $cell.nodeAfter.attrs.colspan
|
||||||
|
for (let row = 0; row < map.height; row++) {
|
||||||
|
let index = col + row * map.width - 1
|
||||||
|
// For positions that are have either a different cell or the end
|
||||||
|
// of the table to their right, and either the top of the table or
|
||||||
|
// a different cell above them, add a decoration
|
||||||
|
if ((col == map.width || map.map[index] != map.map[index + 1]) &&
|
||||||
|
(row == 0 || map.map[index - 1] != map.map[index - 1 - map.width])) {
|
||||||
|
let cellPos = map.map[index]
|
||||||
|
let pos = start + cellPos + table.nodeAt(cellPos).nodeSize - 1
|
||||||
|
let dom = document.createElement("div")
|
||||||
|
dom.className = "column-resize-handle"
|
||||||
|
decorations.push(Decoration.widget(pos, dom))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return DecorationSet.create(state.doc, decorations)
|
||||||
|
}
|
@ -258,15 +258,6 @@ const ordered_list = Object.assign({}, orderedList, {content: "list_item+", grou
|
|||||||
const bullet_list = Object.assign({}, bulletList, {content: "list_item+", group: "block"});
|
const bullet_list = Object.assign({}, bulletList, {content: "list_item+", group: "block"});
|
||||||
const list_item = Object.assign({}, listItem, {content: 'paragraph block*'});
|
const list_item = Object.assign({}, listItem, {content: 'paragraph block*'});
|
||||||
|
|
||||||
const {
|
|
||||||
table_row,
|
|
||||||
table_cell,
|
|
||||||
table_header,
|
|
||||||
} = tableNodes({
|
|
||||||
tableGroup: "block",
|
|
||||||
cellContent: "block+"
|
|
||||||
});
|
|
||||||
|
|
||||||
const table = {
|
const table = {
|
||||||
content: "table_row+",
|
content: "table_row+",
|
||||||
attrs: {
|
attrs: {
|
||||||
@ -277,11 +268,66 @@ const table = {
|
|||||||
group: "block",
|
group: "block",
|
||||||
parseDOM: [{tag: "table", getAttrs: domAttrsToAttrsParser(['style'])}],
|
parseDOM: [{tag: "table", getAttrs: domAttrsToAttrsParser(['style'])}],
|
||||||
toDOM(node) {
|
toDOM(node) {
|
||||||
console.log(extractAttrsForDom(node, ['style']));
|
|
||||||
return ["table", extractAttrsForDom(node, ['style']), ["tbody", 0]]
|
return ["table", extractAttrsForDom(node, ['style']), ["tbody", 0]]
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const table_row = {
|
||||||
|
content: "(table_cell | table_header)*",
|
||||||
|
tableRole: "row",
|
||||||
|
parseDOM: [{tag: "tr"}],
|
||||||
|
toDOM() { return ["tr", 0] }
|
||||||
|
};
|
||||||
|
|
||||||
|
let cellAttrs = {
|
||||||
|
colspan: {default: 1},
|
||||||
|
rowspan: {default: 1},
|
||||||
|
width: {default: null},
|
||||||
|
height: {default: null},
|
||||||
|
};
|
||||||
|
|
||||||
|
function getCellAttrs(dom) {
|
||||||
|
return {
|
||||||
|
colspan: Number(dom.getAttribute("colspan") || 1),
|
||||||
|
rowspan: Number(dom.getAttribute("rowspan") || 1),
|
||||||
|
width: dom.style.width || null,
|
||||||
|
height: dom.style.height || null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function setCellAttrs(node) {
|
||||||
|
let attrs = {};
|
||||||
|
|
||||||
|
const styles = [];
|
||||||
|
if (node.attrs.colspan != 1) attrs.colspan = node.attrs.colspan;
|
||||||
|
if (node.attrs.rowspan != 1) attrs.rowspan = node.attrs.rowspan;
|
||||||
|
if (node.attrs.width) styles.push(`width: ${node.attrs.width}`);
|
||||||
|
if (node.attrs.height) styles.push(`height: ${node.attrs.height}`);
|
||||||
|
if (styles) {
|
||||||
|
attrs.style = styles.join(';');
|
||||||
|
}
|
||||||
|
|
||||||
|
return attrs
|
||||||
|
}
|
||||||
|
|
||||||
|
const table_cell = {
|
||||||
|
content: "block+",
|
||||||
|
attrs: cellAttrs,
|
||||||
|
tableRole: "cell",
|
||||||
|
isolating: true,
|
||||||
|
parseDOM: [{tag: "td", getAttrs: dom => getCellAttrs(dom)}],
|
||||||
|
toDOM(node) { return ["td", setCellAttrs(node), 0] }
|
||||||
|
};
|
||||||
|
|
||||||
|
const table_header = {
|
||||||
|
content: "block+",
|
||||||
|
attrs: cellAttrs,
|
||||||
|
tableRole: "header_cell",
|
||||||
|
isolating: true,
|
||||||
|
parseDOM: [{tag: "th", getAttrs: dom => getCellAttrs(dom)}],
|
||||||
|
toDOM(node) { return ["th", setCellAttrs(node), 0] }
|
||||||
|
};
|
||||||
|
|
||||||
const nodes = {
|
const nodes = {
|
||||||
doc,
|
doc,
|
||||||
paragraph,
|
paragraph,
|
||||||
|
@ -28,13 +28,27 @@
|
|||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Content 1</td>
|
<td style="width: 250px; height: 30px">Content 1</td>
|
||||||
<td>Content 2</td>
|
<td style="width: 320px; height: 30px">Content 2</td>
|
||||||
|
<td style="width: 320px; height: 30px">Content 2</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td colspan="2">Row 2, Spanning 2</td>
|
||||||
|
<td>Row 2 spanning 1</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td rowspan="2">Row 3/4, Column 1</td>
|
||||||
|
<td>Row 3, Column 2</td>
|
||||||
|
<td>Row 3, Column 3</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Row 4, Column 2</td>
|
||||||
|
<td>Row 4, Column 3</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
<iframe width="560" height="315" src="https://www.youtube.com/embed/n6hIa-fPx0M" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
|
{{-- <iframe width="560" height="315" src="https://www.youtube.com/embed/n6hIa-fPx0M" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>--}}
|
||||||
|
|
||||||
<p><img src="/user_avatar.png" alt="Logo"></p>
|
<p><img src="/user_avatar.png" alt="Logo"></p>
|
||||||
<ul>
|
<ul>
|
||||||
|
Loading…
Reference in New Issue
Block a user