@@ -180,27 +180,39 @@ <h3 class="font-semibold">Layers</h3>
180180 </ div >
181181 < div class ="flex-1 overflow-auto p-2 " id ="layers-list ">
182182 <!-- Layers will be populated dynamically -->
183- < div class ="text-center text-base-content/50 py-8 ">
183+ < div id =" layers-empty " class ="text-center text-base-content/50 py-8 ">
184184 < svg xmlns ="http://www.w3.org/2000/svg " class ="h-8 w-8 mx-auto mb-2 opacity-50 " fill ="none " viewBox ="0 0 24 24 " stroke ="currentColor ">
185185 < path stroke-linecap ="round " stroke-linejoin ="round " stroke-width ="2 " d ="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10 "/>
186186 </ svg >
187187 < p class ="text-sm "> No elements yet</ p >
188188 < p class ="text-xs "> Start drawing to add layers</ p >
189189 </ div >
190+ <!-- Layer items container -->
191+ < div id ="layers-container " class ="space-y-1 hidden "> </ div >
190192 </ div >
191193 < div class ="p-2 border-t border-base-300 ">
192194 < div class ="btn-group w-full ">
193- < button class ="btn btn-sm flex-1 " title ="Move layer up ">
195+ < button class ="btn btn-sm flex-1 " id ="layer-bring-front " title ="Bring to front " disabled >
196+ < svg xmlns ="http://www.w3.org/2000/svg " class ="h-4 w-4 " fill ="none " viewBox ="0 0 24 24 " stroke ="currentColor ">
197+ < path stroke-linecap ="round " stroke-linejoin ="round " stroke-width ="2 " d ="M5 11l7-7 7 7M5 19l7-7 7 7 "/>
198+ </ svg >
199+ </ button >
200+ < button class ="btn btn-sm flex-1 " id ="layer-move-up " title ="Move layer up " disabled >
194201 < svg xmlns ="http://www.w3.org/2000/svg " class ="h-4 w-4 " fill ="none " viewBox ="0 0 24 24 " stroke ="currentColor ">
195202 < path stroke-linecap ="round " stroke-linejoin ="round " stroke-width ="2 " d ="M5 15l7-7 7 7 "/>
196203 </ svg >
197204 </ button >
198- < button class ="btn btn-sm flex-1 " title ="Move layer down ">
205+ < button class ="btn btn-sm flex-1 " id =" layer-move-down " title ="Move layer down " disabled >
199206 < svg xmlns ="http://www.w3.org/2000/svg " class ="h-4 w-4 " fill ="none " viewBox ="0 0 24 24 " stroke ="currentColor ">
200207 < path stroke-linecap ="round " stroke-linejoin ="round " stroke-width ="2 " d ="M19 9l-7 7-7-7 "/>
201208 </ svg >
202209 </ button >
203- < button class ="btn btn-sm flex-1 " title ="Delete layer ">
210+ < button class ="btn btn-sm flex-1 " id ="layer-send-back " title ="Send to back " disabled >
211+ < svg xmlns ="http://www.w3.org/2000/svg " class ="h-4 w-4 " fill ="none " viewBox ="0 0 24 24 " stroke ="currentColor ">
212+ < path stroke-linecap ="round " stroke-linejoin ="round " stroke-width ="2 " d ="M19 13l-7 7-7-7M19 5l-7 7-7-7 "/>
213+ </ svg >
214+ </ button >
215+ < button class ="btn btn-sm flex-1 btn-error " id ="layer-delete " title ="Delete layer " disabled >
204216 < svg xmlns ="http://www.w3.org/2000/svg " class ="h-4 w-4 " fill ="none " viewBox ="0 0 24 24 " stroke ="currentColor ">
205217 < path stroke-linecap ="round " stroke-linejoin ="round " stroke-width ="2 " d ="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16 "/>
206218 </ svg >
@@ -213,6 +225,242 @@ <h3 class="font-semibold">Layers</h3>
213225
214226{% block scripts %}
215227< script >
228+ // Layer Management System
229+ const LayerManager = {
230+ elements : [ ] ,
231+ selectedId : null ,
232+ ws : null ,
233+
234+ init ( websocket ) {
235+ this . ws = websocket ;
236+ this . bindEvents ( ) ;
237+ } ,
238+
239+ bindEvents ( ) {
240+ document . getElementById ( 'layer-bring-front' ) ?. addEventListener ( 'click' , ( ) => this . bringToFront ( ) ) ;
241+ document . getElementById ( 'layer-move-up' ) ?. addEventListener ( 'click' , ( ) => this . moveUp ( ) ) ;
242+ document . getElementById ( 'layer-move-down' ) ?. addEventListener ( 'click' , ( ) => this . moveDown ( ) ) ;
243+ document . getElementById ( 'layer-send-back' ) ?. addEventListener ( 'click' , ( ) => this . sendToBack ( ) ) ;
244+ document . getElementById ( 'layer-delete' ) ?. addEventListener ( 'click' , ( ) => this . deleteSelected ( ) ) ;
245+ } ,
246+
247+ updateElements ( elements ) {
248+ this . elements = elements . sort ( ( a , b ) => b . z_index - a . z_index ) ;
249+ this . render ( ) ;
250+ } ,
251+
252+ addElement ( element ) {
253+ this . elements . push ( element ) ;
254+ this . elements . sort ( ( a , b ) => b . z_index - a . z_index ) ;
255+ this . render ( ) ;
256+ } ,
257+
258+ removeElement ( elementId ) {
259+ this . elements = this . elements . filter ( e => e . id !== elementId ) ;
260+ if ( this . selectedId === elementId ) {
261+ this . selectedId = null ;
262+ }
263+ this . render ( ) ;
264+ } ,
265+
266+ updateElement ( elementId , updates ) {
267+ const idx = this . elements . findIndex ( e => e . id === elementId ) ;
268+ if ( idx !== - 1 ) {
269+ this . elements [ idx ] = { ...this . elements [ idx ] , ...updates } ;
270+ this . elements . sort ( ( a , b ) => b . z_index - a . z_index ) ;
271+ this . render ( ) ;
272+ }
273+ } ,
274+
275+ selectElement ( elementId ) {
276+ this . selectedId = elementId ;
277+ this . render ( ) ;
278+ this . updateButtonStates ( ) ;
279+ } ,
280+
281+ render ( ) {
282+ const container = document . getElementById ( 'layers-container' ) ;
283+ const empty = document . getElementById ( 'layers-empty' ) ;
284+ if ( ! container || ! empty ) return ;
285+
286+ if ( this . elements . length === 0 ) {
287+ empty . classList . remove ( 'hidden' ) ;
288+ container . classList . add ( 'hidden' ) ;
289+ return ;
290+ }
291+
292+ empty . classList . add ( 'hidden' ) ;
293+ container . classList . remove ( 'hidden' ) ;
294+ container . innerHTML = '' ;
295+
296+ this . elements . forEach ( el => {
297+ const item = this . createLayerItem ( el ) ;
298+ container . appendChild ( item ) ;
299+ } ) ;
300+
301+ this . updateButtonStates ( ) ;
302+ } ,
303+
304+ createLayerItem ( element ) {
305+ const item = document . createElement ( 'div' ) ;
306+ item . className = `layer-item flex items-center gap-2 p-2 rounded cursor-pointer hover:bg-base-200 ${ this . selectedId === element . id ? 'bg-primary/20 border border-primary' : 'border border-transparent' } ` ;
307+ item . dataset . elementId = element . id ;
308+
309+ // Element type icon
310+ const typeIcon = this . getTypeIcon ( element . element_type ) ;
311+
312+ // Visibility button
313+ const visibleBtn = document . createElement ( 'button' ) ;
314+ visibleBtn . className = `btn btn-ghost btn-xs ${ element . visible ? '' : 'opacity-40' } ` ;
315+ visibleBtn . title = element . visible ? 'Hide layer' : 'Show layer' ;
316+ visibleBtn . innerHTML = element . visible
317+ ? '<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"/></svg>'
318+ : '<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13.875 18.825A10.05 10.05 0 0112 19c-4.478 0-8.268-2.943-9.543-7a9.97 9.97 0 011.563-3.029m5.858.908a3 3 0 114.243 4.243M9.878 9.878l4.242 4.242M9.88 9.88l-3.29-3.29m7.532 7.532l3.29 3.29M3 3l3.59 3.59m0 0A9.953 9.953 0 0112 5c4.478 0 8.268 2.943 9.543 7a10.025 10.025 0 01-4.132 5.411m0 0L21 21"/></svg>' ;
319+ visibleBtn . addEventListener ( 'click' , ( e ) => {
320+ e . stopPropagation ( ) ;
321+ this . toggleVisibility ( element . id ) ;
322+ } ) ;
323+
324+ // Lock button
325+ const lockBtn = document . createElement ( 'button' ) ;
326+ lockBtn . className = `btn btn-ghost btn-xs ${ element . locked ? 'text-warning' : '' } ` ;
327+ lockBtn . title = element . locked ? 'Unlock layer' : 'Lock layer' ;
328+ lockBtn . innerHTML = element . locked
329+ ? '<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z"/></svg>'
330+ : '<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 11V7a4 4 0 118 0m-4 8v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2z"/></svg>' ;
331+ lockBtn . addEventListener ( 'click' , ( e ) => {
332+ e . stopPropagation ( ) ;
333+ this . toggleLock ( element . id ) ;
334+ } ) ;
335+
336+ // Element name/label
337+ const label = document . createElement ( 'span' ) ;
338+ label . className = `flex-1 text-sm truncate ${ element . visible ? '' : 'opacity-40' } ${ element . locked ? 'italic' : '' } ` ;
339+ label . textContent = this . getElementLabel ( element ) ;
340+
341+ item . innerHTML = typeIcon ;
342+ item . appendChild ( visibleBtn ) ;
343+ item . appendChild ( lockBtn ) ;
344+ item . appendChild ( label ) ;
345+
346+ item . addEventListener ( 'click' , ( ) => this . selectElement ( element . id ) ) ;
347+
348+ return item ;
349+ } ,
350+
351+ getTypeIcon ( type ) {
352+ const icons = {
353+ 'stroke' : '<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15.232 5.232l3.536 3.536m-2.036-5.036a2.5 2.5 0 113.536 3.536L6.5 21.036H3v-3.572L16.732 3.732z"/></svg>' ,
354+ 'shape' : '<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6a2 2 0 012-2h12a2 2 0 012 2v12a2 2 0 01-2 2H6a2 2 0 01-2-2V6z"/></svg>' ,
355+ 'text' : '<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M8 6v14M16 6v14"/></svg>' ,
356+ 'group' : '<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10"/></svg>'
357+ } ;
358+ return icons [ type ] || icons [ 'stroke' ] ;
359+ } ,
360+
361+ getElementLabel ( element ) {
362+ const typeLabels = { stroke : 'Stroke' , shape : 'Shape' , text : 'Text' , group : 'Group' } ;
363+ const baseLabel = typeLabels [ element . element_type ] || 'Element' ;
364+ if ( element . element_type === 'text' && element . content ) {
365+ return element . content . substring ( 0 , 20 ) + ( element . content . length > 20 ? '...' : '' ) ;
366+ }
367+ if ( element . element_type === 'group' && element . name ) {
368+ return element . name ;
369+ }
370+ return `${ baseLabel } ${ element . z_index } ` ;
371+ } ,
372+
373+ updateButtonStates ( ) {
374+ const hasSelection = this . selectedId !== null ;
375+ const selectedElement = this . elements . find ( e => e . id === this . selectedId ) ;
376+ const isLocked = selectedElement ?. locked || false ;
377+
378+ [ 'layer-bring-front' , 'layer-move-up' , 'layer-move-down' , 'layer-send-back' ] . forEach ( id => {
379+ const btn = document . getElementById ( id ) ;
380+ if ( btn ) btn . disabled = ! hasSelection || isLocked ;
381+ } ) ;
382+
383+ const deleteBtn = document . getElementById ( 'layer-delete' ) ;
384+ if ( deleteBtn ) deleteBtn . disabled = ! hasSelection ;
385+ } ,
386+
387+ // WebSocket actions
388+ toggleVisibility ( elementId ) {
389+ if ( this . ws && this . ws . readyState === WebSocket . OPEN ) {
390+ this . ws . send ( JSON . stringify ( {
391+ type : 'layer_action' ,
392+ action : 'toggle_visibility' ,
393+ element_id : elementId
394+ } ) ) ;
395+ }
396+ } ,
397+
398+ toggleLock ( elementId ) {
399+ if ( this . ws && this . ws . readyState === WebSocket . OPEN ) {
400+ this . ws . send ( JSON . stringify ( {
401+ type : 'layer_action' ,
402+ action : 'toggle_lock' ,
403+ element_id : elementId
404+ } ) ) ;
405+ }
406+ } ,
407+
408+ bringToFront ( ) {
409+ if ( ! this . selectedId ) return ;
410+ if ( this . ws && this . ws . readyState === WebSocket . OPEN ) {
411+ this . ws . send ( JSON . stringify ( {
412+ type : 'layer_action' ,
413+ action : 'bring_to_front' ,
414+ element_id : this . selectedId
415+ } ) ) ;
416+ }
417+ } ,
418+
419+ moveUp ( ) {
420+ if ( ! this . selectedId ) return ;
421+ if ( this . ws && this . ws . readyState === WebSocket . OPEN ) {
422+ this . ws . send ( JSON . stringify ( {
423+ type : 'layer_action' ,
424+ action : 'move_forward' ,
425+ element_id : this . selectedId
426+ } ) ) ;
427+ }
428+ } ,
429+
430+ moveDown ( ) {
431+ if ( ! this . selectedId ) return ;
432+ if ( this . ws && this . ws . readyState === WebSocket . OPEN ) {
433+ this . ws . send ( JSON . stringify ( {
434+ type : 'layer_action' ,
435+ action : 'move_backward' ,
436+ element_id : this . selectedId
437+ } ) ) ;
438+ }
439+ } ,
440+
441+ sendToBack ( ) {
442+ if ( ! this . selectedId ) return ;
443+ if ( this . ws && this . ws . readyState === WebSocket . OPEN ) {
444+ this . ws . send ( JSON . stringify ( {
445+ type : 'layer_action' ,
446+ action : 'send_to_back' ,
447+ element_id : this . selectedId
448+ } ) ) ;
449+ }
450+ } ,
451+
452+ deleteSelected ( ) {
453+ if ( ! this . selectedId ) return ;
454+ if ( this . ws && this . ws . readyState === WebSocket . OPEN ) {
455+ this . ws . send ( JSON . stringify ( {
456+ type : 'layer_action' ,
457+ action : 'delete' ,
458+ element_id : this . selectedId
459+ } ) ) ;
460+ }
461+ }
462+ } ;
463+
216464// Initialize canvas-specific WebSocket connection
217465document . addEventListener ( 'DOMContentLoaded' , function ( ) {
218466 const canvas = document . getElementById ( 'drawing-canvas' ) ;
@@ -221,6 +469,36 @@ <h3 class="font-semibold">Layers</h3>
221469 const container = canvas . parentElement ;
222470 canvas . width = container . offsetWidth ;
223471 canvas . height = container . offsetHeight ;
472+
473+ // Initialize WebSocket and layer manager
474+ const canvasId = canvas . dataset . canvasId ;
475+ const protocol = window . location . protocol === 'https:' ? 'wss:' : 'ws:' ;
476+ const ws = new WebSocket ( `${ protocol } //${ window . location . host } /ws/canvas/${ canvasId } ` ) ;
477+
478+ ws . onopen = ( ) => {
479+ LayerManager . init ( ws ) ;
480+ // Request initial elements
481+ ws . send ( JSON . stringify ( { type : 'get_elements' } ) ) ;
482+ } ;
483+
484+ ws . onmessage = ( event ) => {
485+ const data = JSON . parse ( event . data ) ;
486+
487+ switch ( data . type ) {
488+ case 'elements_list' :
489+ LayerManager . updateElements ( data . elements || [ ] ) ;
490+ break ;
491+ case 'element_added' :
492+ LayerManager . addElement ( data . element ) ;
493+ break ;
494+ case 'element_updated' :
495+ LayerManager . updateElement ( data . element_id , data . updates ) ;
496+ break ;
497+ case 'element_deleted' :
498+ LayerManager . removeElement ( data . element_id ) ;
499+ break ;
500+ }
501+ } ;
224502 }
225503} ) ;
226504</ script >
0 commit comments